Paul's Internet Landfill/ 2013/ Scripted Installations of Windows 7 via PXE

Scripted Installations of Windows 7 via PXE

The end of life date for Windows XP is quickly approaching, and like many other lazy/trailing-edge sysadmins, we are just beginning to transition to something else. If I was an ideological purist that "something else" would be some flavour of FLOSS, but because I am a sellout we are moving to Windows 7.

This blog post documents the infrastructure we use to install and maintain Windows images. Here are some of the goals for this installer:

The infrastructure we set up took weeks and weeks of work to develop, but it works pretty well.

There are other valiant attempts to do this, but I have not seen other cases that match our needs exactly. For example, there is a procedure here that claims not use use the WAIK at all: http://www.ultimatedeployment.org/win7pxelinux1.html . But I could not follow it well enough to get it working, and it was too ideologically pure for me. Since I am installing Windows 7 computers, getting a Windows 7 computer to use as a technician workstation did not seem that much of a stretch.

  1. Scripted Installations of Windows 7 via PXE
    1. Overview
    2. Terminology/Stuff You Need
    3. Remaster bootable WIM
    4. Configure a PXE server
    5. Configure PXE for the Windows Installer
    6. Add drivers
    7. Add drivers to boot.wim
    8. Set up runsetup.cmd
    9. Set up unattend.xml
      1. 1 windowsPE
      2. 3 generalize
      3. 4 specialize
      4. 7 oobeSystem
    10. Configure postinstall.cmd
    11. Install third-party software with WPKG
    12. Future Work
  2. Sidebar!

Overview

The installation process is kind of intricate, so here is a high-level overview:

Terminology/Stuff You Need

For this walkthrough, let's assume the PXE server and Samba shares are all hosted by a machine called ntinstall with IP address 10.168.1.5 .

The share containing the Windows 7 install files is \\ntinstall\win7-install

The share containing WPKG configuration is \\ntinstall\wpkg

The PXE configuration files live in /var/lib/tftpboot/ of ntinstall`

Remaster bootable WIM

Most of these steps are taken from the following site: https://sites.google.com/site/godunder/windows-build-files/how-to-create-a-custom-winpe-3-0-3-1-boot-cd-iso-or-usb-boot-key

Start by installing the WAIK on the technician computer.

Next, create the file structure for the WinPE environment into c:\winpe :

copype.cmd x86 c:\winpe

Next, get the contents of the Windows 7 installation DVD someplace useful. (Eventually, this will have to be on a network share so that PXE clients can use it.) For now say that the Windows 7 contents are on drive D:.

At this point, there are two possible files that could serve as the bootable ISO: boot.wim (in d:\sources\boot.wim) or winre.wim which is embedded in d:\sources\install.wim (in C:\Windows\System32\Recovery\) . I cannot remember whether it makes a difference, so I will modify boot.wim. Refer to the link above if you want to use winre.wim (and please contact me to let me know that it makes a difference).

First, copy boot.wim someplace useful. "Someplace useful" means the ISO\sources folder, because the oscdimg command below depends on this location:

copy d:\sources\boot.wim c:\winpe\ISO\sources\

then mount boot.wim for editing:

dism /mount-wim /wimfile:c:\winpe\ISO\sources\boot.wim /index:1 /mountdir:c:\winpe\mount

If you look in c:\winpe\mount you should see some files. These are the mounted image. You can make changes here and commit those changes.

Start by editing c:\winpe\mount\windows\system32\winpeshl.ini . Mine looks something like this:

[LaunchApps]
wpeinit
%SystemDrive%\local\init.cmd

The wpeinit starts the installation environment.

Now, make a folder called c:\winpe\mount\local and add an init.cmd command. This will mount the network share and call a runsetup.cmd command. (It is possible that this could all be integrated into winpeshl.ini, which would save a step. Oh well.)

My init.cmd looks something like this:

@echo off

echo Mounting network drive..
net use q: \\ntinstall\win7-install

echo Launching runsetup.cmd

q:\local\runsetup.cmd

Next, we want to remove the irritating "press any key to boot from CD or DVD" message when the ISO boots. To do this, delete the file c:\winpe\ISO\boot\bootfix.bin file.

We should now be ready to commit changes that we made to the ISO:

dism /unmount-wim /mountdir:c:\winpe\mount /commit

and remaster the boot CD:

oscdimg -n -bc:\winpe\etfsboot.com c:\winpe\ISO c:\winpe\winpe_x86.iso

If all went well, you should have a 200MB winpe_x86.iso file that can be used for PXE booting.

Configure a PXE server

I will not go through detailed steps of how to install a PXE server here. There are other good tutorials online. I used the ones here: http://www.howtoforge.com/ubuntu_pxe_install_server

Here are a few quick tips that saved me some hassle:

The beginning of my pxelinux.cfg/default file looks something like this:

#PATH libs 

DISPLAY introscreens/f1-boot.txt

f1 introscreens/f1-boot.txt 
f2 introscreens/f2-hwtest.txt
f9 introscreens/f9-windows.txt

and my f9-windows.txt file looks something like this:

^Xintroscreens/cr_splash_danger.rle


These Windows installers are for AUTOMATED INSTALLATION. They will
WIPE 
OUT your hard drive without prompting you!


autoinstall-win7      : Windows 7 Installer

The first character is special. I think it is a ctrl-X character. The cr_splash_danger.rle is a splash screen graphic file. You make it by creating a PPM file and converting it with the ppmtolss16 command (available in the syslinux package). There are some reasonable instructions here:

Next you need to configure the DHCP server to point to your PXE machine. I use pfSense as the DHCP server for this subnet. I needed to add a next-server directive to the "DHCP Server" section (labelled "Enable network booting". The IP of the next-server is the IP address of the PXE server (10.168.1.5 in my example) and the filename is /var/lib/tftpboot/pxelinux.0

If you do not have access to your DHCP server, you could add a second network card to your PXE server and set up a private subnet there (with its own DHCP server). Then you could use that private subnet to install Windows.

To test the PXE server, it might be easiest to test with memtest86+ . Install the memtest86+ package, grab the binary /boot/memtest86+.bin, and copy it to the folder /var/lib/tftpboot/memtest/. Then use the following stanza in your pxelinux.cfg/default file:

LABEL memtest
    kernel memtest/memtest86+.bin

If all goes well, you should be able to network boot a machine, have it connect to your PXE server, and get a prompt. Typing "memtest" should start the memory tester.

Some computers do not support network booting, or they do not support network booting well (in particular, they do not know what to do with a next-server directive from the DHCP server). If this is the case, generate a GPXE ISO or USB key image from http://rom-o-matic.net/ and boot from that.

Configure PXE for the Windows Installer

Once your PXE server is working, set up Windows 7 installer files:

Next, add a stanza that will boot your Windows 7 installer. Here is what I used in my pxelinux.cfg/default file:

LABEL autoinstall-win7
    kernel memdisk
    append iso raw initrd=win7/winpe_x86.iso

Now if you type autoinstall-win7 at the PXE boot prompt, you should see the ISO file getting downloaded (which will produce a lot of dots on the screen) and then you should boot into the Windows installation environment. Pressing <shift>+<F10> should bring up a command window. Typing wpeutil reboot in a command line might get you out if you are stuck.

Because there is no runsetup.cmd file, the installer will get stuck and probably reboot. But if you are lucky it will mount the Q: network drive.

Add drivers

Windows 7 does a pretty good job of finding drivers on its own, but if you have drivers that are not detected by particular machines, adding your own is usually pretty easy.

In the windvd\sources folder, make the following folder hierarchy: $OEM$\$$\Inf . Inside this Inf folder you can add the (unpacked) drivers for your hardware. I make one folder for each model of machine (dc7100, ibm-6072-c1u, etc) and then make folders in those machine folders for each component (Sound, Video, etc). Then I put the unpacked drivers in those subfolders.

This is great because Windows will only integrate drivers that it needs for the particular installation.

In addition, once you have the $OEM$\$$\Inf path working, the underlying folder hierarchy seems pretty forgiving. I do not put spaces in filenames for superstitious reasons.

Note that this technique will not work for components that don't have enough drivers to get through the install (namely, network cards). I believe those drivers need to be slipstreamed into the winpe_x86.iso file above. UPDATE: See the next section.

I learned about this trick from the following forum post: http://forums.mydigitallife.info/threads/22915-Fully-unattended-win7-x64-install-with-integrated-%28not-injected%29-unsigned-drivers

Add drivers to boot.wim

I ended up having to slipstream network card drivers into winpe_x86.iso, and found that these instructions were insufficient. Here are some quick notes on how I did it, assuming that the driver files are unpacked into a folder called c:\drivers:

Get your winpe_x86_cr.iso. Use 7-zip or something to extract boot.wim to c:\winpe\ISO\sources.

Next, mount the WIM file:

dism /mount-wim /wimfile:boot.wim /index:1 /mountdir:c:\winpe\mount

Now you can install the drivers. All the drivers in c:\drivers will be added. You can also point this command to a single .inf file if you only want to add one driver.

dism /image:c:\winpe\mount /add-driver /driver:\c:\drivers /recurse

Next you have to unmount boot.wim:

dism /unmount-wim /mountdir:c:\winpe\mount /commit

Finally you have to remaster the ISO file:

oscdimg -n -bc:\winpe\etfsboot.com c:\winpe\ISO c:\winpe\winpe_x86.iso

Then upload winpe_x86.iso to your PXE folder, and you are done.

Set up runsetup.cmd

The purposes of this file are:

It is worth talking about user customization a little. You might want different kinds of Windows installations for different computers. Maybe some computers should get Office 2010 installed; some Office 2007 or some no Office at all. Maybe you need to install Windows 7 Professional for some computers and Windows 7 Enterprise for others.

Ideally, we would have different PXE stanzas for each case:

LABEL autoinstall-win7-office2010 
    kernel memdisk
    append iso raw initrd=win7/winpe_x86.iso office=o2010


LABEL autoinstall-win7-office2007
    kernel memdisk
    append iso raw initrd=win7/winpe_x86.iso office=o2007

but I never found an effective way of getting parameters passed on the PXE command line to the Windows installation environment. Therefore, we have to make the decision once the installer environment has launched, which is where the runsetup.cmd command fits in.

To do this, I use the choice.exe command, which I copied from the technician machine to the network share (probably violating some EULAs in the process). Then I had a runsetup.cmd file that looks something like this:

@echo off

echo Starting runsetup.cmd... 

echo(


rem =========== CHOOSE OFFICE OPTIONS ==================

echo Irritating installation Menu
echo ----------------------------

echo(
echo a - Install Office 2010
echo b - Install Office 2007 


rem if statements must be in reverse order of choice

q:\local\choice.exe /C ab /t 30 /m "Choose or lose (timeout: 30s):" /d b
if errorlevel 2 goto office_2007
if errorlevel 1 goto office_2010

echo "Timeout!"

:office_2010

echo You chose to install Office 2010!
copy q:\local\unattend-o2010.xml %TEMP%\unattend.xml
goto endchoice

:office_2007

echo You chose to install Office 2007!
copy q:\local\unattend-o2007.xml %TEMP%\unattend.xml
goto endchoice

:endchoice

rem ================ START SETUP (x86) ==================

echo Starting setup.exe

q:\win7dvd\setup.exe /unattend:%TEMP%\unattend.xml

In this case, Office 2007 gets installed if there is no other input for 30 seconds.

You can add other options as well, but if your selections get too multivariate you will face a combinatorial explosion quickly.

The unattend-o2010.xml file is a Windows answer file that specifies how the installation should proceed. In a flagrant violation of the "don't repeat yourself" principle, both of the different XML files are almost copies of each other. (In this case only the preferred hostname changes.)

Note that the XML file must be named unattend.xml for the setup.exe to recognise it as an answer file. (Actually, autounattend.xml might also work as a name, but unattend-o2007.xml will not, and it will frustrate you if you try.)

Set up unattend.xml

The easiest way to create the set of unattend.xml files that you need is to use the "Windows Image System Manager" program. Basically, you need to create a new answer file, and then you need to populate it.

The Windows installer goes through a number of different "passes", and different configuration options apply to each pass.

Here are two good guides to getting a handle on the passes and how to create answer files:

Here are specific options I set. Note that I did not set any options for 2 offlineServicing, 5 auditSystem or 6 auditUser.

1 windowsPE

The ImageInstall | OSImage | InstallFrom | MetaData key determines what version of Windows gets installed. You find out what to type for "Value" by typing

imagex /info \\ntinstall\win7-install\win7dvd\sources\install.wim

This will dump an XML file. Each flavour of Windows has an index and a name. The names available in my install.wim are:

Using "IMAGE/INDEX" instead of "IMAGE/NAME" is probably less error-prone, but harder for other people to understand.

3 generalize

4 specialize

The primary way I distinguish between different types of builds (Office 2010 vs Office 2007, for example) is by changing ComputerName in the different unattend.xml files.

If you do not specify a ProductKey then I believe that the installer will use a default key, and I believe the installer will try to activate itself by contacting a KMS host (for flavours that support this). You can find a list of the default keys available for Windows 7 by looking for product.ini in the sources folder of the Windows install DVD. Of course, the default keys will not activate Windows, so you will have to activate afterwards.

The list of TimeZones is picky. You can find a list of strings that Microsoft claims works here: http://technet.microsoft.com/library/cc749073%28WS.10%29

The RunSynchronous commands will come up again later. I wish I had documented why I put in the second registry edit. I believe it disables UAC (which is later restored in the postinstall.cmd script).

7 oobeSystem

The LogonCount means that the user firstuser will be able to log in once without a password. This gives the postinstall.cmd script a chance to run and complete the installation.

The postinstall.cmd does most of the customization, which is why we do not need the WAIK "Packages" functionality. Note that the command is all one line.

The SkipMachineOOBE and SkipUserOOBE are deprecated, but they still work. The NetworkLocation does not seem to work for me -- after the install finishes the user is still prompted for network locations.

Configure postinstall.cmd

The postinstall.cmd command does a lot of the tweaks that are hard to enter into the unattend.xml file. The script runs as the first user you defined (firstuser in my case, which runs as administrator) , but if UAC is enabled, then the script will give UAC prompts.

Here are some choice elements of my postinstall.cmd script. Sorry that the line lengths are do ridiculous.

:: Make Admin password never expire
WMIC USERACCOUNT WHERE "Name='firstuser'" SET PasswordExpires=FALSE

You can do a lot of interesting things with WMIC.

:: ============ STARTUP SOUND ===========

cmd /c reg add HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\LogonUI\BootAnimation /v DisableStartupSound /t REG_DWORD /d 0 /f

This disables the startup sound.

:: ============ THEME STUFF =============

:: Copy default theme (no sound)
copy \\ntinstall\win7-install\local\includes\twc-default.theme %SYSTEMROOT%\Resources\Themes\

:: This supposedly sets the theme for all users
:: From: http://www.sevenforums.com/tutorials/80384-theme-specify-default-theme-load-new-users.html

:: First load the default user hive and edit it:
:: Edit HKLM\TempHive instead of HKCU

cmd /c reg load HKLM\TempHive "%SystemDrive%\Users\Default\NTUSER.DAT"

cmd /c reg add HKLM\TempHive\Software\Microsoft\Windows\CurrentVersion\Themes /v CurrentTheme  /t REG_SZ /d "%SYSTEMROOT%\Resources\Themes\twc-default.theme" /f

cmd /c reg unload HKLM\TempHive

:: Now install the theme for firstuser
:: See http://www.sevenforums.com/themes-styles/93397-there-silent-command-line-operation-change-theme-2.html

\\ntinstall\win7-install\local\ThemeTool.exe changetheme %SYSTEMROOT%\Resources\Themes\twc-default.theme

Getting Windows 7 to shut up and not play sounds is pretty important for us. The good news is that this was scriptable -- I had a harder time in Windows XP (but I did not know about loading the default user hive then).

To make this twc-default.theme I logged into a Windows machine, right-clicked on the desktop, chose "Personalize" and changed the sounds to be "No sounds". Then I saved the theme. This appears in the current user's %userprofile%\AppData\Local\Microsoft\Windows\Themes folder. If you do not use any custom wallpapers, this is a simple file you can drop in.

The registry changes load the profile for the default user, make a change, and then save that profile. This means that every subsequent user will have the quiet theme by default. This does not force anybody to have a quiet theme, however -- users can still change their sounds. It is kind of confusing because simply changing the theme to a different one -- for example with different wallpaper -- will usually re-enable sounds.

Changing the theme for the current user is actually kind of hard. The ThemeTool.exe program is wacky. You have to generate it in a secret way, as documented in the www.sevenforums.com link:

Copy the ThemeTool.exe executable to the network share (no doubt violating more EULAs).

:: ============ SOUND DRIVER STUFF =============

:: Install soundcard driver for dc7100s
:: WMIC trick from
:: http://myserverissick.com/2008/04/find-a-computers-model-using-the-command-line-or-in-a-batch-file/

FOR /F "tokens=2 delims==" %%A IN ('WMIC csproduct GET Name /VALUE ^| FIND /I "Name="') DO SET machine=%%A
ECHO Computer model: "%machine%"

:: Testing strings is complicated! The idea is to make a new string
:: by GETTING RID OF the text you want, and then compare results

set machine_testdc7100=%machine:dc7100=%

ECHO DC7100 test: %machine_testdc7100%

:: Gah. There is a "Would you like to install this device
:: software?" prompt. We need to trust some nonsense cert.
:: http://www.migee.com/2010/09/24/solution-for-unattendedsilent-installs-and-would-you-like-to-install-this-device-software/


:: The strings are different, so "dc7100" was removed
IF NOT "%machine%" == "%machine_testdc7100%" (
certutil -f -addstore "TrustedPublisher" \\ntinstall\win7-install\drivers\dc7100-audio\analog-devices-inc.cer
\\ntinstall\win7-install\drivers\dc7100-audio\sp36530\setup.exe /s
)

This is an example of installing a driver for a particular machine -- in our case, a Compaq/HP dc7100. The basic idea is to use WMIC to get the name of the computer, and then execute the code only if that machine matches.

:: Run wpkg script
\\ntinstall\wpkg\warez\wpkgsync\wpkgsync.bat

This installs third party software, as described in the next section.

:: Enable stupid "The publisher could not be verified" warnings again
:: From http://www.mockbox.net/windows-7/278-how-to-disable-windows-7-popup-the-publisher-could-not-be-verified

cmd /c reg add HKCU\Software\Microsoft\Internet Explorer\Security\ /v DisableSecuritySettingsCheck /t REG_DWORD /d 0 /f

:: Enable UAC again
cmd /c reg add HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System /v EnableLUA /t REG_DWORD /d 1 /f

This undoes the registry changes done in the 4 specialize section of the unattend.xml file.

Install third-party software with WPKG

Configuring WPKG is an entire topic unto itself, so I am just going to sketch the basics here. There is lots of documentation available at http://wpkg.org . Once WPKG is configured and working it is pretty sweet, but it took a lot of time to set up.

If you have an IT budget, then I would recommend using a service such as Ninite Pro instead: https://ninite.com/pro . It handles a lot of third party software with a lot less hassle than WPKG. There is a version of Ninite that is free for home users.

The advantage of using WPKG is that it can potentially install any software used at your organization, whether open or closed source. The critical factor is that it must be possible to script the program's installation non-interactively. This is usually possible, either because the applicaton has a "silent installer" flag, or because a program like AutoIT can be used to click the buttons required to install the application.

In addition to the http://wpkg.org site, you can find information about silent installers on the old (and loved) Unattended installation site: http://unattended.sourceforge.net/installers.php . (We used the Unattended infrastructure to install Windows XP, and its approach definitely influenced this one.)

To set up WPKG, you need yet another samba share, which I placed at \\ntinstall\wpkg. This needs to be visible by all clients (and it should be read-only).

Inside this share are the following files:

For example, here is a fragment from my profiles.xml file which specifies all the software that gets installed on three different classes of machine:

<profile id="default">
        <package package-id="WinDirStat" />
        <package package-id="AdobeFlashPlayer" />
        <package package-id="SumatraPDF" />
        <package package-id="JavaJRE" />
        <package package-id="Firefox" />
        <package package-id="7zip" />
</profile>

<profile id="staff-base">
        <depends profile-id="default" />
        <package package-id="TrueCrypt" />
        <package package-id="LibreOffice" />
        <package package-id="AdobeReader" />
        <package package-id="WinDirStat" />
</profile>

<profile id="staff-win7">
        <depends profile-id="staff-base" />
        <package package-id="Office2010ProfessionalPlus-staff" />
</profile>

The default class is common to every machine. The staff-base is used to keep Windows XP machines up to date, and the staff-win7 profile is used to install Win7 clients (which, unlike Windows XP machines, have Office 2010 installed.)

Here is a snippet from my hosts.xml that shows some of these profiles getting assigned:

<host groups="WPKG-Abstainers" profile-id="nothing" />

<host name="to-be-named" profile-id="staff-win7" />

<host groups="Domain Computers" os="professional.+6\.1\.\d{4}" profile-id="staff-win7" />

<host groups="WPKG-Clients-WinXP" profile-id="staff-base" />

The first line specifies an Active Directory group that is used to exclude certain machine from any WPKG updates at all (because the "nothing" profile will have no packages defined.

The second line assigns clients with the name "to-be-named" (as defined in the WAIK answer file unattend.xml) to be given the "staff-win7" profile.

The third line filters on two things: the Active Directory group "Domain Computers" AND the operating system Windows 7 Professional.

The fourth line filters Windows XP clients, which must be in the Active Directory group "WPKG-Clients-WinXP" to have WPKG updates applied to them. (Note that these groups are COMPUTER groups, not USER groups.)

Again, the WPKG website has good documentation about filtering on hosts: http://wpkg.org/Extended_host_attribute_matching

Finally, here is a (very simple) package configuration file, to install and uninstall Inkscape:

<packages>
<package
  id="Inkscape"
  name="Inkscape"
  revision="3"
  reboot="false"
  priority="50">

  <variable name="version" value="0.48.4" />
  <variable name="extra" value="-1-win32" />

  <check
      type="uninstall"
      condition="exists"
      path="Inkscape %version%" />

  <install cmd='"%SOFTWARE%\Inkscape\Inkscape-%version%%extra%.exe" /S' />

  <upgrade include="install" />

  <remove cmd='"%PROGRAMFILES%\Inkscape\uninstall.exe" /S' />

</package>
</packages>

Many packages require some tweaking to install cleanly, but often other people have done the hard work already, and you can download XML files from the WPKG website. There is good documentation on creating a package script file here: http://wpkg.org/packages.xml

The postinstall.cmd script above calls a small batch wpkgsync.bat, which looks something like this:

@echo off
rem Synchronize WPKG from server without any client

set SOFTWARE=\\ntinstall\wpkg\warez

%SOFTWARE%\tools\wpkgMessage.exe /package "Installing.."

start /b %SOFTWARE%\tools\wpkgMessage.exe

cscript \\wpkg-server\wpkg\wpkg.js /synchronize

%SOFTWARE%\tools\wpkgMessage.exe /package "Software updates completed!"

%SOFTWARE%\tools\wpkgMessage.exe /terminate

There are only two important lines in this script:

set SOFTWARE=\\ntinstall\wpkg\warez
cscript \\wpkg-server\wpkg\wpkg.js /synchronize

The rest of the file is to display messages on the screen, using the wpkgMessage.exe program available here: http://www.gig-mbh.de/edv/index.htm?/edv/software/wpkgtools/wpkg-message-english.htm

We also call the wpkgsync.bat script to keep software up to date. In Active Directory, we use a shutdown script. Unfortunately, there is a bug: Unless otherwise specified, Windows 7 terminates shutdown scripts after 10 minutes. So we have to change some parameters and make registry edits on the clients, as documented here: http://www.mail-archive.com/wpkg-users@lists.wpkg.org/msg05228.html

Here are the steps:

(This part of the installer is not fully tested yet.)

Future Work