A Retrospective: Xen Paravirtualization Part 1 – CentOS 5 and 6

Introduction

A blog post about CentOS 5 or 6 isn’t going to excite anyone or show up in many intentional Google searches. CentOS 5 and 6 are 18 and 14 years old respectively in 2025 and are mostly viewed as insecure at this point; they hasn’t reached the point of being “retro cool” yet (and may never be). However, for me there will always be a certain amount of nostalgia for them, CentOS 5 in particular: it and related Enterprise Linux 5 variants were the first Linux distributions I used in my work—previously I had only used Ubuntu, Debian, and Fedora on the desktop. My first home server also ran CentOS 5, with 6-8 VMs running on a Xen hypervisor in paravirtualized mode. Paravirtualization at the time provided better performance than full virtualization (VMWare ESXi, KVM, VirtualBox, etc.) and allowed running VMs on CPUs that didn’t have virtualization extensions. My main home server at the time supported hardware virtualization, but I was also able to run Xen on some spare systems that did not, such as a Pentium D desktop with only 2GB of RAM. My impressions at the time were that Xen was lightweight, resilient, and easy to configure. The bridged network functioned right out of the box and the machine config files consisted of only a few lines. Thus it was with some reluctance that I eventually rebuilt my lab with CentOS 6 and KVM. Since then, I’ve always been a KVM user and my experience with it has been mostly positive. Still, my perpetual dislike of libvirt’s XML files and excessive number of virsh subcommands has made me nostalgic for my first lab setup with Xen. In this post I will fire up Xen once again on both CentOS 5 and 6, while doing a little bit of scripting to make it more interesting.

Host setup

The host machine on which I am running CentOS 5 and Xen is era-appropriate: an HP Compaq 8710w laptop from 2007, the same year that CentOS 5 was released. It has a 2.6GHz Core2 Duo CPU and the maximum 4GB RAM, along with a regular old spinning hard drive—cutting edge stuff in 2025! Notably, it does support hardware virtualization:

Installing Xen was very simple and straightforward. First, I started with a clean CentOS 5.11 installation. After installation, I disabled all repos in /etc/yum.repos.d/CentOS-Base.repo except for the [base] one and pointed this at my internal web server hosting the contents of the DVD ISO (or you can point it at vault.centos.org). I then installed Xen with yum -y install xen kernel-xen. After the yum command completed, I edited /boot/grub/grub.conf and made the following changes:

  1. Set default=0 (if the el5xen kernel is the first entry).
  2. Append the kernel /xen.gz* line. Add dom0_mem=512M or however much RAM you want to allocate to the host OS (a system running a GUI may need more). You could leave this off altogether if you’re confident that the host OS won’t consume all of the available RAM.

Some guides mention that you also need to run chkconfig xend on and chkconfig xendomains on, but I didn’t need to do this. After making the grub.conf changes, I rebooted and Xen was ready to use.

Note: in this post I use the terms “VM” and “instance” interchangeably.

Writing a provisioning script in Perl

For this exercise, I followed the simple guide on the archived CentOS wiki, found here. The guide provides all of the steps for doing a kickstart installation (Red Hat automated installation) of a CentOS 5 Xen domU instance. The only thing I would have added to the guide would have been the step of generating a MAC address for the instance, as Xen generates a new MAC address each time a host is booted.

Based off this historic guide, I wrote a simple provisioning script for spinning up Xen instances. The script uses Perl and the Template Toolkit module, both of which are easy to install on a CentOS 5 system. I originally considered using Python, but did not want to write anything in Python 2; whereas Perl 5 and the Template Toolkit don’t change much. This is also a “nostalgia” blog post, and I wanted to write something similar to the Perl KVM provisioning script I wrote back in 2013.

For installing systems via kickstart, I first needed to set up a web server to host the kickstart files and installation packages. I simply installed Apache on the host system (yum install httpd), started up httpd with service httpd start and enabled it with chkconfig httpd on. Within /var/www/html, I created two directories: kickstart and centos/5. I then copied the contents of the CentOS 5.11 part 1 DVD to /var/www/html/centos/5.

Next, I created a configuration file for the script, prov_xen.cfg. This step is optional and the configuration variables could be placed inside the script instead. However, I prefer to place “site-specific” configuration options outside of the script. It uses a simple parameter=value format and is parsed with the Config::Tiny module. Only three parameters are mandatory: os_url, ks_url, and root_pw_hash. os_url and ks_url are the URLs to the operating system installation files and kickstart files, respectively. root_pw_hash is the root password hash, generated with openssl passwd -1. The script itself will show the default values for the other parameters. Below is my example configuration file:

os_url=http://192.168.1.2/centos/5/
ks_url=http://192.168.1.2/kickstart
root_pw_hash=$1$vCKxycEr$W3MQKTjm7TzDGvLf5vE49/

To define the instance, the script needs to generate a file with the name of the instance in /etc/xen. The instance can then be started with xm create . However, the parameters for the configuration file need to change once the installation completes (for example, removing the kernel and ramdisk lines). To do this, I made the Xen instance file into a template and pass the “install” parameter. Once installation is complete, the template is rendered again and the unneeded lines are removed. Below is the template:

name = "[% name %]"
memory = "[% ram %]"
vcpus = [% vcpu %]
disk = [ 'tap:aio:[% image_path %]/[% name %].img,xvda,w', ]
vif = [ 'mac=[% mac %],bridge=[% bridge %]', ]
[% IF install == 1 -%]
kernel = "[% kernel_path %]/vmlinuz"
ramdisk = "[% kernel_path %]/initrd.img"
extra = "text ks=[% ks_url %]/[% name %].cfg"
on_reboot = 'destroy'
on_crash = 'destroy'
[% ELSE -%]
bootloader = "/usr/bin/pygrub"
on_reboot = 'restart'
on_crash = 'restart'
[% END -%]

To install a CentOS 5 system automatically, one needs to create a Red Hat kickstart file. The kickstart file contains a number of options for setting items such as the partition layout, root password, SELinux state, and more. Compared to more recently versions of Enterprise Linux, the kickstart file for EL5 is much more limited. For example, it lacks options for adding normal users or setting NTP servers. Many of these tasks would need to be performed using ordinary shell commands in the %post section, or after installation using automation tools such as Puppet or Ansible.

For this post, I created a simple kickstart template, following the example in the CentOS 5 Xen guide. The file is parsed using the Perl Template Toolkit and written out to /var/www/html/kickstart/hostname.cfg. Below is the template:

install
url --url [% os_url %]
repo --name=epel --baseurl=http://archives.fedoraproject.org/pub/archive/epel/5/x86_64/
lang en_US.UTF-8
[% IF dhcp == 1 -%]
network --bootproto=dhcp --device=eth0 --hostname=[% hostname %]
[% ELSE -%]
network --bootproto=static --device=eth0 --gateway=[% gateway %] --ip=[% ip %] --nameserver=[% dns_server %] --netmask=[% netmask %] --hostname=[% hostname %]
[% END -%]
rootpw --iscrypted [% root_pw_hash %]
firewall --enabled --ssh
services --disabled iscsid,iscsi
authconfig --enableshadow --enablemd5
selinux --disabled
timezone --utc Etc/UTC
bootloader --location=mbr
text
skipx
poweroff

# Partitioning
zerombr
clearpart --all --initlabel
autopart

%packages --nobase
[% FOREACH package = extra_pkgs -%]
[% package %]
[% END %]

%post
cat << 'EOF' > /etc/yum.repos.d/CentOS-Base.repo
[base]
name=CentOS-$releasever - Base
baseurl=[% os_url %]
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-5
EOF

cat << 'EOF' > /etc/yum.repos.d/epel.repo
[epel]
name=epel
baseurl=http://archives.fedoraproject.org/pub/archive/epel/5/x86_64/
gpgcheck=0
EOF

Including the EPEL Yum repository is optional.

Finally, the script itself. It will need the following Perl modules installed from the EPEL repository: perl-Template-Toolkit and perl-Config-Tiny. The script accepts a number of options, which are listed below:

  • -i IP_address
  • -r RAM_in_MB (default 512MB)
  • -c virtual_CPU_count
  • -d disk_in_GB (default 10GB)
  • -p (additional packages to be installed)
  • -a (set the VM to start at boot)
  • -f (forcefully re-provision the instance/VM even if it already exists)

The script also requires a name for the instance, which is the last argument.

#!/usr/bin/perl -w

use strict;
use Config::Tiny;
use Getopt::Long;
use Template;

# Subroutine for generating MAC addresses
sub mac_gen {
    my @m;
    my $x = 0;
    while ($x < 3) {
        $m[$x] = int(rand(256));
        $x++;
    }
    my $mac = sprintf("00:16:3E:%02X:%02X:%02X", @m);
    return $mac;
}

my $config_file = 'prov_xen.cfg';
my $config = Config::Tiny->read($config_file) || die "Unable to open $config_file\n";

# Parse config file
my $image_path = $config->{_}->{image_path} || '/srv/xen';
my $ks_path = $config->{_}->{ks_path} || '/var/www/html/kickstart';
my $kernel_path = $config->{_}->{kernel_path} || '/var/www/html/centos/5/images/xen';
my $os_url = $config->{_}->{os_url} || die "os_url is undefined\n";
my $ks_url = $config->{_}->{ks_url} || die "ks_url is undefined\n";
my $root_pw_hash = $config->{_}->{root_pw_hash} || die "root_pw_hash is undefined\n";
my $netmask = $config->{_}->{netmask} || '255.255.255.0';
# If gateway is undefined, it will be set to xxx.xxx.xxx.1
my $gateway = $config->{_}->{gateway} || '';
my $dns_server = $config->{_}->{dns_server} || '1.1.1.1';
my $domain = $config->{_}->{domain} || '';
my $bridge = $config->{_}->{bridge} || 'xenbr0';

# Parse command line options
my($ip, $pkgs, $autostart, $force, $help);
# RAM is in MB
my $ram = 512;
my $vcpu = 1;
# Disk is in GB
my $disk = 10;
GetOptions ("ip=s"      => \$ip,
            "ram=i"     => \$ram,
            "cpu=i"     => \$vcpu,
            "disk=i"    => \$disk,
            "pkgs=s"    => \$pkgs,
            "autostart" => \$autostart,
            "force"     => \$force,
            'help|?'    => \$help,
            );

if (defined($help)) {
    print "prov_xen.pl usage: [-f (force)] [-i <ip_address>] [-r <ram_mb>] [-d <disk_gb>] [-c <cpus>] [-p <comma-separated-list-of-packages>] [--autostart] <vm_name>";
    exit 0;
}

my $name = $ARGV[0] || die "You must specify a name for the virtual machine.\n";
my $ks = "${ks_path}/${name}.cfg";
my $disk_size = $disk*1000;

unless (-d $image_path) {
    system("mkdir -p $image_path");
}

# Checks if the VM already exists.
# If -f is specified, the VM is stopped and deleted.
# Otherwise, fail the script.
if ((-f "/etc/xen/${name}") || (-f "/etc/xen/auto/${name}")) {
    if (defined($force)) {
        open(XM_LIST, "/usr/sbin/xm list |");
        while (my $line = <XM_LIST>) {
            if ($line =~ /^$name/) {
                system("/usr/sbin/xm destroy $name");
            }
        }
        close(XM_LIST);
        unlink("/etc/xen/auto/${name}") if (-f "/etc/xen/auto/${name}");
        unlink("${image_path}/${name}") if (-f "${image_path}/${name}");
    } else {
        die "A virtual machine with the name $name already exists.\n";
    }
}

my $dhcp = 1;
if (defined($ip)) {
    $dhcp = 0;
    unless ($ip =~ /^(\d{1,3}\.){3}\d{1,3}$/) {
      die "$ip failed regex IP test. Exiting.\n";
    }

    # Set gateway to xxx.xxx.xxx.1 if not set.
    if ($gateway eq '') {
        $gateway = $ip;
        $gateway =~ s/\.\d+$/\.1/;
    }
}

# Additional package specified with -p pkg1,pkg2,etc
my @extra_pkgs = qw(sudo vim-enhanced);
if (defined($pkgs)) {
    foreach my $pkg (split(/,/, $pkgs)) {
        push(@extra_pkgs, $pkg);
    }
}

my $hostname = $name;
unless ($domain eq '') {
    $hostname .= ".${domain}";
}

# Variables for the kickstart template.
my $ks_vars = {
  os_url       => $os_url,
  root_pw_hash => $root_pw_hash,
  dhcp         => $dhcp,
  ip           => $ip,
  netmask      => $netmask,
  gateway      => $gateway,
  dns_server   => $dns_server,
  hostname     => $hostname,
  extra_pkgs   => \@extra_pkgs,
};

# Parse kickstart template.
my $ks_tt = Template->new();
$ks_tt->process('ks.cfg.tt', $ks_vars, $ks) || die $ks_tt->error;

# Variables for Xen config template.
my $xen_vars = {
    name        => $name,
    bridge      => $bridge,
    kernel_path => $kernel_path,
    ks_url      => $ks_url,
    ram         => $ram,
    vcpu        => $vcpu,
    image_path  => $image_path,
    mac         => &mac_gen(),
    install     => 1,
};

# Parse initial Xen config template.
my $ks_xen_tt = Template->new();
$ks_xen_tt->process('xen.tt', $xen_vars, "/etc/xen/${name}") || die $ks_xen_tt->error;

print "Starting kickstart install of Xen VM ${name}\n.";
system("/bin/dd if=/dev/zero of=${image_path}/${name}.img oflag=direct bs=1M seek=${disk_size} count=1");
system("/usr/sbin/xm create -c $name");
print "Installation finished. VM can be started with /usr/sbin/xm create $name\n";

# Re-parse Xen config template with post-install settings.
$xen_vars->{'install'} = 0;
my $xen_tt = Template->new();
$xen_tt->process('xen.tt', $xen_vars, "/etc/xen/${name}") || die $xen_tt->error;

if (defined($autostart)) {
    print "Setting VM to be autostarted as requested.\n";
    symlink("/etc/xen/${name}", "/etc/xen/auto/${name}");
}

# Delete kickstart template.
unlink($ks);

The below screenshot shows me starting the installation of the VM web01 on my Xen host. The installation is quick, taking under two minutes to complete, as the second screenshot shows (a few times it took over three minutes). And mind you: this is running on an 18-year-old laptop with a spinning hard drive, not a modern PC with an NVMe drive and 32GB of RAM.


After provisioning three instances, a MySQL server and two web servers, I set up a load-balanced LAMP stack for WordPress and installed WordPress 4.9.27, which is compatible with MySQL 5.0 and PHP 5.3. The uploads directory was shared between the two web servers using an NFS share. The two web servers were load-balanced with HAProxy running in a Docker container on a different system. Everything worked as expected, and WordPress responded quickly.

Running CentOS 6

In preparation for upgrading the lab system to CentOS 6 (😄), I modified the script to add the capability to build CentOS 6 instances. This involved adding the parameter -v , where one would specify 5 or 6. The configuration file was modified to remove the version-specific OS URL. Finally, the 5 and 6 kickstart templates were split into ks_el5.cfg.tt and ks_el6.cfg.tt.

install
url --url [% os_url %]
repo --name=epel --baseurl=http://archives.fedoraproject.org/pub/archive/epel/6/x86_64/
lang en_US
keyboard us
[% IF dhcp == 1 -%]
network --bootproto=dhcp --device=[% mac %] --hostname=[% hostname %]
[% ELSE -%]
network --bootproto=static --device=[% mac %] --gateway=[% gateway %] --ip=[% ip %] --nameserver=[% dns_server %] --netmask=[% netmask %] --hostname=[% hostname %]
[% END -%]
rootpw --iscrypted [% root_pw_hash %]
user --name=ansible_user
firewall --enabled --ssh
authconfig
selinux --permissive
timezone --utc Etc/UTC
bootloader --location=mbr
text
skipx
poweroff

# Partitioning
zerombr
clearpart --all
autopart

%packages --nobase
[% FOREACH package = extra_pkgs -%]
[% package %]
[% END %]

%post
cat << 'EOF' > /etc/yum.repos.d/CentOS-Base.repo
[base]
name=CentOS-$releasever - Base
baseurl=[% os_url %]
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-6
EOF

cat << 'EOF' > /etc/yum.repos.d/epel.repo
[epel]
name=epel
baseurl=http://archives.fedoraproject.org/pub/archive/epel/6/x86_64/
gpgcheck=0
EOF
os_url=http://192.168.1.2/centos
ks_url=http://192.168.1.2/kickstart
root_pw_hash=$1$vCKxycEr$W3MQKTjm7TzDGvLf5vE49/
#!/usr/bin/perl -w

use strict;
use Config::Tiny;
use Getopt::Long;
use Template;

# Subroutine for generating MAC addresses
sub mac_gen {
    my @m;
    my $x = 0;
    while ($x < 3) {
        $m[$x] = int(rand(256));
        $x++;
    }
    my $mac = sprintf("00:16:3E:%02X:%02X:%02X", @m);
    return $mac;
}

my $config_file = 'prov_xen.cfg';
my $config = Config::Tiny->read($config_file) || die "Unable to open $config_file\n";

# Parse config file
my $image_path = $config->{_}->{image_path} || '/srv/xen';
my $ks_path = $config->{_}->{ks_path} || '/var/www/html/kickstart';
my $os_path = $config->{_}->{os_path} || '/var/www/html/centos';
my $os_url = $config->{_}->{os_url} || die "os_url is undefined\n";
my $ks_url = $config->{_}->{ks_url} || die "ks_url is undefined\n";
my $root_pw_hash = $config->{_}->{root_pw_hash} || die "root_pw_hash is undefined\n";
my $netmask = $config->{_}->{netmask} || '255.255.255.0';
# If gateway is undefined, it will be set to xxx.xxx.xxx.1
my $gateway = $config->{_}->{gateway} || '';
my $dns_server = $config->{_}->{dns_server} || '1.1.1.1';
my $domain = $config->{_}->{domain} || '';
my $bridge = $config->{_}->{bridge} || 'xenbr0';

# Parse command line options
my($ip, $pkgs, $autostart, $force, $help);
# RAM is in MB
my $ram = 512;
my $vcpu = 1;
# Disk is in GB
my $disk = 10;
# Default to CentOS 5
my $os_ver = 5;
GetOptions ("ip=s"      => \$ip,
            "ram=i"     => \$ram,
            "cpu=i"     => \$vcpu,
            "disk=i"    => \$disk,
            "pkgs=s"    => \$pkgs,
            "autostart" => \$autostart,
            "force"     => \$force,
            "version=i" => \$os_ver,
            'help|?'    => \$help,
            );

if (defined($help)) {
    print "prov_xen.pl usage: [-f (force)] [-i <ip_address>] [-r <ram_mb>] [-d <disk_gb>] [-c <cpus>] [-p <comma-separated-list-of-packages>] [-v <os_version>] [--autostart] <vm_name>";
    exit 0;
}

# Check for a valid EL major release number
my @os_vers = (5..7);
unless (grep { $_ == $os_ver } @os_vers) {
    die "$os_ver is not a valid OS version. It must be " . join(', ', @os_vers) . ".\n";
}

# CentOS 5 uses images/xen. 6 or later uses imagex/pxeboot.
my $kernel_path;
if ($os_ver == 5) {
    $kernel_path = "${os_path}/5/images/xen";
} else {
    $kernel_path = "${os_path}/${os_ver}/images/pxeboot";
}

my $name = $ARGV[0] || die "You must specify a name for the virtual machine.\n";
my $ks = "${ks_path}/${name}.cfg";
my $disk_size = $disk*1000;
my $mac = &mac_gen();
$os_url .= "/${os_ver}";

# Set commands for Xen 4 or 3
my ($xen_cmd, $xen_conf, $xen_create_cmd);
if (-f '/usr/sbin/xl') {
    $xen_cmd = '/usr/sbin/xl';
    $xen_conf = "${name}.cfg";
    $xen_create_cmd = "$xen_cmd create -c /etc/xen/${xen_conf}";
} else {
    $xen_cmd = '/usr/sbin/xm';
    $xen_conf = $name;
    $xen_create_cmd = "$xen_cmd create -c $xen_conf";
}

unless (-d $image_path) {
    system("mkdir -p $image_path");
}

# Checks if the VM already exists.
# If -f is specified, the VM is stopped and deleted.
# Otherwise, fail the script.
if ((-f "/etc/xen/${xen_conf}") || (-f "/etc/xen/auto/${xen_conf}")) {
    if (defined($force)) {
        open(XM_LIST, "$xen_cmd list |");
        while (my $line = <XM_LIST>) {
            if ($line =~ /^$name/) {
                system("$xen_cmd destroy $name");
            }
        }
        close(XM_LIST);
        unlink("/etc/xen/auto/${xen_conf}") if (-f "/etc/xen/auto/${xen_conf}");
        unlink("${image_path}/${name}") if (-f "${image_path}/${name}");
    } else {
        die "A virtual machine with the name $name already exists.\n";
    }
}

my $dhcp = 1;
if (defined($ip)) {
    $dhcp = 0;
    unless ($ip =~ /^(\d{1,3}\.){3}\d{1,3}$/) {
      die "$ip failed regex IP test. Exiting.\n";
    }

    # Set gateway to xxx.xxx.xxx.1 if not set.
    if ($gateway eq '') {
        $gateway = $ip;
        $gateway =~ s/\.\d+$/\.1/;
    }
}

# Additional package specified with -p pkg1,pkg2,etc
my @extra_pkgs = qw(sudo vim-enhanced);
if (defined($pkgs)) {
    foreach my $pkg (split(/,/, $pkgs)) {
        push(@extra_pkgs, $pkg);
    }
}

my $hostname = $name;
unless ($domain eq '') {
    $hostname .= ".${domain}";
}

# Variables for the kickstart template.
my $ks_vars = {
  os_url       => $os_url,
  root_pw_hash => $root_pw_hash,
  dhcp         => $dhcp,
  ip           => $ip,
  netmask      => $netmask,
  gateway      => $gateway,
  dns_server   => $dns_server,
  hostname     => $hostname,
  mac          => $mac,
  extra_pkgs   => \@extra_pkgs,
};

# Parse kickstart template.
my $ks_tt = Template->new();
$ks_tt->process("ks_el${os_ver}.cfg.tt", $ks_vars, $ks) || die $ks_tt->error;

# Variables for Xen config template.
my $xen_vars = {
    name        => $name,
    kernel_path => $kernel_path,
    ks_url      => $ks_url,
    ram         => $ram,
    vcpu        => $vcpu,
    image_path  => $image_path,
    mac         => $mac,
    install     => 1,
};

# Parse initial Xen config template.
my $ks_xen_tt = Template->new();
$ks_xen_tt->process('xen.tt', $xen_vars, "/etc/xen/${xen_conf}") || die $ks_xen_tt->error;

print "Starting kickstart install of Xen VM ${name}\n.";
system("/bin/dd if=/dev/zero of=${image_path}/${name}.img oflag=direct bs=1M seek=${disk_size} count=1");
system($xen_create_cmd);
print "Installation finished. VM can be started with $xen_create_cmd\n";

# Re-parse Xen config template with post-install settings.
$xen_vars->{'install'} = 0;
my $xen_tt = Template->new();
$xen_tt->process('xen.tt', $xen_vars, "/etc/xen/${xen_conf}") || die $xen_tt->error;

if (defined($autostart)) {
    print "Setting VM to be autostarted as requested.\n";
    symlink("/etc/xen/${xen_conf}", "/etc/xen/auto/${xen_conf}");
}

# Delete kickstart template.
unlink($ks);

Installing CentOS 6 takes more than twice the amount of time as CentOS 5. If I had to guess, this is due to SELinux labeling. For some reason, the CentOS 6 kickstart ignores the option selinux –disabled.


Upgrading the host to CentOS 6 and Xen 4.10

While exploring the directory tree at vault.centos.org, I discovered that Xen packages were created for CentOS 6. I don’t know if these existed back when I rebuilt my lab server on CentOS 6 back in 2014, but if they had, I probably would have chosen Xen over KVM. The CentOS 6 Xen repositories can be found here. I chose to install the 4.10 packages. Prior to installation, I installed CentOS 6.10 on the 8710W and added the Xen 4.10 Yum repo:

[xen]
name=CentOS-$releasever - Xen
baseurl=http://vault.centos.org/6.10/virt/x86_64/xen-410/
gpgcheck=0

… then installed the packages with sudo yum install xen kernel-xen.

Unlike on CentOS 5, the Grub entry is not added automatically and I had to search for how to add the Xen kernel stuff to /boot/grub/grub.conf. I ending up finding an old page on the Xen Project Wiki got me pointed in the right direction. The directions are for Xen 4.2, not 4.10, but I was able to use them. Below is what my grub.conf looked like:

default=0
timeout=2
splashimage=(hd0,1)/boot/grub/splash.xpm.gz
hiddenmenu
title CentOS (4.9.241-37.el6.x86_64)
	root (hd0,1)
	kernel /boot/xen.gz dom0_mem=512M,max:512M loglvl=all guest_loglvl=all
	module /boot/vmlinuz-4.9.241-37.el6.x86_64 ro root=UUID=disk-uuid rd_NO_LUKS rd_NO_LVM LANG=en_US.UTF-8 rd_NO_MD SYSFONT=latarcyrheb-sun16 crashkernel=auto  KEYBOARDTYPE=pc KEYTABLE=us rd_NO_DM rhgb quiet
	module /boot/initramfs-4.9.241-37.el6.x86_64.img

The default bridge xenbr0 is not created on CentOS 6 also. To set up bridged networking, I needed to follow the steps similar to what I needed to do for KVM. First, ensure that the bridge-utils package is installed. Then, make the following modifications to the interface configuration files in /etc/sysconfig/network-scripts (adjust if your interface isn’t eth0):

  • Make a backup of /etc/sysconfig/network-scripts/ifcfg-eth0.
  • cp ifcfg-eth0 ifcfg-br0.
  • Edit ifcfg-br0. Change DEVICE to “br0” and TYPE to “Bridge”. Remove the UUID line.
  • Edit ifcfg-eth0. Remove all lines except for DEVICE, HWADDR, ONBOOT, TYPE, and UUID. Add the line BRIDGE=”br0″.
  • Reboot the system. br0 should show up with an IP address

The below snippet shows an example of this (the MAC address was randomly-generated):

[matthew@8710w network-scripts]$ cat ifcfg-br0
DEVICE="br0"
BOOTPROTO="dhcp"
DHCP_HOSTNAME="8710w.example.com"
HOSTNAME="8710w.example.com"
HWADDR="00:16:3e:3e:2d:21"
IPV6INIT="yes"
MTU="1500"
NM_CONTROLLED="yes"
ONBOOT="yes"
TYPE="Bridge"
[matthew@8710w network-scripts]$ cat ifcfg-eth0
DEVICE="eth0"
HWADDR="00:16:3e:3e:2d:21"
ONBOOT="yes"
TYPE="Ethernet"
UUID="some-random-uuid"
BRIDGE="br0"

Some guides omit the HWADDR from ifcfg-eth0, but this did not work for me.

Finally, add the parameter bridge=br0 to prov_xen.cfg. Everything should then work with prov_xen.pl.

On the CentOS 6 host I was able to provision a CentOS 7.9 instance. The installation takes considerably longer to complete on the old laptop (Enterprise Linux really started to get bloated in version 7). It also requires 2GB of RAM to kickstart, which consumes half of the total RAM on the host. I would probably choose a more powerful computer if I wanted to try out CentOS 7 on PV in the future. When installation completed, I was unable to boot the instance. I would get the below error:

[root@8710w xen]# /usr/sbin/xl create -c /etc/xen/cent7db.cfg
Parsing config from /etc/xen/cent7db.cfg
libxl: warning: libxl_bootloader.c:427:bootloader_disk_attached_cb: Domain 5:bootloader='/usr/bin/pygrub' is deprecated; use bootloader='pygrub' instead
libxl: error: libxl_bootloader.c:649:bootloader_finished: Domain 5:bootloader failed - consult logfile /var/log/xen/bootloader.5.log
...
[root@8710w ~]# cat /var/log/xen/bootloader.5.log
Traceback (most recent call last):
  File "/usr/bin/pygrub", line 929, in 
    raise RuntimeError, "Unable to find partition containing kernel"
RuntimeError: Unable to find partition containing kernel

After searching for a solution for a while, I was fortunate enough to stumble across this guide: How to Install Paravirtualized CentOS 7 DomU on Xen. A commenter at the bottom, Scott R., mentioned that /boot needed to be formatted with EXT3, not XFS, in order for pygrub to read it (EXT4 failed for me also). I then modified this line in the kickstart file: autopart –nolvm –nohome –fstype=ext3. However, if you have a custom partition table, only /boot needs to be formatted with EXT3. After reinstalling, everything booted successfully. Below is the kickstart file I used for CentOS 7:

install
url --url [% os_url %]
repo --name=epel --baseurl=http://archives.fedoraproject.org/pub/archive/epel/7/x86_64/
lang en_US
keyboard --vckeymap=us
[% IF dhcp == 1 -%]
network --bootproto=dhcp --device=[% mac %] --hostname=[% hostname %]
[% ELSE -%]
network --bootproto=static --device=[% mac %] --gateway=[% gateway %] --ip=[% ip %] --nameserver=[% dns_server %] --netmask=[% netmask %] --hostname=[% hostname %]
[% END -%]
rootpw --iscrypted [% root_pw_hash %]
user --name=ansible_user
firewall --enabled --ssh
authconfig
selinux --permissive
timezone --ntpservers=0.pool.ntp.org Etc/UTC
bootloader --location=mbr
text
skipx
poweroff

# Partitioning
zerombr
clearpart --all
autopart --nolvm --nohome --fstype=ext3

%packages
[% FOREACH package = extra_pkgs -%]
[% package %]
[% END %]
%end

%post
cat << 'EOF' > /etc/yum.repos.d/CentOS-Base.repo
[base]
name=CentOS-$releasever - Base
baseurl=[% os_url %]
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7
EOF

cat << 'EOF' > /etc/yum.repos.d/epel.repo
[epel]
name=epel
baseurl=http://archives.fedoraproject.org/pub/archive/epel/7/x86_64/
gpgcheck=0
EOF
%end

Conclusion

Was there any point to anything I did here? Not really. Nobody is deploying any of these deprecated versions of CentOS in production. This was mainly an exercise in nostalgia for me, giving me an opportunity to look back at my first Linux lab setup and write a Perl script to manage it. Nowadays I might find myself using cloud images and deploying KVM VMs with Terraform, as opposed to waiting for a kickstart install to complete. If wanted something more lightweight, I might use LXD/Incus or just spin up some Docker containers to run a few services. Still, I really like Xen. I like that it uses simple configuration files and commands, and doesn’t try to do too much like libvirt (virsh | wc -l is 309 lines!). I really want to experiment with Xen more in the future, but on a more modern OS such as Debian 12. That will be part 2. As always, thanks for reading!

Useful Links/Sources

  1. Red Hat Enterprise Linux 5 kickstart options
  2. Red Hat Enterprise Linux 6 kickstart options
  3. Red Hat Enterprise Linux 7 kickstart options
  4. How to Install Paravirtualized CentOS 7 DomU on Xen
  5. Xen4 CentOS QuickStart
  6. Xen4CentOS – Xen Wiki