Building a GNU/Linux Based Windows Deployment Server

I find there is a scarcity of coherent and complete internet documentation on how to perform processes like these. Sources I found written in a fashion where the reader is expected to already understands 90% of the background information so I wanted to discuss one method of how to setup a GNU/Linux based Windows deployment server beginning to end in better detail than the sources I had to work with (though I won’t cover how to install the OS itself or basic operation).

Now this server can be used to perform clean installs from extracted Windows.iso files OR complete pre-compiled image files (.WIM) of a Windows instance for large scale deployment over a network.

For this demonstration I’ll be using Ubuntu Server 20.04.5 LTS but these network services are openly available on other distributions including UNIX platforms such as FreeBSD if you rather use your preference. The general process is the same if you know the commands.

1. Downloading Packages & Necessities

It’s going to be the easiest to explain if we go ahead and download everything we need at one time. Enter the Terminal and run the command:

sudo apt install dnsmasq isc-dhcp-server tftpd-hpa apache2 samba make gcc binutils perl mtools liblzma-dev git -y

This takes care of all our repository package needs.

Now we need to download a few things from GitHub:

git clone https://github.com/ipxe/ipxe.git
wget https://github.com/ipxe/wimboot/releases/latest/download/wimboot

Put these in a safe directory for now.

Next we need to create a small folder hierarchy. You can opt to change the location if you wish:
mkdir /win-deploy /win-deploy/http-boot /win-deploy/ipxe-boot /win-deploy/windows

From a Windows PC we need to download the Windows ADK & WinPE Add-On Deployment Tools. Once both are downloaded and installed run Deployment and Imaging Tools Environment located under StartWindows Kits. From here run the command copype amd64 C:\WinPE_amd64. Once complete navigate to C:\WinPE_amd64 and move or copy the contents into a folder called winpe .

From a Windows PC download a windows10.iso or windows11.iso either directly or through the Windows Media Creation Tool. What you do with it from here will depend on what direction you want to go in Step 3. Preparing OS for Deployment.

2. Configuring Network Services

In this section I’ll be going over the preliminary setup of each service in order of operation: DHCP, TFTP, HTTP, & SMB.

2.1 - DHCP

This is where the packages isc-dhcp-server and dnsmasq come into play. You can use one or the other. Which one depends on your needs.

  • isc-dhcp-server advertises it’s own DHCP service. This is only going to want to be used on a network with no other active DHCP server such as a router.
    • If you don’t want or can’t use isc-dhcp-server you can remove it and the TFTP package with sudo apt autoremove isc-dhcp-server tftpd-hpa
  • dnsmasq can provide a multitude of services including TFTP and one such being ProxyDHCP. ProxyDHCP can be used on a network with an existing DHCP server such as a router.
    • If you don’t want or can’t use dnsmasq you can remove it with sudo apt autoremove dnsmasq

2.1.1 - ISC-DHCP-SERVER

Start by editing the servers config file /etc/dhcp/dhcpd.conf . In here define the network you want to advertise (this will bind to the physical interface and network you have configured in /etc/netplan/00-installer-config.yaml), the IP of the TFTP server (next-server), and add an option if statement to serve each client a different iPXE file based on their BIOS being Legacy or UEFI:

subnet 10.0.0.0 netmask 255.255.255.0 {
range 10.0.0.2 10.0.0.254;
next-server 10.0.0.1; }

option client-arch code 93 = unsigned integer 16;
  if option client-arch != 00:00 {
     filename "ipxe.efi";
  } else {
     filename "undionly.kpxe";
  }

Save the file and start the service ensuring that it’s running:

root@WinPE-Server:~# systemctl restart isc-dhcp-server
root@WinPE-Server:~# systemctl status isc-dhcp-server
* isc-dhcp-server.service - ISC DHCP IPv4 server
     Loaded: loaded (/lib/systemd/system/isc-dhcp-server.service; enabled; vendor preset: enabled)
     Active: active (running) since Mon 2023-01-16 03:02:33 UTC; 4s ago
       Docs: man:dhcpd(8)
   Main PID: 8469 (dhcpd)
      Tasks: 4 (limit: 230375)
     Memory: 4.7M
        CPU: 14ms
     CGroup: /system.slice/isc-dhcp-server.service
             `-8469 dhcpd -user dhcpd -group dhcpd -f -4 -pf /run/dhcp-server/dhcpd.pid -cf /etc/dhcp/dhcpd.conf

Jan 16 03:02:33 WinPE-Server dhcpd[8469]: Sending on   LPF/net1/66:28:5e:52:af:75/10.0.0.0/24
Jan 16 03:02:33 WinPE-Server dhcpd[8469]: 
Jan 16 03:02:33 WinPE-Server dhcpd[8469]: No subnet declaration for eth0 (192.168.0.126).
Jan 16 03:02:33 WinPE-Server dhcpd[8469]: ** Ignoring requests on eth0.  If this is not what
Jan 16 03:02:33 WinPE-Server dhcpd[8469]:    you want, please write a subnet declaration
Jan 16 03:02:33 WinPE-Server dhcpd[8469]:    in your dhcpd.conf file for the network segment
Jan 16 03:02:33 WinPE-Server dhcpd[8469]:    to which interface eth0 is attached. **
Jan 16 03:02:33 WinPE-Server dhcpd[8469]: 
Jan 16 03:02:33 WinPE-Server dhcpd[8469]: Sending on   Socket/fallback/fallback-net
Jan 16 03:02:33 WinPE-Server dhcpd[8469]: Server starting service.

At this point in time if we connect a client and PXE boot you should get an IP address and possibly see the TFTP server IP with the ipxe.efi or undionly.kpxe filename being specified before receiving an error message. This is normal.

2.1.2 - DNSMASQ

Start by editing the server’s config file /etc/dnsmasq.conf . In here copy/paste the following parameters substituting the IP’s in dhcp-range & dhcp-boot for your server’s IP:

port=0
tftp-root=/win-deploy/ipxe-boot
dhcp-no-override
pxe-prompt="PXE booting in", 10
pxe-service=X86PC, "Boot from network", undionly.kpxe
pxe-service=X86-64_EFI, "Boot from network", ipxe.efi
dhcp-range=10.0.0.1,proxy
dhcp-boot=,,10.0.0.1

Save the file and start the service ensuring that it’s running:

root@WinPE-Server:~# systemctl restart dnsmasq
root@WinPE-Server:~# systemctl status dnsmasq
* dnsmasq.service - dnsmasq - A lightweight DHCP and caching DNS server
     Loaded: loaded (/lib/systemd/system/dnsmasq.service; enabled; vendor preset: enabled)
     Active: active (running) since Mon 2023-01-16 03:25:32 UTC; 4s ago
    Process: 8507 ExecStartPre=/usr/sbin/dnsmasq --test (code=exited, status=0/SUCCESS)
    Process: 8508 ExecStart=/etc/init.d/dnsmasq systemd-exec (code=exited, status=0/SUCCESS)
    Process: 8517 ExecStartPost=/etc/init.d/dnsmasq systemd-start-resolvconf (code=exited, status=0/SUCCESS)
   Main PID: 8516 (dnsmasq)
      Tasks: 1 (limit: 230375)
     Memory: 536.0K
        CPU: 25ms
     CGroup: /system.slice/dnsmasq.service
             `-8516 /usr/sbin/dnsmasq -x /run/dnsmasq/dnsmasq.pid -u dnsmasq -7 /etc/dnsmasq.d,.dpkg-dist,.dpkg-old,.dpkg-new --local-service --trust-anchor=>

Jan 16 03:25:32 WinPE-Server systemd[1]: Starting dnsmasq - A lightweight DHCP and caching DNS server...
Jan 16 03:25:32 WinPE-Server dnsmasq[8507]: dnsmasq: syntax check OK.
Jan 16 03:25:32 WinPE-Server dnsmasq[8516]: started, version 2.80 DNS disabled
Jan 16 03:25:32 WinPE-Server dnsmasq[8516]: compile time options: IPv6 GNU-getopt DBus i18n IDN DHCP DHCPv6 no-Lua TFTP conntrack ipset auth nettlehash DNSSE>
Jan 16 03:25:32 WinPE-Server dnsmasq-dhcp[8516]: DHCP, proxy on subnet 10.0.0.1
Jan 16 03:25:32 WinPE-Server systemd[1]: Started dnsmasq - A lightweight DHCP and caching DNS server.

At this point in time if we connect a client and PXE boot you should get an IP address from the Router, possibly see the ProxyDHCP server IP, and TFTP server IP with the ipxe.efi or undionly.kpxe filename being specified before receiving an error message. This is normal.

2.2 - TFTP

This is where the tftpd-hpa package and the iPXE GitHub download are going to come into play. We need to:

  1. Configure the TFTP server (skip if using dnsmasq).
  2. Compile iPXE from source code with an embedded script.
  3. Write the main.ipxe file for our boot option menu.

2.2.1 - Configuring TFTP

To start we need to modify the TFTP server configuration file /etc/default/tftpd-hpa to match the following settings:

TFTP_USERNAME="tftp"
TFTP_DIRECTORY="/win-deploy/ipxe-boot"
TFTP_ADDRESS=":69"
TFTP_OPTIONS="--secure"

Save the file, restart the service, and make sure it’s running:

root@WinPE-Server:/# systemctl restart tftpd-hpa
root@WinPE-Server:/# systemctl status tftpd-hpa
* tftpd-hpa.service - LSB: HPA's tftp server
     Loaded: loaded (/etc/init.d/tftpd-hpa; generated)
     Active: active (running) since Mon 2023-01-16 17:07:05 UTC; 44min ago
       Docs: man:systemd-sysv-generator(8)
    Process: 223 ExecStart=/etc/init.d/tftpd-hpa start (code=exited, status=0/SUCCESS)
      Tasks: 1 (limit: 230375)
     Memory: 244.0K
        CPU: 9ms
     CGroup: /system.slice/tftpd-hpa.service
             `-243 /usr/sbin/in.tftpd --listen --user tftp --address :69 --secure /win-deploy/ipxe-boot

Jan 16 17:07:05 WinPE-Server systemd[1]: Starting LSB: HPA's tftp server...
Jan 16 17:07:05 WinPE-Server tftpd-hpa[223]:  * Starting HPA's tftpd in.tftpd
Jan 16 17:07:05 WinPE-Server tftpd-hpa[223]:    ...done.
Jan 16 17:07:05 WinPE-Server systemd[1]: Started LSB: HPA's tftp server.

The server is now prepared for the iPXE files in the coming steps.

2.2.2 - Compiling iPXE from Source w/ Embedded Script

Navigate to the folder where you downloaded iPXE using Git and perform the following operations:

cp -r ipxe/ ipxe2
mv ipxe legacy && mv ipxe2 uefi

These operations are not explicitly necessary but will make sense as we progress.

In the current folder we want to create a script. This script is a set of instructions iPXE will execute upon startup. Copy/paste the contents below into an empty file called embed.ipxe:

#!ipxe
dhcp && goto netboot || goto dhcperror

:dhcperror
prompt --key s --timeout 10000 DHCP failed, hit 's' for the iPXE shell; reboot in 10 seconds && shell || reboot

:netboot
chain tftp://${next-server}/main.ipxe ||
prompt --key s --timeout 10000 Chainloading failed, hit 's' for the iPXE shell; reboot in 10 seconds && shell || reboot

Save, exit, then copy the file into the respective directories:

cp embed.ipxe legacy/src/ && cp embed.ipxe uefi/src/

Now to compile both files if being performed from your home folder (note: both compiles will take a short while to complete):

cd legacy/src/ && make bin/undionly.kpxe EMBED=embed.ipxe
cd
cd uefi/src/ && make bin-x86_64-efi/ipxe.efi EMBED=embed.ipxe
cd

With both files compiled we want to now create main.ipxe. This is the file iPXE will call when it chainloads. Go ahead and create an empty file with this name and copy/paste the following script substituting the URL IP for your servers:

#!ipxe
set url http://10.0.0.1/winpe/media/

menu
item --gap -- ---------------- iPXE boot menu ----------------
item winpe    Launch WinPE
choose target && goto ${target}

:winpe
kernel ${url}Boot/wimboot
initrd ${url}Boot/BCD BCD
initrd ${url}Boot/boot.sdi boot.sdi
initrd -n boot.wim ${url}sources/boot.wim boot.wim
boot

Save, exit, and now to move everything to the TFTP server directory:

mv main.ipxe /win-deploy/ipxe-boot/ && mv legacy/src/bin/undionly.kpxe /win-deploy/ipxe-boot/ && mv uefi/src/bin-x86_64-efi/ipxe.efi /win-deploy/ipxe-boot/

At this stage if you PXE boot a network client PXE should chainload iPXE and the embedded script should load main.ipxe (a blue menu on your screen).

2.3 - HTTP

Due to limitations on the maximum file sizes TFTP can download we need to host an HTTP server. This is where the apache2 package comes into play. Right now if you visit the IP of your server from a web browser you should see an Apache Default Page. We will be doing away with this as we just want it to host large files for iPXE to chainload.

Re-configuring Apache from it’s default state is a very short & easy task starting with editing /etc/apache2/sites-available/000-default.conf to look like the following:

<VirtualHost *:80>
        ServerAdmin [email protected]
        DocumentRoot /win-deploy/http-boot

        ErrorLog ${APACHE_LOG_DIR}/error.log
        CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>

<Directory />
        Options +FollowSymLinks +Indexes
        Require all granted
</Directory>

Save, exit, restart apache2, and make sure the service is running:

root@WinPE-Server:~# systemctl restart apache2
root@WinPE-Server:~# systemctl status apache2
* apache2.service - The Apache HTTP Server
     Loaded: loaded (/lib/systemd/system/apache2.service; enabled; vendor preset: enabled)
     Active: active (running) since Mon 2023-01-16 22:28:05 UTC; 4s ago
       Docs: https://httpd.apache.org/docs/2.4/
    Process: 545 ExecStart=/usr/sbin/apachectl start (code=exited, status=0/SUCCESS)
   Main PID: 549 (apache2)
      Tasks: 55 (limit: 230375)
     Memory: 5.3M
        CPU: 32ms
     CGroup: /system.slice/apache2.service
             |-549 /usr/sbin/apache2 -k start
             |-550 /usr/sbin/apache2 -k start
             `-551 /usr/sbin/apache2 -k start

Jan 16 22:28:05 WinPE-Server systemd[1]: Starting The Apache HTTP Server...
Jan 16 22:28:05 WinPE-Server systemd[1]: Started The Apache HTTP Server.

Now copy the entire winpe folder created on the Windows PC into the servers /win-deploy/http-boot directory using your choice of SFTP, a thumb drive, or other copy method. Additionally move wimboot from it’s downloaded directory to the new winpe sub-folder on the server with:
mv wimboot /win-deploy/http-boot/winpe/media/Boot/

At this stage if you start a PXE client DHCP should chainload iPXE, which will fetch main.ipxe from TFTP thanks to the embedded script, then by manually choosing the first option listed on the screen fetch the kernel and boot files needed from HTTP to enter you into the Windows Pre-installation Enviroment or WinPE for short:

X:\windows\system32>wpeinit

X:\windows\system32>

2.4 - SMB

This is where the samba package comes in. This is a relatively simple to setup SMB network share for Windows users. To start we need to create a SAMBA user account with the same name as the system administrator account (other than root). These are accounts that coincide with system user accounts for the purpose of SAMBA. You can add a SAMBA user with the example:

smbpasswd -a admin
New SMB password:
Retype new SMB password:
Added user admin.

Afterwords we want to define the parameters that govern the network share by appending to /etc/samba/smb.conf the following information:

[windows]

path = /win-deply/windows
available = yes
valid users = admin
read only = no
browseable = yes
public = yes
writable = yes

The first line inside the brackets is what people on the network will see the share named as. Everything else including unlisted options can be modified but this will work for our application.

Next change the owner of /win-deploy/windows to the system user you created a SAMBA account for with chown admin:admin /win-deploy/windows.

Now restart the service and make sure it’s running:

root@WinPE-Server:~# systemctl restart smbd
root@WinPE-Server:~# systemctl status smbd
* smbd.service - Samba SMB Daemon
     Loaded: loaded (/lib/systemd/system/smbd.service; enabled; vendor preset: enable>
     Active: active (running) since Tue 2023-01-17 00:30:38 UTC; 20s ago
       Docs: man:smbd(8)
             man:samba(7)
             man:smb.conf(5)
    Process: 799 ExecStartPre=/usr/share/samba/update-apparmor-samba-profile (code=ex>
   Main PID: 808 (smbd)
     Status: "smbd: ready to serve connections..."
      Tasks: 4 (limit: 230375)
     Memory: 6.6M
        CPU: 72ms
     CGroup: /system.slice/smbd.service
             |-808 /usr/sbin/smbd --foreground --no-process-group
             |-810 /usr/sbin/smbd --foreground --no-process-group
             |-811 /usr/sbin/smbd --foreground --no-process-group
             `-812 /usr/sbin/smbd --foreground --no-process-group

Jan 17 00:30:38 WinPE-Server systemd[1]: Starting Samba SMB Daemon...
Jan 17 00:30:38 WinPE-Server systemd[1]: Started Samba SMB Daemon.

We can check to see if it’s working by connecting to it from our running WinPE instance:

X:\windows\system32>net use n: \\10.0.0.1\windows
The password is invalid for \\10.0.0.1\windows.

Enter the user name for '10.0.0.1': admin
Enter the password for 10.0.0.1:
The command completed successfully.

X:\windows\system32>n:

N:\

If everything executes as exampled above you’re in good shape.

3. Preparing OS for Deployment

At this stage there are two directions you can go. One is much more complicated to do than the other but saves a considerable amount of time when needed for large scale deployment.

3.1 - Using Windows Setup Files (easy but only for small scale)

If you don’t have many clients to install a clean OS onto you can opt for this solution by mounting the windows10.iso on a Windows client connected to the installing servers network. Windows supports the ability to mount .ISO’s to virtual CD drives just by double-clicking or using Right-Click → Mount.

Connect to the servers SAMBA (SMB) network share using your credentials and copy the contents of the mounted .ISO file to a folder such as w10.

After this return to a WinPE client, re-connect to the SMB share if necessary, and enter this folders directory. With the dir command you should now see all of the Windows setup files:

X:\windows\system32>n:

n:\>cd w10

n:\w10>dir
 Volume in drive N is windows
 Volume Serial Number CD92-6DB2
 
 Directory of n:\w10
01/17/2023  08:50 AM   <DIR>          .
01/17/2023  08:48 AM   <DIR>          ..
09/08/2023  12:07 AM              128 autorun.inf
01/17/2023  08:49 AM   <DIR>          boot
09/08/2022  12:07 AM          413,738 bootmgr
09/08/2022  12:07 AM        1,541,648 bootmgr.efi
01/17/2023  08:49 AM   <DIR>          efi
09/08/2022  12:07 AM           74,184 setup.exe
01/17/2023  08:50 AM   <DIR>          sources
01/17/2023  08:50 AM   <DIR>          support
               4 File(s)     2,029,698 bytes
               6 Dir(s) 130,635,268,096 bytes free
               
n:\w10>

From here simply running setup.exe will initiate the installation process as if you were using local media such as a bootable USB:

Within reason this can be performed on as many simultaneous clients as you desire.

3.2 - Using an Extracted Windows.wim Image File (hard but good for large scale)

If you have a large number of identical clients all of whom need the latest Windows updates, 3rd party applications installed, user accounts, and customized settings such as Active Directory (AD) or firewall options you’re going to want the most time efficient method which would be this however the setup is a fair bit more complicated.

3.2.1 - Initial Setup

To start you need a sacrificial install of Windows with everything configured as you want it. The reason I say sacrificial is because as part of the setup process, for unknown reasons, it sometimes bricks the Windows install. So proceed at your own risk.

Once you have your install configured just the way you want it create an empty text file in an easy directory like C:\ and copy/paste the following instructions:

<?xml version="1.0" encoding="utf-8"?>
<unattend xmlns="urn:schemas-microsoft-com:unattend">
    <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>
                <HideLocalAccountScreen>true</HideLocalAccountScreen>
                <HideOEMRegistrationScreen>true</HideOEMRegistrationScreen>
                <HideOnlineAccountScreens>true</HideOnlineAccountScreens>
                <HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE>
                <NetworkLocation>Home</NetworkLocation>
                <ProtectYourPC>3</ProtectYourPC>
                <SkipMachineOOBE>true</SkipMachineOOBE>
                <SkipUserOOBE>true</SkipUserOOBE>
                <UnattendEnableRetailDemo>false</UnattendEnableRetailDemo>
            </OOBE>
        </component>
    </settings>
</unattend>

Now rename the file deploy.xml making sure the file extension changed from .txt to .xml .

Next launch an Administrative PowerShell and from the C:\Windows\System32 directory move to the \Sysprep directory:

Windows PowerShell
Copyright (C) Microsoft Corporation. All rights reserved.

Try the new cross-platform PowerShell https://aka.ms/pscore6

PS C:\Windows\system32> cd .\Sysprep\
PS C:\Windows\system32\Sysprep>

In order to prepare the OS for large scale deployment we want the following operations to occur:

  1. Out Of Box Experience - We want OOBE so that the windows instance behaves as though it’s brand new.

  2. Generalize - There are many attributes about the current install you do not want to image to other computers such as the Product Key, Computer Name, Hardware information, Wi-Fi credentials, among a plethora of other details.

  3. Shutdown - Automatically shutdown the computer once Sysprep is done with it’s job.

  4. Unattend - This will point to our deploy.xml file. It’s purpose is to skip the normal Windows operations at first startup since everything has already been defined for Sysprep by us.

In the language of PowerShell that will look like this:

sysprep.exe /oobe /generalize /shutdown /unattend:c:\deploy.xml

Assuming you do not immediately receive a Sysprep Was Not Able to Validate Your Windows Installation error (if you do though please refer to the .log file for details on the reason) Sysprep will slowly run the preparation process and shut down the computer.

At this time PXE boot this machine and load WinPE as we need to create a windows.wim file from the C:\ partition. Once WinPE has loaded the connected drive should have automatically mounted the C:\ partition with it’s respective drive letter. If not there are steps to assign it. Now connect to the SAMBA (SMB) network share.

With both the C:\ drive and N:\ share accessible it’s time to image the partition with:

dism /capture-image /imagefile:"n:\windows.wim" /capturedir:c:\ /name:windows

This will launch DISM the Deployment Image Servicing and Management tool. If everything goes correctly the windows.wim file will be created after a couple minutes and deposited onto the SAMBA (SMB) network share folder.

3.2.2 - Manual Deployment

To perform a manual deployment we need to create a few files and put them on the SAMBA (SMB) server. These files include:

  • ApplyImage.bat
  • uefi.txt
  • legacy.txt

The contents of each file are as follows:

ApplyImage.bat:

@echo off
rem == ApplyImage.bat ==

rem == These commands deploy a specified Windows
rem    image file to the Windows partition, and configure
rem    the system partition.

rem    Usage:   ApplyImage WimFileName 
rem    Example: ApplyImage E:\Images\ThinImage.wim ==

rem == Set high-performance power scheme to speed deployment ==
call powercfg /s 8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c

rem == Apply the image to the Windows partition ==
dism /Apply-Image /ImageFile:%1 /Index:1 /ApplyDir:W:\

rem == Copy boot files to the System partition ==
W:\Windows\System32\bcdboot W:\Windows /s S:

:rem == Verify the configuration status of the images. ==
W:\Windows\System32\Reagentc /Info /Target W:\Windows

echo All operations executed. Press any key to shutdown.
pause >nul
wpeutil shutdown

uefi.txt:

rem == CreatePartitions-UEFI-FFU.txt ==
rem == These commands are used with DiskPart to
rem    create four partitions
rem    for a UEFI/GPT-based PC.
rem    Adjust the partition sizes to fill the drive
rem    as necessary. ==
select disk 0
clean
convert gpt
rem == 1. System partition =========================
create partition efi size=100
rem    ** NOTE: For Advanced Format 4Kn drives,
rem               change this value to size = 260 ** 
format quick fs=fat32 label="System"
assign letter="S"
rem == 2. Microsoft Reserved (MSR) partition =======
create partition msr size=16
rem == 3. Windows partition ========================
rem ==    a. Create the Windows partition ==========
create partition primary 
rem ==    c. Prepare the Windows partition ========= 
format quick fs=ntfs label="Windows"
assign letter="W"
list volume
exit

legacy.txt:

rem == CreatePartitions-BIOS-FFU.txt ==
rem == These commands are used with DiskPart to
rem    create three partitions
rem    for a BIOS/MBR-based computer.
rem    Adjust the partition sizes to fill the drive
rem    as necessary. ==
select disk 0
clean
rem == 1. System partition ======================
create partition primary size=100
format quick fs=ntfs label="System"
assign letter="S"
active
rem == 2. Windows partition =====================
rem ==    a. Create the Windows partition =======
create partition primary
rem ==    c. Prepare the Windows partition ====== 
format quick fs=ntfs label="Windows"
assign letter="W"
list volume
exit

The contents of each file can be customized quite a bit to suit your application needs including details on how to include a Recovery Partition if your deployment requires it. Documentation on this can be found on Microsoft’s website. With the files created move them to the SAMBA (SMB) share.

To deploy the windows.wim image first determine weather the computer to be imaged is booted into Legacy or UEFI mode as this will determine how the drive is to be partitioned. Once this is known connect to the SAMBA (SMB) share and run the single respective partitioning script. Note this is only recommended if the computer has a single storage drive:

diskpart /s n:\uefi.txt
diskpart /s n:\legacy.txt

Regardless of disk capacity these scripts as they are will fill the drive.

Now to apply the image file:

N:\ApplyImage.bat N:\windows.wim

If everything works correctly the created partitions will be prepped and the .wim file written to the primary partition. Once complete you should be offered to shutdown the machine. Once turned back on it may take a few minutes but you will be dropped to the Windows login screen or desktop.

3.2.3 - Automated Deployment

If you want to make things go even faster you can automate the WinPE process with an embedded script by modifying X:\Windows\System32\startnet.cmd but to do this is a process in itself.

To start begin by creating the three files outlined in 3.2.2 - Manual Deployment and upload them to the server.

From here connect to the HTTP server and download /winpe/amd64/media/sources/boot.wim. Boot.wim is the core of WinPE. Once downloaded put it in an easy directory like C:\ then create a folder called winpe next to it.

Open an Administrative PowerShell and run the command:

Dism /Mount-image /imagefile:c:\boot.wim /index:1 /mountdir:c:\winpe

This cracks open WinPE’s own .wim file and makes it so WinPE in itself can be modified.

From here enter the winpe folder you created and navigate to /Windows/System32/startnet.cmd . This file is only responsible for executing a single program wpeinit but you can append to and modify the file to suit your auto deployment needs beyond that. For example:

@echo off
wpeinit

echo Connecting to SMB Server.
:CONNECTING
net use n: \\10.0.0.4\windows userpass /user:\username
if %errorlevel% equ 0 (goto :IMAGE) else (goto :CONNECTING)

:IMAGE
n:\image.bat

This modification to startnet.cmd connects WinPE to the SMB server at startup. If there is an error it will try again and again until the error clears or the user intervenes. It will them run a batch file called image.bat.

To save the changes made to boot.wim and unmount it run the command:

dism /unmount-image /mountdir:c:\winpe /commit

With the file now updated with our modifications re-upload it to the HTTP server in the same directory we got it from replacing the currently existing one.

Now in the SMB share create a file called image.bat in the highest directory. Below is an example of what image.bat will look like:

cls
if defined SPN (goto RETRY)
@echo off

wpeutil UpdateBootInfo
for /f "tokens=2* delims=	 " %%A in ('reg query HKLM\System\CurrentControlSet\Control /v PEFirmwareType') DO SET Firmware=%%B

echo Checking if PC is booted in Legacy or UEFI mode.
@if x%Firmware%==x echo ERROR: Cannot determine boot method. Manual entry required. & goto CHOICE
@if %Firmware%==0x1 echo Formatting storage drive for Legacy Boot. & diskpart /s N:\legacy.txt
@if %Firmware%==0x2 echo Formatting storage drive for UEFI Boot. & diskpart /s N:\uefi.txt
if %errorlevel% equ 0 (goto IMG) else (echo ERROR: Diskpart ran into an issue during formatting! & ver > nul & goto SHELL)

:CHOICE
echo To format storage for Legacy Boot run: diskpart /s N:\legacy.txt
echo To format storage for UEFI Boot run: diskpart /s N:\uefi.txt
echo ------------------------------------------------------------- 
echo To complete install run: N:\image.bat
goto SHELL

:IMG
echo.
for /f "tokens=2* delims=	 " %%A in ('reg query HKLM\HARDWARE\DESCRIPTION\System\BIOS /v SystemProductName') DO SET SPN=%%B
echo BIOS system product name is "%SPN%".
echo Locating %SPN% Windows image...
echo.

:RETRY
if "%SPN%"=="10M8S07U00" (SET IMAGE=ltcm710st-w11.wim)
if "%SPN%"=="10MAS02R00" (SET IMAGE=ltcm710st-w11.wim)
if "%SPN%"=="10MAS02R1X" (SET IMAGE=ltcm710st-w11.wim)
if "%SPN%"=="10MQS0C300" (SET IMAGE=ltcm710q-w11-v2.wim)
if "%SPN%"=="10SRS0GJ00" (SET IMAGE=ltcm720st-w11.wim)
if "%SPN%"=="10SRS0GJ0Q" (SET IMAGE=ltcm720st-w11.wim)
if "%SPN%"=="10SUS1AU00" (SET IMAGE=ltcm720st-w11.wim)
if "%SPN%"=="10T8S1NC00" (SET IMAGE=ltcm720q-w11-v2.wim)
if "%SPN%"=="20L6S37P00" (SET IMAGE=ltpt480-w11.wim)
if "%SPN%"=="20LAS1G200" (SET IMAGE=ltpt580-w11.wim)
if "%SPN%"=="CF-20-1" (SET IMAGE=pcf-20-w10.wim)
if "%SPN%"=="HP EliteDesk 800 G2 SFF" (SET IMAGE=hped800g2sff-w11.wim)
if "%SPN%"=="HP EliteDesk 800 G3 SFF" (SET IMAGE=hped800g3sff-w11.wim)
if "%SPN%"=="HP EliteBook 840 G3" (SET IMAGE=hp840g3-w10-v2.wim)
if "%SPN%"=="HP EliteBook 840 G5" (SET IMAGE=hp840g5-w11-v2.wim)
if "%SPN%"=="HP EliteBook 840 G6" (SET IMAGE=hp840g6-w11-v2.wim)
if "%SPN%"=="HP ProDesk 400 G3 DM" (SET IMAGE=hppd400g3dm-w10.wim)
if "%SPN%"=="HP ProDesk 600 G3 MT" (SET IMAGE=hppd600g3mt-w10.wim)
if "%SPN%"=="HP ProDesk 600 G4 SFF" (SET IMAGE=hppd600g4sff-w11.wim)
if "%SPN%"=="HP Z640 Workstation" (SET IMAGE=hpz640-w11.wim)
if "%SPN%"=="HP Z6 G4 Workstation" (SET IMAGE=hpz6g4-w11.wim)
if "%SPN%"=="Latitude 5290 2-in-1" (SET IMAGE=dl5290_2in1-w11.wim)
if "%SPN%"=="Latitude 5300 2-in-1" (SET IMAGE=dl5300_2in1-w11.wim)
if "%SPN%"=="Latitude 5310 2-in-1" (SET IMAGE=dl5310_2in1-w11.wim)
if "%SPN%"=="Latitude 5400" (SET IMAGE=dl5400-w11.wim)
if "%SPN%"=="Latitude 5490" (SET IMAGE=dl5490-w11.wim)
if "%SPN%"=="Latitude 5500" (SET IMAGE=dl5400-w11.wim)
if "%SPN%"=="Latitude 5590" (SET IMAGE=dl5590-w11.wim)
if "%SPN%"=="Latitude 7200 2-in-1" (SET IMAGE=dl7200_2in1-w11.wim)
if "%SPN%"=="Latitude 7212 Rugged Extreme Tablet" (SET IMAGE=dl7212ret-w11.wim)
if "%SPN%"=="Latitude 7300" (SET IMAGE=dl7300-w11.wim)
if "%SPN%"=="Latitude 7390" (SET IMAGE=dl7390-w11.wim)
if "%SPN%"=="Latitude 7390 2-in-1" (SET IMAGE=dl7390_2in1-w11.wim)
if "%SPN%"=="Latitude 7400" (SET IMAGE=dl7400-w11.wim)
if "%SPN%"=="Latitude 7400 2-in-1" (SET IMAGE=dl7400_2in1-w11.wim)
if "%SPN%"=="Latitude 7490" (SET IMAGE=dl7490-w11.wim)
if "%SPN%"=="OptiPlex 3050" (SET IMAGE=dop3050m-w10.wim)
if "%SPN%"=="OptiPlex 3060" (SET IMAGE=dop3060t-w11.wim)
if "%SPN%"=="OptiPlex 3070" (SET IMAGE=dop3060t-w11.wim)
if "%SPN%"=="OptiPlex 5050" (SET IMAGE=dop5050t-w10.wim)
if "%SPN%"=="OptiPlex 5060" (SET IMAGE=dop5060t-w11.wim)
if "%SPN%"=="OptiPlex 5070" (SET IMAGE=dop5060t-w11.wim)
if "%SPN%"=="Precision 3630 Tower" (SET IMAGE=dp3630t-w11.wim)
if "%SPN%"=="Precision 7530" (SET IMAGE=dp7530-w11.wim)
if "%SPN%"=="Precision 7540" (SET IMAGE=dp7540-w11-v2.wim)
if "%SPN%"=="Surface Pro 6" (SET IMAGE=msurfacepro6-w11.wim)
if "%SPN%"=="XPS 13 9365" (SET IMAGE=dxps13-9365-w11.wim)
if "%SPN%"=="XPS 13 9380" (SET IMAGE=dxps13-9380-w11.wim)

if defined IMAGE (if exist N:\%IMAGE% (echo Image file found: %IMAGE% & N:\ApplyImage.bat N:\%IMAGE%))

echo ERROR: No image file for %SPN% could be located. Please make sure:
echo.
echo 1. That a .WIM file for this computer was created.
echo 2. That the file is present on the SMB server.
echo 3. That an entry was added to this script file.
echo.
echo Press any key to restart the image script after correcting the problem.
pause >nul
N:\image.bat

:SHELL
@echo on

Now it looks like a whole lot is going on here but the process is really quite linear and simple.

  1. First thing that happens is the script figures out weather we’re booting in Legacy or UEFI mode. NOTE after delims= you MUST use Tab + Space. Otherwise it will not work!

  2. Based on this condition the script then partitions the storage drive accordingly using diskpart and the appropriate .txt instructions.

  3. The script now queries the system for it’s SystemProductName and assigns it to a variable.

  4. From here the SystemProductName is compared against a list of known systems you will have to define yourself. If a match is found the name of that computers .WIM file is assigned to a variable.

  5. At the end of the list if a match was found and if the .WIM file exists the deploy script is ran using our assigned variable containing the .WIM file name and deploys it to the compatible computer(s) as needed.

Issues you may run into are computers that share identical products names but which have slight hardware variations. This may cause missing drivers in Device Manager. Solutions can be to daisy-chain Sysprep over the different models until the .WIM file you have sufficiently contains all the drivers needed for each variant.

For example not all Precision 7530’s are identical. Some may have an extra Intel 300 series family chipset. You may want to sysprep over one, then image another with said variation to cover both bases. It’s not perfect but the extra driver shouldn’t be assigned to anything if the hardware device isn’t present.

3 Likes

Wow amazing writeup :clap:

1 Like

Thanks. Current work in progress is figuring out if I can get WinPE to self-identify a computer and install a given .wim file accordingly but it’s not going well. :confused:

Supposedly WinPE is supposed to support WMIC but the command doesn’t work and the only discussion I see saying it does dates back to 2010~2012.

This is all the reason I want to share this information but finding it myself is a grind. :sweat_smile:

WMIC has been removed from 10, 11, and Server as of last year. I’m presuming they probably removed it from WinPE too.

I would figure as much but that could be OK as I might have just found a suitable alternative. Will need to do a small field test first to confirm though. I’m not going to add it to the Wiki til I can confirm if it’s useful:

From WinPE if you make a regestry request to HKLM\HARDWARE\DESCRIPTION\System\BIOS there is a field called SystemProductName. Which sucesfully identified the name of my motherboard. I can plug this into a condition and use if statements to make WinPE perform a different operation based on what it reads automating the process without the need for multiple boot.wim files.

Will probably end up working on some motherboards and not others though…

WMCI still there as of Windows 11 22H2

C:\Users\Dave>wmic bios get serialnumber
SerialNumber
To be filled by O.E.M.


C:\Users\Dave>ver

Microsoft Windows [Version 10.0.22621.1105]

Yeah, it makes me wonder if there’s an argument I need to add into copype amd64 C:\WinPE_amd64 to include the WMIC tool because it doesn’t make sense that the desktop would still have it but WinPE would drop it.

Interestingly if you read some of the file names in X:\Windows\Sysem32> there are at least three .dll files with WMI in the name so :man_shrugging: makes me think support isn’t totally dropped in WinPE but the .exe is definitely missing. This was compiled using the Windows ADK for Windows 10 ver: 2004.

Apparently it’s an optional feature now from my brief research. Maybe disabled by default on new installs?

Seems to be included in Windows 11 Pro. I’ve been deploying a lot of Windows 11 Pro machines lately from a custom autounattend.xml that I put together. I have a couple of batch files that use wmic for various functions, so I guess it’s still shipped with it.

I still need to test it with a broader scope of computers but I had the opportunity today to test this:

wpeutil UpdateBootInfo
for /f "tokens=2* delims=	 " %%A in ('reg query HKLM\HARDWARE\DESCRIPTION\System\BIOS /v SystemProductName') DO SET SPN=%%B
if %SPN%==X10DRi-T echo Located. Applying image... & N:\ApplyImage.bat N:\windows.wim

WinPE just makes a registry query to the BIOS and assigns the product name to a variable. I can then put that in an IF condition and do whatever I want with it.

Today I was able to test it with HP Elitebook G5/G6’s & Dell OptiPlex 3060 Tinys. WinPE had no trouble distingushing the clients based on this and running the image script with the desired .WIM file.

I plan to continue testing with more models.

Nice writeup. Just out of curiosity, would FOG be an alternative if you don’t want to build the server from scratch?

https://wiki.fogproject.org/wiki/index.php?title=Introduction#What_is_FOG

I have only briefly read into FOG but it sounds promosing. My first introduction to PXE/iPXE as a whole was building it from scratch and don’t get me wrong it’s a heck of a hurdle going in blind.

If you need something simpler and user friendly you might like to look into TinyPXE for Windows. That works for clean installs. Might possibly work for .WIM’s if you change some settings.

Alternatively if you break it down the process to do it from scratch isn’t as overwhelming as the guide might make it look. Once you get the hang of it you can have it built from scratch in a matter of a couple hours. In addition to that if you virtualize it you can treat it like a lego block and plug’n’play it into existing infrastructure faster than deploying a WebUI/GUI solution from scratch. A sort of one and done deal.

1 Like

I’ve gone ahead and updated step 3.2.3 - Automated Deployment. It now includes what I’ve tested to so far be an objectively better solution for automation and accuracy.