In my previous post I was talking about Windows deployment, which required quite a lot of hackery by the people at http://unattended.sf.net to make it work with standard tools. For Linux, as usual, this is all provided by the vendors. We use the preseeded Debian installer which we netboot to get a standard Ubuntu desktop installed and then we use puppet to make customisations.
To do that you need to download the netboot installer from one of the ubuntu mirrors and then add the following section to you pxelinux configuration:
pxelinux.cfg/default
LABEL ubuntu-804
MENU LABEL ^1. Ubuntu 8.04 Standard Desktop
kernel ubuntu-8.04-installer/i386/linux
append vga=normal initrd=ubuntu-8.04-installer/i386/initrd.gz debian-installer/framebuffer=true console-setup/ask_detect=false console-setup/layoutcode=gb locale=en_GB netcfg/wireless_wep= netcfg/choose_interface=eth0 netcfg/get_hostname= url=http://<your-webserver>/preseed/install.preseed netcfg/dhcp_timeout=60 splash --The append line is quite lengthy and contains automations for things that can't be preseeded with the file from the webserver, because at that stage the network is not yet initialised :). The install.preseed file is just a modified file from the example preseeding file, which can be found in the installation-guide-i386 debian package.
However we don't only want to install a standard desktop, we also want to make some automated customisations to it right after the installation. For this (and general configuration management) we use Puppet (http://reductivelabs.com/projects/puppet/), which is packaged for Ubuntu. To get that activated, we use two lines in the preseeding file:
d-i pkgsel/include string puppet d-i preseed/late_command string wget -O /target/etc/default/puppet <a href="http://install.maths.ox.ac.uk/preseed/puppet" title="http://install.maths.ox.ac.uk/preseed/puppet">http://install.maths.ox.ac.uk/preseed/puppet</a> ; in-target apt-get remove network-manager -y
The first line tells the installer to pull in the puppet client. The second line runs a few commands after the installation. The idea for the first command is taken from Mike Renfro's Blog (http://blogs.cae.tntech.edu/mwr/2007/04/17/unattended-debian-installatio...).
/etc/default/puppet
# Puppet doesn't like starting before it can contact the puppetmaster. # Wait up to 30 seconds before continuing. echo "* Waiting for network to come up..." for n in `seq 1 60`; do ping -c 1 <puppetmaster> > /dev/null 2>&1 && break echo -n "$n..." sleep 1 done DAEMON_OPTS="--server <puppetmaster>"
The only thing this does is wait for the network to come up before puppet is started, and telling puppet which master to use. (Puppet will use the hostname "puppet" by default, I believe).
Finally NetworkManager is removed because it breaks the networking on boot, so puppet can't run. Since of our desktops use a static network configuration we don't need that.
After that the host automatically reboots, and starts puppet. Since we have a separate puppet instance for clients, which does not contain any secret information, we autosign the keys for hosts within our domain. Then the host starts pulling in all the config information (like LDAP Authentication, NTP configuration, and much more). However the biggest problem turned out to be the package installation. Puppet does support Debian package installation, however, it should only be used for single packages. If you install many-many packages then it will barf, if you define any requirements for it. My objective was that I wanted all the configuration to be through and the package installation to be the final step in the initial configuration, so that the host can already be used during this hour or so. If I defined a big array of packages with a dependency on all the previous configuration steps, then puppet would take ages, as it would single out each pair of dependencies and deal with it separately. Another problem is that when you install that many packages you need to run an "apt-get clean" once in a while to clear the package cache, which might otherwise fill up your disk.
Hence I have written my own puppet procedure for initial package installations (I am not sure whether this is the best way of doing it, I did a lot of searching and was not able to find anything better)
functions.pp
define mi-packages($packages) {
# The function join had to be defined in order to make this custom package management more transparent.
# The puppet internal mechanism for packages is not great for installing this big bulk of packages.
$packages_join = join($packages, " ")
exec { "aptitude -y install $packages_join && apt-get clean":
onlyif => "dpkg-query -W -f='\${Status}\\n' $packages_join 2>&1 |grep -v installed",
path => "/bin:/sbin:/usr/bin:/usr/sbin",
timeout => 7200,
require => [
Service[gdm],
Class[preseeds],
Class[ntp],
Class[ldap-auth],
Class[xorg-nvidia],
Class[oxmaths-themes],
Class[alpine],
Class[vnc],
Class[fvwm],
Class[userconf],
Class[cups],
Class[scratch],
Class[sudo],
Class[firefox],
Class[hosts],
Class[grub]
];
}
}What this does is converting an array of packages to a space-separated string and the installing it with aptitude if they are not all already installed. the usage then works like this:
/etc/puppet/manifests/classes/packages-mi-maths-apps1.pp
class packages-mi-maths-apps1 {
mi-packages {
packages-mi-maths-apps1:
packages =>
[
"maple",
"mathematica",
"mathematica-extras",
"matlab"
];
}
}certainly we could have just used a space separated string to define these packages, and with 4 packages, as in the example, that would be alright. However, we have many more such files, some of which contain up to 80 packages, and then it gets very messy if one looks for something. This is why we need that custom join function, as defined here:
/usr/local/lib/site_ruby/1.8/i486-linux/puppet/parser/functions/join.rb
module Puppet::Parser::Functions
newfunction(:join, :type => :rvalue) do |args|
args[0].join(args[1])
end
endAfter setting all this up, the preseeded installer and puppet, make our installation a one-click install with very easy upgrade paths (unlike an imaging solution would be, for example). When a new distribution comes out we just stick in a new installer and see which packagenames or similar were changed. No reimaging necessary, and everything can be initiated from the PXE menu.
Puppet has many other far more advanced features. We can upgrade all hosts through it, after checking that the upgrades are sane etc. But that is something for a different post.
Comments
automating puppet configuration
Instead of modifying the puppet init script, one can do something like this in the preseed:
d-i pkgsel/include string puppet
d-i preseed/late_command string in-target sed -i '/127.0.1.1/ a 192.168.1.100 puppet.domain puppet' /etc/hosts; \
in-target sed -i 's/DAEMON_OPTS="-w 0"/DAEMON_OPTS="--server puppet.domain"/' /etc/init.d/puppet; \
in-target sed -i 's/exit 0/sleep 30/' /etc/rc.local; \
in-target sed -i '$a /etc/init.d/puppet start' /etc/rc.local; \
in-target sed -i '$a exit 0' /etc/rc.local
This way, puppet starts after dhcp and networking finish setting up.
Installing packages in bulk
$packages = ['a', 'b', 'c']
package { $packages:; }
Doesn't scale
Hi! Yes, I had tried that. Unfortunately this doesn't scale to hundreds of packages. We are using this approach for single package installs. If you want to install a base package set, one needs to come up with other means, besides one needs to run "apt-get clean" every now and then to avoid filling up the disk :)