Video Screencast Help
Symantec to Separate Into Two Focused, Industry-Leading Technology Companies. Learn more.
Endpoint Management Community Blog

A Control Freak's Guide to Windows 7 Deployment (Part 1)

Created: 05 Apr 2011 • Updated: 27 Apr 2011
MurrayW's picture
+3 3 Votes
Login to vote

Sometimes, you just can't bare the thought of relinquishing control of your processes to automated actions. Some times, the control freak within gets the better of you. Often you end up with a much more reliable, well-documented, and tweakable solution - even if you did have to reinvent the wheel. And this is where this blog post comes in. You see, we've never been quick to apply the latest-and-greatest Deployment Solution service pack, but we do very much like our new technologies. So when it came time to deploy "Windows 7 Enterprise (64-Bit)" to our site, we were still only running "Deployment Solution v6.9 SP2". At this very early stage (as SP3 had only recently been released), I was left without the aide of a "Scripted OS Install" (SOI) process for "Windows 7", and instead needed to build all the processes and scripts that would allow us to deploy and manage the solution. The biggest surprise was how well it all worked. The second big surprise can from the fact we've installed SP3 and SP4 since then, and this process is STILL the one we prefer to work with.

So, not only will this (hopefully) be of use to people trying to deploy the new Windows, but may also prove useful for those that rely on the in-built, automated methods to work out what is happening under the hood.

Step 1 - How will the deployment work?
The first thing I had to work out was what approach to duplicating the disk image I wanted to take. In the early days, we used to deploy our "Windows XP RTM" images with Sysprep and had a horrible time of it. You see, aside from "Windows XP" not being very clever in terms of being locked to a specific Hardware Abstraction layer (HAL), that is unless Hardware Independent Imaging (HII) processes were put in place, it was also not that clever when it came to disk duplication. It suffered badly from an unreliable Sysprep executable, difficult-to-configure unattended files to automate the process, and also the innate ability to trash the important things for your deployment like your default user profile settings, wireless configuration, etc. So as of "Windows XP SP1" era, we had dumped Sysprep and implemented a raw clone process that was made unique via the Mark Russinovich tool "NewSID". We simply scripted this tool so that it would rename the computer while generating a new SID for the computer... and this worked well. Once executed, we used the new computer name/SID and a "Computer Configuration" task to add the computer to the domain, which worked (mostly).

So when we had to work out the deployment method for Windows 7, we were initially hesitant of the Sysprep processes touted by Microsoft and went in search of an updated "NewSID". To our amazement... there was no replacement. In place of the usual downloads, we found this post by Mark:

The Machine SID Duplication Myth (and Why Sysprep Matters)
http://blogs.technet.com/b/markrussinovich/archive/2009/11/03/3291024.aspx

That seemed to put an end to that line of inquiry. So it was decided on Sysprep... but how? Windows 7's Syspre is a very different animal. It's Out of Box Experience (OOBE) processes are very powerful and load drivers, run performance benchmarks, can step over different HALs, and can run a variety of different scripts and tasks during the several processes and steps it takes. Luckily, it was nowhere near as complicated to get this working as I first thought thanks to the "Windows Automated Installation kit" (WAIK) - which is a free download from Microsoft (found here: http://www.microsoft.com/downloads/en/details.aspx?FamilyID=696DD665-9F76-4177-A811-39C26D3B3B34).

But what about joining the computers to the domain? In my previous paragraphs I mentioned the "Computer Configuration" task - but that was not the only method we used. You see, around the "DS v6.8" era ("Windows XP SP2" at the time for us), we started seeing strange behaviour with this. Excuse me while I digress to cover this:

You see, we started to see computers that would come out of automation and would check in with DS and immediately "Configure" themselves onto the domain as part of the restore task. Later, when the "Configuration Task" was run, it would either break this domain membership, or relocate the computer to the root of our "Active Directory" (Computers OU) and would miss out on important policies. But we needed the task, because when we deployed new computers, they wouldn't have the AD information as part of the restore process and would not join the domain. It was at that point that we tried a "Configure Task" to force the computers onto a "Workgroup" and then a reboot, and then another "Configure Task" to put them back on the domain, and then a VBScript to move them to the appropriate OU - ouch, right? So later, we wrote a script that would join the computer to the domain and relocate it to the appropriate OU (only if it didn't already exist) - but this wasn't that great either.

So, what method would we use to handle this for Windows 7. We had all kinds of issues with VBScript methods, and we knew we couldn't really rely on the "Configure Task"... and since we're using Sysprep anyway, why not get that to handle the joining to the domain? It sounded good, but we had other issues to address; How would we rename the computer before we join it? How can we join a computer with non-standard network card drivers? How would it join? How could we avoid a script that contains plain-text domain user credentials for the join (if possible)? How can we tell the computer which OU to join in? And many more.

Another thing that played on our minds... do we continue to capture images using the Altiris Image Format (IMG), or do we switch to a Microsoft WIM format? As multi-cast is important to us could ImageX replace RDeploy? Although, at the time, that answer was pretty easy. No, ImageX under our "Windows Server 2003" box could NOT perform multicast. And as the image format was a file-based image and not a sector-based format, we'd need to script formatting of the disk partitions as well. Ouch. Further complicating that, we have a two-partition "Windows XP" environment and we needed a way to switch to a three-partition "Windows 7" environment without touching the data on the second "XP" partition. Fun and games.

In the end, we decided to set ourselves a pretty massive task. We wanted to handle no more than two deployment jobs for "Windows 7". These two jobs would be used to perform a full (bare metal or full wipe) install of Windows, and the second would reset partitions and be used as a re-image or XP/W7 migration job. The heavy lifting here would be done by scripted DiskPart commands and Altiris IMG formats. The rest of the installation would occur thanks to a token-ised unattended XML file and the Sysprep process.

To make this new process work, the first thing we had to do was set up a process for generating the "Unattended.XML" file we'll be passing to the systems for deployment, and that's where I'll begin.

Step 2 - Let the games begin...
We all know, or should know, that Deployment Server has a neat feature called "ReplaceTokens". This is the automation king in our process. You see, not only does this process let you replace tokens like "%ID%" (the Altiris Computer ID number), "%CompName%" (the Altiris Console Computer Name), etc, within files, but it also let's you replace tokens using lookups into a SQL table and via SQL expressions. Further, Deployment Solution has a configuration tab called "Custom Data Sources" which let's you set up access to a table for this very purpose (among others).

  1. To get things started, I simply wanted to create a new SQL table and see if I could pull data from it using a "Run Script" task in a job. So, I opened up "Microsoft SQL Management Studio" and connected to the SQL instance where the Deployment Solution resides. I proceeded to create a new database called "CustomData" (mostly using the defaults). I then ensured that the Active Directory group for the "IT Staff" was available as a logon/security group and assigned that with "Connect" rights to the new database.
     
  2. The next step was to generate a new table within that database, which I called "DeployVars". That table contained six columns. We had;
  • "ID" - Which is used to store the two letter prefixes we use to designate systems in our naming convention.
     
  • "DomainName" - Which contains the full domain name (i.e. "site.mydomain.com") for use in "JoinDomain" entry of "Unattend.XML".
     
  • "DefaultOU" - Which contains the full container path for the OU of the computer i.e. "OU=Student Workstations,DC=SITE,DC=MYDOMAIN,DC=COM".
     
  • "DefaultPass" - Which is the WAIK-encoded password for the local administrator account (which we set via "Unattend.XML").
     
  • "TypeOfSOE" - Which is a flag we use in other scripts. These include "Faculty", "Students", "Loan" and "Virtual", etc.
     
  • "TypeOfSystem" - Similar to the above, this is a flag we use later on. includes entries like "Desktop", "Notebook", "Tablet", "Virtual" and "Master", etc.
  1. The next step, obviously, was to populate this table with meaningful data. We used the WAIK tools to generate a few sample XMLs with different passwords for "Staff", "Students", "Loan", etc, and added the prefixes a row at a time i.e. "ST" designates a "Student Tablet" computer. With this prefix, we know what the following two-digit code stands for, what domain to joined, in what OU it will be joined, and what local administrator password it will be using.
     
  2. The next step was to configure this for use in Deployment Server. This was just a matter of opening the "Deployment Server Console", clicking "Tools" > "Options...", clicking on the "Custom Data Sources" tab and clicking "Add...". In my example, I set the "Alias" to "DeployVars", the "Server" to the database server instance, the "Database" to "CustomData", and I specified that we should "Use Integrated Authentication". Click "OK" a couple of times and you're done.
     
  3. We need to test this. I created a new "Job" for the test and added a single "Run Script" task. The contents of the script was simply:

@ECHO OFF
CLS
REM ### Generate Custom Unattended Config...
REM ReplaceTokens .\Deploy\Scripts\W7\W7_PRO_x64.xml .\temp\%ID%_Unattend.xml

  1. I set the above "Run Script" task to run in "Windows" and "Locally on the Deployment Server", and by making sure the "Run when the agent is connected" option is turned off. This makes the script immediately run without the client computer even being turned on, and allows me to generate the file quickly.

    So what does this do? Well, after the obligatory batch script bits, we use the "REM" comment command to allow "ReplaceTokens" to execute without causing a batch script error (this is how "ReplaceTokens" works). The first input is the path (from the "eXpress" share) to the template file that contains all the tokens. This is a simple text file (not an XML at this point) that contains SQL tokens (see below). The example below connects to the "Custom Data Source" alias "DeployVars", and using the DS variable "%CompName%" within a "LEFT" string function, performs a lookup against the "ID" column for a match and returns the associated "DomainName" column value for that row.The second argument, of course, is where the modified (token replaced) file is saved to. in this case I am using the "Temp" folder in the "eXpress" share and have set the name to use the DS variable "%ID%" (Altiris Computer ID Number) to make is specific to the targeted computer.

    Here's the magic line of text:

%#DeployVars^*"Select DomainName from DeployVars where ID = LEFT('%COMPNAME%',2)"%

  1. This particular script takes around one second to run, and successfully replaced all my test variables with the correct data from the database table. At this point I should also say that my column names and data are for my own uses, and you may find it useful to include other data, other data sources, etc. Perhaps a second database table that contains the details for the user account that will add the computer to the domain, the options are endless.

    But now we need to make this all work. The next step requires you to install the WAIK tools on your computer and utilise the "System Image Manager" (SIM) to generate an "Unattend.XML". This can be as simple or as complex as you like, but you will find that to make the install completely automated, a variety of settings are required i.e. "Language" values. Have a play with it.
     

  2. Once you have attached SIM to your WIM, generated a catalog, and played with various settings, you should end up with an XML file like mine below. NOTE: For convenience, I have included the SQL tokens in this so that you can see a full working XML.

<?xml version="1.0" encoding="utf-8"?>
<unattend xmlns="urn:schemas-microsoft-com:unattend">
    <settings pass="windowsPE">
        <component
         name="Microsoft-Windows-International-Core-WinPE"
         processorArchitecture="amd64"
         publicKeyToken="31bf3856ad364e35"
         language="neutral"
         versionScope="nonSxS"
         xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
            <SetupUILanguage>
                <UILanguage>en-AU</UILanguage>
            </SetupUILanguage>
            <InputLocale>en-AU</InputLocale>
            <SystemLocale>en-AU</SystemLocale>
            <UILanguage>en-AU</UILanguage>
            <UserLocale>en-AU</UserLocale>
            <UILanguageFallback>en-US</UILanguageFallback>
        </component>
        <component
         name="Microsoft-Windows-Setup"
         processorArchitecture="amd64"
         publicKeyToken="31bf3856ad364e35"
         language="neutral"
         versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
            <UserData>
                <AcceptEula>true</AcceptEula>
                <FullName>Site Name</FullName>
                <Organization>Company Name</Organization>
            </UserData>
        </component>
    </settings>
    <settings pass="offlineServicing">
        <component
         name="Microsoft-Windows-PnpCustomizationsNonWinPE"
         processorArchitecture="amd64"
         publicKeyToken="31bf3856ad364e35"
         language="neutral"
         versionScope="nonSxS"
         xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
            <DriverPaths>
                <PathAndCredentials wcm:keyValue="4ad29d32" wcm:action="add">
                    <Path>C:\SYSPREP\DRV_MSD</Path>
                </PathAndCredentials>
                <PathAndCredentials wcm:keyValue="6baba8eb" wcm:action="add">
                    <Path>C:\SYSPREP\DRV_NET</Path>
                </PathAndCredentials>
                <PathAndCredentials wcm:keyValue="967c025" wcm:action="add">
                    <Path>C:\SYSPREP\DRV_VIDEO</Path>
                </PathAndCredentials>
            </DriverPaths>
        </component>
    </settings>
    <settings pass="specialize">
        <component
         name="Microsoft-Windows-Shell-Setup"
         processorArchitecture="amd64"
         publicKeyToken="31bf3856ad364e35"
         language="neutral"
         versionScope="nonSxS"
         xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
            <ComputerName>%COMPNAME%</ComputerName>
            <RegisteredOwner>Company Name</RegisteredOwner>
        </component>
        <component
         name="Microsoft-Windows-UnattendedJoin"
         processorArchitecture="amd64"
         publicKeyToken="31bf3856ad364e35"
         language="neutral"
         versionScope="nonSxS"
         xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
            <Identification>
                <JoinDomain>%#DeployVars^*"Select DomainName from DeployVars where ID = LEFT('%COMPNAME%',2)"%</JoinDomain>
                <MachineObjectOU>%#DeployVars^*"Select DefaultOU from DeployVars where ID = LEFT('%COMPNAME%',2)"%</MachineObjectOU>
                <Credentials>
                    <Domain>Domain</Domain>
                    <Password>Password</Password>
                    <Username>User Name</Username>
                </Credentials>
            </Identification>
        </component>
    </settings>
    <settings pass="oobeSystem">
        <component
         name="Microsoft-Windows-Shell-Setup"
         processorArchitecture="amd64"
         publicKeyToken="31bf3856ad364e35"
         language="neutral"
         versionScope="nonSxS"
         xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
            <OOBE>
                <HideEULAPage>true</HideEULAPage>
                <HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE>
                <NetworkLocation>Work</NetworkLocation>
                <ProtectYourPC>3</ProtectYourPC>
            </OOBE>
            <RegisteredOwner>Site Name</RegisteredOwner>
            <ShowWindowsLive>false</ShowWindowsLive>
            <TimeZone>W. Australia Standard Time</TimeZone>
            <RegisteredOrganization>Company Name</RegisteredOrganization>
            <UserAccounts>
                <DomainAccounts>
                    <DomainAccountList wcm:action="add">
                        <DomainAccount wcm:action="add">
                            <Group>Administrators</Group>
                            <Name>IT Staff</Name>
                        </DomainAccount>
                        <Domain>ADMIN</Domain>
                    </DomainAccountList>
                </DomainAccounts>
                <LocalAccounts>
                    <LocalAccount wcm:action="add">
                        <Password>
                            <Value>%#DeployVars^*"Select DefaultPass from DeployVars where ID = LEFT('%COMPNAME%',2)"%</Value>
                            <PlainText>false</PlainText>
                        </Password>
                        <Description>IT Service Account</Description>
                        <DisplayName>Admin</DisplayName>
                        <Group>Administrators</Group>
                        <Name>Admin</Name>
                    </LocalAccount>
                </LocalAccounts>
            </UserAccounts>
        </component>
        <component
         name="Microsoft-Windows-International-Core"
         processorArchitecture="amd64"
         publicKeyToken="31bf3856ad364e35"
         language="neutral"
         versionScope="nonSxS"
         xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
            <InputLocale>en-AU</InputLocale>
            <SystemLocale>en-AU</SystemLocale>
            <UILanguage>en-AU</UILanguage>
            <UILanguageFallback>en-US</UILanguageFallback>
            <UserLocale>en-AU</UserLocale>
        </component>
    </settings>
</unattend>

NOTE: If you wanted to be really cute, you could use the "DeployVars" table to store the language values, company name, registered user, and use this system to assist in deploying various SOEs for various clients in variable languages as well. It's all very easy and very customisable after you've worked out what is going on.

Step 3 - You'll need a disk image for the next bit...
Now, of course, we need a disk image to test this against. I'll presume that everyone reading this is familiar with the "Create" and "Distribute" Disk Image tasks, and that "Do Not Boot to Production" check box is your friend when capturing a SYSPREP image... if not, I am sure I can reply to questions and/or write another blog entry about it all. So, at this point, I will assume that you've installed Windows on a master computer, installed software and updates, tweaked to your liking, and are now about to capture the image... however...

  1. STOP Everything. A lot of information on the web indicates that you should be including the path to an XML file during your SYSPREP command. I am here to tell you to NOT do that. Simply run the "C:\Windows\System32\Sysprep\Sysprep.exe" and set it to "Out of Box Experience (OOBE)", "Shutdown" and "Generalize". It is a VERY bad thing to mess with an unattend file that has already been cached in the Windows "Panther" folder, and Microsoft says as much in the documentation. It is FAR better to leave the system in a vanilla Sysprep state and inject this "Unattend.XML" file during automation.
     
  2. Although a lot of people want to use the Microsoft-supported method of supplying a "CopyProfile" command, I am also here to tell you to NOT do that either. In all likelihood the "CopyProfile" process won't work anyway, and if it does, most of your settings will be missing. Don't use this option. instead, use the built in "Copy To..." button on the "Default User" from within the "Profiles" control panel window. I am assuming, of course, that you intend to join the computer to the domain and will instead use a configured "Default User" account from the Domain Controller's "NETLOGON" share. If this IS your intention, then forget about local "Default User" profiles, copy the local profile to the network, and then proceed to tweak it via "AppData" and registry imports from working systems containing the settings you want. I've found this much more reliable.
     
  3. It's also generally a bad idea to install the DAgent in the base image. Consider handling the install and configuration process for this via the "SetupComplete.cmd" script file later in the process... as this is how the "Scripted OS Install" does it anyway.
     
  4. As for drivers, I know that lots of people want to install network drivers, sound drivers, mass storage controller drivers, video card drivers, etc to ensure the computer comes out of the box in a fully working state. I'm going to tell you not to worry about doing that either. If you decide to use my blog information but use WIM images instead, you gain the ability to inject drivers via the DISM tool. If you do everything int eh IMG format, then a later part of this (multi-part) blog will describe how I use this process for hardware independent imaging - including injecting required network drivers and mass storage controller drivers for Sysprep booting (and domain joining), and also how to enable Aero... but that comes later.

So, as you can see, if you don't need to install drivers and specify mass storage controllers, etc. And if you don't need the CopyProfile command, then there is no need to actually specify an XML during the running of the Sysprep command. Cool hey?

IMPORTANT: Be very careful when Sysprep-ing your computer. I STRONGLY recommend you grab an image of the computer BEFORE you run Sysprep. I also strongly recommend that if you're going to be performing running Sysprep during the build process, that you use the following registry key:

Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\SL]
"SkipRearm"=dword:00000001

During my first "Windows 7" build process, I didn't use this registry key. And as I progressed making newer builds and adding fixes, I was recapturing the image as a Sysprep image. The problem is, Windows 7 only gives you three goes. If you Sysprep and generalize a computer three times, you can't do it a fourth and basically need to start again. I now build ALL my images using this registry key and only remove it during the final capture - after a pre-Sysprep capture. better to be safe than sorry.

Step 4 - Implementing the Unattend XML
Okay, so you now have an image file, an XML file you can save as a template, a database and table full of deployment variables, and a script to make all that work... so now it's time to bring all these bits together.

  1. Create a new test deployment job.
     
  2. Copy in (or recreate) your "Run Script" task with the "ReplaceTokens" command. Ensure that the template file path and name are correct and that the environment is your automation. Don't want to reboot into production to try and run this script.
     
  3. Add in the "Distribute Disk Image" task and set your IMG/WIM file, parameters, sizing, etc.
     
  4. If your image contains a "System Reserved" partition (like mine does), then you'll need to add in another automation "Run Script" task after the image job. The purpose of this is to allow Windows to reset the "BootMGR" and ensure all the boot files and IDs are correctly configured. This script will look like this:

@ECHO OFF
REM ### Configure System - BCD Config...
CLS
ECHO Setting the disk configuration for the BootMGR...
%SYSTEMROOT%/system32/Bcdedit.exe /set {default} device partition=D:
%SYSTEMROOT%/system32/Bcdedit.exe /set {default} osdevice partition=D:
%SYSTEMROOT%/system32/Bcdedit.exe /set {bootmgr} device partition=C:

  1. And now we need to copy across the custom "Unattend.XML" file. This is another automation "Run Script" that will look like this:

@ECHO OFF
REM ### Configure System - Config Scripts...
CLS
ECHO Injecting Windows Deployment Configuration File for System...
IF NOT EXIST D:\Windows\Panther\Unattend ( MD D:\Windows\Panther\Unattend )
COPY "R:\Temp\%ID%_Unattend.xml" "D:\Windows\Panther\Unattend\Unattend.xml"

And that's it for now. Now, you may have noticed that I didn't use the "FIRM" command to copy the file above, and that's because "Windows 7" with a "System Reserved" partition, under "WinPE 2" seems to cause "FIRM" to set "PROD:" as "C:", which is the "System Reserved" volume. As such, we need to do things manually. in this set of scripts I am assuming that the enumerated letter for the OS volume is "D:", however this may be incorrect. If it is, both the "BCD" script and the "Unattend" script will need different drive letters to be used. Conveniently, I have a solution for this in the form of MORE scripts. unfortunately it's getting late here and this blog entry is already massive, so I will save all of the additional scripts for a follow-up post (or three).

Hopefully this has provided a little bit of information into the deployment process and is useful to someone. The "Unattend.XML" contains all the settings required for a fully automated deployment, and the use of a "Custom Data Source" and "ReplaceTokens" should be useful to someone as well.

In the next post I will provide scripts to handle DiskPart (for full and XP migration jobs), DAgent installations, driver detection and installation, critical driver injection, and much more. I hope you'll check back soon.
 

Final Words

Thanks for reading my blog. Please note that, although I have written these scripts for use in our production environment, and they work fine here, this sub-set of script provided in the blog have not been completely tested and may not work correctly in your environment. As with everything, please try to understand what the code is doing and test in a non-production environment first. Neither my employer or I will accept any responsibility for the (mis)use of these scripts and do not claim that they are a final production-ready solution. If the scripts or ideas presented here do help you, then please drop me a comment, email, and.or PM to let me know.