Organizing for virtualization

You may have been wondering what is keeping me so busy that I missed my regular posting schedule.  Among other things, I now have a machine at home that supports virtualization.

This gives a lot more possibilities to experiment with software and blog about those experiences. It became clear to me though that with multiple virtual machines to setup and maintain I would have to organize myself a little better. You can get away with a lot if you only have one machine.

First, it would be nice to set up development environments automatically.  I already spend a lot of my time watching progress bars tick along. It would be nice to be automated as much as possible so I can start something running and walk away.

Second, my source files, examples, and install packages are split between my laptop and desktop machine in no particular pattern.

Lastly, I’ve spent some time learning PowerShell and like putting together useful scripts. Now is a perfect opportunity to create some useful utilities, and can organize them so I can possibly find them again.

After I spent a little time trying to organize the materials I had, and collecting some script snippets that could be useful soon, I decided to look into automated installation.

Windows Automated Installation Kit (AIK)

I found my way to the Windows AIK as a method for installing Windows automatically.  There are a lot of features and tools there and my strategy of reading the documentation wasn’t particularly effective.  I think this strategy might have gone better for me:

  • Scan the documentation reading about what tools are available, the scenarios etc.
  • Satisfy my curiosity how things worked.
  • Go back and understand which scenarios best fit what you want to do.  Make a plan.
  • Hit the sections in detail that are needed.

I read the part that seemed interesting and skipped the planning part.  It took me a while to realize I probably needed two installation plans. One for the reference computer and one for target computers. At first I figured I could use the same plan for both, but that isn’t necessarily the best option.

The other part that threw me a little, is that a setting up a development environment is not really a target scenario for the documentation. This comes into play when deciding which roles can be running on a VM and which need to be on the physical computer. In addition, I needed to learn a few things about Hyper-V and setting up virtual machines.

In any case, reading the documentation for Windows AIK carefully seems to be critical.  Then experimenting to see what works and what does not to get familiar with it. This takes a bit of time waiting for OS installs to complete to see the results and get some experience.

Settings things up

After downloading all the ISOs I needed, I set up the Windows AIK on a virtual machine, and created another virtual machine to be my reference computer. The documentation recommends using a USB (removable drive) for storing the answer file for setup.

This seemed to be a bit of a problem at first. There was no way to get a USB attached to a VM that I could see.  But floppy drives also were fine, so after a bit of searching the internet I found some script that would create a VFD that I could plug into the VM.  I couldn’t find an automated way to format the floppy, so I did it manually.  (I now think this is possible with diskpart.exe, but have not tried).

The PowerShell I used to create the VFD was:

  1: $s = gwmi -namespace root\virtualization Msvm_ImageManagementService
  2: $s.createvirtualfloppydisk("C:\floppy.vfd")

You can also translate from the C# example on MSDN.

I created an answer file with the minimum recommended settings, put it on the floppy, loaded both the floppy and the answer file into a VM and successfully deployed.  The install was mostly automated, but I was disappointed to see that there was one menu (the OS selection) that I could not find a solution for.  Since the dialog comes up at the start of the install, I lived with it for a while.

Recently I found the solution, however. To select an OS image from inside install.wim, you need to set the following:

ImageInstall\OSImage\InstallFrom

Path = install.wim

ImageInstall\OSImage\InstallFrom\MetaData

Key = /image/index

Value = 3

3 seems to be the 2008 R2 Enterprise Full install. How did I find this out?  By looking at the Unattend.xml file generated by Microsoft Deployment Toolkit. 

Adding additional software

The base image needs a lot of updates to bring it current. It needs to pull in IE9 and 2008R2 SP1 among others.  The total number of updates is currently around 80.  It seemed it would be more efficient if I could automatically install the big ones and then automate windows update.

I found some examples using PowerShell with the Windows Update COM Api. If you are interested search “Windows update PowerShell” or something equivalent. MSDN has an example in VBScript that can be easily translated to PowerShell.

After downloading the IE9 and 2008 R2 SP1 installer I ran into a bunch of problems automating the install. I still don’t have a really good solution so if you have suggestions, please leave some bread crumbs in the comment section.

There are few problems that have to be solved to automate this:

1) When you download installer packages, they are marked as “from the internet” and pop up all kinds of security dialogs.

2) Both IE9 and 2008 R2 SP1 installers need to reboot the machine. This means your automation need to figure out where it left off and continue.

3) 2008 R2 SP1 installer is 900M+. It didn’t seem like a good idea to copy it to the VM’s expandable HD and run it from there. I’d rather run it from a share on the host machine.

4) Running installer from a network share has its own set of problems. The most notable, is the open file security warning dialog.

Here are some of the solutions I have found.

Unblocking installers from the internet

It turns out that files downloaded from the internet onto an NTFS volume are marked by using alternate NTFS file streams with the zone it came from.

Normally, you can unblock content by right clicking the file and choosing properties. Then clicking the “unblock” button. There is an automated way to do this in PowerShell by deleting the alternate file stream.

You can search more about alternate file streams and PowerShell or you can read this blog post.

Running from a network share

I have successfully gotten rid of the dialog that happens running from the network share by modifying the registry with the following script:

  1: $securityPath = HKCU:Software\Microsoft\Windows\CurrentVersion\Policies\Associations
  2:  
  3: if (-not (test-path $securityPath))
  4: {
  5:    new-item -itemtype directory -force -path $securityPath
  6:    set-itemproperty $securityPath -name ModRiskFileTypes ".exe;.msi"
  7: }

There may be better ways of doing this, especially if you can integrate the installers into the setup installer. This is the best solution I’ve found thus far. There also may be security issues with relaxing the restriction. In a closed VM environment I think it is fine for my purpose.

I was able to net use the share, run this script, and run one of the IE9 or SP1 installers as separate steps, but not together as one smooth install yet. One installer also isn’t helpful since I have to have a solution that runs two installers with a reboot in between.

Running a second installer

I’ve tried various things to run a second installer.

1) Add the unattended command lines to the specialize step of unattended setup. Setup can handle reboots in the middle so it seemed like a good approach.

Result: Although I did see the IE9 installer dialog, the net result was IE8 was still the browser after setup completed. It may be that it is too early in setup to run installers. It seems that way if you read the AIK documentation carefully.

2) I did a bunch of net use and installer launches in the oobe setup step. This phase doesn’t seem to have the same reboot management support that the specialize step did so I needed to find a solution to this.  The RunOnce registry key seems to be one possibility. Also, at the time I tried this I hadn’t found how to get rid of the open file security dialog yet so it would run the first installer and pop up a dialog.

3) Create a series of PowerShell scripts that manage the installation sequence.

Result: PowerShell execution policy has not been updated yet so you need to ensure you pass “-executionpolicy bypass” for scripts entered as synchronous commands in the unattend.xml file.  This was the first solution I was able to get to work, in combination with the RunOnce registry key.

Working with RunOnce registry key

Since both installers reboot, I considered using the RunOnce registry key to initiate the second installer after reboot and auto logon. Here is a script I used to install an example registry entry:

  1: pushd hklm:\Software\Microsoft\Windows\CurrentVersion
  2: if (-not (test-path RunOnce))
  3: {
  4:     new-item -type directory RunOnce
  5: }
  6:  
  7: set-itemproperty RunOnce -Name First -Value "C:\install\IE9-Windows7-x64-enu.exe /passive"

Note that when there are multiple values, it uses them in alphabetical order, so be aware.

The solution

So the solution I came up with to build the reference computer is made up of the following steps:

specialize

Copy the scripts and IE9 installer to the local VHD

Install RunOnce to install RunOnce for the SP1 installer

Install RunOnce to start the IE9 installer

oobe

Install IE9 and reboot

RunOnce

Install R2 SP1

Install windows update script into RunOnce

Install windows updates.

 

Specialize step in Autounattend.xml

net use z: \\host\install /u:host\install <password>

powershell.exe –executionpolicy bypass z:\reference.ps1

Reference.ps1

This script copies files to the c:\install directory and installs the RunOnce entry installing a RunOnce entry after reboot.

  1: new-item -itemtype directory -force c:\install
  2: copy-item z:\ie9\*.exe c:\install
  3: copy-item z:\*.ps1 c:\install
  4:  
  5: pushd hklm:\Software\Microsoft\Windows\CurrentVersion
  6: if (-not (test-path RunOnce))
  7: {
  8:     new-item -type directory RunOnce
  9: }
  10:  
  11: set-itemproperty RunOnce -Name 001 -Value "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -executionpolicy bypass c:\install\install-sp1-runonce.ps1"
  12: set-itemproperty RunOnce -Name 002 -Value "C:\install\IE9-Windows7-x64-enu.exe /passive"
  13: popd

 

install-sp1-runonce.ps1

  1: pushd hklm:\Software\Microsoft\Windows\CurrentVersion
  2: if (-not (test-path RunOnce))
  3: {
  4:     new-item -type directory RunOnce
  5: }
  6:  
  7: set-itemproperty RunOnce -Name sp1 -Value "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -executionpolicy bypass c:\install\install-r2sp1.ps1"
  8: popd

install-rs2sp1.ps1

This script uses the install share, installs the RunOnce entry for the windows update script, disables the dialog for the SP1 installer, and starts the install. It uses a registry setting available on VM clients to find the host name of the VM host machine.

  1: rite-host "Setting up network share."
  2:  
  3: # Find the VM host machine name by querying the registry
  4: $hostMachine = (get-itemproperty "HKLM:\Software\Microsoft\Virtual Machine\Guest\Parameters").HostName
  5:  
  6: # Get the network share.
  7: net use z: /d
  8: net use z: \\$hostMachine\install /u:$hostMachine\install <password>
  9:  
  10: write-host "Network share setup complete."
  11:  
  12: . c:\install\install-update-runonce.ps1
  13:  
  14: # Set security update
  15:  
  16: $securityPath = "HKCU:Software\Microsoft\Windows\CurrentVersion\Policies\Associations"
  17:  
  18: if (-not (test-path $securityPath))
  19: {
  20:    write-host "Making security changes to allow running from share."
  21:    new-item -itemtype directory -force -path $securityPath | out-null
  22:    set-itemproperty $securityPath -name ModRiskFileTypes ".exe;.msi"
  23: }
  24:  
  25: write-host "Starting install."
  26: start-process -wait Z:\r2sp1\windows6.1-KB976932-X64.exe -arg /unattend,/nodialog

 

install-update-runonce.ps1

  1: # Install windowsupdate into runonce
  2:  
  3: pushd hklm:\Software\Microsoft\Windows\CurrentVersion
  4: if (-not (test-path RunOnce))
  5: {
  6:     new-item -type directory RunOnce
  7: }
  8:  
  9: set-itemproperty RunOnce -Name share -Value "net use z: /d"
  10: set-itemproperty RunOnce -Name update -Value "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -executionpolicy bypass c:\install\windowsupdate.ps1"
  11:  
  12: popd

 

windowsupdate.ps1

This script searches for updates that need to be installed, downloads them and installs them. If a reboot is needed, it does one final reboot to finish the install. It doesn’t install the optional updates however.

  1: function Convert-ToWIAStatusValue($value)
  2: {
  3:    switch -exact ($value)
  4:    {
  5:       0   {"NotStarted"}
  6:       1   {"InProgress"}
  7:       2   {"Succeeded"}
  8:       3   {"SucceededWithErrors"}
  9:       4   {"Failed"}
  10:       5   {"Aborted"}
  11:    } 
  12:  
  13: }
  14:  
  15: $needsReboot = $false
  16: $UpdateSession = new-object -ComObject Microsoft.Update.Session
  17: $updateSearcher = $updateSession.CreateUpdateSearcher()
  18:  
  19: write-host "- Searching for updates."
  20:  
  21: $searchResult = $updateSearcher.Search("IsAssigned=1 and IsHidden=0 and IsInstalled=0")
  22:  
  23: write-host " -Found [$($searchResult.Updates.Count)] Updates"
  24: write-host
  25:  
  26: foreach ($update in $searchResult.Updates)
  27: {
  28:     # Add Update to collection
  29:     $updateCollection = new-object -comobject Microsoft.Update.UpdateColl
  30:     if ($update.EualAccepted -eq 0)
  31:     {
  32:         $update.AcceptEula()
  33:     }
  34:     $updateCollection.Add($update) | out-null
  35:  
  36:     # Download
  37:     write-host " + Downloadin update $($update.Title)"
  38:     $updatesDownloader = $updateSession.CreateUpdateDownloader()
  39:     $updatesDownloader.Updates = $updateCollection
  40:     $downloadResult = $updatesDownloader.Download()
  41:     $message = " - Download {0} " -f (Convert-ToWIAStatusValue $downloadResult.ResultCode)
  42:     write-host $message
  43:  
  44:     # Install
  45:     write-host " - Installing update"
  46:  
  47:     $updateInstaller = $updateSession.CreateUpdateInstaller()
  48:     $updateInstaller.Updates = $updateCOllection
  49:     $installResult = $updateInstaller.Install()
  50:     $message = " - Install {0}" -f (Convert-ToWIAStatusValue $InstallResult.ResultCOde)
  51:     write-host $message
  52:     write-host
  53:  
  54:     if (-not $needsReboot)
  55:     {
  56:         $needsReboot = $installResult.rebootRequired
  57:     }
  58: }
  59:  
  60: if ($needsReboot)
  61: {
  62:     restart-computer
  63: }

Note: you can find example PowerShell scripts by searching for “Windows update PowerShell” . This variation came from a MSDN forums thread.

Summary

Although my solution works, there is probably a more elegant solution. Specifically, there is probably a way to add the installers to the autounatted.xml by creating a new ISO image.  If you have suggestions for improvement, please leave a comment.

My next task is to try to deploy a domain controller and example corp deployment automatically. I would like to set it up on an internal Hyper-V network, but have not successfully gotten such a host to connect back to the VM host machine to access the install share. If you have a hint on that, I’d appreciate a comment on that as well.