Tag Archives: Classes

PSDay.uk Slides & Code

I had the pleasure of presenting at PSDay.uk recently. PSDay uk was sponsored by Ticketmaster and G-Research in London.Β I presented the following topic:

PowerShell Classes for Ops transitioning to Dev

I’ve been fascinated by PowerShell classes since its introduction in v5. When Rob Sewell ask me to do a session at the PSDay.uk, I didn’t have to think too long about the topic πŸ™‚

I’ll be honest that the first 5 minutes was kinda nerve wrecking. I wasn’t nervous at first until I stood there and had some kind of outer-body experience…

Holy crap this is really happening!!! Calm down Urv… Whooza… Get to the code…

Once I got settled in, my nerves calmed down…

The code is bases on an interaction I had with Jaap Brasser (The legend himself πŸ˜‰ ) on his blog a while back. At first I thought I didn’t have enough code but it all timed-out perfectly in the end.

Here’s the link to the code & presentation.

Shout out to Doug Finke for taking the time to break down classes for me. Doug is a true Developer and an great guy! I truly believe that the right people are placed in our paths when we need them most. Yet only if we remain open-minded and willing to learn can these mentors help us reach new levels of development. I can’t thank you enough Doug, your contribution to the community is plain awesome! You make me want to be a better coder!

Thank you PSDay.uk for this great experience! Til’ next time? πŸ˜›

Ttyl,

Urv

 

ACLs Folder class

‘Sup PSHomies,

I recently had to make a quick Backup & Restore of ACLs three levels deep. Nothing fancy, just two functions, but that got me thinking…

Why not make a class of this?

And so I did! Here’s the framework for the class:

classACLFolders

Here’s a list of the methods:

  • Backup. Backup SDDL from the property $Folder of the class
  • Restore. Restore SDDL to the property $Folder of the class
  • Clone. Clone will take a target Folder and clone the SDDL of $Folder to it
  • ConvertSDDLToAccess. This will enumerate what the SDDL stands for

Default Constructor

classACLDefConstructor

The default constructor will evaluate the folder used to instantiate the object. If successful, the SDDL, Owner and Access is retrieved using  the Backup() method. All actions are registered for future reference.

Instantiating is pretty straightforward:

instantiateClass

Backup()

classACLBackup

This will retrieve the SDDL for the folder and enumerate the Access.

Restore()

classACLRestore

Restore is a bit tricky. For one you need to make sure it isn’t empty. Set-Acl has no problem setting an empty SDDL, blowing everything open (worst case scenario, and that’s why you should test this in a lab first!). The other challenge is having a valid SDDL string. You can change the SDDL string if you want to something gibberish, hence the typecast as a precaution.

Clone()

classACLDefClone

The same goes for cloning. In this case we need to test the target path. Alternatively, you could  also change the Folder to a new path… It works, you’d just have misleading ActionHistory entries… I wonder if there’s a way to make this read-only,  just thinking out loud here… (note to self)

ConvertSDDLToAccess()

This is just a lil’ something extra. Like I said in a previous blog SDDL really gives more information. For one, the SID will let you know which domain this object belongs to. One thing I ran into with ReACL is that SIDHistory will resolve to the current NTAccount. This had me puzzled for a while until I saw that the SIDs in SDDL where different.

Here’s what the ouput looks like:

convertSDDLToAccess

Now for those of you that are wondering just what is this AccessMask, wonder no more! πŸ™‚

Remember the RoboCopy ExitCodes blog I did a while back? Well it’s the same principal πŸ™‚ This is why classes & everything related should be on your radar…

enumAccessmask

Here’s how this works…

Say I wanted to evaluate the AccessMask of a SDDL entry

AccessMaskEnum
classACLConvertSDDLToAccess

Here I have the SID & the NTAccount. This is the builtin administrators account but it also works for Domain accounts.

There’s a private function that will translate the SID accordingly.

To see what the account can actually do we can enumerate the AccessMask

AccessMaskEnum1
AdvancedPermissions

This is what we’d see using the advanced Security GUI tab of a folder.

Not bad… Not bad at all…

I can’t state this enough, SDDL is NOT to be be trifled with. Yes you need admin rights and please try this is a testlab first.  SDDL is very potent, if used with caution, it could do a whole bit of good!

So finally, here’s the code…

[Flags()] Enum AccessMask{
Read = 1
Write = 2
Append = 4
ReadExtendedAttributes = 8
WriteExtendedAttributes = 16
Execute = 32
DeleteDirectory = 64
ReadAttributes = 128
WriteAttributes = 256
Delete = 65536
ReadSecurity = 131072
WriteACL = 262144
WriteOwner = 524288
Synchronize = 1048576
}
class aclsFolder{
[String]$Folder
[String]$SDDL
[String]$Owner
[PSObject[]]$Access
[PSObject[]]$ActionHistory
#Default Constructor
aclsFolder($fldr){
if(Test-Path -Path $fldr){
$this.Folder = $fldr
$Tags = @('Default','Constructor','valid')
$MessageData = "Path $($fldr) found"
$this.ActionHistory += Write-Information -MessageData $MessageData 6>&1 -Tags $Tags | Select-Object *
$this.Backup()
}
else{
$Tags = @('Default','Constructor','invalid')
$MessageData = "Path $($fldr) not found"
$this.ActionHistory += Write-Information -MessageData $MessageData 6>&1 -Tags $Tags | Select-Object *
}
}
#Methods
Backup(){
if(Test-Path $this.Folder){
$result = Get-Acl $this.Folder
$this.SDDL = $result.Sddl
$this.Owner = $result.Owner
$this.Access = $($result.Access | Select-Object File*,Access*,Identity*,IsInherited,*Flags)
$Tags = @('Backup','Success')
$MessageData = "Backup SDDL of $($this.Folder) was successful"
$this.ActionHistory += Write-Information -MessageData $MessageData 6>&1 -Tags $Tags | Select-Object *
}
else{
Write-Warning "Invalid Path $($this.Folder)"
$Tags = @('Backup','Failed')
$MessageData = "Backup SDDL of $($this.Folder) has failed"
$this.ActionHistory += Write-Information -MessageData $MessageData 6>&1 -Tags $Tags | Select-Object *
}
}
Restore(){
if((Test-Path $this.Folder) -and
![string]::isNullOrEmpty($this.SDDL) -and
[Security.AccessControl.RawSecurityDescriptor]$this.SDDL){
$acl = Get-Acl -Path $this.Folder
$acl.SetSecurityDescriptorSddlForm($this.SDDL)
Set-Acl -Path $($this.Folder) -AclObject $($acl)
$Tags = @('Restore', 'Success')
$MessageData = "Restoring SDDL on $($this.Folder) was succesful"
$this.ActionHistory += Write-Information -MessageData $MessageData 6>&1 -Tags $Tags | Select-Object *
#Reset values
$this.Backup()
}
else{
Write-Warning "Invalid Path $($this.Folder) or SDDL is invalid"
$Tags = @('Restore', 'Failed')
$MessageData = "Restoring SDDL on $($this.Folder) has failed"
$this.ActionHistory += Write-Information -MessageData $MessageData 6>&1 -Tags $Tags | Select-Object *
}
}
Clone($tgt){
if((Test-Path -Path $tgt) -and
![string]::isNullOrEmpty($this.SDDL) -and
[Security.AccessControl.RawSecurityDescriptor]$this.SDDL){
$acl = Get-Acl -Path $tgt
$acl.SetSecurityDescriptorSddlForm($this.SDDL)
Set-Acl -Path $($tgt) -AclObject $($acl)
$Tags = @('Clone', 'Success')
$MessageData = "Cloning SDDL on $($this.Folder) was succesful"
$this.ActionHistory += Write-Information -MessageData $MessageData 6>&1 -Tags $Tags | Select-Object *
}
else{
Write-Warning "Invalid Path $($this.Folder) or SDDL is invalid"
$Tags = @('Clone', 'Failed')
$MessageData = "Cloning SDDL on $($this.Folder) has failed"
$this.ActionHistory += Write-Information -MessageData $MessageData 6>&1 -Tags $Tags | Select-Object *
}
}
[PSObject]ConvertSDDLToAccess(){
Function Convert-SID2NTAccount{
param(
$SID
)
$objSID = New-Object System.Security.Principal.SecurityIdentifier($SID)
$objUser = $objSID.Translate( [System.Security.Principal.NTAccount])
$objUser
}
$accessSDDL = ([Security.AccessControl.RawSecurityDescriptor]$this.SDDL).DiscretionaryAcl |
ForEach-Object{
[PSCustomObject]@{
SID = $_.SecurityIdentifier
NTAccount = (Convert-SID2NTAccount -SID $_.SecurityIdentifier)
AceQualifier = $_.AceQualifier
AccessMask = $_.AccessMask
AceType = $_.AceType
AceFlags = $_.AceFlags
IsInherited = $_.IsInherited
InheritanceFlags = $_.InheritanceFlags
}
}
return $accessSDDL
}
}

Hope it’s worth something to you…

Ttyl,

Urv

RoboCopy class

β€˜Sup PSHomies,

It all started a year ago… Always wanting to learn anything PowerShell related, classes caught my eye ever since it was introduced in v5.  I wanted to try my hand at classes with a real life application… So I got on twitter for some tips…

powershell-class-tweet-2
powershell-class-tweet-3

Doug was kind enough to reach out and point me in the right direction, for which I owe him a great debt! Appreciate it Doug!!!

Like I said, I wanted to try my hand at classes with a real life application… If you’ve read my blogs then you’ll know that I’m a fan of robocopy, seriously, huge fan! . Did I mention how awesome robocopy is? πŸ˜› I think I found my real life application πŸ˜‰

When I started out with my Robocopy class, it was just about logging initially, but it could be so much more! Classes are native to v5. Now that v5 is mainstream I decided to finish the class. Richard Siddaway’s article  was just the spark I needed to get me going (again)!

Here’s what the Robocopy class looks like:

robocopy-class

Here a quick rundown on the properties:

The source/destination properties of the class are self explanatory (if you’re familiar with robocopy). The property logdir and JobID will be used to define a valid logfile name (with extension .log). Robocopy has quite a bit of options. I wanted to keep it as generic as possible. The property $this.Options is still a work in progress. The property $this.WildCards  is where you’ll define what will be filtered.  I’ll get back to rcCMDs and rcExitCodes later on…

These are the methods I came up with (so far, still a work in progress)

  • Mirror(). Mirrors $this.Source to $this.Destination with some default options
  • Move(). Moves this.Source to $this.Destination with some default options
  • RollBack(). Rollback $this.Destination to $this.Source with some default options
  • Sync(). Sync will synchronize the delta’s from $this.Source to $this.Destination using any additional $this.Options defined (at least that’s the idea). I’ve added a few options by default, mostly to exclude files and folders, think recycle.bin “System Volume Information” and the likes.
  • VerifyPaths(). This let’s you know if the $this.Source, $this.Destination and $this.LogDir are valid.
  • GetlistSource(). This will list the content of the $this.Source
  • GetListDestionation(). This will list the content of $this.Destination
  • GetLogSummary. This will return a summary of the log file (Hehe). The method is static so that you don’t have to instantiate the class in order to use it. (Thanks again Doug!)

The two methods: StartRCProcess and ExecuteRCCMD are actually helper methods. I just haven’t figured out how that works in classes. Ideally I’d like to have them hidden or as a function if that even makes sense. So here’s where they come in. At first I just executed robocopy with the necessary arguments. If you’re not interested in the exitcode then using ExecuteRCCMD is what you need. I wrote a blog about enumerating RoboCopy Exitcodes. Using $LastExitCode isn’t going to cut it if you decide to run robocopy jobs parallel. That’s where StartRCProcess comes in.Using Start-Process comes with an overhead of say 20 MB, which could add up in the long run. You do need to wait until the process has finished to retrieve the exitcode. If you really need the exitcode then StartRCProcess is what you need. The property $this.rcExitCodes will only be populated if StartRCProcess is used. Both will populate the $this.rcCMDs property.

Ok I think I’ve covered the basics, time to show some code! πŸ˜‰

[Flags()] Enum RoboCopyExitCodes{
NoChange = 0
OKCopy = 1
ExtraFiles = 2
MismatchedFilesFolders = 4
FailedCopyAttempts = 8
FatalError = 16
}
Class RoboCopy{
[String]$Source
[String]$Destination
[String]$LogDir
[String]$JobID
[String]$Options
[String[]]$WildCards
[PSObject[]]$rcCMDs
[PSObject[]]$rcExitCodes
#default constructor
RoboCopy(){}
#constructor
RoboCopy([String]$src,[String]$des,[String]$jid,[String]$ld,[string[]]$wc){
$this.Source = $src
$this.Destination = $des
$this.JobID = $jid
$this.LogDir = $ld
$this.WildCards = $wc
}
[PSCustomObject]VerifyPaths(){
return [PSCustomObject]@{
Source = $(Test-Path -Path $this.Source)
Destination = $(Test-Path -Path $this.Destination)
LogDirectory = $(Test-Path -Path $this.LogDir)
}
}
StartRCProcess($source,$target,$params,$logFile,$tag){
#Save RoboCopy Command
$MessageData = $('robocopy "{0}" "{1}" {2} {3}' -f $source,$target,$($params -join ' '),$logFile)
$this.rcCMDs += Write-Information -MessageData $MessageData 6>&1 -Tags $tag | Select-Object *
#Execute Process
$rcArgs = "`"$($source)`" `"$($target)`" $params $logFile"
$rcResult = Start-Process robocopy -ArgumentList $rcArgs -WindowStyle Hidden -PassThru -Wait
#Save ExitCode
$this.rcExitCodes += [PSCustomObject]@{
ExitCode = [RoboCopyExitCodes]$rcResult.ExitCode
StartTime = $rcResult.StartTime
ExitTime = $rcResult.ExitTime
rcCMD = $MessageData
}
}
ExecuteRCCMD($source,$target,$params,$logFile,$tag){
#Save RoboCopy Command
$MessageData = $('robocopy "{0}" "{1}" {2} {3}' -f $source,$target,$($params -join ' '),$logFile)
$this.rcCMDs += Write-Information -MessageData $MessageData 6>&1 -Tags $tag | Select-Object *
#Execute Robocopy CMD
$rcArgs = @("$($source)","$($target)",$params,$logFile)
robocopy @rcArgs
}
Mirror(){
$tag = @('Mirror',$this.JobID)
$params = New-Object System.Collections.Arraylist
$params.AddRange(@('/MIR','/E','/BYTES','/NP','/NDL','/R:1','/W:1'))
$logFile = '/LOG:{0}\mirror-{1}.log' -f $this.LogDir,$this.JobID
$this.StartRCProcess($this.Source,$this.Destination,$params,$logFile,$tag)
#$this.ExecuteRCCMD($this.Source,$this.Destination,$params,$logFile,$tag)
}
Move(){
$tag = @('Move',$this.JobID)
$params = New-Object System.Collections.Arraylist
$params.AddRange(@('/COPY:DATS','/DCOPY:T','/SECFIX','/BYTES','/S','/E','/NP','/NDL','/R:1','/W:1','/MOVE'))
$logFile = '/LOG:{0}\move-{1}.log' -f $this.LogDir,$this.JobID
#$this.StartRCProcess($this.Source,$this.Destination,$params,$logFile,$tag)
$this.ExecuteRCCMD($this.Source,$this.Destination,$params,$logFile,$tag)
}
RollBack(){
$tag = @('RollBack',$this.JobID)
$params = New-Object System.Collections.Arraylist
$params.AddRange(@('/COPY:DATS','/DCOPY:T','/SECFIX','/BYTES','/S','/E','/NP','/NDL','/R:1','/W:1','/MOVE'))
$logFile = '/LOG:{0}\rollback-{1}.log' -f $this.LogDir,$this.JobID
#$this.StartRCProcess($$this.Source,$this.Destination,$params,$logFile,$tag)
$this.ExecuteRCCMD($this.Destination,$this.Source,$params,$logFile,$tag)
}
Sync(){
$tag = @('Sync',$this.JobID)
#Excluded Files & Directories
$XF = @(
'/XF'
'thumbs.db'
'~*.*'
'*.pst'
'desktop.ini'
'*.lnk'
)
$XD = @(
'/XD'
"`$Recycle.bin"
'Boot Recycler'
'IECompatCache'
'IEDownloadHistory'
'Cookies'
'WINDOWS'
'PrivacIE'
"`"System Volume Information`""
)
$Excluded = "$($XF -join ' ') $($XD -join ' ')"
$params = New-Object System.Collections.Arraylist
$params.AddRange(@($($this.WildCards),'/COPY:DATS','/SECFIX','/BYTES','/S','/PURGE','/E','/NP','/NDL','/R:1','/W:1'))
$logFile = '/LOG:{0}\sync-{1}.log' -f $this.LogDir,$this.JobID
#Save RoboCopy Command
$arrMessageDataInput = @(
$this.Source
$this.Destination
$($params -join ' ')
$($this.Options -join ' ')
$Excluded
$logFile
)
$MessageData = $('robocopy "{0}" "{1}" {2} {3} {4} {5}' -f $arrMessageDataInput )
$this.rcCMDs += Write-Information -MessageData $MessageData 6>&1 -Tags $tag | Select-Object *
#Execute Robocopy CMD
$rcArgs = @("$($this.Source)","$($this.Destination)",$params,$XF,$XD,$logFile)
robocopy @rcArgs
}
GetListSource(){
$tag = @('ListSRC',$this.JobID)
$params = New-Object System.Collections.Arraylist
$params.AddRange(@($($this.WildCards),'/L','/S','/E', '/BYTES','/FP','/NC','/NDL','/TS','/R:0','/W:0'))
$logFile = '/LOG:{0}\listSRC-{1}.log' -f $this.LogDir,$this.JobID
$this.StartRCProcess($this.Source,'NULL',$params,$logFile,$tag)
#$this.ExecuteRCCMD($this.Source,$this.Destination,$params,$logFile,$tag)
}
GetListDestination(){
$tag = 'ListDES'
$params = New-Object System.Collections.Arraylist
$params.AddRange(@($($this.WildCards),'/L','/S','/E','/BYTES','/FP','/NC','/NDL','/TS','/R:0','/W:0'))
$logFile = '/LOG:{0}\listDES-{1}.log' -f $this.LogDir,$this.JobID
#$this.StartRCProcess($this.Source,$this.Destination,$params,$logFile,$tag)
$this.ExecuteRCCMD($this.Destination,'NULL',$params,$logFile,$tag)
}
static [PSCustomObject]GetLogSummary([String]$rcLogFile){
filter Get-CapacitySize {
'{0:N2} {1}' -f $(
if ($_ -lt 1kb) { $_, 'Bytes' }
elseif ($_ -lt 1mb) { ($_/1kb), 'KB' }
elseif ($_ -lt 1gb) { ($_/1mb), 'MB' }
elseif ($_ -lt 1tb) { ($_/1gb), 'GB' }
elseif ($_ -lt 1pb) { ($_/1tb), 'TB' }
else { ($_/1pb), 'PB' }
)
}
$rcLogSummary = [PSCustomObject]@{
Start = $null
End = $null
LogFile = $null
Source = $null
Destination = $null
TotalDirs = $null
CopiedDirs = $null
FailedDirs = $null
TotalFiles = $null
CopiedFiles = $null
FailedFiles = $null
TotalBytes = $null
CopiedBytes = $null
FailedBytes = $null
TotalTimes = $null
Speed = $null
}
$rcLogSummary.LogFile = $rcLogFile.Split('\')[-1].ToLower()
$logFileContent = Get-Content $rcLogFile -Raw
[regex]$regex_Start = 'Started\s:\s+(?<StartTime>.+[^\n\r])'
if ($logFileContent -match $regex_Start){
$rcLogSummary.Start = $Matches['StartTime']
}
[regex]$regex_End = 'Ended\s:\s+(?<EndTime>.+[^\n\r])'
if ($logFileContent -match $regex_End){
$rcLogSummary.End = $Matches['EndTime']
}
[regex]$regex_Source = 'Source\s:\s+(?<Source>.+[^\n\r])'
if($logFileContent -match $regex_Source){
$rcLogSummary.Source = $Matches['Source'].Tolower()
}
[regex]$regex_Target = 'Dest\s:\s+(?<Target>.+[^\n\r])'
if($logFileContent -match $regex_Target){
$rcLogSummary.Destination = $Matches['Target'].ToLower()
}
[regex]$regex_Dirs = 'Dirs\s:\s+(?<TotalDirs>\d+)\s+(?<CopiedDirs>\d+)(?:\s+\d+){2}\s+(?<FailedDirs>\d+)\s+\d+'
if ($logFileContent -match $regex_Dirs){
$rcLogSummary.TotalDirs = [int]$Matches['TotalDirs']
$rcLogSummary.CopiedDirs = [int]$Matches['CopiedDirs']
$rcLogSummary.FailedDirs = [int]$Matches['FailedDirs']
}
[regex]$regex_Files = 'Files\s:\s+(?<TotalFiles>\d+)\s+(?<CopiedFiles>\d+)(?:\s+\d+){2}\s+(?<FailedFiles>\d+)\s+\d+'
if ($logFileContent -match $regex_Files){
$rcLogSummary.TotalFiles = [int]$Matches['TotalFiles']
$rcLogSummary.CopiedFiles = [int]$Matches['CopiedFiles']
$rcLogSummary.FailedFiles = [int]$Matches['FailedFiles']
}
[regex]$regex_Speed = 'Speed\s:\s+(?<Speed>.+\/min)'
if ($logFileContent -match $regex_Speed){
$rcLogSummary.Speed = $Matches['Speed']
}
$arrBytes = @(
'Bytes\s:\s+(?<TotalBytes>(\d+\.\d+\s)[bmg]|\d+)\s+' #TotalBytes
'(?<CopiedBytes>\d+.\d+\s[bmg]|\d+)\s+' #CopiedBytes
'(?:(\d+.\d+\s[bmg]|\d+)\s+){2}' #Skip two
'(?<FailedBytes>\d+.\d+\s[bmg]|\d+)' #FailedBytes
)
[regex]$regex_Bytes = -join $arrBytes
if ($logFileContent -match $regex_Bytes){
$rcLogSummary.TotalBytes = [int64]$Matches['TotalBytes'] | Get-CapacitySize
$rcLogSummary.CopiedBytes = [int64]$Matches['CopiedBytes'] | Get-CapacitySize
$rcLogSummary.FailedBytes = [int64]$Matches['FailedBytes'] | Get-CapacitySize
}
[regex]$regex_Times = 'Times\s:\s+(?<TotalTimes>\d+:\d+:\d+)'
if ($logFileContent -match $regex_Times){
$rcLogSummary.TotalTimes = $Matches['TotalTimes']
}
return $rcLogSummary
}
[String]GetRoboCopyCMD(){
$paramRoboCopy = @(
$this.Source
$this.Destination
$($this.WildCards -join ' ')
$this.Options
$('/LOG:{0}\{1}.log' -f $this.LogDir,$this.JobID)
)
return $('robocopy "{0}" "{1}" {2} {3} {4}' -f $paramRoboCopy)
}
}

Here what’s happening in the List methods:

methods-lists

GetListSource() is using $this.StartRCProcess to generate a list of $this.Source using some default option. While writing I noticed that I forgot to add the wildcards to the parameter. All I had to do was add it!. I added it at the beginning so it lines up accordingly… Robocopy is fickle like that…  GetListDestination does the same only it uses ExecuteRCCMD instead.

Here’s what’s going on in StartRCProcess and ExcuteRCCMD

startexecuterc

Both StartRCProcess and ExcuteRCCMD will save the robocopy command using Write-Information. I’m loving Write-Information more and more! StartRCProcess saves the exitcode with some extra information. Here’s where the robocopy exitcode enumeration came in handy! ExecuteRCCMD will run robocopy with the specified arguments. Truth be told I’m more partial to the ExecuteRCCMD method. I added the StartRCProcess more for demo purposes and finally getting to use my Robocopy exitcode enumeration!

For Mirror(),Move() and RollBack(), I omitted the Wildcards. These methods all or nothing in my opinion. If omitted, . will be the default.

Sync() had me going for a while. I still have some issues with Options. For now Sync() uses some default switches. Like I said work in progress…

Quite a bit of code, so does it work? Here’s some code to play with. be sure to edit the source,destination and logdir to your liking. Just remember that robocopy is unforgiving so make sure not to use it of production folders!

#region Main
$rc = [RoboCopy]::New('C:\scripts\move','C:\temp\move','rc-0001','c:\scripts\log',@('*.*'))

#Run RoboCopy methods
$rc.Sync()
$rc.GetListSource()
$rc.GetListDestination()

#Get RoboCopy LogFile Summary
[RoboCopy]::GetLogSummary("$env:HOMEDRIVE\scripts\log\listSRC-rc-0001.log")
[RoboCopy]::GetLogSummary("$env:HOMEDRIVE\scripts\log\listDES-rc-0001.log")
[RoboCopy]::GetLogSummary("$env:HOMEDRIVE\scripts\log\sync-rc-0001.log")

#Get RoboCopy executed CMDs
$rc.rcCMDs
$rc.rcExitCodes
#endregion

First I instantiate the class with arguments. I then run the methods Sync(),GetListSource() and GetListDestination(). Up next is retrieve the LogSummaries from the methods. Here’s a screenshot of the Sync LogSummary

synclogfile

I did a select of $rc.rcCMDs to give you an idea what is being stored

rc-rccmds

Only want ListDES?

$rc.rcCMDs |
Where-Object{$_.Tags -contains 'ListDes'} |
Select-Object -Property Time*,Tag*,Mess*
rc-rccmdswhereobject

The information stream is quite handy! The tags will definitely come in handy when you need to filter on action verb or job ID.

The methods GetListSource() & Mirror() both make use of StartRCProcess(), so let’s see what $rc.rcExitcode has shall we?

rcexitcodes

Nice!

This is by far my longest blog, if you made this far then… Congratulations! There’s still so much to discover when it comes to classes.

Classes definitely  makes your code look and feel more like a developer πŸ˜‰ . I feel more comfortable giving a colleague this class than a bunch of scripts. In Richard’s article he’s using both classes and modules. There are sure to be some gotcha’s… Do you go all in with classes or only partial?

I’m hoping that the community can shed some light on the subject. I’d love to hear from you guys on how to improve on this… Let’s make this year, a year of PowerShell Classes! πŸ˜›

Hope it’s worth something to you…

Ttyl,

Urv

A simple logger using PowerShell Class

β€˜Sup PSHomies,

At our last DuPSUG meeting, Jaap Brassers did a short Demo on the latest addition to PowerShell Streams, Information. How did I miss this?

So what’s so great about the new Information stream? The Information stream solves the Write-Host issue of not sending output to a stream like: error;verbose;warning etc. As such we were told that Write-Host is evil and every time you use it a puppy dies… Who wants that on their conscious right?

Well with the Information stream Write-Host is allowed… no more puppy genocide!

Jaap showed us what the stream output looks like and how to write to the information stream. Here’s what the output looks like

informationstream

Here are a few properties we can play with: TimeGenerated,Source,User,Computer,Tags and MessageData. You get all of this by writing/redirecting to the information stream. The TimeGenerated property caught my eye, imagine creating a timeline of sorts. While googling (don’t act like it’s just me…) I came across a great article by Jeff Hicks on the subject (Seriously, how did I miss this?) In it he also talked about generating a timeline view… πŸ˜‰

After I had some time to think about how to make use of this, it dawned on me… You know what else is native to PowerShell v5? Classes! When Classes were introduced in v5, they were another way to write DSC Resources. I’ve always thought that there’s more to classes than meets the eye.

If you’re interested in classes, Richard Siddaway has a great article taking you through each step, worth trying out.

Jaap’s demo gave me the idea to try my hand at a simple logger using classes. Here’s what I came up with…

Class WriteLog{
[string]$FileBaseName = 'Temp'
[string]$LogDirectory = 'C:\Scripts\export'
[string]$Preference = 'SilentContinue'
[bool]$Console = $true
[PSObject[]]$InfoMessages
#default constructor
WriteLog(){}
#constructor
WriteLog($fbn,$ld){
$this.FileBaseName = $fbn
$this.LogDirectory = $ld
}
WriteInformation($MessageData){
$info = $false
if ($this.Preference -eq 'Continue') { $Info = $true }
if ($Info) {
$this.InfoMessages += Write-Information -MessageData $MessageData 6>&1 | Select-Object *
}
if($this.Console){
Write-Information -MessageData $MessageData -InformationAction Continue
}
}
ExportToText(){
$this.InfoMessages |
Select TimeGenerated,MessageData |
Out-File "$($this.LogDirectory)\$($this.FileBaseName).txt" -Encoding utf8 -Force -Append -Width 1000
}
ExportToXML(){
$this.InfoMessages |
Export-Clixml -Path "$($this.LogDirectory)\$($this.FileBaseName).xml" -Encoding UTF8 -Force
}
}

Here’s a quick rundown:

construtors

The idea is to gather output from the Information stream. Constructors are a way of initiating an object for use (Shout out to Doug Finke for breaking this down for me!)

writeloginformation

I “borrowed” Jeff Hicks idea of validating the preference first. This way you can toggle between showing Write-Information and/or Write-Host data. You can use Write-Host for a more colorful experience. Did I already mention that Write-Host is cool again? :-P.  I also added the possibility to show data in the console if you choose to, a verbose action of sorts…

exportlogger

I also added two methods of exporting: text and xml. That’s where LogDirectory and FileBaseName come in. For the text export, I selected TimeGenerated and MessageData for a timeline view (I know I know Jeff Hicks did it first :-P). Export to xml saves InfoMessages in raw format.

Here’s some code to play with.

function Get-PowerShellProcess{
[cmdletbinding()]
param()

Write-Verbose "InformationPreference = $InformationPreference"
$Logger.Preference = $InformationPreference
$Logger.WriteInformation("Starting $($MyInvocation.MyCommand)")

Get-Process -Name *PowerShell*
$Logger.WriteInformation("Finishing $($MyInvocation.MyCommand)")
}

#region Main
#Initiate variable
$Logger = [WriteLog]::New('psProcess','C:\scripts\temp')

#Run with InformationAction. Information will be saved and not shown
Get-PowerShellProcess -InformationAction Continue -Verbose

#Run without InformationAction with Console set to $true
$Logger.Console = $true
Get-PowerShellProcess -Verbose

#Run without InformationAction with Console set to $false
$Logger.Console = $false
Get-PowerShellProcess -Verbose
#endregion

Here are some screenshots of the output

I initiated the object a FileBaseName value ‘psProcess’ and LogDirectory ‘C:\scripts\temp’. Default Preference is ‘SilentContinue’ and no ouput to the console

initiate

First up run with action set to continue

loggerinfoaction

$Logger.InfoMessages has redirected data.

loggerinfomessages

When console is set to true we’ll see the Information message without explicitly setting InformationAction to ‘continue’. Using InformationAction will save the Information data to InfoMessages property.

infostreamconsole

Setting the console preference back to $false stops displaying output to the console

infostreamconsolefalse

I hope that you were able to follow it all. 2016 was all about Operation Validation for me. I think classes are going to be mainstream in 2017! PowerShell + Classes will definitely give you an edge and not to mention make you feel more like a developer! πŸ˜‰

The line between Dev and Ops is being blurred…

Hope it’s worth something to you…

Ttyl,

Urv