Extending Kickstart During Installation
This document discusses approaches to extending and customizing the Anaconda/Kickstart framework that is used during OS installation.
~+WARNING: It is recommended that before exploring these options, you exhaust the available ones.+~
~-The fine print: the techniques discussed within this document require a thorough understanding of the Linux boot process in general, an understanding of the Kickstart boot process in particular, and some skill with scripting. If you use this approach with a commercially-supported Linux distribution like the Enterprise branches, you will probably void some sort of warranty and/or support agreement. If this is not a compromise you are willing to make, you should probably stop reading right now.-~
As a site administrator you may find this document, and the techniques discussed within, to be of some use:
- if you have to install more than one Linux distribution across multiple systems (e.g. RHEL4, FC3, and CentOS4)
- if you have to install more than one type of system (e.g. workstations, development systems, and servers)
- if you have to install large compute clusters with similar but not necessarily identical configurations (e.g. render farms, department labs)
There is a wide range of options available to both Kickstart and the Linux boot process itself. Options understood by the boot process are usually used to adjust kernel parameters: to disable ACPI, adjust initrd information, etc. The options available to Kickstart can be used to alter the behaviour of the program itself: define installation methods, set network parameters, etc. What can you do, though, when you want to get "under the hood" and customize the install environment?
Kickstart itself offers two methods internally - the %pre and %post macros can be used to run scripts before and after the Kickstart installation process runs. But what if you want - or need - to define your install environment dynamically ... and I mean __really dynamically__?
You can define a Kickstart configuration for every possible installation candidate you have - but that is time-consuming, difficult to maintain, a nuisance to extend, and prone to error. Even if you calculate all your install scenarios, you might still be required at install time to define paths, network parameters, partition tables, or any of the other possible configurations available during installation. Who has time to baby-sit?
Kickstart makes unattended installations a snap - if it has all the right answers. As an administrator, you know the answers. The problem is handing them off to Kickstart.
The Bootstrap Environment, and How to Extend It
A Linux system bootstraps itself once the kernel is loaded by starting a program called init, which is capable of starting other system services and applications.
The Kickstart process is the same, although a little more rarefied. The only significant wrinkle, however, is that instead of using /sbin/init, the Anaconda boot process uses /sbin/loader. This is a C-based executable (part of the Anaconda source), which performs functions similar to a traditional init, but is tuned specifically for Anaconda and Kickstart.
After /sbin/loader has started, it will launch the /usr/bin/anaconda script found on whatever boot image it was told to use with whatever Kickstart arguments were passed in at boot time, and Kickstart installation begins. If you haven't provided the information Kickstart wants, you'll have to answer its questions.
You can circumvent the question-and-answer process entirely by providing the information in a ks.cfg file - but as discussed in the previous section, you have to 'hard-wire' a lot of this information into your ks.cfg file ahead of time - unless there was a way to provide them on the fly by adding them to the boot command line, and have an intermediate application convert your input into values that can be understood and used by Kickstart once the installation process has begun.
Fortunately, there is such a way.
The technique I found to be the most successful, portable, and reproducible, is to insert a parser into the bootstrap execution path, like so:
/sbin/loader --> parser --> /usr/bin/anaconda /sbin/loader * first step * bootstrap OS, load kernel drivers, initialize hardware * invoke second step of installation parser * second step * determine runtime parameters from boot line, sanity check environment, generate ks.cfg * invoke third step on success, halt on failure /usr/bin/anaconda * third step * run normally using information generated by previous step * provide a finished install when complete
You then provide your extended information at boot time; the parser intercepts the execution sequence between /sbin/loader and /usr/bin/anaconda, defines your installation environment on the fly and generates a ks.cfg file accordingly; the parser then passes off control to the normal Anaconda script, and installation proceeds normally.
Creating the Parser
The bootstrap environment has, at least in recent versions, a Python interpreter, a Perl interpreter, and an sh-compatible shell. This means that you have at your disposal considerable power - but it is still much less than what is offered by the normal Linux environment. In particular, if you use shell script to drive your parser, all of the tools are Busybox versions - they offer only a subset of the full functionality of the programs they provide.
Remember to keep things simple: the bootstrap environment has leaner tools, is constrained by available RAM, and has zero debugging tools. Be certain your code is fully functional, if not bulletproof, before you deploy it - making more headaches for yourself at install time is exactly what you don't want.
One way to incorporate more programming horsepower into your parser is to offload some or most of its functionality to more powerful tools using a method like the Common Gateway Interface (CGI). The parser that I built was a fairly simple and compact shell script; in effect, it:
- extracted "useful" information from the boot command line
- concatenated this information into "name=value" pairs
- strung all the key-value pairs together with '&' characters
- constructed a URL from this string (relying on the normal behaviour of HTTP GET)
- retrieved this URL using wget, saving the response as /tmp/ks.cfg
- started Anaconda, instructing it to use /tmp/ks.cfg as its Kickstart file
All of the information provided by you at boot time will be stored in /proc/cmdline (minus any arguments that may have been consumed by the kernel). Here is one of mine, as an example (wrapped for legibility):
initrd=initrd.img text ramdisk_size=32768 install method=http://10.15.237.16/library/redhat/FC4 vers=FC4 lang=en_US keymap=us ip=dhcp class=wkstn BOOT_IMAGE=vmlinuz name=gilliam static=1
When the parser has finished with it, here is the URL that wget fetches (wrapped for legibility):
The generated ks.cfg file looks like this:
install lang en_US.UTF-8 langsupport --default en_US.UTF-8 en.US-UTF-8 keyboard us mouse generic3ps/2 --device psaux firewall --disabled selinux --disabled xconfig --card "NVIDIA Quadro 4 (generic)" --videoram 131072 --hsync 30-81 --vsync 56-85 --resolution 1280x1024 --depth 24 --startxonboot --defaultdesktop kde network --device eth0 --netmask 255.255.255.0 --bootproto static --gateway 10.15.237.254 --nameserver 10.15.225.2 --ip 10.15.237.114 --hostname gilliam rootpw --iscrypted RpUKzjDc9k2gU authconfig --enablenis --nisdomain=PRODUCTION --nisserver=diablo timezone EST5EDT reboot bootloader --location=mbr zerombr yes clearpart --all --drives=hda part / --fstype ext3 --size 500 --ondisk hda part swap --recommended --ondisk hda part /var --fstype ext3 --size 2000 --ondisk hda part /usr --fstype ext3 --size 1000 --grow --ondisk hda url --url http://10.15.237.16/library/redhat/FC4 %packages --resolvedeps @ compat-arch-support @ kde-desktop @ legacy-network-server @ workstation-common grub kernel-smp -autofs.i386 -am-utils.i386 -htdig.i386 -libgcj.i386 -selinux-policy-targeted.i386 -xorg-x11-Mesa-libGL.i386 %post #!/bin/sh cat << EOF > /creation-stamp Kickstart config: \$Id: wkstn.cfg,v 1.16 2005/06/30 01:20:04 klaus Exp $ Installed: <code>date</code> EOF echo -n "Adjusting services ... " /sbin/chkconfig --level 5 gpm off /sbin/chkconfig --level 2345 isdn off /sbin/chkconfig --level 2345 kudzu off /sbin/chkconfig --level 345 rhnsd off /sbin/chkconfig --level 2345 cfenvd on /sbin/chkconfig --level 2345 cfexecd on /sbin/chkconfig --level 2345 cfservd on /sbin/chkconfig --level 2345 ntpd on /sbin/chkconfig --level 345 rstatd on /sbin/chkconfig --level 345 rwhod on /sbin/chkconfig rlogin on /sbin/chkconfig rsh on echo "done" echo -n "Installing and configuring nVidia drivers ... " wget -q -O /etc/X11/xorg.conf http://10.15.237.16/library/redhat/configs/xorg.conf (cd /lib/modules; for i in * do depmod -a $i >/dev/null 2>&1 done) modprobe -v nvidia echo "done" echo -n "Installing printer configuration for CUPS ... " wget -q -O /etc/cups/printers.conf http://10.15.237.16/library/redhat/configs/printers.conf echo "done" echo -n "Installing KDM display configuration ... " wget -q -O /etc/X11/xdm/kdmrc http://10.15.237.16/library/redhat/configs/kdmrc wget -q -O /usr/share/config/kdesktoprc http://10.15.237.16/library/redhat/configs/kdesktoprc echo "done" echo -n "Appending Windows boot information to grub configuration ... " wget -q -O - http://10.15.237.16/library/redhat/configs/grub.conf >> /boot/grub/menu.lst echo "done" /bin/rm /var/lib/rpm/Pubkeys /bin/rpm --rebuilddb /bin/rpm --import /usr/share/rhn/RPM-GPG-KEY %post --nochroot cp /tmp/ks.cfg /mnt/sysimage/root/install-ks.cfg cp /proc/cmdline /mnt/sysimage/root/install-cmdline
The combination of parser and CGI were able to generate a complete Kickstart installation profile from a handful of boot time parameters; adjusting only one of these parameters would be enough to dynamically generate a substantially different Kickstart configuration file.
Running the Parser
Unfortunately, /sbin/loader has the path /usr/bin/anaconda hard-wired into it. It is possible to change this, of course, but it involves rebuilding Anaconda itself, and there is an easier way.
To ensure that your parser gets run first, simply add it to your bootstrap image as /usr/bin/anaconda, and rename the original Anaconda script to something else (I used /usr/bin/real_anaconda); when /sbin/loader invokes /usr/bin/anaconda, your code will now run and do whatever you design it to do; when it finishes, simply exec the actual Anaconda script and things proceed as normal.
A few important caveats:
- make sure you preserve the argument vector passed from /sbin/loader to your script - these parameters may not mean anything to your script, but they will mean something to Anaconda itself; if they are expected, your script will have to preserve them so they can be passed to Anaconda.
- make sure your script only runs once! /usr/bin/anaconda may be called more than once during installation. It is fairly reasonable to assume that your script will only need to be run the first time it is called (during bootstrap, by /sbin/loader). It is sufficient to set and test for the presence of an environment variable or temporary file to see if your script has already been run; if it has, it should invoke Anaconda with any arguments that were supplied.
The following shell script fragment is a working example of a mechanism will prevent your script from running itself, or running more than once:
if [ ! -f /tmp/my_script_flag ] ; then touch /tmp/my_script_flag ... my script runs in here ... else exec /usr/bin/real_anaconda $@ fi
Running the CGI
The CGI itself can be as simple or as complex as you want; because it runs in a fully functional OS environment, it has access to information that would either be non-trivial for Kickstart to obtain by itself (e.g. complex DNS lookups), or which would have to be hard-wired into the Kickstart configuration (e.g. site configurations).
The design constraints of the CGI are almost entirely dependent on the parser that uses it; for the design that I used, the constraints were:
- it had to support HTTP GET
- it had to output plain text (that is, Content-type: text/plain)
My CGI makes use of two files to generate the ks.cfg file provided to the client. The first file is the base template file, which contains all the information I want to keep common across all my installed systems. It resembles a standard Kickstart config, and looks a little like this (abridged) version:
install lang en_US.UTF-8 langsupport --default en_US.UTF-8 en.US-UTF-8 keyboard us mouse generic3ps/2 --device psaux firewall --disabled selinux --disabled _CONFIGX_ network --device _KSDEVICE_ --netmask 255.255.255.0 _NETINFO_ rootpw _ROOTPW_ authconfig _AUTHCONFIG_ timezone _TIMEZONE_ reboot bootloader --location=mbr zerombr yes clearpart --all --drives=_DISKDEV_ part / --fstype ext3 --size 500 --ondisk _DISKDEV_ part swap --recommended --ondisk _DISKDEV_ part /var --fstype ext3 --size 2000 --ondisk _DISKDEV_ part /usr --fstype ext3 --size 1000 --grow --ondisk _DISKDEV_ url --url _URL_ _PACKAGES_ ... and so on ...
You may notice that a good deal of this was featured in the full ks.cfg example posted in the previous section. Indeed, when merged with the (abridged) system-specific configuration below:
$configx = "xconfig --card \"NVIDIA Quadro 4 (generic)\" --videoram 131072 --hsync 30-81 --vsync 56-85 --resolution 1280x1024 --depth 24 --startxonboot --defaultdesktop kde"; $netparams = " --gateway _GW_ --nameserver _DNS_"; $rootpw = "password"; $authconfig = "--enablenis --nisdomain=PRODUCTION --nisserver=diablo"; $timezone = "EST5EDT"; $url = "http://_SITE_/library/redhat/_VERS_"; $packages = ' %packages --resolvedeps @ compat-arch-support @ kde-desktop @ legacy-network-server @ workstation-common grub kernel-smp -autofs.i386 -am-utils.i386 -htdig.i386 -libgcj.i386 -selinux-policy-targeted.i386 -xorg-x11-Mesa-libGL.i386 '; $pre = ''; $post = ' %post #!/bin/sh cat << EOF > /creation-stamp ... and so on ...
After both files are passed through the CGI, which replaces the "_" tokens with the information provided by the parser, the full ks.cfg file would now emerge.
The system-specific configuration is actually a module written in the grammar of my CGI (Perl, in this case). Using this framework, I can support any number of permutations of Anaconda/Kickstart-based Linux systems simply by:
- defining a base template for the OS variant
- defining the specific parameters for the particular permutation
During installation, I inform the parser by providing the basic parameters to define my system type at the boot prompt; it passes this information to the CGI using simple URL-encoded syntax, and the CGI merges all this information on the fly into the configuration I want and returns it to the client.
To give you a rough idea: during testing, after starting with RHEL4, I was able to prototype installers for four different system configurations for RHEL3, FC3, and FC4 in a matter of a few minutes. I spent more time copying RPMs to the server!
Changes can be made in two ways, as appropriate. Adjusting the master template file (the one that looks like a Kickstart file) will enable changes across all configurations that use it as a base. Specific configurations are adjusted by making changes to the system-specific file (the one that in this case looks like Perl code). This allows for both broad and minute adjustments, according to the needs of the administrator.
Embedding the Parser
In order to make use of all of this, you will need to incorporate your parser into the boot image(s) you use. At my site, I use the netstg2.img image for all my supported distributions. My boot media can be used with all my supported distros; it remains extremely small and almost entirely agnostic to any changes I make to either the parser or the CGI. The life cycle of the medium itself is very long: because all of the installer logic is embedded into netstg2.img itself, the only real task of the boot medium is to retrieve that image file and run it.
This section assumes that you are using an install procedure like mine: booting from CD and installing over a network.
I have found that /sbin/loader will run a CD installation, even if you specify another method, if it can find anything resembling a distribution on the ISO you're using. In order to avoid this headache, you can safely strip everything out of your ISO except the contents of the isolinux/ directory. You may want or need to make adjustments to the isolinux.cfg file to suit your requirements or provide installer aliases for the system configurations you use (I know I did). At this point, your CD has been dealt with, and you can turn your attention to modifying the netstg2.img.
One important note about the boot process itself: when performing network installs, you still have to supply a "method=<installer URL>" argument to the boot process. This URL should be the top level of your distribution directory. Here is an example from my isolinux.cfg:
LABEL wkstn KERNEL vmlinuz APPEND initrd=initrd.img text ramdisk_size=32768 install method=http://10.15.237.16/library/redhat/FC4 vers=FC4 lang=en_US keymap=us ip=dhcp class=wkstn
With the exception of the 'class=' and 'vers=' options, all of the parameters seen here are understood by either the kernel or Anaconda itself. The Anaconda documentation contains a full list of its command-line parameters.
Make sure your system has mkcramfs installed, and supports 'loop' filesystems.
- Mount the original netstg2.img (the one that came with the distribution itself):
- Copy the contents of this filesystem to another directory:
- Move the real Anaconda script to another name, and copy your parser into its place:
- Make sure you get your permissions correct:
- Create a new netstg2.img file using mkcramfs
Drop this new netstg2.img in place of the old one into the base/ directory (e.g. Fedora/base, RedHat/base - the same directory that holds comps.xml) in your install tree.
That's it. Your new installer should be ready to go.