Windows VM using LXD

It’s not entirely obvious how to create a Windows Virtual Machine when using LXD. Here are the most basic steps to get it up and running. This is largely for my own documentation but will probably help someone else out there I’m sure.

The easiest option is to embed the VirtIO drivers directly into the Windows ISO using distrobuilder (via snap). This is the method I’ll be demonstrating. Alternatively, you could attach both ISO’s and when it comes to selecting a drive to install to (during windows installer) you’ll have to click the ‘load drivers from removable media’ and select them from their.

First, install the needed tools. Note, you’re more than welcome to compile distrobuilder from source, but using the snap is much quicker.

sudo snap install distrobuilder --classic
sudo apt install -y libguestfs-tools wimtools

Proceed to download the needed ISOs. You can download Windows ISO HERE and VirtIO ISO HERE.

Now we’re ready to embed the drivers directly into the Windows ISO. Here’s the command:

sudo distrobuilder repack-windows /home/shaner/Downloads/win10_21H2.iso ./win10_packed.iso --drivers=/home/shaner/Downloads/virtio-win.iso

Next, we create an empty machine (VM),set the disk size, and attach our custom ISO with virtio drivers.

lxc init win10pro --empty --vm -c security.secureboot=false
lxc config device override win10pro root size=40GiB
lxc config device add win10pro iso disk source=/home/shaner/win10_packed.iso  boot.priority=10

Now, start the machine and attach to the VGA console to walk-through the installer:

lxc start win10pro --console=vga

Once the install is complete, be sure to enable Remote Desktop as it’s a much better experience than using the LXC (spice) console.

IP Address Discovery for LXD Machines

I’m currently working on a side project that uses LXD as the primary back-end hypervisor (more backends to come, looking at libcloud). It seems pretty well thought out and, so far, it’s been really nice to work with.

I did run into a snag however, but it’s not really a fault of LXD. I needed to be able to learn the IP address of a VM that doesn’t have the LXD-agent running. This is the case for any machine images not provided by Canonical’s upstream image servers. For example, Windows, FreeBSD, OpenBSD, and any custom Linux distribution certainly will not have it available by default.

Doing some research, I discovered that the dnsmasq DHCP server has the ability to call external programs when creating new leases. This is perfect and should be easy enough to implement. Here’s snippet from the dnsmasq man page:

--dhcp-script=<path> Whenever a new DHCP lease is created, or an old one destroyed, or a TFTP file transfer completes, the executable specified by this option is run. <path> must be an absolute pathname, no PATH search occurs. The arguments to the process are "add", "old" or "del", the MAC address of the host (or DUID for IPv6) , the IP address, and the hostname, if known.

So, we need some sort of middleware that I could push new leases to when they’re created by dnsmasq. Since I already had a redis server running handling celery tasks, I decided to just use that.

Great, let’s whip up a python script for dnsmasq to call out to and update redis.

#!/usr/bin/env python3
# /usr/local/bin/dhcpredis.py

import sys
import redis


if len(sys.argv) < 4:
    sys.exit(1)
op = sys.argv[1]
mac = sys.argv[2]
ip = sys.argv[3]
# hostname = sys.argv[4]  # Not interested at this time.

RHOST = '172.16.0.252'
RPORT = 6379
RDB = 0

r = redis.Redis(host=RHOST, port=RPORT, db=RDB)
if 'add' in op:
    r.set(mac, ip)

if 'del' in op:
    r.delete(mac)

Certainly some room for improvement in the script above, but it gets the point across. If it’s not apparent, we’re using the MAC address as the redis key, and the IP as it’s value. Now, configure dnsmasq to point at the script and restart it to pick up the changes.

# echo "dhcp-script=/usr/local/bin/dhcpredis.py" >> /etc/dnsmasq.conf
# systemctl restart dnsmasq

Awesome, we’re good to go! The last thing I had to do is just update my code to pull the KEY:VALUE pair out of redis. If you’re curious to see what that might look like, here’s the (poorly-coded) function I used. Note, I’m using the python LXD library (pylxd) and getting the MAC address from LXD directly.

def get_mach_ip(mach):
    retries = 60
    sleep_time = 5
    hwaddr = None
    while retries > 0:
        retries -= 1
        r = redis.Redis(host=app.config['REDIS_HOST'],
                        port=app.config['REDIS_PORT'])
        try:
            hwaddr = mach.config['volatile.eth0.hwaddr']
            cip = r.get(hwaddr).decode('utf-8') if hwaddr else None
            return cip if cip
        except Exception:
            logging.info(f'Waiting on IP for ether {hwaddr}')
        time.sleep(sleep_time)
    return None

That’s pretty much it!