Using PowerShell to Migrate DHCP Servers: Part 2

Doctor Scripto

Summary: Microsoft PFE, Ian Farr, continues his quest to use Windows PowerShell to migrate DHCP servers.

Microsoft Scripting Guy, Ed Wilson, is here. Ian Farr is back today. You should read the first part of this series before getting into this one: Using PowerShell to Migrate DHCP Servers: Part  1.

Image of slogan

Episode 2: Return of the Scripter

A scripter’s strength flows from the PowerShell. In Episode 1: A New Hope, we began our scripter training. We imported the configuration from a split-scope DHCP infrastructure on computers running Windows Server 2003 to two DHCP servers running Windows Server 2012, with the aid of the Windows PowerShell-based Windows Server Migration Tools.

We also ran a script to compare the servers running Windows Server 2012, and a report identified any configuration discrepancies. Where appropriate, these were manually corrected. So we completed Steps 1 and 2 of our example migration process:

  1. Transfer the configuration from the DHCP servers running Windows 2003 to two newly-prepared DHCP servers running Windows Server 2012.
  2. Compare the DHCP server configuration on the new servers.
  3. Compare the DHCP scope settings on the DHCP servers running Windows Server 2012.
  4. Configure a load-balanced failover relationship between the DHCP servers running Windows Server 2012.
  5. Activate the two servers running Windows Server 2012 in Active Directory.

Let’s pick-up the training at Step 3…

Step 3: Another comparison script

You’d expect the scope settings in a DHCP split-scope setup to be consistent. Experience tells me that assumptions (and light sabers) are dangerous things, so I wrote another comparison script… you might even call it a sister script!

The PowerShell runs strong in my family. You can download the script here: Compare DHCP Scope Settings with PowerShell DHCP Cmdlets.

This time, the Excel output is different. Rather than a worksheet per check, all the scope related checks are found on the second worksheet. Again, matching objects are presented side-by-side in yellow and turquoise for ease of comparison. Again, specific property discrepancies will have the property name highlighted in red.

Image of worksheet

In this example, as you’d expect with a split-scope setup, the exclusion ranges are different. There’s also a discrepancy with the scope start range. We’ll look at how to correct this in Step 4, where we set up the failover relationship.

Windows PowerShell is similar to the server settings script. There’s a Compare-Multiple objects function and a “control panel” to configure the checks executed by the DHCP cmdlets.

Note  Leave all control panel checks in in place to support migration Step 4.

We start by using the Get-DhcpServerv4Scope cmdlet to obtain a list of scope IDs for both servers:

$DhcpServer1Scopes = (Get-DhcpServerv4Scope -ComputerName $DhcpServer1).ScopeID.IPAddressToString

$DhcpServer2Scopes = (Get-DhcpServerv4Scope -ComputerName $DhcpServer2).ScopeID.IPAddressToString

The checks and the scope IDs are then fed into the Execute-DhcpScopeChecks function:

$DhcpServer1CustomScopeInfo = Execute-DhcpScopeChecks $Checks $DhcpServer1 $DhcpServer1Scopes

$DhcpServer2CustomScopeInfo = Execute-DhcpScopeChecks $Checks $DhcpServer2 $DhcpServer2Scopes

This loops through each scope ID, creating a custom Window PowerShell object for the current scope:

$DhcpScopeInfo = New-Object -TypeName PSObject -Property @{ScopeID = $DhcpServerScope}

For the current scope, each supplied check is executed:

ForEach ($Check in $Checks) {    

            #Create a variable for the check (cmdlet) syntax

            $Command = "Get-$Check"

            #Execute the check

            $CheckResult = &$Command -ScopeId $DHCPServerScope -ComputerName $DhcpServer

A Switch statement then filters on the current check and uses the results to build an ordered hash table. This is added to our $DhcpScopeInfo object. For example, here’s the script for populating the DhcpServerv4Scope hash table:

             Switch ($Check) {               

                ##Process the DhcpServerv4Scope check

                "DhcpServerv4Scope" {

                    #Define properties to be added to the custom scope object

                    $Properties = [Ordered]@{

                        Name = $CheckResult.Name

                        SubnetMask = $CheckResult.SubnetMask.IPAddressToString

                        ScopeStartRange = $CheckResult.StartRange.IPAddressToString

                        ScopeEndRange = $CheckResult.EndRange.IPAddressToString

                        Description = $CheckResult.Description

                        State = $CheckResult.State

                        Type = $CheckResult.Type

                            }   #End of $Properties…                                

                    #Add the new property set to the custom scope object

                    $DhcpScopeInfo | Add-Member -NotePropertyMembers $Properties

                }   #End of "DhcpServerv4Scope"…

At the end of the $Checks loop, the object that contains the results from all the checks for the current scope is added to an array:

[Array]$TotalScopes += $DhcpScopeInfo

After all the scopes are processed, $TotalScopes is returned to the script scope and stored in $DhcpServer1CustomScopeInfo for the first DHCP server, or in $DhcpServer2CustomScopeInfo for the second DHCP server.

With $DhcpServer1CustomScopeInfo and $DhcpServer1CustomScopeInfo populated, we call Compare-MultipleObjects (see Episode 1 for more information).

$Discrepancies = Compare-MultipleObjects $DhcpServer1 $DhcpServer2 $DhcpServer1CustomScopeInfo $DhcpServer2CustomScopeInfo

We now have a comparison report for the two sets of scopes. What to do with the information? Read on…

Step 4: Configure DHCP Failover

You cannot escape your destiny. You must configure a DHCP failover relationship. In this section, we’ll discuss using the Excel report and a final script to set up a load-balanced DHCP server failover relationship. You can execute the discussed commands separately; however, using the script gives you error checking, script logging, progress bars, and much less typing!

Our only hope…

You can download the script here: Configure DHCP Failover Load Balanced Relationship with PowerShell DHCP Cmdlets.

There’s a little pre-execution work…

The Excel output from the scope comparison script should be assessed. If no discrepancies are reported, you’re good to run the script. Ignoring the exclusion ranges, if there’s red, you’ll need to review and perhaps update settings.

The script uses the Excel report and the values in column 2 of sheet 2 (the yellow column) as the “good” desired scope configuration, so you’ll need to make corrections here. Make sure that you stick to the original formatting. This is the most time consuming task in the whole migration process.

Also, look out for scope types that are configured as BootP or Both. You cannot add these to a failover relationship, and any attempt to do so will fail. If they are left in the spreadsheet, the script will convert them to type DHCP, which may be undesirable. The following is an example command to assess the number of BootP and Both scopes:

Get-DHCPServerv4Scope -ComputerName HALODC01 | Where-Object {$_.Type -ne "DhcP"}

Important  If there are BootP or Both scopes that you want to keep, remove these from the spreadsheet, and make sure that three rows are left between the remaining scopes.

One last point…

To avoid the possibility of DNS records for DHCP reservations being removed, the DHCP servers running Windows Server 2012 should remain unauthorized in Active Directory until the last stage of the migration process.

Right! To the script! Here’s the flow of Windows PowerShell:

  1. Create log file
  2. Export DHCP configuration
  3. Convert XLS to CSV
  4. Convert CSV to custom Windows PowerShell objects
  5. Remove scopes
  6. Check remaining scopes
  7. Create new scopes
  8. Create failover relationship
  9. Add scopes to failover relationship
  10. Close log file

Now, the interesting bits…

Export DHCP configuration

Here, we backup each server’s configuration. You’ll notice the export is written to an XML file:

Export-DhcpServer -ComputerName $DhcpServer1 -File "$($ReportBase)_$($DhcpServer1)_DHCP_Export.xml"

Upon completion, a backup file for each server will be in the script’s execution directory.

Convert XLS to CSV

In the Convert-XlsToCsv function, the Excel com object edits and saves the .xls file so we’re left with the contents of column 2, sheet 2.

Of interest here is the $Script:ConfigCSV variable. The $Script: prefix means the data stored in the ConfigCSV variable will be available in the script scope, which, in this instance, is the calling the parent scope.

Note  These scopes have nothing to do with DHCP scopes!

Making the ConfigCSV variable available in the script scope could also be achieved by using New-Variable cmdlet with the Scope parameter. A value of 1 or Script could be used in our failover configuration script. If there was a grandfather scope or a great-grandfather scope, respective values of 2 or 3 could be used to make the variable available at those levels. Powerful stuff! For more information, take a look at about_Scopes with Get-Help.

Convert CSV to custom Windows PowerShell objects

The newly converted CSV file is imported and header information is added with the Header parameter of Import-CSV. This prevents the first line of our data from being used as the header.

$CSV = Import-CSV $ConfigCSV -Header PROPERTY,VALUE

The CSV file line count is obtained with $TotalLineCount. This is used as part of the terminating condition of a Do/Until loop. A counter is also established by using $LineCount . The loop runs until the $LineCount variable is greater than the $TotalLineCount variable. An exact number of CSV lines is processed in a block, which allows each scope and its configuration options to be added to a unique custom Windows PowerShell object. Each scope object is then added to an array of scope objects. Here’s what this section of script looks like:

    #Obtain the number of lines in the CSV file

    $TotalLineCount = $CSV.Count

    #Create a counter for the number of lines processed

    $LineCount = 1

        <#Create custom objects for each scope listed in the CSV file…

        Use Do / Until to process blocks of lines until $TotalLineCount value is reached#>

        Do {   

            #Create a custom object containing the details of each scope

            $Scope = [PSCustomObject]@{

                $CSV[$LineCount -1].PROPERTY = $CSV[$LineCount -1].VALUE           #ScopeID

                $CSV[($LineCount)].PROPERTY = $CSV[($LineCount)].VALUE             #Name

                $CSV[($LineCount + 1)].PROPERTY = $CSV[($LineCount + 1)].VALUE     #SubnetMask

                $CSV[($LineCount + 2)].PROPERTY = $CSV[($LineCount + 2)].VALUE     #ScopeStartRange

                $CSV[($LineCount + 3)].PROPERTY = $CSV[($LineCount + 3)].VALUE     #ScopeEndRange

                $CSV[($LineCount + 9)].PROPERTY = $CSV[($LineCount + 9)].VALUE     #OptionId

                $CSV[($LineCount + 11)].PROPERTY = $CSV[($LineCount + 11)].VALUE   #OptionValue

                $CSV[($LineCount + 12)].PROPERTY = $CSV[($LineCount + 12)].VALUE   #ReservationName

                $CSV[($LineCount + 13)].PROPERTY = $CSV[($LineCount + 13)].VALUE   #ReservationIPAddress

                $CSV[($LineCount + 14)].PROPERTY = $CSV[($LineCount + 14)].VALUE   #ReservationClientId

                $CSV[($LineCount + 15)].PROPERTY = $CSV[($LineCount + 15)].VALUE   #ReservationType

            }   #End of $Scope

            #Add the custom object to an array of custom objects

            [Array]$ScopeObjects += $Scope

            #Increment the line count to accommodate the predefined format of the CSV file

            $LineCount += 20

        }   #End of Do

        #Define the condition the loop runs until

        Until ($LineCount -gt $TotalLineCount)

The $LineCount variable is manipulated to target specific scope information in the $CSV array—that is, $CSV[($LineCount + 12)] equates to ReservationName values. Notice the Property and Value headers are used in the PSCustomObject to construct Key/Value pairs. Finally, $LineCount is incremented by 20 to move to the next block of scope information from the CSV. Not particularly elegant—but like a blaster, it gets the job done.

Note  Removal of lines from the scope sections of the .xls file breaks this logic.

Remove scopes

Next, we call the Remove-Scopes function for each server, passing the array of scopes and configuration options. Each custom scope object is processed as part of a ForEach loop:

Remove-DhcpServerv4Scope -ComputerName $DhcpServer1 -ScopeId $ScopeObject.ScopeID –Force

Check remaining scopes

Unlike the intelligence on the operational status of a new armoured space station, I’m thorough. So, I’ve included a function that checks for scopes left on the servers. The Get-RemainingScopes function runs the Get-DhcpServerv4Scope cmdlet and writes the results to the script log:

$RemainingScopes = Get-DhcpServerv4Scope -ComputerName $DhcpServer1 

Intermission and a short interlude…

DV: How’s your Wookie steak, my lord?

Big E: Chewy, my friend…

The old ones are the best!

Create new scopes

Ah, now for the Create-Scope function. This has a lot of parameters, one for each of the configurable Scope properties:

  • Scope ID
  • Name
  • Subnet Mask
  • Scope Start Range
  • Scope End Range
  • Option ID
  • Option Value
  • Reservation Name
  • Reservation IP Address
  • Reservation Client ID
  • Reservation Type

In the main body of the script, we loop through the array of custom scope objects, and for each object, we populate a set of parameters to splat into the Create-Scope function:

ForEach ($ScopeObject in $ScopeObjects) {

        $Properties = [Ordered]@{

            ScopeID = $ScopeObject.ScopeID

            Name = $ScopeObject.Name

            SubnetMask = $ScopeObject.SubnetMask

            ScopeStartRange = $ScopeObject.ScopeStartRange

            ScopeEndRange = $ScopeObject.ScopeEndRange

            OptionID = $ScopeObject.OptionID

            OptionValue = $ScopeObject.OptionValue

            ReservationName = $ScopeObject.ReservationName

            ReservationIPAddress = $ScopeObject.ReservationIPAddress

            ReservationClientID = $ScopeObject.ReservationClientID

            ReservationType = $ScopeObject.ReservationType

        }   #End of $Properties

$CreateScope = Create-Scope @Properties

Inside the Create-Scope function, we use Add-DhcpServerv4Scope to process the Name, SubnetMask, ScopeStartRange, and ScopeEndRange function parameters:

Add-DhcpServerv4Scope -ComputerName $DhcpServer1 -Name "$Name"

   -SubnetMask "$SubnetMask"

   -StartRange "$ScopeStartRange"

   -EndRange "$ScopeEndRange"

With the scope created, we can now configure any additional scope settings.

First, scope options…

Scope options are made up of an Option ID and an Option Value. The complete list of IDs for a scope is stored as a comma-separated list of values in the current custom scope object:

Image of command output

This string is easily split and tidied up:

            $OptionID = $OptionID -Replace " ", "" -Split ","

For example:

Image of list

The option values are also stored as a single string. This time we have to deal with the fact that some option values are a list of items also separated by a comma, and quotation marks wrap these values. See the highlighted example:

Image of command output

The Split operator accepts a Regular Expression. This particular expression ensures that we split on commas that are NOT found between quotation marks:

            $OptionValue = $OptionValue -Split ',(?=(?:[^"]|"[^"]*")*$)' -Replace "`"", "" 

We then remove the quotation marks with the Replace operator and end up with an array of strings, for example:

Image of command output

Each option ID is looped through and created with Set-DhcpServerv4OptionValue. The counter, $IdCounter , is used to ensure that we associate the correct value string with the correct ID. The option value sequence matches that of the option ID sequence—that is, the first ID corresponds to the first value and so on.

            ForEach ($ID in $OptionID) {

$CurrentOptionValue = ($OptionValue[$IdCounter] -Split ",").TrimStart()

       Set-DhcpServerv4OptionValue -ComputerName $DhcpServer1 -OptionId $ID -ScopeId $ScopeID
       -Value $CurrentOptionValue

The same process is used to create reservations with the aid of the Add-DhcpServerv4Reservation cmdlet. Each reservation is made up of four elements:

  • Reservation Name
  • Reservation IP Address
  • Reservation Client ID
  • Reservation Type

Add-DhcpServerv4Reservation -ComputerName $DhcpServer1 -ScopeId $ScopeID `

                         -Name $Reservation -IPAddress $ReservationIPAddress[$ResCounter] `

                         -ClientId $ReservationClientID[$ResCounter] -Type $ReservationType[$ResCounter]

Create failover relationship

With the scopes in place, we can create the failover relationship. The Create-FailoverRelationship function is called with the first scope from the scope objects array:

Create-FailoverRelationship -FirstScope $ScopeObjects[0].ScopeID

  -FailoverRelationship "$($DhcpServer1)-$($DhcpServer2)-Failover"

The function uses the Add-DhcpServerv4Failover cmdlet to create a load-balanced relationship with Auto State Transition enabled:

     Add-DhcpServerv4Failover -Name $FailoverRelationship -ScopeID $FirstScope -ComputerName $DhcpServer1 `

                     -PartnerServer $DhcpServer2 -AutoStateTransition $True `

                     -SharedSecret $SharedSecret

To create a hot standby relationship, add the ServerRole parameter and specify Active or Standby. In Windows PowerShell 4.0, the Set-DhcpServerv4Failover cmdlet has a Mode parameter to easily switch between HotStandby and LoadBalance failover configurations.

Add scopes to failover relationship

Now to add our scopes to the failover relationship with another function. We’ve already used the first scope from the array to create the relationship, but it needs the Skip parameter of Select-Object to execute:

$ScopeObjects = $ScopeObjects | Select-Object -Skip 1

Next, add each scope with the aid of a ForEach loop and the Add-DhcpServerv4FailoverScope cmdlet:

     ForEach ($ScopeObject in $ScopeObjects) {

        Add-DhcpServerv4FailoverScope -Name $FailoverRelationship -ComputerName $DhcpServer1
       -ScopeId $ScopeObject.ScopeID

And, that’s it for the last of the three scripts. Please remember to check this script’s log before moving onto Step 5 of the migration process. The log is found in the execution directory. You’ll need SMS Trace or CM Trace to view it properly. (For more information, see Log-ScriptEvent Function).

Step 5: Activate the DHCP shield generator

When you’ve investigated and corrected any red from the script log, you’re ready to activate the new DHCP platform in Active Directory. To begin, shuttle down to a forest moon, enter the secret entrance to a data center bunker, and then power down those DHCP servers running Windows Server 2003.

Note  Being able to quickly power on the servers running Windows Server 2003 provides a simple roll-back option.

Next, register the new DHCP servers in Active Directory. Here’s an example command:

            Add-DhcpServerInDc –DnsName –IPAddress

Now, test, test, and test again. When you are happy, deactivate the DHCP servers running Windows Server 2003 in Active Directory and decommission them. Here, you will witness the final destruction of your Windows Server 2003 DHCP Split-Scope alliance…


Some might take issue with the episode numbering. Others may comment on a supposedly missing episode:


I’m hoping that my example automation of a tricky DHCP migration will quiet any grumbling. We’ve used Windows PowerShell to seamlessly update a split-scope platform on DHCP servers running Windows Server 2003 to a resilient, load-balanced DHCP service on servers running Windows Server 2012. As part of the process, DHCP server and scope settings were reviewed and aligned. For a moment, the galaxy was a better place. Now for fireworks and dubious dancing!

Oh…there’s also some bonus material: The new platform can be managed by the PowerShell too! Man, life is good.

Remember, the PowerShell will be with you, always.


Thank you, Ian, for once again sharing your time and knowledge. This has been an excellent two-part series. Join us tomorrow for more cool Windows PowerShell stuff.

I invite you to follow me on Twitter and Facebook. If you have any questions, send email to me at, or post your questions on the Official Scripting Guys Forum. See you tomorrow. Until then, peace.

Ed Wilson, Microsoft Scripting Guy


Discussion is closed.

Feedback usabilla icon