Sunday, November 15, 2015

Native class definition in PowerShell 5 – a real life use case

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

001
$B.Servers

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
        }
    }

No comments:

Post a Comment