Skip to main content.

Dynamic configuration with server-side perl scripts

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 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;

Some examples

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.

Using a database

If you wish to store and organize installation settings for a large number of computers you are best off with a database of some sort. This is where Unattended really shines in large organizations.

Unattended offers two options:

Setting up a CSV flat file system

Setting up a MySQL system

Making the most of your Database

If you prefer to have a more straightforward list of MAC addresses associated with computer names, organization names, passwords, etc., you could create this list and then generate the MySQL DB or CSV file that Unattended prefers from this list.

Further development

If you wish to modify unattend.txt setting that Unattended does not have properties for, you can use a database to generate the Unattended MySQL database linking MAC addresses -> computer names and computer names -> specific unattend.txt file. Then you use your master database to generate these unattend.txt files.

In Active Directory environments, remember you don't need to do everything with Unattended. Software can be added and settings configured with Group Policy. With the ComputerName and MachineObjectOU you can add the computer directly to the OU you want it in.

If you developed other database-modules (e.g. for LDAP or PostgreSQL or such) to meet your environment, we would love to hearing from you and appreciate your contribution!