This document is part of Unattended, a Windows deployment system.


Introduction

After you use Unattended a few times, you will probably get tired of answering the same questions over and over. This document describes how to create site-specific configuration files to set (or compute) the answers automatically.

Unattended looks for all site-specific configuration in Z:\site. In particular, the master installation script (install.pl) reads Z:\site\unattend.txt and Z:\site\config.pl.

To perform simple customizations, just create and populate z:\site\unattend.txt. The values you put there will override the defaults and cause the installation script to skip any corresponding questions. Do not worry about creating a "complete" answer file; if you fail to provide some required value, the installation script will prompt you for it.

Read on for a description of what to put in your unattend.txt file.

(The Z:\site\config.pl mechanism is more complex and is discussed way below.)

Syntax of unattend.txt

The general syntax of an answer file is the same as any .ini (or .inf) file. It looks something like this:

[SECTION 1]
  ; COMMENT
  KEY1=VALUE1
  KEY2
  KEY3="funny;quoted=value"

[SECTION 2]
.
.
.

That is, it consists of sections, each headed by a section name in square brackets. Each section has zero or more entries (also called settings). Each entry assigns a value to a key, or else consists of a key by itself. Any line beginning with a semicolon, or whitespace followed by a semicolon, is a comment.

If the value contains any special characters, or if it is the empty string, it must be placed in quotation marks. Which characters qualify as "special" is undocumented; the .ini file parser/generator in Unattended tries to be fairly conservative, and will exit with an error message if it encounters an unrecognized line. Quotes are allowed even when they are not required, so when in doubt, use them.

For a complete sample unattend.txt file, use Unattended to install Windows and then examine C:\NETINST\UNATTEND.TXT. For a partial file containing some of Unattended's defaults, see Z:\lib\unattend.txt from the distribution.

Microsoft's documentation

The most thorough documentation for the answer file is Microsoft's own, but unfortunately, it is a bit annoying to obtain. There are different versions for Windows 2000 and Windows XP, although the answer files are very similar.

Windows 2000 Guide to Unattended Setup
Microsoft put the SP1 version of this document on the Web, but they seem to have stopped with later service packs. The canonical way to obtain this document is to download and install the Windows 2000 SP4 Support Tools, then use Internet Explorer to extract the unattend.doc file from the deploy.cab archive.

You can also find an earlier version of deploy.cab on the Windows 2000 CD in the \support\tools folder.

Microsoft Windows Preinstallation Reference
As far as I know, this document is not available directly on the Web. You must first download the Windows XP SP1a Corporate Deployment Tools, deploy.cab. Then use Internet Explorer to extract ref.chm, a Windows "Compiled Help" file, which you can double-click to browse.

Here again, deploy.cab may also be found on the Windows XP CD in the \support\tools folder.

Unattended Installation Tools and Settings
For Windows Server 2003, Microsoft has once again put the relevant document on the Web. Go figure. The available settings are mostly the same across the whole NT/2000/XP/2003 family, so this is actually a pretty good reference.

Settings used by Windows Setup

Here are examples for how to configure some common settings. All of these are used by Windows Setup, which means they are fully explained in Microsoft's documentation. These examples are just to help you get started quickly.

User Name, Organization Name, and Computer Name

Example:

[UserData]
    FullName="Jane Doe"
    OrgName="FooBar Widgets, Incorporated"
    ComputerName=magneto

Setting the ComputerName to * tells Windows Setup to pick a random name.

Product Key

[UserData]
    ProductKey=XXXXX-YYYYY-ZZZZZ-00000-11111

NOTE: Prior to Windows XP, this key was named "ProductID".

Local Administrator Password

To set the local Administrator account password:

[GuiUnattended]
    AdminPassword=sekrit

Joining a domain

To join the domain FOOBAR using the account FOOBAR\wsadmin and password verysekrit:

[Identification]
    JoinDomain=FOOBAR
    DomainAdmin=FOOBAR\wsadmin
    DomainAdminPassword=verysekrit

If you do not want to store the password in cleartext, you can omit the DomainAdminPassword entry; remember that the installation script will prompt you for any required values which you do not already provide.

Joining a workgroup

To join the workgroup FOOBAR:

[Identification]
    JoinWorkgroup=FOOBAR

Note that you are required to join either a domain or a workgroup.

Joining an OU

To specify the Organizational Unit to join within an Active Directory domain:

[Identification]
    MachineObjectOU="OU=Foo,OU=FooParent,DC=department,DC=example,DC=com"

See also KB article 226315.

Time Zone

To set the workstation's time zone:

[GuiUnattended]
    ; U.S. Pacific
    TimeZone=004

The time zone setting is numeric; see the table of index numbers for a complete list. The default value for Unattended is 035 (U.S. Eastern).

OEM Plug&Play Drivers

You may specify additions to the search path for Plug&Play drivers. Elements of this path are separated by semicolons. Windows uses this path when searching for a driver for a piece of hardware. These elements are relative to the C: drive, and are usually used in conjunction with the $oem$/$1 mechanism.

For example:

[Unattended]
    OemPnPDriversPath="drivers\net\eepro;drivers\video\nVidia"

Windows XP look and feel

The [Shell] section controls the general look and feel of Windows XP. To use the classic Windows Start menu (with My Computer and My Documents on the desktop), and to use the "classic Windows visual style":

[Shell]
    ; Use classic start menu
    DefaultStartPanelOff=Yes
    ; Use classic visual style
    DefaultThemesOff=Yes

The [_meta] section

Unattended adds some functionality on top of Windows Setup. This functionality is controlled by a new section of the answer file, the [_meta] section. This section is ignored by Windows Setup; it exists solely to let you provide answers to some of the new questions install.pl asks.

Partitioning and formatting

The installation script begins by partitioning and formatting the disk.

To automatically answer the "Use large disk support" question, set the fdisk_lba key to 1 for "yes" and 0 for "no".

To automatically partition the drive, use the fdisk_cmds key. This is a semicolon-separated list of commands invoking FreeDOS FDISK. Keep in mind that the result of these commands must be a partition table with an active FAT partition. To suppress the confirmation for disk partitioning, set fdisk_confirm to 0.

To automatically format the drive, set the format_cmd key. This is normally an invocation of the FreeDOS format utility.

Finally, to automatically replace the Master Boot Record (MBR) or not, use the replace_mbr key. Set it to 1 for "yes" and 0 for "no".

For example, to use large disk support, partition the drive as a single large partition (without confirmation), format it, and replace the MBR:

[_meta]
    fdisk_lba=1
    fdisk_cmds="fdisk /clear 1;fdisk /pri:4000;fdisk /activate:1"
    fdisk_confirm=0
    format_cmd="format /y /z:seriously /q /u /a /v: c:"
    replace_mbr=1

Post-install scripts, Local Administrators, NTP servers, final edits

To configure which top-level post-installation script to run, set the top key.

To configure which "optional" scripts to run, set the middle key.

To configure a script to run last, just before the final cleanup and reboot, set the bottom key (normally unset).

To configure which domain accounts are added to the local Administrators group, set the local_admins key to a semicolon-separated list of user names. An empty list is allowed, but it must be quoted. User names may be fully qualified (DOMAIN\user), or they may be bare (user); in the latter case, the [Identification]/JoinDomain value will be used as the domain.

To configure the NTP servers, set the ntp_servers key. This is a space-separated list.

To control the final question, where you are asked if you want to make any final edits, use the edit_files key. Set it to 0 to avoid being asked the question.

For example, to perform a base install, add Spybot Search&Destroy and the Sun JRE, configure NTP servers named "ntp-0" and "ntp-1", not add any accounts to the local Administrators group, and skip the final question:

[_meta]
    top=base.bat
    middle="spybot.bat;sun-jre.bat"
    local_admins=""
    ntp_servers="ntp-0 ntp-1"
    edit_files=0

Other keys

There are several other keys which appear in the [_meta] section, like macaddr and ipaddr. Most of these are generated automatically from sane defaults, so unless you are sure about what you are doing, you should probably omit them from your unattend.txt file.

Programmatic configuration using config.pl

If the static configuration options provided by unattend.txt are not sufficient, you can create arbitrarily complex rules using Z:\site\config.pl. This is a Perl file which install.pl reads.

To write your own config.pl, you need to know a little Perl and you need to understand how the installation script works.

How the installation script works

The installation script generates the answer file in memory, placing it in an Unattend::IniFile object named $u.

Programmatically, this object behaves like a Perl hash (associative array). It maps section names to sections, where each section is another hash which maps keys to values. So, for example, the value of the FullName key in the [UserData] section is just $u->{'UserData'}->{'FullName'}, and you may read or assign this value in your config.pl.

But these hashes are special in two ways.

First, they are case-insensitive, so that $u->{'UserData'}->{'FullName'} and $u->{'userdata'}->{'fullname'} refer to the same thing.

Second, if you assign a Perl subroutine to a key, something magic happens when you read the key: The subroutine will be called with no arguments, and the subroutine will be replaced by its own return value. These stored subroutines are called "promises", and the act of evaluating the subroutine and replacing the value is called "forcing" the promise. (I knew that CS degree would be useful someday.)

For example, suppose you wanted the local Administrator password to be the same as the user's FullName. This is not a very realistic example, perhaps, but it will serve for illustration. You would put this in config.pl:

$u->{'GuiUnattended'}->{'AdminPassword'} =
    sub {
        return $u->{'UserData'}->{'FullName'};
    };

1;

This promise will not be forced until the AdminPassword key is read (possibly not until the unattend.txt file is actually being generated). When that happens, the subroutine will read the value of the FullName key in order to return it. That, in turn, may cause another promise to be forced, and so on... But in the end, the FullName will be returned by this subroutine, and it will be stored and used as the value for AdminPassword.

In fact, install.pl simply assigns a "default value" for most keys which is a subroutine to ask the user an appropriate question. Then it reads unattend.txt and config.pl, each of which may override the defaults with static values or with different subroutines.

This design requires that you think in a "declarative" style rather than an "imperative" one. That is, you should think about how each key is to be computed from other data (including other keys). Except for the top-level assignments of subroutines, you should avoid assigning to keys themselves.

One more thing. The config.pl script is executed by Perl's "do" operator, which returns the value of the last expression in the file. So the last line of config.pl should always be a constant true expression, like this:

1;

This is to ensure that the "do" operator returns success, and to prevent any promises from being forced prematurely.

Some examples should help.

Computing OemPnPDriversPath automatically

To automatically add all drivers to OemPnPDriversPath, you just crib the code from install.pl but skip the part where it asks the question:

use warnings;
use strict;

$u->{'Unattended'}->{'OemPnPDriversPath'} =
    sub {
        my $media_obj = Unattend::WinMedia->new ($u->{'_meta'}->{'OS_media'});
        my @pnp_driver_dirs = $media_obj->oem_pnp_dirs (1);
        # No driver directories means no drivers path
        scalar @pnp_driver_dirs > 0
            or return undef;
        print "...found some driver directories.\n";

        my $ret = join ';', @pnp_driver_dirs;
        # Setup does not like empty OemPnPDriversPath
        $ret =~ /\S/
            or undef $ret;
        return $ret;
     };

1;

This code illustrates a few points.

First, all Perl code you ever write should "use warnings" and "use strict". Do not even think twice about it.

Second, the last line of the file is 1;.

Third, if a key has a value of "undef", it will not appear in unattend.txt at all. If you want to delete a key completely, make it undef.

Finally, this code demonstrates the use of the Unattend::WinMedia helper object. You create an instance of this object by giving it the path to your Windows installation media ([_meta]/OS_media value). It knows lots of things about such media, including how to grovel it for OEM Plug&Play drivers (oem_pnp_dirs() method).

Assigning product key based on OS type

To pick the product key based on OS type, you would use code like this:

use warnings;
use strict;

$u->{'UserData'}->{'ProductKey'} =
    sub {
        my $media_obj = Unattend::WinMedia->new ($u->{'_meta'}->{'OS_media'});
        my $os_name = $media_obj->name ();
        if ($os_name =~ /Windows XP/) {
            return 'MY-WINDOWS-XP-KEY';
        }
        elsif ($os_name =~ /Windows Server 2003/) {
            return 'MY-SERVER-2003-KEY';
        }
        return undef;
    };

$u->{'UserData'}->{'ProductID'} =
    sub {
        my $media_obj = Unattend::WinMedia->new ($u->{'_meta'}->{'OS_media'});
        my $os_name = $media_obj->name ();
        if ($os_name =~ /Windows 2000/) {
            return 'MY-WINDOWS-2000-KEY';
        }
        elsif (defined $u->{'UserData'}->{'ProductKey'}) {
            # It is OK for us to return undef as long as there is a
            # ProductKey.
            return undef;
        }
        die "No ProductKey nor ProductID!";
    };

1;

This code sets ProductID for Windows 2000 and ProductKey for Windows XP and Windows Server 2003. (Although the later OSes accept ProductID for backwards compatibility, ProductKey is now canonical and we like to be pedantic.) The code dispatches on the name of the chosen operating system, as returned by the name() method of the Unattend::WinMedia object.

Reading different answer files based on OS type

If you want to use different unattend.txt files depending on the type of OS being installed:

my $media_obj = Unattend::WinMedia->new ($u->{'_meta'}->{'OS_media'});
my $os_name = $media_obj->name ();

if ($os_name =~ /Windows 2000/) {
    $u->read (dos_to_host ('z:\\site\\win2k-un.txt'));
}
elsif ($os_name =~ /Windows XP/) {
    $u->read (dos_to_host ('z:\\site\\winxp-un.txt'));
}
else {
    die "Unrecognized OS name: $os_name";
}

1;

Then put the answer files for Windows 2000 and Windows XP in z:\site\win2k-un.txt and z:\site\winxp-un.txt, respectively.

Note the call to dos_to_host. This function does nothing on the DOS-based boot disk, but on the Linux-based boot disk it converts DOS-style file names (e.g., z:\site\foo.txt) to Linux-style (/z/site/foo.txt). It lets you write most config.pl files to work unaltered with either boot disk.

With this code, you will probably prefer to use the Linux-based boot disk. Since this code depends on the OS, it will cause the OS selection question to be asked immediately, even before you select how to partition the drive. (This is actually correct behavior, since you might have partitioning commands in your OS-dependent answer files.) If you must reboot after partitioning, as is usually the case with DOS, you will end up having making the OS selection again.

Assigning ComputerName based on DNS hostname

To automatically set the machine's ComputerName based on the DNS hostname associated with the IP address assigned to the machine:

use warnings;
use strict;
use Socket;
use Net::hostent;

$u->{'UserData'}->{'ComputerName'} =
    sub {
        my $addr = $u->{'_meta'}->{'ipaddr'};
        defined $addr
            or return undef;
        my $host = gethostbyaddr (inet_aton ($addr));

        if (!defined $host) {
            warn "Unable to gethostbyaddr ($addr): $? $^E\n";
            return undef;
        }

        my $name = $host->name ();
        # Strip off domain portion
        $name =~ s/\.(.*)//;
        return $name;
    };

1;

There are two things to note about this code. First, it will only work with the Linux-based boot disk. And second, I have not actually tested it yet. If you try it, please let me know how it goes :-).

More...

More examples to come, someday.


Patrick J. LoPresti patl@users.sourceforge.net