For one of our projects we found ourselves in need of a PXE server. In order to make proper use of a PXE server you will quickly find you need to change settings in your DHCP server. So we figured we'd probably needed one of those as well. We tried to roll our own but soon enough we came across a package that does all this combined. It’s called Cobbler.
Also being somewhat biased, we would prefer to run Cobbler in a container. That comes with some challenges in itself. If you search for Cobbler on the Docker Hub, there are 10 hits. 4 of them are not automated builds, which means you can’t really tell what’s in them. None of the others seem to be using systemd, which makes it kind of hard for Cobbler to manage the services, which in turn makes it rather impossible to change the config of a running Cobbler container.
So the obvious thing to do is to roll our own. Which we did.
Building the Docker image
Docker is primarily meant to run a single process, but Cobbler consists of mulitple services that you cannot easily separate. There’s cobblerd itself, httpd for the webfrontend, tftpd for serving the PXE images, dhcpd serving ip addresses, and possibly dns for resolving hostnames. Cobbler wants control over all of these services, i.e. after you make changes to the configuration it needs to restart the services so they can pick up the changes. This is why it makes sense to put all these things in a single container, although it makes for a bit of a bloated one. Restarting services requires an init system such as systemd, and luckily the official Centos image is able to run with systemd. Details on how to use it can be found on the CentOS Docker Hub page. So the beginning of the Dockerfile will look something like this:
FROM centos:7.2.1511
MAINTAINER thijs.schnitger@container-solutions.com
RUN (cd /lib/systemd/system/sysinit.target.wants/; for i in *; do [ $i == systemd-tmpfiles-setup.service ] || rm -f $i; done); \
rm -f /lib/systemd/system/multi-user.target.wants/*;\
rm -f /etc/systemd/system/*.wants/*;\
rm -f /lib/systemd/system/local-fs.target.wants/*; \
rm -f /lib/systemd/system/sockets.target.wants/*udev*; \
rm -f /lib/systemd/system/sockets.target.wants/*initctl*; \
rm -f /lib/systemd/system/basic.target.wants/*;\
rm -f /lib/systemd/system/anaconda.target.wants/*;
VOLUME [ “/sys/fs/cgroup” ]
Next we install the EPEL repositories that contain the Cobbler packages we need. Once we add EPEL we can install Cobbler and the rest of the necessary packages.
RUN yum -y install epel-release
RUN yum -y install cobbler cobbler-web dhcp bind syslinux pykickstart
We need to enable the services in systemd.
RUN systemctl enable cobblerd
RUN systemctl enable httpd
RUN systemctl enable dhcpd
Then we change the tftp xinetd config file to enable the tftp service.
# enable tftp
RUN sed -i -e 's/\(^.*disable.*=\) yes/\1 no/' /etc/xinetd.d/tftp
And we make sure the rsync file exists, or else Cobbler will fail to start.
# create rsync file
RUN touch /etc/xinetd.d/rsync
We expose the ports we plan to use:
EXPOSE 69
EXPOSE 80
EXPOSE 443
EXPOSE 25151
And we end by invoking init
CMD ["/sbin/init"]
Once we have declared the Dockerfile, we build it with docker build -t cobbler .
and we should be good to go. But not quite.
Running the Container
We can't just run it, we need to specify --privileged
because we're using systemd which needs elevated capabilities, like CAP_SET_FILE.
We also want to use the network stack of the host, so Cobbler will listen on and offer ip addresses on a subnet that the host is connected to. If it would use it's own network stack Cobbler would only be able to issue addresses in the private docker subnet, which would not really make any sense because every container already gets an address from the docker engine.
Another thing we want is to save the Cobbler configuration and the imported OS distributions in between restarts of the container. For this we need to mount a bunch of volumes. Lastly we also need to mount an iso inside the container to be able to import it's contents into Cobbler. So the resulting docker run
command is gonna look like this:
docker run \
-d \
--privileged \
--net host \
-v /sys/fs/cgroup:/sys/fs/cgroup:ro \
-v etc/cobbler/settings:/etc/cobbler/settings \
-v etc/cobbler/dhcp.template:/etc/cobbler/dhcp.template \
-v var/www/cobbler/images:/var/www/cobbler/images \
-v var/www/cobbler/ks_mirror:/var/www/cobbler/ks_mirror \
-v var/www/cobbler/links:/var/www/cobbler/links \
-v var/lib/cobbler/config:/var/lib/cobbler/config \
-v var/lib/tftpboot:/var/lib/tftpboot \
-v dist/centos:/mnt:ro \
-p 69:69 \
-p 80:80 \
-p 443:443 \
-p 25151:25151 \
--name cobbler cobbler
Before we can run though, we need to configure some files. Get these files by starting the Cobbler container without the volumes mounted, and use docker cp
to copy over at least the settings
and hcp_template
files from /etc/cobbler
inside the container to the host. Put them in a directory etc/cobbler
under the current dir, which would be where the Dockerfile is.
Next, edit the settings
file, change the value for manage_dhcp
to 1
and enter the ip address where your host will be listening on in both the next_server
and server
variables.
In the dhcp_template
file, configure the subnet details. Be sure to use a subnet that your host can actually connect to, or Cobbler will fail to start later. Debugging this is kind of hard and cost me a lot of time. There are no logs written anywhere and you can only find out what's wrong by checking systemctl status <servicename>
or journalctl -xe
. Once you have the addresses sorted though you should be able to use the run command listed above.
Importing a distribution
You see we used a CentOS iso for importing into Cobbler, we mounted it on dist/centos
on our host and attached that as a volume to /mnt
inside the container. I found the cobbler import
command fails if you use it with any other path than /mnt
. The complete mount command is:
sudo mount -t iso9660 -o loop,ro -v dist/centos.iso dist/centos
and for importing the distribution:
docker exec -it cobbler cobbler import --name=centos7 --arch=x86_64 --path=/mnt
Checking the web interface
After you have started the container, you should be able to go to the web interface under https://localhost/cobbler_web.
Login using cobbler
as the username and cobbler
as the password. If the web interface doesn't respond, exec into the container and check the systemctl status
for dhcpd
, cobblerd
and httpd
and also journalctl -xe
.
Seeing Cobbler in action
For my test setup i used a Virtualbox VM to connect to the Cobbler server. I created a host-only network and attached an empty client to that. I configured the ip address of the vboxnet
interface in the settings
and dhcp_template
files.
Last, not unimportant step is to create a system in Cobbler that actually uses the distribution you imported. Configuration is straightforward, but be sure to match the MAC-address assigned to the VM by VirtualBox.
Once the vm boots, press F12 and select the LAN boot device. You should see the VM booting into the CentOS installer.
Final thoughts
All the code from this post can be found on our Github project page. I also tried to get Docker Hub to do an automated build but due to an issue with Docker Hub (see here and here), installing httpd in a CentOS machine doesn't work on the Docker Hub. It appears to be related to using aufs as the storage backend. For now, just build it on your local machine.