2013 Scripting Games Event 6

Instructions for the Event 6, entitled “The Core Configurator”:

Reading the instructions made me realize that I need a test lab.

  • Building the test lab in 60 minutes
  • Here’s how I quickly built a lab from scratch with a PC running a 4-cores processor and 16GB memory.
    To achieve this so fast, I already had an ISO of windows Server 2012 and just downloaded the VM build kit that Thomas Lee realeased on his blog. He wrote the following articles about Building a Hyper-V Test Lab on Windows 8:

    Basically, here are the steps that I followed to get that test lab environment:

    • Boot the ISO image extracted on a bootable USB stick and install a Windows 2012 server Standard edition with a GUI. (I already had it)
    • Install Hyper-V
    • Install-WindowsFeature -Name Hyper-V -Restart -IncludeManagementTools -WhatIf
    • Grab the kit and extract it
    • iwr 'http://www.reskit.net/powershell/vmbuild.zip' -OutFile E:\vmbuild.zip
    • Setup an Internal VM switch
    • New-VMSwitch -Name Internal -SwitchType Internal -WhatIf
    • Change the code execution policy as I’ll run scripts from the VM build kit
    • Set-ExecutionPolicy RemoteSigned
    • Edit unattend.xml and Create-ReferenceVHDX.ps1 from the extracted VM build kit, specify, the Windows Server 2012 ISO file location and change the password
    • Run Create-ReferenceVHDX.ps1

    • (Yes, it took 7 minutes to build the parent partition for virtual machines).

    • Edit Create-VM.ps1

    • Run Create-VM.ps1 to create the non domain joined VM named DC1
    • Edit Configure-DC1-1.ps1 to change passwords and run it
    • I have had an error saying that
      [DC1] Connecting to remote server DC1 failed with the following error message : The WinRM client cannot process the request. If the authentication scheme is different from Kerberos, or if the client computer is not joined to a domain, then HTTPS transport must be used or the destination machine must be added to the TrustedHosts configuration setting. Use winrm.cmd to configure TrustedHosts. Note that computers in the TrustedHosts list might not be authenticated. You can get more information about that by running the following command: winrm help config. For more information, see the about_Remote_Troubleshooting Help topic.
      I fixed it quickly by typing this and finally re-run Configure-DC1-1.ps1 without any issue

      winrm --% set winrm/config/client @{TrustedHosts="DC1"}

      My Last step consisted in setting up a static IP Address on the Internal Hyper-V VM switch:

      # Set a static IP address on Hyper-V switch            
      Get-NetAdapter |             
       ? InterfaceDescription -match "Hyper-V Virtual Ethernet Adapter" |            
       New-NetIPAddress -IPAddress 10.0.0.1 -PrefixLength 24            
      Get-NetAdapter |             
       ? InterfaceDescription -match "Hyper-V Virtual Ethernet Adapter" |            
       Set-DnsClientServerAddress -ServerAddresses 10.0.0.10            
      

    Again, thanks to the awsome work of Thomas Lee and Niklas Goude, I have a virtual machine named DC1, built from a parent reference disk (refWS2012.vhdx), running Windows Server 2012 Standard Core Edition and that it’s the Primary Domain Controller of the reskit.org Active Directory forest and domain. I didn’t need to install a DHCP server as Configure-DC1-1.ps1 already installed and configured it for the 10.0.0.0 scope 😎 Cool, isn’t it?

    Now to get in the conditions specified in the instructions and get some random workgroup based virtual machines, I duplicated and edited the following files from the VM build kit: unattend.xml and create-VM.ps1. Inside the xml file, I removed the XML section where VM get a static IP Address:

    and just left this:

    In my Create-VMEVT6.ps1, I commented lines 69 to 80 where a static IP Address is written in the XML unattend answer file.
    I also defined a new unattend XML template:

    $UnaEVT6 = 'E:\vmbuild\UnAttendEVT6.xml'

    And I finally added the following 4 lines.

    At this point, I have a test lab running with 3 non domain joined virtual machines.
    To make sure the DHCP was running correctly and that the changes I made are effective, I typed at command prompt on DC1:

    Get-DHCPServerv4Scope -ScopeId 10.0.0.0 | Get-DHCPServerv4Lease
  • Writing the tool to solve Event 6
  • I tried to import the DHCP cmdlets from DC1 on the Hyper-V server where the tool should run. I used the following code successfully at command prompt:

    # Import the DHCP cmdlets from the VM DC1            
    try {            
        $session = New-PSSession -ComputerName $DomainController -Authentication Kerberos -Credential $DomainCredential -ErrorAction Stop            
        Import-PSSession -Module DHCPServer -Prefix PSDC -Session  $session -ErrorAction Stop | Out-Null            
    } catch {            
        Write-Warning "Failed to open a PS remote session on domain controller and import DHCP cmdlets"            
        break            
    }

    But for some reasons, it failed when being run inside the script with the following error:
    WARNING: Failed to open PS Remote Session on domain controller DC1 and import DHCP cmdlets because Attribute cannot be
    added because it would cause the variable MacAddress with value to become invalid.


    So, I found a workaround for this as I hadn’t enough time to figure out why and how to fix the issue.

    Again, as the Hyper-V is not joined to a domain, I needed to add the IP address of virtual machines to the trusted hosts list of the Hyper-V WinRM client.

    As the instructions states that:

    You can safely assume that SERVER1, SERVER2, etc. do not already exist in the domain.

    I first went with the following code:

    # Join it to the domain            
    Invoke-Command -ComputerName $_.IPAddress -Credential $credVM -ScriptBlock {            
        $HT = @{            
            NewName = $Using:NewName ;            
            Credential = $Using:DomainCredential ;            
            DomainName = $Using:DomainName ;            
            Force   = $true ;            
            Restart = $true ;            
        }            
        try {            
            Add-Computer @HT -ErrorAction Stop            
            Write-Verbose "Computer $Using:NewName successfully joined the domain $Using:DomainName" -Verbose            
        } catch {            
            Write-Warning "Failed to join the domain because $($_.Exception.Message)"            
        }            
    } # end of scriptblock            
    



    I encountered the following errors with the above code: The account already exists and The directory service is busy.
    The first one is expected as the Add-Computer cmdlet only handles a limited subset of Fjoin options flags. The JoinWithNewName flag is automatically added to the Add-computer cmdlet when you use it with the -NewName parameter. However, this is not enough to overwrite an existing account in Active Directory with the same name.
    For the second error, I don’t have any idea why it fails. I hadn’t enough time to figure out why and how to fix it.

    As the instructions also state that:

    You can assume none of the server names exist on the network (if they do, it’s not your fault if something breaks).
    […]
    It’s fine if you do this for one computer at a time, but when your script finishes running all of the computers must be properly provisioned.

    I changed my mind about the Add-Computer and switched over a slower but more reliable solution based on WMI. The WMI JoinDomainOrWorkgroup method of the Win32_ComputerSystem class allows to specify all the Fjoin options flags listed on this MSDN page: http://msdn.microsoft.com/en-us/library/aa370433%28VS.85%29.aspx

    The last thing I added to my script is a validation script about the prefix name of the computers. SamAccountName are limited to 15 characters and a single Hyper-V server running on Windows Server 2012 can host a maximum of 1024 active virtual machines. I decided to validate the length and check for invalid characters like this:

    [Parameter()]
    [ValidateNotNullOrEmpty()]
    [ValidateScript({
        if (($_ -match '[\"/\\\[\]:;\|,\+\*\?\<\>]') -or ($_.Length -gt 11)) {
            $ErrorMsg = "Invalid name: either length greater than 11 characters or invalid character(s) being used"
            throw (New-Object System.ArgumentException -ArgumentList $ErrorMsg)
        }
        $true
    })]
    [string]$ComputerNamePrefix='SERVER'
    

    This also throws a nicely formatted error:

    Last thing, I wanted to allow MAC Addresses in the following 3 formats: either colon or dash separated or with no separator at all.
    So I used the following regular expression to validate these 3 formats.

    [Parameter(Mandatory,ValueFromPipeline)]            
    [ValidatePattern("^([0-9A-F]{2}[:\-]?){5}([0-9A-F]{2})$")]            
    [string[]]$MacAddress

    And I’ve added the following in the PROCESS bloc because MAC addresses collected from the DHCP server have no separator:

    Process {            
        # Process each MAC Address            
        $MacAddress | ForEach-Object {            
            Switch -Regex ($_) {            
                "-" {            
                    $MacAddressWL += $_ -replace "-","" ;            
                    break ;            
                }            
                ":" {            
                    $MacAddressWL += $_ -replace ":","" ;            
                    break ;            
                }            
                default {            
                    $MacAddressWL += $_            
                }            
            }            
        }            
    }
  • The entry I submitted for this event: http://scriptinggames.org/entrylist_.php?entryid=1137
  • #Requires -Version 3
    #Requires -Modules Hyper-V
    
    Function Add-VMToDomain {
    <#
    .SYNOPSIS
        Adds virtual machines to the domain based on dynamic IP Addresses from the DHCP server.
    
    .DESCRIPTION
        Adds virtual machines to the domain based on dynamic IP Addresses from the DHCP server.
            
    .PARAMETER MacAddress
        Array of MAC Addresses of target virtual machines. 
        Can be either comma or dash separated. No separator is also allowed
    
    .PARAMETER DomainName
        The netbios name of the target active directory domain to join
    
    .PARAMETER DomainFQDN
        The fully qualified domain name to join
    
    .PARAMETER DHCPServer
        The netbios name of the DHCP server.
    
    .PARAMETER DomainAdminAccountName
        The name of a privileged account able to join multiple computers to the domain
    
    .PARAMETER DomainAdminPassword
        String that represents the password of the domain account used to join computers to the domain.
    
    .PARAMETER LocalAdminAccountName
        The name of a local account on the virtual machines, member of the local administrators group.
    
    .PARAMETER LocalAdminPassword
        String that represents the password of the local account used to connect to virtual machines.
    
    .PARAMETER DHCPv4Scope
        Name of the DHCP scope of IPV4 addresses 
    
    .PARAMETER ComputerNamePrefix
        Prefix to rename computers before they join the domain. 
    
    .EXAMPLE
        Add-VMToDomain -MacAddress "00155DCE0001","00:15:5D:CE:00:02","00-15-5D-CE-00-03"
    
    .EXAMPLE
        Get-Content .\MAC.txt | Add-VMToDomain -Verbose -DHCPServer DC1
    
    .NOTES
        Virtual machines being provisionned are running Windows 2012 Core edition and have PSremoting enabled (by default).
        The DHCP server is running on a Windows 2012 Server.
        Prefix for naming computers is limited to 11 characters because Hyper-V 2012 can host a maximum of 1024 active VMs.
        The Hyper-V server doesn't belong to an Active Directory domain, that's why the use of WSMan trusted hosts is required.
        Using WMI method instead of Add-Computer to be able to specify the NETSETUP_JOIN_WITH_NEW_NAME (0x400) join option flag.
    
    .LINK
        http://msdn.microsoft.com/en-us/library/aa370433%28VS.85%29.aspx
        http://msdn.microsoft.com/en-us/library/aa392154%28VS.85%29.aspx
    
    .OUTPUTS
        None
    
    .INPUTS
        [string[]]
    
    #>
    [CmdletBinding()]
    Param(
    [Parameter(Mandatory,ValueFromPipeline)]
    [ValidatePattern("^([0-9A-F]{2}[:\-]?){5}([0-9A-F]{2})$")]
    [string[]]$MacAddress,
    
    [Parameter()]
    [ValidateNotNullOrEmpty()]
    [string]$DomainName='RESKIT',
    
    [Parameter()]
    [ValidateNotNullOrEmpty()]
    [string]$DomainFQDN ='Reskit.org',
    
    [Parameter()]
    [ValidateNotNullOrEmpty()]
    [string]$DHCPServer='DHCP1',
    
    [Parameter()]
    [ValidateNotNullOrEmpty()]
    [string]$DomainAdminAccountName="administrator",
    
    [Parameter()]
    [ValidateNotNullOrEmpty()]
    [string]$DomainAdminPassword='P@ssw0rd',
    
    [Parameter()]
    [ValidateNotNullOrEmpty()]
    [string]$LocalAdminAccountName='administrator',
    
    [Parameter()]
    [ValidateNotNullOrEmpty()]
    [string]$LocalAdminPassword='Pa$$w0rd',
    
    [Parameter()]
    [ValidateNotNullOrEmpty()]
    [string]$DHCPv4Scope =  "10.0.0.0",
    
    [Parameter()]
    [ValidateNotNullOrEmpty()]
    [ValidateScript({
        if (($_ -match '[\"/\\\[\]:;\|,\+\*\?\<\>]') -or ($_.Length -gt 11)) {
            $ErrorMsg = "Invalid name: either length greater than 11 characters or invalid character(s) being used"
            throw (New-Object System.ArgumentException -ArgumentList $ErrorMsg)
        }
        $true
    })]
    [string]$ComputerNamePrefix='SERVER'
    )
    Begin {
    
        # Save previous trusted hosts
        $PrevTrustedHosts = (Get-Item WSMan:\localhost\Client\TrustedHosts).Value
    
        # Add the DHCP of the VM to the trustedhosts
        if ($PrevTrustedHosts) {
            $TrustedHosts =  $PrevTrustedHosts,$DHCPServer -join ","
        } else {
            $TrustedHosts = $DHCPServer
        }
        Set-Item WSMan:\localhost\Client\TrustedHosts –Value $TrustedHosts -Force
    
        $DomPwd = ConvertTo-SecureString -String  $DomainAdminPassword -AsPlainText -Force
        $DomainCredential = New-Object -Typename PSCredential -Argumentlist "$DomainName\$DomainAdminAccountName",$DomPwd
    
        $DHCPLeaseAr = @(Invoke-Command -ComputerName $DHCPServer -Credential $DomainCredential -ScriptBlock {
            try {
                Import-Module -Name DHCPServer -Scope Local -Prefix PSDC -ErrorAction Stop
            } catch {
                Write-Warning "Failed to import DHCP module on server $Using:DHCPServer"
                break
            }
            Get-PSDCDhcpServerv4Lease -ScopeId $Using:DHCPv4Scope
        })
    
        # Restore trusted hosts
        Set-item WSMan:\localhost\Client\TrustedHosts –Value $PrevTrustedHosts -Force
    
        $MacAddressWL = @()
    }
    Process {
        # Process each MAC Address
        $MacAddress | ForEach-Object {
            Switch -Regex ($_) {
                "-" {
                    $MacAddressWL += $_ -replace "-","" ;
                    break ;
                }
                ":" {
                    $MacAddressWL += $_ -replace ":","" ;
                    break ;
                }
                default {
                    $MacAddressWL += $_
                }
            }
        }
    }
    End {
        if ($MacAddressWL) {
            # Hyper-V can give us the full list of running VMs and their IPAdresses filtered with our MAC addresses
            $TargetVMIPfromHV = @()
            $TargetVMIPfromHV += (Get-VM | Where-Object { 
                $_.State -eq 'Running' -and
                $_.NetworkAdapters.MacAddress -in $MacAddressWL
            }).NetworkAdapters.IPAddresses
    
            # Enum the IPAddresses from the DHCP filtered with our MAC addresses
            $TargetVMfromDHCP = $DHCPLeaseAr | Where-Object {
                $_.AddressState  -eq 'Active'
                $_.ClientId -in $MacAddressWL
            } | Select IPAddress,
                @{l='HostName'  ;e={($_.HostName -split '\.')[0]}},
                @{l='MacAddress';e={$_.ClientId -replace '\-',''}}
    
            # Make sure we can access the VM over PSRemoting (WSman)
            $VM = @()
            $TargetVMfromDHCP | Where-Object { $_.IPAddress -in $TargetVMIPfromHV } | 
            ForEach-Object -Process {
                $Target = $_
                try {
                    Test-WSMan -ComputerName $_.IPAddress -ErrorAction Stop | Out-Null
                    Write-Verbose "Adding $($_.IPAddress) to the array of VMs to provision" 
                    $VM += $Target 
                } catch {
                    Write-Warning "IP $($Target.IPAddress) from DHCP scope doesn't seem to be currently in use"
                }
            }
    
            $Count = 1
            $VM | ForEach-Object -Process {
                
                $VMHost = $_
    
                # New name of the VM
                $NewName = "$ComputerNamePrefix$Count"
            
                # Build credentials to access the VM over PSRemoting
                $LocalPwd = ConvertTo-SecureString -String $LocalAdminPassword -AsPlainText -Force
                $CredVM = New-Object  PSCredential -ArgumentList ("$($VMHost.HostName)\$LocalAdminAccountName",$LocalPwd)
    
                # Add the targetIP of the VM to the trustedhost
                if ($PrevTrustedHosts) {
                    $TrustedHosts =  $PrevTrustedHosts,$($VMHost.IPAddress) -join ","
                } else {
                    $TrustedHosts = $($VMHost.IPAddress)
                }
                Set-Item WSMan:\localhost\Client\TrustedHosts –Value $TrustedHosts -Force
    
                # Join it to the domain
                try {
                    Invoke-Command -ComputerName $VMHost.IPAddress -Credential $CredVM -ErrorAction Stop -ScriptBlock {
    
                        # 1. Rename w/o reboot
                        try {
                            Rename-Computer -NewName $Using:NewName -Restart:$false -Force -ErrorAction Stop
                        } catch {
                            Write-Warning "Failed to rename the computer because $($_.Exception.Message)"
                        }
                        # 2. Join with the new name using FJoinOptions: 0x1 + 0x2 + 0x20 + 0x400
                        try {
                            $result = (Get-WMIObject -Class Win32_ComputerSystem).JoinDomainOrWorkGroup(
                                $Using:DomainFQDN,$Using:DomainAdminPassword,
                                "$($Using:DomainName)\$($Using:DomainAdminAccountName)",$null,(1+2+32+1024)
                            )
                            Write-Verbose "Computer $Using:NewName successfully joined the domain $Using:DomainName" -Verbose
                        } catch {
                            Write-Warning "Computer $Using:NewName failed to join the domain because $($_.Exception.Message)"
                        
                        }
                        if ($result.ReturnValue -eq 0 ) {
                            Write-Verbose "Restarting computer $Using:NewName" -Verbose
                            Restart-Computer -Force
                        } else {
                            Write-Warning "Computer $Using:NewName failed to join the domain and WMI return code is: $($result.ReturnValue)"
                        }
                    } # end of scriptblock
                } catch {
                    Write-Warning "Failed to invoke commands on $($VMHost.IPAddress) because $($_.Exception.Message)"
                    continue
                }
                $Count++
    
            } # end of foreach
        }
    
        # Restore trusted hosts
        Set-item WSMan:\localhost\Client\TrustedHosts –Value $PrevTrustedHosts -Force
    }
    } # end of function
    
    Advertisements

    One thought on “2013 Scripting Games Event 6

    Leave a Reply

    Fill in your details below or click an icon to log in:

    WordPress.com Logo

    You are commenting using your WordPress.com account. Log Out / Change )

    Twitter picture

    You are commenting using your Twitter account. Log Out / Change )

    Facebook photo

    You are commenting using your Facebook account. Log Out / Change )

    Google+ photo

    You are commenting using your Google+ account. Log Out / Change )

    Connecting to %s