Fully automated Linux desktop install by preseeding a PXE booted LiveCD



PXE booting a Linux distribution is not difficult, and neither is preseeding the installation process. But combining the two for a fully automated Linux desktop installation over PXE in my home network took quite a lot of googling and trial+error. This post covers three non-obvious facts I learned which hopefully will save you some time.
----

After you've done it a few times, installing an OS from a CD/DVD becomes very boring. Faced with yet another laptop to wipe and install Linux Mint on, I decided the days of selecting my Time Zone and partitioning scheme in an installer GUI were over.
It's time to automate!

The end goal

My end goal is to automate the installation process, so all I end up doing is:
  1. Download the Linux distribution ISO file from internet, and store it on my local file server
  2. Plug a network cable into the machine to be upgraded
  3. PXE boot the machine, and select the ISO to install from a menu
  4. Come back half an hour later to find the distribution successfully installed on the hard disk, exactly as I've specified in the preseed file.
No DVD to burn, no installer questions to answer. What's not to like?

The preseed file provides some mechanisms for unattended system tweaking, and this may be all you need. But tools like Ansible, Salt, Puppet, Chef, etc. provide more flexibility for more advanced post-installation tweaks, so as a secondary goal I want the preseed file to also configure the machine for post-install, effortless running of my Ansible scripts. To this end, the preseed file instructs the installer to:
  • create a particular user/password, capable of sudo
  • install a SSH server, start it on every boot, and make it listen on my preferred port (not 22)
  • NFS mount my file server's shared directory of Ansible scripts
If I can accomplish this, the only manual work after the unattended installation has finished will be to:
  • change the user's password, and
  • kick off my Ansible scripts, which tweak my system further
and I will end up with a desktop machine configured just the way I prefer it, with minimal effort.

Three nuggets of info to get us there

Unfortunately most internet sources leave out three facts which can save you days of work when trying to preseed a PXE booted LiveCD installation.
I'll go through them here.

1. A proxy DHCP server saves you from touching your DHCP server

My home router also acts as the network's DHCP server, and not surprisingly this consumer market device does not support PXE booting clients. (I'm reluctant to delegate the role of DHCP server to e.g. my NAS or a separate Linux machine, because I have to be able to shut down everything but my router, and still have internet access from the household's smart phones).

No worries, the PXE specification already has a solution: the proxy DHCP server (Wikipedia)

There are many ways to add a proxy DHCP server to your network, do whatever works for you. My solution was to dig out an old Raspberry Pi model A (abandoned long time ago when the newer models came out), load the latest version of Raspian onto its SD card, boot up the Pi, and simply add 4 lines to the end of its /etc/dnsmaq.conf:

interface=eth0
dhcp-range=10.2.0.0,proxy
dhcp-boot=pxelinux.0
pxe-service=x86PC,"Raspian DHCP forwards to TFTP server for PXE-files",pxelinux,10.2.0.102


This tells PXE clients in my LAN (range 10.2.0.0 - 10.2.0.255) to request the file pxelinux.0 from the TFTP server at address 10.2.0.102 (i.e. the static address of my file server, running TFTP). Note that I don't keep any pxelinux files, images, or preseed files on the Pi, it's simply a proxy DHCP server, so there is no need to enable TFTP on the Pi.

Thanks to iT-Joe for sharing the Pi proxy DHCP configuration.

2. Unpacking the ISO is an alternative to mounting it

Most guides assume you will mount the ISO on the NFS server's file system, but what if your file server doesn't have this capability, or the mount point does not survive reboots of the NFS server?
Easily solved; simply unpack the ISO, so it becomes a tree of files on your file server. (Disclaimer: Only tested for Linux Mint)

3. IMPORTANT! How to separate your installation parameters from your target system parameters 

Providing the right parameters to the relevant "layer" of the installation process is what makes preseeding a PXE booted desktop LiveCD installation tricky:
  1. PXE parameters - The "PXE layer" needs to know the name and location of the kernel and initrd to boot, the boot method, and how/where to find the rest of the distribution files
  2. Install system parameters - Once the Linux system has booted and the window manager presents the desktop, we need to tell it to start Ubiquity (the installer for LiveCDs). Ubiquity will stop early in the installation process unless we up front provide it a locale and keyboard configuration to use during installation, outside of the preseed file. Naturally, we also need to tell Ubiquity to use a preseed file and provide the URL to it.
  3. Target system parameters - The preseed file defines how the target system should be configured. The file's preseeding directives must be recognized as such by Ubiquity
The key point when lining up these ducks is to understand how the lone double dash (' -- ') is interpreted by Linux when it digests boot parameters. Here is an explanation or two

And here (drum roll!)  is how you combine it all as a stanza in your pxelinux.cfg/default file:

LABEL LinuxMint-18-Cinnamon
    MENU LABEL Wipe disk and install Linux Mint 18
    KERNEL images/linuxmint/18/x86_64/casper/vmlinuz
    APPEND initrd=images/linuxmint/18/x86_64/casper/initrd.lz boot=casper netboot=nfs nfsroot=10.2.0.102:/volume1/pxe/images/linuxmint/18/x86_64 -- locale=en_US keyboard-configuration/modelcode=SKIP automatic-ubiquity url=http://10.2.0.102/mypreseeds/linuxmint18-preseed.txt


Let's dissect the APPEND. First of all, it is a single line, although your browser will insert line breaks for improved readability.
Note the '--':
  • Everything in front of '--' is used by pxelinux to netboot the kernel and ramdisk from their respective paths over NFS, before handing over to casper to pull the rest of the distro files from the specified NFS root path (of my Synology NAS file server)
  • Everything behind '--' is used by the Linux Mint 18 system, after Linux Mint has booted for the very first time. 'automatic-ubiquity' starts Ubiquity, and the subsequent URL points to my preseed file (on my Synology NAS' HTTP server).  From my discussion above you will recognize the parameters specifying the locale and keyboard configuration. Without them Ubiquity will hang forever.

And that's really all there is to parameter separation.

My preseed file

For reference I'm pasting in my Linux Mint 18 preseed file here, it should work for Ubuntu desktops too.  A couple of points worth noticing about this file:
  • For my secondary automation goal, see the success_command at the end of the file. in-target means the shell command runs in the target system's shell (affecting the installed system, usually what you want), not the installer system's shell (which runs in RAM and is lost after the reboot)
  • Make sure you end all but the last line of a multi-line success_command with '\'. Failing to do so will not trigger an error message, the remaining lines are simply ignored (!!!)
  • The 'select no' for xkb-keymap and layoutcode selects a Norwegian keyboard, don't confuse this with a negative answer (No vs. Yes)
  • Don't preseed passwords in clear text. Preseed a hashed password instead, or find some other method.
  • Typically I use echo to append text to files, but my echoes in the success_command were consistently ignored, so I ended up using sed instead. (Please leave a comment if you can explain why. Maybe the '>>' confuses Ubiquity?).
 

# My working preseed file for LinuxMint 18 Cinnamon

d-i     localechooser/supported-locales    en_US.UTF-8
d-i     keyboard-configuration/xkb-keymap  select no
d-i     keyboard-configuration/layoutcode  string no

d-i     debian-installer/splash            boolean false
d-i     console-setup/ask_detect           boolean false
d-i     console-setup/layoutcode           string no
d-i     console-setup/variantcode          string

### Partitioning
# If the system has free space you can choose to only partition that space.
# This is only honoured if partman-auto/method (below) is not set.
#d-i partman-auto/init_automatically_partition select biggest_free

# Alternatively, you may specify a disk to partition. If the system has only
# one disk the installer will default to using that, but otherwise the device
# name must be given in traditional, non-devfs format (so e.g. /dev/sda
# and not e.g. /dev/discs/disc0/disc).
# For example, to use the first SCSI/SATA hard disk:
#d-i partman-auto/disk string /dev/sda
# In addition, you'll need to specify the method to use.
# The presently available methods are:
# - regular: use the usual partition types for your architecture
# - lvm:     use LVM to partition the disk
# - crypto:  use LVM within an encrypted partition
d-i partman-auto/method string regular

# You can choose one of the three predefined partitioning recipes:
# - atomic: all files in one partition
# - home:   separate /home partition
# - multi:  separate /home, /var, and /tmp partitions
d-i partman-auto/choose_recipe select multi

d-i     partman/default_filesystem                     string ext3
d-i     partman/choose_partition                       select finish

# If one of the disks that are going to be automatically partitioned
# contains an old LVM configuration, the user will normally receive a
# warning. This can be preseeded away...
d-i partman-lvm/device_remove_lvm boolean true
# The same applies to pre-existing software RAID array:
d-i partman-md/device_remove_md boolean true
# And the same goes for the confirmation to write the lvm partitions.
d-i partman-lvm/confirm             boolean true
d-i partman-lvm/confirm_nooverwrite boolean true

# Just in case, the positive answer to all other imagineable conformation questions:
d-i     partman-partitioning/confirm_write_new_label   boolean true
d-i     partman/confirm                                boolean true
d-i     partman/confirm_nooverwrite                    boolean true
d-i     partman/confirm_write_new_label                boolean true
d-i     partman-md/confirm                             boolean true

# Time
d-i     time/zone               string  Europe/Oslo
d-i     clock-setup/utc         boolean true
d-i     clock-setup/ntp         boolean true
d-i     clock-setup/ntp-server  string ntp.ubuntu.com

#   LinuxMint will *demand* a user during installation, ignoring the value
#   of the passwd/make-user flag, so we need to provide the user data here.
#   You should not preseed the password in clear text, this is just an example!
d-i     passwd/user-fullname            string ubuntu
d-i     passwd/username                 string ubuntu
d-i     passwd/user-password            password ubuntu
d-i     passwd/user-password-again      password ubuntu
d-i     user-setup/allow-password-weak  boolean true
d-i     user-setup/encrypt-home         boolean false

# Use non-free packages
ubiquity ubiquity/use_nonfree boolean true

# -------- Customize at the end of a successful installation.

# Ubiquity completly ignores the debian installer command 'preseed/late_command',
# instead we need to use 'ubiquity/success_command'.

# We permanently enable the wired network,
# install OpenSSH, and move its port to 12345,
# install Ansible locally (just in case),
# and add a NFS mount point in /etc/fstab to my Ansible scripts
# (which will be automatically mounted after the reboot).
ubiquity ubiquity/success_command string \
  in-target sed -i 's/^managed=false/managed=true/'/etc/NetworkManager/NetworkManager.conf; \
  in-target apt-get -y install openssh-server; \
  in-target sed -i 's/Port 22/Port 12345/' /etc/ssh/sshd_config; \
  in-target apt-get -y install nfs-common; \
  in-target apt-get install software-properties-common;\
  in-target apt-add-repository ppa:ansible/ansible; \
  in-target apt-get update; \
  in-target apt-get -y install ansible; \
  in-target mkdir /mnt/Paas; \
  in-target sed -i -e "\$a10.2.0.102:/volume1/public /mnt/Paas nfs defaults,user 0 0" /etc/fstab;

# Finish off the install by rebooting the freshly installed Linux Mint desktop
d-i  ubiquity/reboot  boolean true



Further reading

The internet is full of guides on how to get PXE booting or preseeding going, but many are outdated, inaccurate and/or misleading. Here some of the better ones I came across:
  • The installer in modern desktop versions in the Debian family (Ubuntu, Linux Mint, ...) is Ubiquity, not Debian Installer, and this directly affects your preseed file in many subtle and direct ways:
    Ubiquity automation, the success_command
  • Some examples, mainly targeting Ubuntu, but probably works for most/all Debian-based distros: 1, 2, 3, 4, 5,
  • A Google translated page from a German Synology DiskStation NAS site.