The case of Internet Explorer missing

I’ve updated recently a test server from Windows 2012 datacenter core edition to Windows 2012 R2.
Although I chose the GUI version during the upgrade, it didn’t change anything.

After the upgrade I moved from my core edition to the GUI version by following my previous article on “Switching from Windows 2012 Core to GUI mode…hands on!

I got my Start button back 🙂 but Internet explorer 11 still appeared to be missing although I restored all the GUI features 😦

(Get-WindowsFeature) | ? Name -match "internet"

Internet Explorer 11 on Windows 2012 R2 isn’t a feature or a role:

It’s actually an optional feature:

Get-WindowsOptionalFeature -Online | ? "FeatureName" -match "internet"

To restore Internet Explorer 11, I did

Get-WindowsOptionalFeature -Online |             
 ? "FeatureName" -match "Internet-Explorer-Optional-amd64" |            
 Enable-WindowsOptionalFeature -NoRestart:$true -Verbose -Online

Defeat the new Oracle Java Runtime(JRE) 1.7 update notification mechanism

You may have already seen this popup when your Oracle Java Runtime (JRE) 1.7 is out-of-date.

Your administrator may have already turned off the first mecanism of update using group policies to prevent update notifications’ balloons to pop out in the system tray.

Guess what! Oracle introduced a 2nd update notification mechanism since version 1.7_10 that phones home to check whether it’s up-to-date or expired.
If it’s unable to phone home, the expiry date is set using an hardcoded value that follows the CPU (Critical Patch Update) lifecycle. 😦

JRE Expiration Date
The JRE relies on periodic checks with an Oracle Server to determine if it (the JRE)is still considered up-to-date with all the available security fixes (above the security baseline). In the past, if the JRE was unable to contact the Oracle Server, it continued to behave as though it is still the most recent version with regard to security, for an indefinite period.
To avoid this problem, a secondary mechanism, that does not rely on external communication, has been added to the JDK 7u10. From this release onwards, all JREs will contain a hard-coded expiration date. The expiration date is calculated to end after the scheduled release of the next Critical Patch Update.
This means that JREs that are unable to contact Oracle Servers for an extended period of time, will now start offering additional protection after a reasonable period, and will not continue to behave as if they were still up-to-date with security fixes.

Sources: http://docs.oracle.com/javase/7/docs/technotes/guides/jweb/client-security.html#secure and http://www.oracle.com/technetwork/java/javase/7u10-relnotes-1880995.html

No, you’re not dreaming. We’re in 2013 and software developpers at Oracle launched this security awareness campaign using the above pop-up 😉
The problem with this new feature is that Oracle didn’t provide guidance on how to turn off this notification in a corporate environment.
You can of course click “Later” and tick “don’t ask again until next update” during a certain period of time. This will write some values in the registry and the current user deployment.properties file located in “%userprofile%AppData\LocalLow\Sun\Java\Deployment”. (Note that registry settings take precedence over the deployment.properties file content)

Worse, when the expiry date is reached, the pop-up doesn’t rely anymore on the above registry settings.
They are simply ignored and you start being nagged by this pop-up every time an applet is loaded by the browser.

Here’s what I asked on twitter to Donald Smith who is a member of the Oracle Java SE PM team:

The current situation is that we, I mean corporate administrators, have great hopes and may expect the JRE to really take fully advantages of Group Policies in a close future.

Source: http://www.symantec.com/connect/forums/how-everyone-addressing-forced-java-dialog-java-update-needed-your-java-version-insecure

As you can see, Donald was very kind and answered my questions very quickly.

But, as a corporate administrator, I don’t deploy and manage software with hope.

Now, let’s see how to defeat this new update notification feature.

The idea is simple. I’ll just prevent the DLL file responsible for these notifications from being loaded in Internet Explorer.

To achieve this, I need to identify the GUID (Global Unique identifiers) of the component and set its kill-bit value to COMPAT_EVIL_DONT_LOAD.

Let me reintroduce 3 small helper functions I wrote last year and that I revisited:

Function Get-KillBit {
[CmdletBinding()]
param(
[Parameter(Mandatory=$true,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
[ValidatePattern('^\{[A-Z0-9]{4}([A-Z0-9]{4}-){4}[A-Z0-9]{12}\}$')]
[string[]]$GUID
)
Begin{
    $x86='Software\Wow6432Node'
    $x64='Software'
    $FLAGS = DATA {                        
        ConvertFrom-StringData @'
            1=COMPAT_AGGREGATE
            2=COMPAT_NO_OBJECTSAFETY
            4=COMPAT_NO_PROPNOTIFYSINK
            8=COMPAT_SEND_SHOW
            16=COMPAT_SEND_HIDE
            32=COMPAT_ALWAYS_INPLACEACTIVATE
            64=COMPAT_NO_SETEXTENT
            128=COMPAT_NO_UIACTIVATE
            256=COMPAT_NO_QUICKACTIVATE
            512=COMPAT_NO_BINDF_OFFLINEOPERATION
            1024=COMPAT_EVIL_DONT_LOAD
            2048=COMPAT_PROGSINK_UNTIL_ACTIVATED
            4096=COMPAT_USE_PROPBAG_AND_STREAM
            8192=COMPAT_DISABLEWINDOWLESS
            16384=COMPAT_SETWINDOWRGN
            32768=COMPAT_PRINTPLUGINSITE
            65536=COMPAT_INPLACEACTIVATEEVENWHENINVISIBLE
            131072=COMPAT_NEVERFOCUSSABLE
            262144=COMPAT_ALWAYSDEFERSETWINDOWRGN
            524288=COMPAT_INPLACEACTIVATESYNCHRONOUSLY
            1048576=COMPAT_NEEDSZEROBASEDDRAWRECT
            2097152=COMPAT_HWNDPRIVATE
            4194304=COMPAT_SECURITYCHECKONREDIRECT
            8388608=COMPAT_SAFEFOR_LOADING
'@        
    }
}
Process{
    $GUID | ForEach-Object -Process {
        $GUIDitem = $_
        Write-Verbose "Testing GUID $GUIDitem"
        $x86,$x64 | ForEach-Object {
            Write-Verbose "Testing Hive $_"
            $RegPath = $_
            $flag = $null
            if (Test-Path "HKLM:\$RegPath\Microsoft\Internet Explorer\ActiveX Compatibility\$GUIDitem") {
                try {
                    $flag = Get-ItemProperty -Path "HKLM:\$RegPath\Microsoft\Internet Explorer\ActiveX Compatibility\$GUIDitem" -Name 'Compatibility Flags' -ErrorAction Stop
                } catch {
                    Write-Warning "Failed because $($_.Exception.Message)"
                    return
                }
                $Meaning = @()
                $FLAGS.Keys | ForEach-Object {
                    if (($flag.'Compatibility Flags') -band $_) {
                        $Meaning += $FLAGS["$_"]
                    }
                }
                New-Object -TypeName PSobject -Property @{
                    Meaning = $Meaning
                    GUID = $GUIDitem
                    Path = $flag.PSPath
                    Value = $flag.'Compatibility Flags'
                    HexValue = -join ('0x',('{0:X0}' -f $flag.'Compatibility Flags'))
                    DisplayName = (Get-ItemProperty -Path "HKLM:\$RegPath\Classes\CLSID\$GUIDitem" -Name '(default)' -ErrorAction Silentlycontinue).'(default)'
                }
            } else {
                Write-Verbose "Skipped HKLM:\$RegPath\Microsoft\Internet Explorer\ActiveX Compatibility\$GUIDitem as it doesn't exist"
            }
        }
    }
}
End{}
}

Function Set-KillBit {            
[CmdletBinding()]
param(
[Parameter(Mandatory=$true,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
[ValidatePattern('^\{[A-Z0-9]{4}([A-Z0-9]{4}-){4}[A-Z0-9]{12}\}$')]
[string[]]$GUID
)
Begin {
    # Make sure we run as admin
    $usercontext = [Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()
    $IsAdmin = $usercontext.IsInRole(544)
    if (-not($IsAdmin)) {
        Write-Warning "Must run powerShell as Administrator to perform these actions"
        return
    }
}
Process {
    $GUID | ForEach-Object -Process {
        $GUIDitem = $_
        Write-Verbose "Handling GUID $GUIDitem"
        $null,"Wow6432Node" | ForEach-Object {
            $HiveLocation = $_
            $flagged = $null
            try {
                if (-not(Test-Path "HKLM:\Software\$HiveLocation\Microsoft\Internet Explorer\ActiveX Compatibility\$GUIDitem")) {
                    New-Item -Path "HKLM:\Software\$HiveLocation\Microsoft\Internet Explorer\ActiveX Compatibility" -Name $GUIDitem -Force -ErrorAction Stop | Out-Null
                    New-ItemProperty -Path "HKLM:\Software\$HiveLocation\Microsoft\Internet Explorer\ActiveX Compatibility\$GUIDitem" -PropertyType DWORD -Name 'Compatibility Flags' -Value 1024 -Force | Out-Null
                } else {
                    Set-ItemProperty -Path "HKLM:\Software\$HiveLocation\Microsoft\Internet Explorer\ActiveX Compatibility\$GUIDitem" -Name 'Compatibility Flags' -Value 1024 -Type DWORD -Force -ErrorAction Stop
                }
                $flagged = $true
            } catch {
                $flagged = $false
                Write-Warning "Failed because $($_.Exception.Message)"
            }
            if ($flagged) {
                Write-Verbose -Message "Successfully set kill-bit on $GUIDitem in hive $HiveLocation"
            } else {
                Write-Verbose -Message "Failed to set kill-bit on $GUIDitem in hive $HiveLocation"
            }
        }
    }
}
End{}
}

Function Clear-KillBit {
[CmdletBinding()]
param(
[Parameter(Mandatory=$true,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
[ValidatePattern('^\{[A-Z0-9]{4}([A-Z0-9]{4}-){4}[A-Z0-9]{12}\}$')]
[string[]]$GUID
)
Begin {
    # Make sure we run as admin
    $usercontext = [Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()
    $IsAdmin = $usercontext.IsInRole(544)
    if (-not($IsAdmin)) {
        Write-Warning "Must run powerShell as Administrator to perform these actions"
        return
    }
}
Process {
    $GUID | ForEach-Object -Process {
        $GUIDitem = $_
        Write-Verbose "Handling GUID $GUIDitem"
        $null,"Wow6432Node" | ForEach-Object {
            Write-Verbose "Setting it in hive $_"
            $HiveLocation = $_
            $flagged = $null
            try {
                if (Test-Path "HKLM:\Software\$HiveLocation\Microsoft\Internet Explorer\ActiveX Compatibility\$GUIDitem") {
                    Remove-Item -Path "HKLM:\Software\$HiveLocation\Microsoft\Internet Explorer\ActiveX Compatibility\$GUIDitem" -Force -ErrorAction Stop | Out-Null
                } else {
                    # already removed
                }
                $flagged = $true
            } catch {
                $flagged = $false
                Write-Warning "Failed because $($_.Exception.Message)"
            }
            if ($flagged) {
                Write-Verbose -Message "Successfully cleared kill-bit on $GUIDitem in hive $HiveLocation"
            } else {
                Write-Verbose -Message "Failed to clear kill-bit on $GUIDitem in hive $HiveLocation"
            }
        }
    }
}
End{}
}

I found the meaning of the decimal/hexadecimal values on this MSDN page where it’s also mentioned that

These enumeration members are bit masks that determine how ActiveX controls are used in Internet Explorer.

My Set-KillBit function only uses the single value “COMPAT_EVIL_DONT_LOAD”.
It doesn’t “merge” bit mask values although it should and note that it could overwrite any preexisting values, so use it with caution.
Idem, my Clear-KillBit function doesn’t remove the 0x400 flag.
Only my Get-KillBit function isn’t considered as written in quick and dirty mode 😛

  • Step 1: Identify GUIDs
  • $allJREGUIDs = @()            
    $Nodes = @($null,'Wow6432Node')            
    $WMI = [wmiclass]"root\default:stdRegProv"            
    $allJREGUIDs += $Nodes | ForEach-Object {             
        $WMI.EnumKey(2147483650,"SOFTWARE\$_\Classes\CLSID").sNames            
    } | Where-Object { $_ -match "CAFEEFAC"}             
    $allJREGUIDs | ? {$_ -match "-DEC"}
  • Step 2: Set the kill-bit on filtered JRE GUIDs, only those related to the “Deployment Toolkit”
  • $allJREGUIDs | ? {$_ -match "-DEC"} | Set-KillBit -Verbose
  • Step 3: Check what was set
  • $allJREGUIDs | ? {$_ -match "-DEC"} | Get-KillBit |            
     ft GUID,DisplayName,Meaning            
    

To test, you need 4 things:

  • an out-of-date JRE version
  • a test URL
  • http://www.java.com/en/download/testjava.jsp

  • A way to revert back to the original state with no kill-bit set on the deployment kit component
  • $allJREGUIDs | ? {$_ -match "-DEC"} | Get-KillBit |            
     Clear-KillBit -Verbose
  • Change your system clock to 31 days after the next CPU planned for 15 October 2013, i.e., after the 15th of November

Happy testing until Oracle fixes its products and releases an official guidance about the new notification, a.k.a JRE expiration, feature 😎

What processes are responsible for the low memory warning?

I’ve a VM where I had the following warning:

To find the culprit using the event logs I did:

# Get the latest event            
[xml]$xml  = (Get-WinEvent -FilterHashtable @{            
    LogName='System';            
    ProviderName='Microsoft-Windows-Resource-Exhaustion-Detector';            
} -MaxEvents 1).ToXML()            
            
# Same console ouput as | fl *            
$xml.Event.UserData.MemoryExhaustionInfo.ProcessInfo.GetEnumerator()            
            
# Get a nice display            
$xml.Event.UserData.MemoryExhaustionInfo.ProcessInfo.GetEnumerator()|            
ft -Property Name,ID,HandleCount,@{            
    l='CommitCharge (MB)';            
    e={'{0:N2}'-f($_.CommitCharge/1MB)}            
}            

Here’s the nice output 😎

So, nothing unusual or malicious…

Create an external VM switch in Hyper-V

This morning I wanted to quickly add an External Hyper-V switch in my lab.
I’ve already added an Internal switch but as I needed to get access to Internet, an External switch was required.

Without reading the help, I naturally did:

New-VMSwitch -Name External -SwitchType External

Even if the External value was technically allowed by tab completion, the following error reminded me how stupid was my above assumption.
New-VMSwitch : Cannot validate argument on parameter ‘SwitchType’. The argument “External” does not belong to the set
“Internal,Private” specified by the ValidateSet attribute. Supply an argument that is in the set and then try the
command again.

Of course, to get access to Internet or to the same network as the physical nic, the External switch needs to be bound to a physical network card!

To help also states:

-SwitchType
Specifies the type of the switch to be created. Allowed values are Internal and Private. To create an External
virtual switch, specify either the NetAdapterInterfaceDescription or the NetAdapterName parameter, which
implicitly set the type of the virtual switch to External.

Then I remembered that I already did this operation some months ago where I had only one physical adapter:

New-VMSwitch -Name Prod -NetAdapterName ((Get-NetAdapter|            
? Status -eq "Up")[0].Name)

I couldn’t use exactly the same above command I previously used because the firt item returned by the filtered Get-NetAdapter” cmdlet was the Internal switch I previously set.

As you can see, filtering with the ‘Status’ wasn’t enough. I decided to compare the properties between the 2 network cards instances returned by the Get-NetAdapter cmdlet.

I quickly did…

$a =  (Get-NetAdapter  | ? Status -eq "Up")[0]            
$b =  (Get-NetAdapter  | ? Status -eq "Up")[1]            
Compare-Object -ReferenceObject $a -DifferenceObject $b

…and couldn’t get the result I expected 😦

Comparing instances of WMI objects isn’t as trivial as it seems to be. Here’s the quick and dirty thing I did:

$a =  (Get-NetAdapter  | ? Status -eq "Up")[0]            
$b =  (Get-NetAdapter  | ? Status -eq "Up")[1]            
# Fill-in an array with properties that have a value            
$props = @()            
($a | gm -MemberType Property).Name | % {            
    if (($a.$_)|Out-String) {             
        $props += $_             
    }            
}            
# Compare properties            
$props | % {             
 Compare-Object -Ref $a -Diff $b -Property $_ | Out-String            
}

After that, I found the NdisPhysicalMedium that seems promising and able to differentiate the Internal switch and the physical Nic of the Hyper-V host.
I found the meaning of the different values on http://www.powershellmagazine.com/2013/04/04/pstip-detecting-wi-fi-adapters

To install my External VM switch, I just extended the filter and did:

New-VMSwitch -Name External -NetAdapterName (            
Get-NetAdapter |?{            
 $_.Status -eq "Up" -and $_.NdisPhysicalMedium -eq 14             
 }).Name

About keyboard layouts

A colleague from the helpdesk team recently asked if I could report the keyboad layout set before users log onto the computer.

I started digging into WMI classes by typing

Get-CimClass -ClassName *Keyboard*

Then did:

Get-CimInstance Win32_Keyboard

… and noticed the Layout property.

Both the MSDN page about the WMI Win32_Keyboard class…

…and the following powershell commands…

([wmiclass]'Win32_Keyboard').GetText("MOF")            
([wmiclass]'CIM_Keyboard').GetText("MOF")

…confirmed that the layout property is returned as a string value.

The problem with this value is that it’s not human readable and represents actually a hexadecimal value.
Although I can find the mapping of hexadecimal values to a user friendly value in the following registry key HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Control\Keyboard Layout\DosKeybCodes, I decided to go another way.

First, do you know that you can use DISM.exe locally:

Dism /online /Get-Intl


As you can see, it displays both the keyboard and input language. The input language is 407 (German) and the keyboard layout is 809 (English UK). This corresponds to what you find under the HKU\.DEFAULT\Keyboard Layout registry key:

As you can see, DISM is precise but doesn’t really help on the readability issue.
Here’s what I propose that is based on the Win32_Keyboard WMI Class:

#Requires -Version 2            
Function Get-KeyboardLayout {            
[CmdletBinding()]            
Param(            
[Parameter(Mandatory=$false,ValueFromPipeline=$true,ValueFromPipeLineByPropertyName=$true)]            
[Alias('CN','__Server','IPAddress','Server','hostname')]            
[string[]]$ComputerName=".",            
             
[parameter()]            
[Alias('RunAs')]            
[System.Management.Automation.Credential()]$Credential = [System.Management.Automation.PSCredential]::Empty            
)            
Begin {}            
Process {            
  $ComputerName | ForEach-Object -Process {            
                     
        $Computer = $_            
        Write-Verbose -Message "Processing computer $Computer"            
                     
        # Prepare a hashtable for splatting            
        $WMIHT = @{            
            ErrorAction = "Stop" ;            
            ComputerName = $Computer ;            
        }            
                     
        # Add creds to hastable...            
        if ($PSBoundParameters.ContainsKey('Credential')) {            
            Write-Verbose "Adding Credentials to hashtable"            
            $WMIHT += @{ Credential = $Credential}            
        }            
        # ...but avoid WMI error: User credentials cannot be used for local connections            
        'localhost','.','127.0.0.1','::1',$env:COMPUTERNAME | ForEach-Object {            
            if ($Computer -eq $_) {            
                Write-Verbose "Removing credentials as localhost is targeted"            
                $WMIHT.Remove('Credential') | Out-Null            
                $WMIHT.Remove('ComputerName') | Out-Null            
            }            
        }            
             
        try {            
            $ok = $true            
            $KB = Get-WmiObject -Class Win32_Keyboard -Property Layout @WMIHT            
            Write-Verbose -Message "Performing WMI query of computer $Computer"            
        } catch {            
            Write-Warning -Message "WMI query for computer $Computer failed because $($_.Exception.Message)"            
            $ok = $false            
        }            
                     
        # Send a PSObject through the pipeline            
        if ($ok) {            
            New-Object -TypeName PSObject -Property @{            
                PSComputerName = $Computer            
                Layout = [System.Globalization.CultureInfo]([int32]"0x$($KB.Layout)")            
                HexValue = $KB.Layout            
            }            
            Write-Verbose -Message "WMI query of computer $Computer completed"            
        }            
    }            
}            
End {}            
} # end of function            

Let’s see what this function does by turning on its verbose mode.

As you can see, the WMI Win32_Keyboard class reports the keyboard layout correctly for my localhost that had German as input language and English UK as keyboard layout.

Follow-up on Scripting Games 2013 Event 6


As you can see, MVP Richard Siddaway voted so far on more than 1000 script, WOW!!!!!
He left the following comment on my Event 6 entry:

Don’t get me wrong. I really appreciate the feedback. The Scripting Games are a unique occasion to get a review from your peers and most valuable persons, who are all part of the PowerShell community. Richard Siddaway is one of the judges in the Scripting Games 2013 that you can follow on twitter:

Source: http://powershell.org/wp/2013/04/06/2013-scripting-games-judges and http://powershell.org/wp/2013/04/06/2013-scripting-games-mighty-panel-of-celebrity-judges

Let’s get back to the comment of Richard Siddaway and here’s what I’d like to say:

@RichardSiddaway,

Hi, Thanks for the feedback. Please allow me to comment on event 6, answer your questions and explain the choices I made:

Well, the instructions don’t say what hypervisor we should use. I choose Hyper-V because I know it, because it’s installed by a single cmdlet. I could have used VMware but I don’t know it. I could have also used KVM but there isn’t any PowerShell module for it…To continue on the Hypervisor, the instructions don’t say what version, if it’s clustered, if it’s domain joined, if there are other virtual machines running,…

The instructions don’t say if the DHCP is running Windows, if it’s running Windows 2012 that has the built-in DHCP module, if it’s joined to the domain…

Yes, you’re right it would have been easier to use the wildcard for the trusted host. However it doesn’t sound as a good practice for me. I also wanted to have reusable code for handling trusted host additions and removals.

Yes, I’m sure that I can rename and join and reboot the computer the way I did. I started with Add-computer but had some weird issues. So, I switched over the above WMI approach. I’ve tested and it worked better than Add-computer. Add-computer has limited Fjoin flags whereas WMI hasn’t. In my testing, WMI appeared to be more reliable. I’ve a blog post about this https://p0w3rsh3ll.wordpress.com/2013/06/04/2013-scripting-games-event-6/

Event 6 was the hardest event of the Scripting Games. Currently, only Bartek Bielawski shared how he’d have done this event: http://becomelotr.wordpress.com/2013/06/05/event-6-my-way (I’m glad I’m not the only one who noticed that you can get IP and MAC addresses directly from Hyper-V running on Windows 2012 😀 )

Nobody asked or commented on it yet, but there are actually 2 reasons why I collected IP Addresses from Hyper-V in my entry:
First, I installed a few VMs, deleted them, installed a new serie,… but DHCP leases of dead VMs were still “active”. (My bad, yeah, I know, I’m not a DHCP guy 😛 )
The second reason was that I wanted to test the remoting with the built-in Test-WSMan cmdlet before trying to use Invoke-Command on the target VMs and join them to the domain.
So, to speed-up WSMan/PSremoting tests, I ended filtering the DHCP data with the active IPs I got from Hyper-V.

Desired State Configuration (DSC) in Powershell version 4


The title of the “super secret” session by Jeffrey Snover is actually: Desired State Configuration in Windows Server 2012 R2
…and it’s now publicly available, you can watch the video on Channel9

The 5 steps for a great automation system
… that you can find in the Monad Manifesto written in 2002.

What Desired Configuration State (DSC) looks like

What is DSC ?
Here’s the definition proposed by Jeffrey Snover @20:20

Desired State Configuration is a standard space management thing, first implemented on Windows, using a PowerShell language and WMI and a PowerShell extension model.

Declarative vs. Imperative code

How this really works? (start from the right)

Other related links from Don Jones

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