Source code for utah.provisioning.vm

# Ubuntu Testing Automation Harness
# Copyright 2012 Canonical Ltd.

# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License version 3, as published
# by the Free Software Foundation.

# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranties of
# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
# PURPOSE.  See the GNU General Public License for more details.

# You should have received a copy of the GNU General Public License along
# with this program.  If not, see <http://www.gnu.org/licenses/>.

"""Provide functions for virtual machine provisioning."""
# TODO: Maybe combine the VM stuff into one file?


import apt
import libvirt
import os
import random
import shutil
import string
import tempfile

from xml.etree import ElementTree

from utah.config import config
from utah.process import ProcessChecker, ProcessRunner
from utah.provisioning.exceptions import UTAHProvisioningException
from utah.provisioning.inventory import SQLiteInventory
from utah.provisioning.provisioning import CustomInstallMixin, Machine
from utah.provisioning.rsyslog import RSyslog
from utah.provisioning.ssh import SSHMixin
from utah.timeout import UTAHTimeout


[docs]class UTAHVMProvisioningException(UTAHProvisioningException): """Provide a foundation class for UTAH VM provisioning exceptions.""" pass
[docs]class LibvirtVM(Machine): """Provide a class to utilize VMs using libvirt. Capable of utilizing existing VMs Creation currently handled by sublcasses """ def __init__(self, *args, **kw): super(LibvirtVM, self).__init__(*args, **kw) self.vm = None libvirt.registerErrorHandler(self.libvirterrorhandler, None) self.lv = libvirt.open(config.qemupath) if self.lv is None: raise UTAHVMProvisioningException('Cannot connect to libvirt') self.syslog = os.path.join(config.logpath, '{}.syslog.log'.format(self.name)) self.logger.debug('LibvirtVM init finished') @property
[docs] def rsyslog(self): """Return rsyslog instance.""" # TODO: Check if this should go in Machine # Or maybe it should be in the new install stuff if not getattr(self, '_rsyslog', None): self._rsyslog = RSyslog(self.name, config.logpath, self.syslog) return self._rsyslog
def _load(self): """Load an existing VM.""" self.logger.info('Loading VM') self.vm = self.lv.lookupByName(self.name) self.logger.info('VM loaded')
[docs] def activecheck(self): """Verify the machine is provisioned, then start it if needed. """ self.logger.debug('Checking if VM is active') self.provisioncheck() if self.vm.isActive() == 0: self._start() else: self.active = True
def _start(self): """Start the VM.""" # TODO: discuss whether we need a start method here # this becomes a question of separating Install from Machine, probably self.logger.info('Starting VM') if self.vm is not None: if self.vm.isActive() == 0: self.vm.create() else: raise UTAHVMProvisioningException('Failed to provision VM') self.cleanfunction(self.stop, force=True) self.active = True
[docs] def stop(self, force=False): """Stop the machine. :param force: Do a forced shutdown :type force: bool """ self.logger.info('Stopping VM') if self.vm is not None: if self.vm.isActive() == 0: self.logger.info('VM is already stopped') else: if force: self.logger.info('Forced shutdown requested') self.vm.destroy() else: self.vm.shutdown() else: self.logger.info('VM not yet created') self.active = False
[docs] def libvirterrorhandler(self, _context, err): """Log libvirt errors instead of sending them to the console.""" errorcode = err.get_error_code() if errorcode in [9, 42]: # We see these as part of normal operations, # so we send them to debug # 9 is trying to create a VM that already exists # 42 is trying to load a VM that doesn't exist logmethod = self.logger.debug else: logmethod = self.logger.error logmethod('libvirt error: %s', err['message']) logmethod('libvirt error number is: %s', str(errorcode))
[docs]class CustomVM(CustomInstallMixin, SSHMixin, LibvirtVM): """Install a VM from an image using libvirt direct kernel booting.""" # TODO: probably remove parameters from the private methods def __init__(self, directory=config.machinedir, diskbus=config.diskbus, disksizes=config.disksizes, emulator=config.emulator, machineid=config.machineid, macs=config.macs, name=config.name, prefix=config.prefix, *args, **kw): # Make sure that no other virtualization solutions are running # TODO: see if this is needed for qemu or just kvm process_checker = ProcessChecker() for cmdline, app in [('/usr/lib/virtualbox/VirtualBox', 'VirtualBox'), ('/usr/lib/vmware/bin', 'VMware')]: if process_checker.check_cmdline(cmdline): message = process_checker.get_error_message(app) raise UTAHVMProvisioningException(message) self.diskbus = diskbus self.disksizes = disksizes self.emulator = emulator self.macs = macs self.prefix = prefix self.disks = [] if name is None: autoname = True name = '-'.join([str(prefix), str(machineid)]) else: autoname = False super(CustomVM, self).__init__(machineid=machineid, name=name, *args, **kw) if self.inventory is not None: self.cleanfunction(self.inventory.destroy, machineid=self.machineid) # TODO: do a better job of separating installation # into _create rather than __init__ if self.image is None: raise UTAHVMProvisioningException( 'Image file required for VM installation') self._cmdlinesetup() if autoname: self._namesetup() self._loggerunsetup() self._loggersetup() self._cmdlinesetup() if self.emulator is None: if self._supportsdomaintype('kvm'): self.logger.info( 'Setting type to kvm since it is in libvirt capabilities') self.emulator = 'kvm' elif self._supportsdomaintype('qemu'): self.logger.info( 'Setting type to qemu since it is in libvirt capabilities') self.emulator = 'qemu' else: cache = apt.cache.Cache() try: package = cache['qemu-system-x86'] except KeyError: raise UTAHVMProvisioningException( "kvm packages don't seem to be available " 'in the apt cache') if package.is_installed: raise UTAHVMProvisioningException( 'kvm and qemu not supported in libvirt capabilities; ' 'please make sure libvirt is configured correctly') raise UTAHVMProvisioningException( "kvm packages don't seem to be installed; " "please try: 'sudo apt-get install kvm' " 'to get virtualization support') self.directory = (directory or os.path.join(config.vmpath, self.name)) self.cleanfile(self.directory) self.logger.info('Checking if machine directory {} exists' .format(self.directory)) if not os.path.isdir(self.directory): self.logger.debug('Creating {}'.format(self.directory)) try: os.makedirs(self.directory) except OSError as err: raise UTAHVMProvisioningException(err) self.logger.debug('CustomVM init finished') def _createdisks(self, disksizes=None): """Create disk files if needed and build a list of them.""" self.logger.info('Creating disks') if disksizes is None: disksizes = self.disksizes for index, size in enumerate(disksizes): disksize = '{}G'.format(size) basename = 'disk{}.qcow2'.format(index) diskfile = os.path.join(self.directory, basename) if not os.path.isfile(diskfile): cmd = ['qemu-img', 'create', '-f', 'qcow2', diskfile, disksize] self.logger.debug('Creating %s disk using: %s', disksize, ' '.join(cmd)) if ProcessRunner(cmd).returncode != 0: raise UTAHVMProvisioningException( 'Could not create disk image at {}'.format(diskfile)) disk = {'bus': self.diskbus, 'file': diskfile, 'size': disksize, 'type': 'qcow2'} self.disks.append(disk) self.cleanfile(diskfile) self.logger.debug('Adding disk to list') def _supportsdomaintype(self, domaintype): """Check emulator support in libvirt capabilities. :param domaintype: Which domain type to check :type domaintype: str :returns: Whether the specified domain type is supported :rtype: bool """ capabilities = ElementTree.fromstring(self.lv.getCapabilities()) for guest in capabilities.iterfind('guest'): for arch in guest.iterfind('arch'): for domain in arch.iterfind('domain'): if domaintype in domain.get('type'): return True return False def _installxml(self, cmdline=None, image=None, initrd=None, kernel=None, tmpdir=None, xml=None): """Return the XML tree to be passed to libvirt for VM installation.""" self.logger.info('Creating installation XML') if cmdline is None: cmdline = self.cmdline if image is None: image = self.image.image if initrd is None: initrd = self.initrd if kernel is None: kernel = self.kernel if xml is None: xml = self.xml if tmpdir is None: tmpdir = self.tmpdir xmlt = ElementTree.ElementTree(file=xml) if self.rewrite in ['all', 'minimal']: self.logger.debug('Setting VM to shutdown on reboot') xmlt.find('on_reboot').text = 'destroy' if self.rewrite == 'all': self._installxml_rewrite_all(cmdline, image, initrd, kernel, xmlt) else: self.logger.info('Not rewriting XML because rewrite is %s', self.rewrite) if self.debug: xmlt.write(os.path.join(tmpdir, 'install.xml')) self.logger.info('Installation XML ready') return xmlt def _installxml_rewrite_all(self, cmdline_txt, image, initrd_txt, kernel_txt, xmlt): """Rewrite the whole configuration file for the VM.""" self.logger.debug('Rewriting basic info') xmlt.find('name').text = self.name xmlt.find('uuid').text = self.uuid self.logger.debug( 'Setting type to qemu in case no hardware virtualization present') xmlt.getroot().set('type', self.emulator) ose = xmlt.find('os') if self.image.arch == ('i386'): ose.find('type').set('arch', 'i686') elif self.image.arch == ('amd64'): ose.find('type').set('arch', 'x86_64') else: ose.find('type').set('arch', self.image.arch) self.logger.debug('Setting up boot info') for kernele in list(ose.iterfind('kernel')): ose.remove(kernele) kernele = ElementTree.Element('kernel') kernele.text = kernel_txt ose.append(kernele) for initrde in list(ose.iterfind('initrd')): ose.remove(initrde) initrde = ElementTree.Element('initrd') initrde.text = initrd_txt ose.append(initrde) for cmdlinee in list(ose.iterfind('cmdline')): ose.remove(cmdlinee) cmdlinee = ElementTree.Element('cmdline') cmdlinee.text = cmdline_txt ose.append(cmdlinee) self.logger.debug('Setting up devices') devices = xmlt.find('devices') self.logger.debug('Setting up disks') for disk in list(devices.iterfind('disk')): if disk.get('device') == 'disk': devices.remove(disk) self.logger.debug('Removed existing disk') #TODO: Add a cdrom if none exists if disk.get('device') == 'cdrom': if disk.find('source') is not None: disk.find('source').set('file', image) self.logger.debug('Rewrote existing CD-ROM') else: source = ElementTree.Element('source') source.set('file', image) disk.append(source) self.logger.debug( 'Added source to existing CD-ROM') for disk in self.disks: diske = ElementTree.Element('disk') diske.set('type', 'file') diske.set('device', 'disk') driver = ElementTree.Element('driver') driver.set('name', 'qemu') driver.set('type', disk['type']) diske.append(driver) source = ElementTree.Element('source') source.set('file', disk['file']) diske.append(source) target = ElementTree.Element('target') dev = ("vd{}" .format(string.ascii_lowercase[self.disks.index(disk)])) target.set('dev', dev) target.set('bus', disk['bus']) diske.append(target) devices.append(diske) self.logger.debug('Added %s disk', str(disk['size'])) macs = list(self.macs) for interface in devices.iterfind('interface'): if interface.get('type') in ['network', 'bridge']: if len(macs) > 0: mac = macs.pop(0) interface.find('mac').set('address', mac) self.logger.debug( 'Rewrote interface to use specified mac address: %s', mac) else: mac = random_mac_address() interface.find('mac').set('address', mac) self.macs.append(mac) self.logger.debug( 'Rewrote interface to use random mac address: %s', mac) if interface.get('type') == 'bridge': interface.find('source').set('bridge', config.bridge) serial = ElementTree.Element('serial') serial.set('type', 'file') source = ElementTree.Element('source') source.set('path', self.syslog) serial.append(source) target = ElementTree.Element('target') target.set('port', '0') serial.append(target) devices.append(serial) def _installvm(self, lv=None, tmpdir=None, xml=None): """Install a VM, then undefine it in libvirt. The final installation will recreate the VM using the existing disks. """ self.logger.info('Creating VM') if lv is None: lv = self.lv if xml is None: xml = self.xml if tmpdir is None: tmpdir = self.tmpdir os.chmod(tmpdir, 0755) vm = lv.defineXML(ElementTree.tostring(xml.getroot())) try: vm.create() self.logger.info( 'Installing system on VM (may take over an hour)') self.logger.info('You can watch the progress with virt-viewer') log_filename = os.path.join(config.logpath, '{}.syslog.log'.format(self.name)) self.logger.info('Logs will be written to %s', log_filename) self.rsyslog.wait_for_install() while vm.isActive() is not 0: pass finally: try: vm.destroy() except libvirt.libvirtError: pass finally: vm.undefine() self.logger.info('Installation complete') def _finalxml(self, tmpdir=None, xml=None): """Create the XML to be used for the post-installation VM. This may be a transformation of the installation XML. :param tmpdir: Directory to output XML :type tmpdir: str or None :param xml: XML to edit :type xml: xml.etree.ElementTree.ElementTree or None :returns: XML to use for VM post-install :rtype: xml.etree.ElementTree.ElementTree .. seealso:: :meth:`installxml` """ self.logger.info('Creating final VM XML') if xml is None: xml = ElementTree.ElementTree(file=self.xml) if tmpdir is None: tmpdir = self.tmpdir if self.rewrite in ['all', 'minimal']: self.logger.debug('Setting VM to reboot normally on reboot') xml.find('on_reboot').text = 'restart' if self.rewrite == 'all': self.logger.debug('Removing VM install parameters') ose = xml.find('os') for kernel in ose.iterfind('kernel'): ose.remove(kernel) for initrd in ose.iterfind('initrd'): ose.remove(initrd) for cmdline in ose.iterfind('cmdline'): ose.remove(cmdline) devices = xml.find('devices') for disk in list(devices.iterfind('disk')): if disk.get('device') == 'cdrom': disk.remove(disk.find('source')) else: self.logger.info('Not rewriting XML because rewrite is %s', self.rewrite) if self.debug: xml.write(os.path.join(tmpdir, 'final.xml')) return xml def _tmpimage(self, image=None, tmpdir=None): """Create a temporary copy of the image so libvirt will lock that one. This allows other simultaneous processes to update the cached image. :returns: The path to the temporary image :rtype: str """ if image is None: image = self.image.image if tmpdir is None: tmpdir = self.tmpdir self.logger.info('Making temp copy of install image') tmpimage = os.path.join(tmpdir, os.path.basename(image)) self.logger.debug('Copying %s to %s', image, tmpimage) shutil.copyfile(image, tmpimage) return tmpimage def _create(self, provision_data): """Create the VM, install the system, and prepare it to boot. This primarily calls functions from CustomInstallMixin and CustomVM. """ self.logger.info('Creating custom virtual machine') tmpdir = tempfile.mkdtemp(prefix='/tmp/{}_'.format(self.name)) self.cleanfile(tmpdir) self.logger.debug('Working dir: %s', tmpdir) os.chdir(tmpdir) kernel = self._preparekernel(kernel=self.kernel, tmpdir=tmpdir) initrd = self._prepareinitrd(initrd=self.initrd, tmpdir=tmpdir) self._unpackinitrd(initrd=initrd, tmpdir=tmpdir) if provision_data: initrd_dir = os.path.join(tmpdir, 'initrd.d') provision_data.update_initrd(initrd_dir) self._setuplatecommand(tmpdir=tmpdir) self._setuppreseed(tmpdir=tmpdir) if self.rewrite == 'all': self._setuplogging(tmpdir=tmpdir) else: self.logger.debug('Skipping logging setup because rewrite is %s', self.rewrite) initrd = self._repackinitrd(tmpdir=tmpdir) self._createdisks() image = self._tmpimage(image=self.image.image, tmpdir=tmpdir) xml = self._installxml(cmdline=self.cmdline, image=image, initrd=initrd, kernel=kernel, tmpdir=tmpdir, xml=self.xml) self._installvm(lv=self.lv, tmpdir=tmpdir, xml=xml) xml = self._finalxml(tmpdir=tmpdir, xml=xml) self.logger.info('Setting up final VM') self.vm = self.lv.defineXML(ElementTree.tostring(xml.getroot())) self.cleanfunction(self.vm.destroy) self.cleanfunction(self.vm.undefine) def _start(self): """Start the VM.""" self.logger.info('Starting CustomVM') if self.vm is not None: if self.vm.isActive() == 0: self.vm.create() else: raise UTAHVMProvisioningException('Failed to provision VM') self.rsyslog.wait_for_booted(self.uuid) try: self.logger.info( 'Waiting %d seconds for ping response', config.boottimeout) self.pingpoll(timeout=config.boottimeout) except UTAHTimeout: # Ignore timeout for ping, since depending on the network # configuration ssh might still work despite of the ping failure. self.logger.warning('Network connectivity (ping) failure') self.sshpoll(timeout=config.boottimeout) self.active = True # See http://kennethreitz.com/blog/generate-a-random-mac-address-in-python/
[docs]def random_mac_address(): """Return a random MAC address.""" mac = [0x52, 0x54, 0x00, random.randint(0x00, 0xff), random.randint(0x00, 0xff), random.randint(0x00, 0xff)] return ':'.join(map(lambda x: "%02x" % x, mac))
[docs]class TinySQLiteInventory(SQLiteInventory): """Provide basic SQLite inventory for VMs. Only implements request, release, and destroy. No authentication or conflict checking currently exists. Only suitable for VMs at present. """ def __init__(self, *args, **kw): """Initialize simple database.""" super(TinySQLiteInventory, self).__init__(*args, **kw) self.execute( 'CREATE TABLE IF NOT EXISTS ' 'machines(machineid INTEGER PRIMARY KEY, state TEXT)')
[docs] def request(self, machinetype=CustomVM, *args, **kw): """Return a Machine. Takes a Machine class as machinetype, and passes the newly generated machineid along with all other arguments to that class's constructor, returning the resulting object. """ cursor = self.execute("INSERT INTO machines (state) " "VALUES ('provisioned')") machineid = cursor.lastrowid return machinetype(machineid=machineid, *args, **kw)
[docs] def release(self, machineid): """Update the database to indicate the machine is available.""" self.execute( "UPDATE machines SET state='available' WHERE machineid=?", [machineid])
[docs] def destroy(self, machineid): """Update the database to indicate a machine is destroyed.""" self.execute( "UPDATE machines SET state='destroyed' ""WHERE machineid=?", [machineid])
[docs]def get_vm(**kw): """Return a Machine object for a VM with the passed in arguments. :param kw: All parameters are passed to the Machine constructor :type kw: dict :returns: Appropriately constructed Machine object :rtype: object """ inventory = TinySQLiteInventory() return inventory.request(**kw)
Read the Docs v: latest
Versions
latest
Downloads
PDF
HTML
Epub
On Read the Docs
Project Home
Builds

Free document hosting provided by Read the Docs.