Monday, September 26, 2011

Hot-Plugging Data With Python And Libvirt

The CD-ROM drive revolutionized the way we're able to move data around on personal computers.  Never mind the internet, the notion of seamlessly transporting information over the wire between two machines was unthinkable fifteen years ago.  But besides transferring data back and forth, we've also come to depend on portable storage devices for practical reasons.

Sometimes the network isn't available to us.  Or, maybe there is a network, but we simply don't want our data traversing it.  We like to carry data around with us — on USB drives.

The portable storage abstraction persists in virtual infrastructures.  The same practical limitations we experience with physical machines still affect those that run on a hypervisor — machines aren't going to come pre-loaded with all the data they'll ever need.  This is where portable storage — hot-pluggable storage — becomes advantageous.

Data and devices
Plugging data into a virtual machine means setting up a virtual device and attaching it to the domain.  With Libvirt, this is easy to do because everything is defined using an XML description.  This means that we can create a device on the fly and plug it in — even after the domain is running.  Hot-plugging devices like this is important because we can't afford to shut-down the machine, attach a device, and start it up.  This incurs too much overhead — not to mention the inflexibility.  Imagine you had to reboot your laptop in order for your operating system to recognize a USB drive — you might as well install a new hard drive in the mean time.

Here is an example of how you can pass some arbitrary data to a running virtual machine.  There are three phases required here to take a string and transform it into a device the machine can read from:
  1. Write the string to a file.
  2. Create an ISO file.
  3. Build the device XML
The Python code that builds the device an attaches it to a running domain...

import shutil
import os.path
import tempfile
import subprocess

import libvirt

def mkdata(data):
    tmpdir = tempfile.mkdtemp()
    tmpfile = tempfile.mkstemp(dir=tmpdir)[1]
    with open(tmpfile, 'w') as ofile:
        ofile.write(data)
    return tmpdir

def mkiso(indir):
    tmpiso = tempfile.mkstemp(suffix='.iso')[1]
    subprocess.call(['mkisofs', '-o', tmpiso, indir])
    shutil.rmtree(os.path.abspath(indir))
    return tmpiso
    
def mkdevice(iso):
    return """<disk type='file' device='disk'>
                <driver name='qemu' type='raw'/>
                <source file='%s'/>
                <target dev='sdb' bus='usb'/>
              </disk>""" % iso

if __name__ == '__main__':
    
    conn = libvirt.open('qemu:///system')
    domain = conn.lookupByName('MyVM')
    
    tmpdir = mkdata('Some virtual machine data...')
    tmpiso = mkiso(tmpdir)
    device = mkdevice(tmpiso)
    
    domain.attachDevice(device)

Let's take a closer look at what we're doing here.

The first function, mkdata(), takes an input value and writes it to a temporary file.  But before doing so, it creates a temporary directory for the file.  The reason being, we need a directory to create the ISO format for the device.

The second function, mkiso(), takes an input directory and passes it to the mkisofs command.  Once the ISO is built, we can remove the temporary directory and return the ISO file location.

The third function, mkdevice(), builds the Libvirt device we're going to attach to the domain.  The iso parameter is the ISO file we want to pass to the virtual machine, generated by mkiso().  You'll notice that this is a disk device, as that is what an ISO image is — a disk.  The driver element specifies that this disk is in raw format.  There are other options available such as qcow, but with this approach, the format is almost always raw.  The source element in the device description points to the ISO file we've just generated.

The target element tells the domain what the device label should be.  As the Libvirt documentation spells-out, there is never any guarantee that the device label you give it here will be retained by the guest operating system.  For example, in our case, we're telling the guest domain to label the new device as sdbsda is probably taken.  However, if sdb is already taken, then we won't know for sure what the device will be called.  This is one area that this method is lacking in — ensuring that the device will be named appropriately so it may be referenced later on.

Finally, with these three functions that'll build the device we're ready to attach it to a domain.  We do this here by establishing a Libvirt connection and locating the domain we're interested in.  Next, we build the device and attach it.

Using the data
If you ran the above example and all went well, you now have a new disk device attached to your domain.  This is great — new data has been made available, no need to access a network and no need to reboot.  But inside the guest, applications need access to the data to make use of it.

Below is an example of how you could listen for the new device — from within the machine — and read it.

import os, os.path
import subprocess
import time

while True:
    if not os.path.exists('/dev/sdb'):
        time.sleep(10)
        continue
    
    subprocess.call(['mount', '/dev/sdb', '/mnt/sdb'])
    for fname in os.listdir('/mnt/sdb'):
        with open(os.path.join('/mnt/sdb', fname)) as ifile:
            print 'Got some data...'
            print ifile.read()

Here, all we're doing is checking, every ten seconds, if the device we just attached exists.  If it does, we're then able to mount it and read from the file within the ISO.