# Copyright 2013 - 2014 Mirantis, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import datetime from time import sleep import xml.etree.ElementTree as ET import ipaddr import libvirt import os from devops.driver.libvirt.libvirt_xml_builder import LibvirtXMLBuilder from devops.helpers.helpers import _get_file_size from devops.helpers.retry import retry from devops.helpers import scancodes from devops import logger from django.conf import settings class Snapshot(object): def __init__(self, snapshot): self._snapshot = snapshot self._domain = snapshot.getDomain() self._xml_content = '' self._xml = snapshot.getXMLDesc(0) self._repr = "" @property def _xml(self): return self._xml_content @_xml.setter def _xml(self, xml_content): snapshot_xmltree = ET.fromstring(xml_content) cpu = snapshot_xmltree.findall('./domain/cpu') if cpu and 'mode' in cpu[0].attrib: cpu_mode = cpu[0].get('mode') # Get cpu model from domain definition as it is not available # in snapshot XML for host-passthrough cpu mode if cpu_mode == 'host-passthrough': domain_xml = self._domain.XMLDesc( libvirt.VIR_DOMAIN_XML_UPDATE_CPU) domain_xmltree = ET.fromstring(domain_xml) cpu_element = domain_xmltree.find('./cpu') domain_element = snapshot_xmltree.findall('./domain')[0] domain_element.remove(domain_element.findall('./cpu')[0]) domain_element.append(cpu_element) self._xml_content = ET.tostring(snapshot_xmltree) @property def _xml_tree(self): return ET.fromstring(self._xml) @property def children_num(self): return self._snapshot.numChildren() @property def created(self): timestamp = self._xml_tree.findall('./creationTime')[0].text return datetime.datetime.utcfromtimestamp(float(timestamp)) @property def disks(self): disks = {} xml_snapshot_disks = self._xml_tree.find('./disks') for xml_disk in xml_snapshot_disks: if xml_disk.get('snapshot') == 'external': disks[xml_disk.get('name')] = xml_disk.find( 'source').get('file') return disks @property def get_type(self): """Return snapshot type""" snap_memory = self._xml_tree.findall('./memory')[0] if snap_memory.get('snapshot') == 'external': return 'external' for disk in self._xml_tree.iter('disk'): if disk.get('snapshot') == 'external': return 'external' return 'internal' @property def memory_file(self): return self._xml_tree.findall('./memory')[0].get('file') @property def name(self): return self._snapshot.getName() @property def parent(self): return self._snapshot.getParent() @property def state(self): return self._xml_tree.findall('state')[0].text def __repr__(self): if not self._repr: self._repr = "<{0} {1}/{2}>".format( self.__class__.__name__, self.name, self.created) return self._repr class DevopsDriver(object): def __init__(self, connection_string="qemu:///system", storage_pool_name="default", stp=True, hpet=True, use_host_cpu=True): """libvirt driver :param use_host_cpu: When creating nodes, should libvirt's CPU "host-model" mode be used to set CPU settings. If set to False, default mode ("custom") will be used. (default: True) """ libvirt.virInitialize() self.conn = libvirt.open(connection_string) self.xml_builder = LibvirtXMLBuilder(self) self.stp = stp self.hpet = hpet self.capabilities = None self.allocated_networks = None self.storage_pool_name = storage_pool_name self.reboot_timeout = None self.use_host_cpu = use_host_cpu self.use_hugepages = settings.USE_HUGEPAGES if settings.VNC_PASSWORD: self.vnc_password = settings.VNC_PASSWORD if settings.REBOOT_TIMEOUT: self.reboot_timeout = settings.REBOOT_TIMEOUT def __del__(self): self.conn.close() def _get_name(self, *kwargs): return self.xml_builder._get_name(*kwargs) def get_libvirt_version(self): return self.conn.getLibVersion() @retry() def get_capabilities(self): """Get host capabilities :rtype : ET """ if self.capabilities is None: self.capabilities = self.conn.getCapabilities() return ET.fromstring(self.capabilities) @retry() def network_bridge_name(self, network): """Get bridge name from UUID :type network: Network :rtype : String """ return self.conn.networkLookupByUUIDString(network.uuid).bridgeName() @retry() def network_name(self, network): """Get network name from UUID :type network: Network :rtype : String """ return self.conn.networkLookupByUUIDString(network.uuid).name() @retry() def network_active(self, network): """Check if network is active :type network: Network :rtype : Boolean """ return self.conn.networkLookupByUUIDString(network.uuid).isActive() @retry() def node_active(self, node): """Check if node is active :type node: Node :rtype : Boolean """ return self.conn.lookupByUUIDString(node.uuid).isActive() @retry() def network_exists(self, network): """Check if network exists :type network: Network :rtype : Boolean """ try: self.conn.networkLookupByUUIDString(network.uuid) return True except libvirt.libvirtError as e: if e.get_error_code() == libvirt.VIR_ERR_NO_NETWORK: return False else: raise @retry() def node_exists(self, node): """Check if node exists :type node: Node :rtype : Boolean """ try: self.conn.lookupByUUIDString(node.uuid) return True except libvirt.libvirtError as e: if e.get_error_code() == libvirt.VIR_ERR_NO_DOMAIN: return False else: raise @retry() def node_snapshot_exists(self, node, name): """Check if snapshot exists :type node: Node :type name: String :rtype : Boolean """ ret = self.conn.lookupByUUIDString(node.uuid) return name in ret.snapshotListNames() @retry() def volume_exists(self, volume): """Check if volume exists :type volume: Volume :rtype : Boolean """ try: self.conn.storageVolLookupByKey(volume.uuid) return True except libvirt.libvirtError as e: if e.get_error_code() == libvirt.VIR_ERR_NO_STORAGE_VOL: return False else: raise @retry() def network_define(self, network): """Define network :type network: Network :rtype : None """ ret = self.conn.networkDefineXML( self.xml_builder.build_network_xml(network)) ret.setAutostart(True) network.uuid = ret.UUIDString() @retry() def network_destroy(self, network): """Destroy network :type network: Network :rtype : None """ self.conn.networkLookupByUUIDString(network.uuid).destroy() @retry() def network_undefine(self, network): """Undefine network :type network: Network :rtype : None """ self.conn.networkLookupByUUIDString(network.uuid).undefine() @retry() def network_create(self, network): """Create network :type network: Network :rtype : None """ self.conn.networkLookupByUUIDString(network.uuid).create() @retry() def network_filter_define(self, network): """Define network filter""" return self.conn.nwfilterDefineXML( self.xml_builder.build_network_filter(network)) @retry() def network_filter_undefine(self, network): """Undefine network filter""" return self.conn.nwfilterLookupByName( "{}_{}".format(network.environment.name, network.name)).undefine() @retry() def network_block_status(self, network): """Return network block status""" filter_xml = self.conn.nwfilterLookupByName( "{}_{}".format(network.environment.name, network.name)).XMLDesc() filter_xml = ET.fromstring(filter_xml) return filter_xml.find('./rule') is not None @retry() def network_block(self, network): """Block all traffic in network""" filter_xml = self.conn.nwfilterLookupByName( "{}_{}".format(network.environment.name, network.name)).XMLDesc() filter_xml = ET.fromstring(filter_xml) rule = ET.Element( 'rule', {'action': 'drop', 'direction': "inout", 'priority': '-1000'}) rule.append(ET.Element('all')) filter_xml.find('.').append(rule) self.conn.nwfilterDefineXML(ET.tostring(filter_xml)) @retry() def network_unblock(self, network): """Unblock all traffic in network""" filter_xml = self.conn.nwfilterLookupByName( "{}_{}".format(network.environment.name, network.name)).XMLDesc() filter_xml = ET.fromstring(filter_xml) filter_xml.find('.').remove(filter_xml.find('./rule')) self.conn.nwfilterDefineXML(ET.tostring(filter_xml)) @retry() def interface_filter_define(self, interface): self.conn.nwfilterDefineXML( self.xml_builder.build_interface_filter(interface)) @retry() def interface_filter_undefine(self, interface): """Undefine interface filter""" self.conn.nwfilterLookupByName( "{}_{}_{}".format( interface.network.environment.name, interface.network.name, interface.mac_address)).undefine() @retry() def interface_block_status(self, interface): """Return block status of interface""" filter_xml = self.conn.nwfilterLookupByName( "{}_{}_{}".format( interface.network.environment.name, interface.network.name, interface.mac_address)).XMLDesc() filter_xml = ET.fromstring(filter_xml) return filter_xml.find('./rule') is not None @retry() def interface_block(self, interface): """Block traffic on interface""" net_filter_name = "{}_{}".format(interface.network.environment.name, interface.network.name) iface_filter_name = "{}_{}".format(net_filter_name, interface.mac_address) filter_xml = self.conn.nwfilterLookupByName( iface_filter_name).XMLDesc() filter_xml = ET.fromstring(filter_xml) rule = ET.Element( 'rule', {'action': 'drop', 'direction': "inout", 'priority': '-950'}) rule.append(ET.Element('all')) filter_xml.find('.').append(rule) self.conn.nwfilterDefineXML(ET.tostring(filter_xml)) @retry() def interface_unblock(self, interface): """Unblock traffic on interface""" net_filter_name = "{}_{}".format(interface.network.environment.name, interface.network.name) iface_filter_name = "{}_{}".format(net_filter_name, interface.mac_address) filter_xml = self.conn.nwfilterLookupByName( iface_filter_name).XMLDesc() filter_xml = ET.fromstring(filter_xml) filter_xml.find('.').remove(filter_xml.find('./rule')) self.conn.nwfilterDefineXML(ET.tostring(filter_xml)) @retry() def node_define(self, node): """Define node :type node: Node :rtype : None """ emulator = self.get_capabilities( ).find( 'guest/arch[@name="{0:>s}"]/' 'domain[@type="{1:>s}"]/emulator'.format( node.architecture, node.hypervisor)).text node_xml = self.xml_builder.build_node_xml(node, emulator) logger.info(node_xml) node.uuid = self.conn.defineXML(node_xml).UUIDString() @retry() def node_destroy(self, node): """Destroy node :type node: Node :rtype : None """ self.conn.lookupByUUIDString(node.uuid).destroy() @retry() def node_undefine(self, node, undefine_snapshots=False): """Undefine domain. If undefine_snapshot is set, discard all snapshots. :type node: Node :type undefine_snapshots: Boolean :rtype : None """ domain = self.conn.lookupByUUIDString(node.uuid) if undefine_snapshots: # Delete external snapshots snap_list = domain.listAllSnapshots(0) for snapshot in snap_list: self._delete_snapshot_files(snapshot) domain.undefineFlags( libvirt.VIR_DOMAIN_UNDEFINE_SNAPSHOTS_METADATA) else: domain.undefine() @retry() def node_undefine_by_name(self, node_name): """Undefine domain discarding all snapshots :type node_name: String :rtype : None """ domain = self.conn.lookupByName(node_name) domain.undefineFlags(libvirt.VIR_DOMAIN_UNDEFINE_SNAPSHOTS_METADATA) @retry() def node_get_vnc_port(self, node): """Get VNC port :type node: Node :rtype : String """ xml_desc = ET.fromstring( self.conn.lookupByUUIDString(node.uuid).XMLDesc(0)) vnc_element = xml_desc.find('devices/graphics[@type="vnc"][@port]') if vnc_element is not None: return vnc_element.get('port') @retry() def node_get_interface_target_dev(self, node, mac): """Get target device :type node: Node :type mac: String :rtype : String """ xml_desc = ET.fromstring( self.conn.lookupByUUIDString(node.uuid).XMLDesc(0)) target = xml_desc.find('.//mac[@address="%s"]/../target' % mac) if target is not None: return target.get('dev') @retry() def node_create(self, node): """Create node :type node: Node :rtype : None """ self.conn.lookupByUUIDString(node.uuid).create() @retry() def node_list(self): # virConnect.listDefinedDomains() only returns stopped domains # https://bugzilla.redhat.com/show_bug.cgi?id=839259 return [item.name() for item in self.conn.listAllDomains()] @retry() def node_reset(self, node): """Reset node :type node: Node :rtype : None """ self.conn.lookupByUUIDString(node.uuid).reset() @retry() def node_reboot(self, node): """Reboot node :type node: Node :rtype : None """ self.conn.lookupByUUIDString(node.uuid).reboot() @retry() def node_suspend(self, node): """Suspend node :type node: Node :rtype : None """ self.conn.lookupByUUIDString(node.uuid).suspend() @retry() def node_resume(self, node): """Resume node :type node: Node :rtype : None """ domain = self.conn.lookupByUUIDString(node.uuid) if domain.info()[0] == libvirt.VIR_DOMAIN_PAUSED: domain.resume() @retry() def node_shutdown(self, node): """Shutdown node :type node: Node :rtype : None """ self.conn.lookupByUUIDString(node.uuid).shutdown() @retry() def node_get_snapshots(self, node): """Get list of snapshots :rtype : List :type node: Node """ snapshots = self.conn.lookupByUUIDString( node.uuid).listAllSnapshots(0) return [Snapshot(snap) for snap in snapshots] def node_get_snapshot(self, node, name): """Get snapshot with name :rtype : Snapshot :type node: Node :type name: Snapshot name """ snap = self.conn.lookupByUUIDString( node.uuid).snapshotLookupByName(name) return Snapshot(snap) def node_set_snapshot_current(self, node, name): domain = self.conn.lookupByUUIDString(node.uuid) snapshot = self._get_snapshot(domain, name) snapshot_xml = Snapshot(snapshot)._xml domain.snapshotCreateXML( snapshot_xml, libvirt.VIR_DOMAIN_SNAPSHOT_CREATE_REDEFINE | libvirt.VIR_DOMAIN_SNAPSHOT_CREATE_CURRENT) @retry() def node_create_snapshot(self, node, name=None, description=None, disk_only=False, external=False): """Create snapshot :type description: String :type name: String :type node: Node :rtype : None """ if self.node_snapshot_exists(node, name): logger.error("Snapshot with name {0} already exists".format(name)) return domain = self.conn.lookupByUUIDString(node.uuid) # If domain has snapshots we must check their type snap_list = self.node_get_snapshots(node) if len(snap_list) > 0: snap_type = snap_list[0].get_type if external and snap_type == 'internal': logger.error( "Cannot create external snapshot when internal exists") return if not external and snap_type == 'external': logger.error( "Cannot create internal snapshot when external exists") return logger.info(domain.state(0)) xml = self.xml_builder.build_snapshot_xml( name, description, node, disk_only, external, settings.SNAPSHOTS_EXTERNAL_DIR) logger.info(xml) if external: # Check whether we have directory for snapshots, if not # create it if not os.path.exists(settings.SNAPSHOTS_EXTERNAL_DIR): os.makedirs(settings.SNAPSHOTS_EXTERNAL_DIR) if domain.isActive() and not disk_only: domain.snapshotCreateXML( xml, libvirt.VIR_DOMAIN_SNAPSHOT_CREATE_REUSE_EXT) else: domain.snapshotCreateXML( xml, libvirt.VIR_DOMAIN_SNAPSHOT_CREATE_DISK_ONLY | libvirt.VIR_DOMAIN_SNAPSHOT_CREATE_REUSE_EXT) self.node_set_snapshot_current(node, name) else: domain.snapshotCreateXML(xml) logger.info(domain.state(0)) def _delete_snapshot_files(self, snapshot): """Delete snapshot external files""" snap_type = Snapshot(snapshot).get_type if snap_type == 'external': for snap_file in self._get_snapshot_files(snapshot): if os.path.isfile(snap_file): try: os.remove(snap_file) logger.info( "Delete external snapshot file {0}".format( snap_file)) except Exception: logger.info( "Cannot delete external snapshot file {0}" " must be deleted from cron script".format( snap_file)) def _get_snapshot(self, domain, name): """Get snapshot :type domain: Node :type name: String :rtype : libvirt.virDomainSnapshot """ if name is None: return domain.snapshotCurrent(0) else: return domain.snapshotLookupByName(name, 0) def _get_snapshot_files(self, snapshot): """Return snapshot files""" xml_tree = ET.fromstring(snapshot.getXMLDesc()) snap_files = [] snap_memory = xml_tree.findall('./memory')[0] if snap_memory.get('file') is not None: snap_files.append(snap_memory.get('file')) return snap_files @retry() def node_revert_snapshot_recreate_disks(self, node, name): """Recreate snapshot disks.""" domain = self.conn.lookupByUUIDString(node.uuid) snapshot = Snapshot(self._get_snapshot(domain, name)) if snapshot.children_num == 0: for s_disk, s_disk_data in snapshot.disks.items(): logger.info("Recreate {0}".format(s_disk_data)) # Save actual volume XML, delete volume and create # new from saved XML volume = self.conn.storageVolLookupByKey(s_disk_data) volume_xml = volume.XMLDesc() volume_pool = volume.storagePoolLookupByVolume() volume.delete() volume_pool.createXML(volume_xml) @retry() def node_revert_snapshot(self, node, name=None): """Revert snapshot for node :type node: Node :type name: String :rtype : None """ domain = self.conn.lookupByUUIDString(node.uuid) snapshot = Snapshot(self._get_snapshot(domain, name)) if snapshot.get_type == 'external': logger.info("Revert {0} ({1}) from external snapshot {2}".format( node.name, snapshot.state, name)) if self.node_active(node): self.node_destroy(node) # When snapshot dont have children we need to update disks in XML # used for reverting, standard revert function will restore links # to original disks, but we need to use disks with snapshot point, # we dont want to change original data # # For snapshot with children we need to create new snapshot chain # and we need to start from original disks, this disks will get new # snapshot point in node class xml_domain = snapshot._xml_tree.find('domain') if snapshot.children_num == 0: domain_disks = xml_domain.findall('./devices/disk') for s_disk, s_disk_data in snapshot.disks.items(): for d_disk in domain_disks: d_disk_dev = d_disk.find('target').get('dev') d_disk_device = d_disk.get('device') if d_disk_dev == s_disk and d_disk_device == 'disk': d_disk.find('source').set('file', s_disk_data) if snapshot.state == 'shutoff': # Redefine domain for snapshot without memory save self.conn.defineXML(ET.tostring(xml_domain)) else: self.conn.restoreFlags( snapshot.memory_file, dxml=ET.tostring(xml_domain), flags=libvirt.VIR_DOMAIN_SAVE_PAUSED) # set snapshot as current self.node_set_snapshot_current(node, name) else: logger.info("Revert {0} ({1}) to internal snapshot {2}".format( node.name, snapshot.state, name)) domain.revertToSnapshot(snapshot._snapshot, 0) @retry() def node_delete_all_snapshots(self, node): """Delete all snapshots for node :type node: Node """ domain = self.conn.lookupByUUIDString(node.uuid) # Delete all external snapshots end return snap_list = domain.listAllSnapshots(0) if len(snap_list) > 0: snap_type = Snapshot(snap_list[0]).get_type if snap_type == 'external': for snapshot in snap_list: snapshot.delete(2) return for name in domain.snapshotListNames( libvirt.VIR_DOMAIN_SNAPSHOT_LIST_ROOTS): snapshot = self._get_snapshot(domain, name) snapshot.delete(libvirt.VIR_DOMAIN_SNAPSHOT_DELETE_CHILDREN) @retry() def node_delete_snapshot(self, node, name=None): """Delete snapshot :type node: Node :type name: String """ domain = self.conn.lookupByUUIDString(node.uuid) snapshot = self._get_snapshot(domain, name) snap_type = Snapshot(snapshot).get_type if snap_type == 'external': if snapshot.numChildren() > 0: logger.error("Cannot delete external snapshots with children") return if domain.isActive(): domain.destroy() # Update domain to snapshot state xml_tree = ET.fromstring(snapshot.getXMLDesc()) xml_domain = xml_tree.find('domain') self.conn.defineXML(ET.tostring(xml_domain)) self._delete_snapshot_files(snapshot) snapshot.delete(2) else: snapshot.delete(0) @retry() def node_send_keys(self, node, keys): """Send keys to node :type node: Node :type keys: String :rtype : None """ key_codes = scancodes.from_string(str(keys)) for key_code in key_codes: if isinstance(key_code[0], str): if key_code[0] == 'wait': sleep(1) continue self.conn.lookupByUUIDString(node.uuid).sendKey(0, 0, list(key_code), len(key_code), 0) @retry() def node_set_vcpu(self, node, vcpu): """Set VCPU :type volume: Volume :rtype : None """ domain = self.conn.lookupByUUIDString(node.uuid) domain.setVcpusFlags(vcpu, 4) domain.setVcpusFlags(vcpu, 2) @retry() def node_set_memory(self, node, memory): """Set VCPU :type volume: Volume :rtype : None """ domain = self.conn.lookupByUUIDString(node.uuid) domain.setMaxMemory(memory) domain.setMemoryFlags(memory, 2) @retry() def volume_define(self, volume, pool=None): """Define volume :type volume: Volume :type pool: String :rtype : None """ if pool is None: pool = self.storage_pool_name libvirt_volume = self.conn.storagePoolLookupByName(pool).createXML( self.xml_builder.build_volume_xml(volume), 0) volume.uuid = libvirt_volume.key() @retry() def volume_list(self, pool=None): if pool is None: pool = self.storage_pool_name return self.conn.storagePoolLookupByName(pool).listAllVolumes() @retry() def volume_allocation(self, volume): """Get volume allocation :type volume: Volume :rtype : Long """ return self.conn.storageVolLookupByKey(volume.uuid).info()[2] @retry() def volume_path(self, volume): return self.conn.storageVolLookupByKey(volume.uuid).path() def chunk_render(self, stream, size, fd): return fd.read(size) @retry(count=2) def volume_upload(self, volume, path): size = _get_file_size(path) with open(path, 'rb') as fd: stream = self.conn.newStream(0) self.conn.storageVolLookupByKey(volume.uuid).upload( stream=stream, offset=0, length=size, flags=0) stream.sendAll(self.chunk_render, fd) stream.finish() @retry() def volume_delete(self, volume): """Delete volume :type volume: Volume :rtype : None """ self.conn.storageVolLookupByKey(volume.uuid).delete(0) @retry() def volume_capacity(self, volume): """Get volume capacity :type volume: Volume :rtype : Long """ return self.conn.storageVolLookupByKey(volume.uuid).info()[1] @retry() def volume_format(self, volume): """Get volume format :type volume: Volume :rtype : String """ xml_desc = ET.fromstring( self.conn.storageVolLookupByKey(volume.uuid).XMLDesc(0)) return xml_desc.find('target/format[@type]').get('type') @retry() def get_allocated_networks(self): """Get list of allocated networks :rtype : List """ if self.allocated_networks is None: allocated_networks = [] for network_name in self.conn.listDefinedNetworks(): et = ET.fromstring( self.conn.networkLookupByName(network_name).XMLDesc(0)) ip = et.find('ip[@address]') if ip is not None: address = ip.get('address') prefix_or_netmask = ip.get('prefix') or ip.get('netmask') allocated_networks.append(ipaddr.IPNetwork( "{0:>s}/{1:>s}".format(address, prefix_or_netmask))) self.allocated_networks = allocated_networks return self.allocated_networks