With PowerShell 5, we can finally define .Net classes natively in PowerShell. In earlier versions, you would have to do fancy things with custom objects or write a class definition in C# and pipe it into Add-Type as a string.
The first reaction of a lot of PowerShellers out there is, “So what? What can a custom class do for me? Why would I ever need or want to use that?”
A lot of the early chatter about it on the web doesn’t answer that question. Some great PowerShell bloggers out there, like Trevor Sullivan and Ed Wilson, wrote some great blogs about how to do class definition that helped me get started. But nobody explained why. All of the examples I’ve seen out there are generic classes designed to teach concepts about classes, rather than give concrete examples of real life. You can find examples of a car class and a vehicle class and a beer class.
After playing on and off for a few months, I have finally had a few things click, and am starting to see where you can and should use your own custom classes.
Performance
As I’ve blogged about
previously, code inside a class definition runs faster than the same code outside a class definition. PowerShell has a lot of flexibility in how we are allowed to do things. This is great. The cost is that it takes a lot of work and complexities at runtime to be able to handle whatever we throw at it. Because classes are more strictly defined than most PowerShell, not as much complexity is needed in the compiled runtime code, and as a result, it runs faster.
How much faster depends on the specific code. I have had test results all the way from no significant improvement to almost 20 times faster, depending on the code. Some of the test results can be found
here.
Other than for performance, there usually won’t be a reason to use custom classes in your scripts. There are other ways of creating custom objects that are usually easier and perfectly adequate for your needs
Administration
Where classes can come in really handy is actually at the command line and in ad hoc admin scripts. I normally don’t use a PowerShell profile, because I don’t want to accidentally rely on something that exists on my machine in my profile that won’t exist on the production server where this script will run. But for these sorts of classes, I make an exception. These are things I will never use in a script. But they make life at the command line much easier.
I am going to talk through some custom classes that help us support a hypothetical environment where we have a large number of nearly identical groups of servers. In my hypothetical, this is an organization that has a large number of branches, each with the same 8 basic local servers, with only small differences in name and IP. This same concept could be used for supporting a large number of stores, or a large number of heterogeneous development environments, or whatever.
Enum
The first thing I am going to do is define an enumeration. An enumeration is a type, like a class is a type. An enumeration is a list of string values. Each value also has a unique integer value behind the scenes. You can define these integers explicitly if you need them to be something specific. By default, the integers are assigned incrementally starting at zero. The first value will be the default value if the question arises.
My enumeration is a list of the standard server types in each branch. This will be used to define the type of a server as well as provide a list of servers that need to be defined for a given branch. We do this with keyword “Enum”.
001 002 003 004 005 006 007 008 009 010 011
|
enum BranchServerType
{
DC
EXC
SQL
FS1
FS2
AP1
AP2
HV
}
|
Class definition
A class is defined using the keyword “class” followed by the name for the class followed by the definition in curly braces. To base your class on an existing class, follow the class name with a colon and the fully qualified name of the base class. If you omit an explicit base class, System.Object will be the base class.
001 002
|
class BranchServer
{
|
Class properties
First we define the properties. For each property we give it a type and a name. We can also assign a default value.
001 002 003 004
|
[string]$Name
[string]$BranchName
[BranchServerType]$Type
[System.Net.IPAddress]$IPAddress
|
Note that we are using the enumeration we defined above as one of our property types. That property will be limited to one of the choices enumerated in the enumeration.
Static properties are not supported in PowerShell. Script properties are not support within the class keyword structure, but can be added later using Update-Type.
Class constructors
A constructor is a chunk of code that defines how to create a new instance of an object of this type.
We can have more than one constructor defined. Definitions of constructors and methods are called “overloads” because of this capability of overloading them with more than one definition.
The restriction on overloads is that they have to take unique parameters—either a different number of parameters or different types of parameters—so that the system can figure out which definition to use when the constructor is called.
Our first constructor for BranchServer will take a branch name as a string, and a server type as a BranchServerType enumeration. At runtime we will can provide a string or integer for the BranchServerType enumeration, and as long as we won’t have any other constructors with the same number of parameters to confuse it, PowerShell will automatically convert it to the corresponding BranchServerServerType.
We provide the name of the constructor, which is always the same as the name as the class. Then the parameters, if any, in parenthesis, followed by the code definition in curly braces.
001
|
BranchServer( [string]$BranchName, [BranchServerType]$Type )
|
Within a class definition, there is a builtin variable named $This which refers to the object we are currently working on. Within a constructor, $This is the object we are constructing.
We are going to take the two parameters and put them directly into the matching class properties.
001 002 003
|
{
$This.BranchName = $BranchName
$This.Type = $Type
|
Next I am using a Switch command to build the standard name of the server based on the BranchName and BranchServerType and our hypothetical standard naming convention.
001 002 003 004 005 006 007 008 009 010 011 012
|
Switch ( $Type )
{
'DC' { $This.Name = 'SV' + $BranchName + 'DC01' }
'EXC' { $This.Name = 'SV' + $BranchName + 'EXC1' }
'SQL' { $This.Name = 'SV' + $BranchName + 'SQL1' }
'FS1' { $This.Name = 'SV' + $BranchName + 'FS01' }
'FS2' { $This.Name = 'SV' + $BranchName + 'FS02' }
'AP1' { $This.Name = 'SV' + $BranchName + 'AP01' }
'AP2' { $This.Name = 'SV' + $BranchName + 'AP02' }
'HV' { $This.Name = 'SV' + $BranchName + 'HV01' }
}
}
|
Then we define a second constructor. This one takes three parameters, the BranchName and BranchServerType, plus an IP address. Again, at runtime we can provide a string at runtime for the IP address, and PowerShell will automatically convert it to a System.Net.IPAddress object. The constructor definition is the same as the first one, plus another Switch statement to determine the IPAddress for the server based on the first three octets of the IPAddress parameter and a hypothetical standard IP address allocation scheme.
001 002 003 004 005 006 007 008 009 010 011 012 013 014 015 016 017 018 019 020 021 022 023 024 025 026 027 028 029
|
BranchServer( [string]$BranchName, [BranchServerType]$Type, [System.Net.IPAddress]$RootIPAddress )
{
$This.BranchName = $BranchName
$This.Type = $Type
Switch ( $Type )
{
'DC' { $This.Name = 'SV' + $BranchName + 'DC01' }
'EXC' { $This.Name = 'SV' + $BranchName + 'EXC1' }
'SQL' { $This.Name = 'SV' + $BranchName + 'SQL1' }
'FS1' { $This.Name = 'SV' + $BranchName + 'FS01' }
'FS2' { $This.Name = 'SV' + $BranchName + 'FS02' }
'AP1' { $This.Name = 'SV' + $BranchName + 'AP01' }
'AP2' { $This.Name = 'SV' + $BranchName + 'AP02' }
'HV' { $This.Name = 'SV' + $BranchName + 'HV01' }
}
$BaseIPAddress = $RootIPAddress.IPAddressToString.Split( '.' )[0..2] -join '.'
Switch ( $Type )
{
'DC' { $This.IPAddress = $BaseIPAddress + '.1' }
'EXC' { $This.IPAddress = $BaseIPAddress + '.11' }
'SQL' { $This.IPAddress = $BaseIPAddress + '.21' }
'FS1' { $This.IPAddress = $BaseIPAddress + '.31' }
'FS2' { $This.IPAddress = $BaseIPAddress + '.32' }
'AP1' { $This.IPAddress = $BaseIPAddress + '.41' }
'AP2' { $This.IPAddress = $BaseIPAddress + '.42' }
'HV' { $This.IPAddress = $BaseIPAddress + '.101' }
}
}
|
Class methods
Next we will define some methods. Methods are chunks of code built in to a class that usually do something with or to the object itself.
Methods, like constructors can take multiple definitions or “overloads”. (Constructors are basically a special type of method.) The definition syntax is mostly the same as a constructor.
We optionally specify the type of object that the method is going to return. If the method is not going to return anything, we use [void] as the return type. If we omit the return type, [void] is implied.
Then we give it a name, followed by any parameters in parenthesis, followed by the code definition in curly braces.
The first method takes no parameters. It derives the name of the Hyper-V server this server is hosted on based on this server’s name, and runs a Start-VM command against the host.
001 002 003 004 005
|
Start()
{
$HV = $This.Name.Substring( 0, $This.Name.Length - 4 ) + 'HV01'
Start-VM -VMName $This.Name -Server $HV
}
|
A second definition or overload of the same method to allow the caller to specify a –Wait switch.
001 002 003 004 005
|
Start( [boolean]$Wait )
{
$HV = $This.Name.Substring( 0, $This.Name.Length - 4 ) + 'HV01'
Start-VM -VMName $This.Name -Server $HV -Wait:$Wait
}
|
A method to stop the server.
001 002 003 004
|
Stop()
{
Stop-Computer -ComputerName $This.Name
}
|
A method to restart the server.
001 002 003 004
|
Restart()
{
Restart-Computer -ComputerName $This.Name
}
|
A method to launch an RDP connection to the server.
001 002 003 004
|
RDP()
{
mstsc /v:$($This.Name)
}
|
And last, a method to enter a PSSession to the server.
001 002 003 004 005
|
PSSession()
{
Enter-PSSession -ComputerName $This.Name
}
}
|
Another class
Now let’s build a branch. A branch has a name, and an array containing a bunch of servers.
001 002 003 004
|
class Branch
{
[string]$Name
[BranchServer[]]$Servers
|
Our first constructor simply takes a branch name. It loops through the enumaration of BranchServerTypes, and calls the constructor for BranchServer for each of them, resulting in an array of all 8 servers in the branch.
001 002 003 004 005 006 007 008
|
Branch ( [string]$Name )
{
$This.Name = $Name
ForEach ( $Type in [BranchServerType].GetEnumValues() )
{
$This.Servers += [BranchServer]::New( $Name, $Type )
}
}
|
A second constructor does the same thing, but includes IP addresses.
001 002 003 004 005 006 007 008
|
Branch ( [string]$Name, [System.Net.IPAddress]$RootIPAddress )
{
$This.Name = $Name
ForEach ( $Type in [BranchServerType].GetEnumValues() )
{
$This.Servers += [BranchServer]::New( $Name, $Type, $RootIPAddress )
}
}
|
A method to return the server of a given type. So I can ask the Branch object for the SQL server, and it will give me the BranchServer object for the SQL server in that branch. The [BranchServer] type appears before the method name because that is the type of object the method returns.
001 002 003 004
|
[BranchServer] Server ( [BranchServerType]$Type )
{
Return ( $This.Servers | Where Type -eq $Type )
}
|
And then finally, just to show how fancy we can get, a method to restart the host. This method gets all of the running servers, and just for fun since we are in PowerShell 5 anyway, uses some fancy syntax to split them into domain controllers, SQL servers, and other servers. It suspends the other servers, then the SQL servers and then the domain controllers. Then it restarts the host. Then is starts the domain controllers, and then the SQL servers, and then the remaining servers.
Please note: I know this method may not work exactly as intended. It’s a demo draft of a hypothetical scenario. Plus, if this were going to be used in real life, I would add a lot more error handling and idiot handling. This is just to give you an idea of what’s possible.
001 002 003 004 005 006 007 008 009 010 011 012 013 014
|
RestartHost ()
{
$HVHost = ( $This.Servers | Where Type -eq 'HV' ).Name
$DCVM, $OtherVM = ( Get-VM -Server $HVHost ).Where{ $_.State -eq 'Running' }.Where( { $_.Name -like "*DC*" }, 'Split' )
$SQLVM, $OtherVM = $OtherVM.Where( { $_.Name -like "*SQL*" }, 'split' )
$OtherVM | Suspend-VM
$SQLVM | Suspend-VM
$DCVM | Suspend-VM
Restart-Computer $HVHost -Wait -For PowerShell
$DCVM | Start-VM
$SQLVM | Start-VM
$OtherVM | Start-VM
}
}
|
Using the objects based on our defined classes
After running those definitions, using them is easy. We can create a [Branch] object, fully populated with all the information about every server in the branch, with a single line.
001
|
$B = [Branch]::New( 'B123', '10.1.2.0' )
|
To see a full list of the servers in the branch, we simply use the .Servers property
Launching an RDP session to the branch domain controller is as easy as this.
001
|
$B.Server( 'DC' ).RDP()
|
And to enter a remote PSSession on the branch app server 1, we do it like this.
001
|
$B.Server( 'AP1' ).Restart()
|
Or we can do it without creating the object first. We just type this at the command line.
001
|
([Branch]'B234').Server('SQL').PSSession()
|
The full script defining the custom enum and classes
001 002 003 004 005 006 007 008 009 010 011 012 013 014 015 016 017 018 019 020 021 022 023 024 025 026 027 028 029 030 031 032 033 034 035 036 037 038 039 040 041 042 043 044 045 046 047 048 049 050 051 052 053 054 055 056 057 058 059 060 061 062 063 064 065 066 067 068 069 070 071 072 073 074 075 076 077 078 079 080 081 082 083 084 085 086 087 088 089 090 091 092 093 094 095 096 097 098 099 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141
|
enum BranchServerType
{
DC
EXC
SQL
FS1
FS2
AP1
AP2
HV
}
class BranchServer
{
[string]$Name
[string]$BranchName
[BranchServerType]$Type
[System.Net.IPAddress]$IPAddress
BranchServer( [string]$BranchName, [BranchServerType]$Type )
{
$This.BranchName = $BranchName
$This.Type = $Type
Switch ( $Type )
{
'DC' { $This.Name = 'SV' + $BranchName + 'DC01' }
'EXC' { $This.Name = 'SV' + $BranchName + 'EXC1' }
'SQL' { $This.Name = 'SV' + $BranchName + 'SQL1' }
'FS1' { $This.Name = 'SV' + $BranchName + 'FS01' }
'FS2' { $This.Name = 'SV' + $BranchName + 'FS02' }
'AP1' { $This.Name = 'SV' + $BranchName + 'AP01' }
'AP2' { $This.Name = 'SV' + $BranchName + 'AP02' }
'HV' { $This.Name = 'SV' + $BranchName + 'HV01' }
}
}
BranchServer( [string]$BranchName, [BranchServerType]$Type, [System.Net.IPAddress]$RootIPAddress )
{
$This.BranchName = $BranchName
$This.Type = $Type
Switch ( $Type )
{
'DC' { $This.Name = 'SV' + $BranchName + 'DC01' }
'EXC' { $This.Name = 'SV' + $BranchName + 'EXC1' }
'SQL' { $This.Name = 'SV' + $BranchName + 'SQL1' }
'FS1' { $This.Name = 'SV' + $BranchName + 'FS01' }
'FS2' { $This.Name = 'SV' + $BranchName + 'FS02' }
'AP1' { $This.Name = 'SV' + $BranchName + 'AP01' }
'AP2' { $This.Name = 'SV' + $BranchName + 'AP02' }
'HV' { $This.Name = 'SV' + $BranchName + 'HV01' }
}
$BaseIPAddress = $RootIPAddress.IPAddressToString.Split( '.' )[0..2] -join '.'
Switch ( $Type )
{
'DC' { $This.IPAddress = $BaseIPAddress + '.1' }
'EXC' { $This.IPAddress = $BaseIPAddress + '.11' }
'SQL' { $This.IPAddress = $BaseIPAddress + '.21' }
'FS1' { $This.IPAddress = $BaseIPAddress + '.31' }
'FS2' { $This.IPAddress = $BaseIPAddress + '.32' }
'AP1' { $This.IPAddress = $BaseIPAddress + '.41' }
'AP2' { $This.IPAddress = $BaseIPAddress + '.42' }
'HV' { $This.IPAddress = $BaseIPAddress + '.101' }
}
}
Start()
{
$HV = $This.Name.Substring( 0, $This.Name.Length - 4 ) + 'HV01'
Start-VM -VMName $This.Name -Server $HV
}
Start( [boolean]$Wait )
{
$HV = $This.Name.Substring( 0, $This.Name.Length - 4 ) + 'HV01'
Start-VM -VMName $This.Name -Server $HV -Wait:$Wait
}
Stop()
{
Stop-Computer -ComputerName $This.Name
}
Restart()
{
Restart-Computer -ComputerName $This.Name
}
RDP()
{
mstsc /v:$($This.Name)
}
PSSession()
{
Enter-PSSession -ComputerName $This.Name
}
}
class Branch
{
[string]$Name
[BranchServer[]]$Servers
Branch ( [string]$Name )
{
$This.Name = $Name
ForEach ( $Type in [BranchServerType].GetEnumValues() )
{
$This.Servers += [BranchServer]::New( $Name, $Type )
}
}
Branch ( [string]$Name, [System.Net.IPAddress]$RootIPAddress )
{
$This.Name = $Name
ForEach ( $Type in [BranchServerType].GetEnumValues() )
{
$This.Servers += [BranchServer]::New( $Name, $Type, $RootIPAddress )
}
}
[BranchServer] Server ( [BranchServerType]$Type )
{
Return ( $This.Servers | Where Type -eq $Type )
}
RestartHost ()
{
$HVHost = ( $This.Servers | Where Type -eq 'HV' ).Name
$DCVM, $OtherVM = ( Get-VM -Server $HVHost ).Where{ $_.State -eq 'Running' }.Where( { $_.Name -like "*DC*" }, 'Split' )
$SQLVM, $OtherVM = $OtherVM.Where( { $_.Name -like "*SQL*" }, 'split' )
$OtherVM | Suspend-VM
$SQLVM | Suspend-VM
$DCVM | Suspend-VM
Restart-Computer $HVHost -Wait -For PowerShell
$DCVM | Start-VM
$SQLVM | Start-VM
$OtherVM | Start-VM
}
}
|