Easily clone and provision virtual machines

This guide contains useful tips for cloning and provisioning KVM/QEMU virtual machines under Debian. Since this guide uses libvirt it may also work with other VM backends such as Virtualbox although there is no guarantee.

Required Components

This guide relies on the following tools:

virt-clone provided by the virtinst Debian package

virt-sysprep provided by the libguestfs-tools Debian package

virsh provided the libvirt-clients Debian package

In addition, to create the template virtual machine, you can use either:

virt-install tool to create a VM from the command-line, provided by the virtinst Debian package

-or-

virt-manager graphical tool to create and manage VMs, provided by the Debian virt-manager package

Virtual Machine Uses

Being able to easily create virtual machine clusters has a lot of uses. Systems administrators can use VMs to set up clusters of machines for testing. Likewise students can use clusters of VMs for learning.

For example Saltstack recommends to use salt-vagrant-demo which uses Vagrant to provision a small cluster of machines for learning. But such a cluster can easily be created and managed using the tools below.

Other possible projects include setting up HAProxy clusters, database clusters such as MariaDB/Galera, an Nginx proxy with backend servers, etc.

If you have a machine with a good amount of RAM (16G recommended, 32G is better) and fast storage (NVMe storage is recommended) it is no problem to run a small VM cluster or even several.

Important Reference

The Debian KVM wiki page is an important resource for information on KVM.

Set up a Network Bridge on the VM Host

On the VM host machine you will likely want to set up a network bridge as that is the most useful way to provide networking to VM guests. VM guests that connect to the bridge interface will appear as regular hosts on the same network as the VM host.

The KVM wiki page above has an example for how to configure a network bridge with ifupdown via /etc/network/interfaces.

Instead of ifupdown you may use systemd’s networkd directly to create a bridge which I found to be very easy.  Note that if you are going to configure your interfaces with networkd then you should uninstall ifupdown or at least remove any interfaces that are configured via networkd from the ifupdown configuration.

Finally, when you configure the template VM below be sure to select the bridge device of the host machine for the network configuration.

Create a Template VM

Create a template virtual machine either using the virt-manager graphical VM management tool or with the virt-install command (refer to the Debian KVM wiki for examples). The template VM will be used to create clones.

The amount of configuration done to the template VM depends upon how it is to be used. In my case, I want the template VM to be as close as possible to the minimal Debian installation default. Essentially it is like a raw cloud VM image and it saves a lot of time from having to run through the full Debian installation process each time I want to create a new VM.  However, if you’re creating a server template for an organization or other purpose, you may want to include a baseline of tools, utilities, and/or other custom configuration options.

For me the most important baseline after a minimal Debian install is:

apt install openssh-server openssh-client rsync sudo vim vim-tiny- nano-

This will install OpenSSH, rsync, sudo, and regular Vim and will remove vim-tiny and nano.  If you create any users remember to add them to the sudo group:

addgroup user sudo

The Python example near the bottom of this guide also configures Saltstack on the cloned VMs, thus it requires that salt-minion be installed in the template VM.  For info on installing it see here.

In the process outlined below we  clone a VM from the template VM and make sure that the clone is a assigned a unique IP address and set of SSH host keys and contains no residual stuff from the template VM such as log files or temp files.

If you only want the guest VMs to use DHCP for their network address then you don’t need to do anything because that is the Debian installation default configuration for the primary network interface.

If however you want to assign a static IP address to the primary network interface of VM guests then on the template VM edit /etc/network/interfaces and assign a static IP. For example:

# This file describes the network interfaces available on your system
# and how to activate them. For more information, see interfaces(5).

source /etc/network/interfaces.d/*

# The loopback network interface
auto lo
iface lo inet loopback

# The primary network interface
allow-hotplug enp1s0
iface enp1s0 inet static
        address 192.168.0.210/24
        gateway 192.168.0.1

Cloning and Provisioning

Let’s say a a template VM named vm-template has been created. Use virt-clone to create a new VM named vm1:

virt-clone --original vm-template --name vm1 --auto-clone

Introducing virt-sysprep

From the virt-sysprep man page:

Virt-sysprep can reset or unconfigure a virtual machine so that clones can be made from it. Steps in this process include removing SSH host keys, removing persistent network MAC configuration, and removing user accounts. Virt-sysprep can also customize a virtual machine, for instance by adding SSH keys, users or logos. Each step can be enabled or disabled as required.

The default operations performed by virt-sysprep are intended to wipe clean the newly provisioned VM by clearing log, temp, and backup files, SSH keys, and any unique machine or system IDs. The idea is to start with a completely fresh system. For example, you would not want a newly-provisioned guest VM to have the same SSH keys as the template machine, nor would you want it to contain any log or temporary files from the template VM.

Use virt-sysprep to provision the newly-cloned VM:

virt-sysprep -d vm1 --hostname vm1 --edit /etc/network/interfaces:'s/192.168.0.210/192.168.0.215/' --firstboot-command 'dpkg-reconfigure openssh-server'

To see a full list of everything that virt-sysprep clears run virt-sysprep --list-operations. All the operations with a ‘*’ are enabled by default and will be run. For example, to get a list of only the default enabled options:

virt-sysprep --list-operations | egrep ' \* '

Beware with virt-clone that if any operation is explicitly enabled then all other default enabled options will be automatically disabled. ‘Operation’ with virt-clone has a specific meaning.  If you intend to explicitly enable any operation, you must also explicitly enable all other operations you want performed.  In the example above we do not explicitly enable any operations and thus use the default enabled ones.

For Debian VMs we can tell virt-sysprep to generate a new set of SSH host keys via the --firstboot-command 'dpkg-reconfigure openssh-server' flag.

The –edit flag is extremely useful and is used here to edit the contents of /etc/network/interfaces in the VM. It changes the static IP address from the template’s value of 192.168.0.210 to an assigned value of 192.168.0.215. –edit uses a Perl regex to accomplish its task.

Python Script

Here is a Python script to provision VMs from a VM template machine.  This script requires that salt-minion is installed in the template VM.  In addition to configuring the hostname and IP address it also sets the IP address of the salt master in the salt minion configuration as well as the minion’s ID.

#!/usr/bin/python3
#
# This script provisions new KVM virtual machines from a template virtual
#   machine
# 

import shutil

virtclonecmd = shutil.which('virt-clone')
if not virtclonecmd:
    sys.stderr.write("\nThe virt-clone binary was not found in $PATH\n"
        "Please make sure the virtinst package is installed (Debian)\n"
        "or that you have the binary otherwise installed.\n"
        "This program will now exit.\n")
    exit(1)

virtsysprepcmd = shutil.which('virt-sysprep')
if not virtsysprepcmd:
    sys.stderr.write("\nThe virt-sysprep binary was not found in $PATH\n"
        "Please make sure the libguestfs-tools package is installed (Debian)\n"
        "or that you have the binary otherwise installed.\n"
        "This program will now exit.\n")
    exit(1)

from subprocess import run

# Template VM hostname and IP address
tName = 'bullseye-base'
tIP = '192.168.0.120'

# IP address of the salt master
saltmst = '192.168.0.10'

# hostname and IP address of VMs to create
hosts = {
    'bullseye1': '192.168.0.125',
    'bullseye2': '192.168.0.126'
}

# For each host and IP defined above, create a clone VM for the host
#  and assign the IP.  virt-sysprep ensures the cloned image is fresh
#  and doesn't contain leftover log, temp, or other files or data from
#  the template VM from which it is cloned.
for host, ip in hosts.items():

    
    # run() accepts a list as its first argument
    # each item in the list is a token from the string of the command + arguments
    
    # use Python f-strings (template strings) where applicable
    
    # list of virt-clone command and arguments
    clonecmd = [
        f'{virtclonecmd}',
        '--original', f'{tName}',
        '--name', f'{host}',
        '--auto-clone'
        ]
        
    # run the command
    run(clonecmd)
 
    # list containing virt-sysprep command and arguments
    sysprepcmd = [
        f'{virtsysprepcmd}',
        '-d', f'{host}',
        '--hostname', f'{host}',
        '--edit', f'/etc/network/interfaces:s/{tIP}/{ip}/',
        '--edit', f'/etc/hosts:s/{tName}/{host}/g',
        '--edit', f'/etc/salt/minion:s/#master: salt/{saltmst}/',
        '--edit', f'/etc/salt/minion_id:s/{tName}/{host}/',
        '--firstboot-command', 'dpkg-reconfigure openssh-server'
        ]
        
    # run the command
    run(sysprepcmd)

Launch and Manage VMs with virsh

The VMs can now be launched and managed with virsh:

virsh list --all  # list all VMs
virst start bullseye1   # start bullseye1 VM
virst shutdown bullseye1  # gracefully shutdown bullseye1
virsh destroy bullseye1  # force shutdown
virsh undefine --remove-all-storage bullseye1  # completely remove bullseye1 (use with caution)

Going Further

There is potentially a lot more that can be done. One possibility is to set up a VDE virtual switch for a VM cluster.

virt-sysprep has a lot of options to explore.

References and Thanks

wiki.debian.org/KVM

libguestfs.org/virt-sysprep.1.html

https://wiki.debian.org/QEMU (has useful section on setting up VDE)

cyberciti.biz: How to reset a KVM clone virtual Machines with virt-sysprep on Linux