I've been working on a virtinst API to build and install xml for libvirt storage objects. The current version is attached: so far only nfs, filesystem and dir pools are implemented, as well as their associated file volumes, but the remainder will be mostly a cut and paste job. I have some UI wizards for building these in virt-manager mostly complete, so this has been tested to be pretty solid, though there is still some clean up that needs doing. The general workflow is as follows: ========================================= import virtinst.Storage.StoragePool as sp # This gives the appropriate class for the specified pool type pool_class = sp.get_pool_class(sp.TYPE_FOO) # Only required params are a conn/uri and name. Default formats # and target paths have default values, but source paths/ # devices and hostnames obviously have no sensible default, but # they still aren't required for object instantiation pool = pool_class(name="foo", uri="xen:///") pool.source_path = "/dev/foo" etc. # Prints xml config: will error if all required members aren't # specified pool.get_xml_config() # Attempts to install and build pool on the passed connection poolobj = pool.install() # Will return appropriate volume class for this pool type vol_class = pool.get_volume_class() # For volumes, we require a pool instead of conn/uri, as well # as name and capacity vol = vol_class(name="volfoo", pool=poolobj) volobj = vol.install() ===================================== An active connection/URI/pool object is required. I figure there isn't a real use case for wanting to generate xml on a machine without a libvirt setup, and this will ensure in the future we can check against capabilities xml, make sure we aren't colliding names and other things. I think the implemented code covers most of the different cases for generating storage xml, with the exception of username and password for iscsi: I see this in the libvirt code but this isn't documented anywhere, so I'm not sure if their are any real catches. Comments welcome. Thanks, Cole
# # Classes for building libvirt storage xml # # Copyright 2008 Red Hat, Inc. # Cole Robinson <crobinso@xxxxxxxxxx> # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY 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, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # MA 02110-1301 USA. import libvirt import re import logging import libxml2 from xml.sax.saxutils import escape import util from virtinst import _virtinst as _ DEFAULT_DEV_TARGET = "/dev" DEFAULT_LVM_TARGET_BASE = "/dev/" DEFAULT_DIR_TARGET_BASE = "/var/lib/libvirt/images/" DEFAULT_ISCSI_TARGET = "/dev/disk/by-path" # Pools: # DirectoryPool : A flat filesystem directory # FilesystemPool : A formatted partition # NetworkFilesystemPool : NFS # LogicalPool : LVM Volume Group # DiskPool : Raw disk # iSCSIPool : iSCSI class StorageObject(object): """Base class for building any libvirt storage object, meaningless to directly instantiate""" TYPE_POOL = "pool" TYPE_VOLUME = "volume" def __init__(self, object_type, name): if object_type not in [self.TYPE_POOL, self.TYPE_VOLUME]: raise ValueError, _("Unknown storage object type: %s") % type self._object_type = object_type self.name = name # Initialize all optional properties self._perms = None ## Properties """object_type: pool or volume""" def get_object_type(self): return self._object_type object_type = property(get_object_type) """type: type of the underlying object. could be "dir" for a pool, etc.""" def get_type(self): raise RuntimeError, "Must be implemented in child class." type = property(get_type) """name: name of the storage object""" def get_name(self): return self._name def set_name(self, val): if type(val) is not type("string") or len(val) > 50 or len(val) == 0: raise ValueError, _("Storage object name must be a string " + "between 0 and 50 characters") if re.match("^[0-9]+$", val): raise ValueError, _("Storage object name can not be only " + "numeric characters") if re.match("^[a-zA-Z0-9._-]+$", val) == None: raise ValueError, _("Storage object name can only contain " + "alphanumeric, '_', '.', or '-' characters") # Check that name doesn't collide with other storage objects self._check_name_collision(val) self._name = val name = property(get_name, set_name) # Get/Set methods for use by some objects. Will register where applicable def get_perms(self): return self._perms def set_perms(self, val): if type(val) is not dict: raise ValueError(_("Permissions must be passed as a dict object")) for key in ["mode", "owner", "group", "label"]: if not key in val: raise ValueError(_("Permissions must contain 'mode', 'owner', 'group' and 'label' keys.")) self._perms = val # Validation helper functions def _validate_path(self, path): if type(path) is not type("str") or not path.startswith("/"): raise ValueError(_("'%s' is not an absolute path." % path)) def _check_name_collision(self, name): raise RuntimeError, "Must be implemented in subclass" # XML Building def _get_storage_xml(self): """Returns the pool/volume specific xml blob""" raise RuntimeError, "Must be implemented in subclass" def _get_perms_xml(self): if not self.perms: return "" return " <permissions>\n" + \ " <mode>%o</mode>\n" % self.perms["mode"] + \ " <owner>%d</owner>\n" % self.perms["owner"] + \ " <group>%d</group>\n" % self.perms["group"] + \ " <label>%s</label>\n" % self.perms["label"] + \ " </permissions>\n" def get_xml_config(self): """Returns the full xml description of the storage object""" if self.type is None: root_xml = "<%s>\n" % self.object_type else: root_xml = "<%s type='%s'>\n" % (self.object_type, self.type) xml = "%s" % (root_xml) + \ """ <name>%s</name>\n""" % (self.name) + \ """%(stor_xml)s""" % { "stor_xml" : self._get_storage_xml() } + \ """</%s>""" % (self.object_type) return xml def install(self, create=False): """Define the object XML and build if appropriate""" raise RuntimeError, "Must be implemented in subclass" class StoragePool(StorageObject): """Base class for building a libvirt storage pool xml definition""" TYPE_DIR = "dir" TYPE_FS = "fs" TYPE_NETFS = "netfs" TYPE_LOGICAL = "logical" TYPE_DISK = "disk" TYPE_ISCSI = "iscsi" # Pool type descriptions for use in higher level programs _types = {} _types[TYPE_DIR] = _("Filesystem directory") _types[TYPE_FS] = _("Formatted block device") _types[TYPE_NETFS] = _("Network exported directory") _types[TYPE_LOGICAL] = _("LVM Volume Group") _types[TYPE_DISK] = _("Raw disk device") _types[TYPE_ISCSI] = _("iSCSI Target") def get_pool_class(type): """Convenience method, return class associated with passed pool type""" if type not in StoragePool._types: raise ValueError, _("Unknown storage pool type: %s" % type) if type == StoragePool.TYPE_DIR: return DirectoryPool if type == StoragePool.TYPE_FS: return FilesystemPool if type == StoragePool.TYPE_NETFS: return NetworkFilesystemPool if type == StoragePool.TYPE_LOGICAL: return LogicalPool if type == StoragePool.TYPE_DISK: return DiskPool if type == StoragePool.TYPE_ISCSI: return iSCSIPool get_pool_class = staticmethod(get_pool_class) def get_volume_for_pool(pool_type): """Convenience method, returns volume class associated with pool_type""" pool_class = StoragePool.get_pool_class(pool_type) return pool_class.get_volume_class() get_volume_for_pool = staticmethod(get_volume_for_pool) def get_pool_types(): """Return list of appropriate pool types""" return StoragePool._types.keys() get_pool_types = staticmethod(get_pool_types) def get_pool_type_desc(pool_type): """Return human readable description for passed pool type""" return StoragePool._types[pool_type] get_pool_type_desc = staticmethod(get_pool_type_desc) def __init__(self, name, type, target_path=None, uuid=None, uri=None, conn=None): if conn: self.conn = conn self.uri = conn.getURI() else: if uri: self.uri = uri self.conn = util.open_conn(uri=uri) else: raise ValueError(_("A connection or URI must be specified.")) StorageObject.__init__(self, object_type=StorageObject.TYPE_POOL, \ name=name) if type not in self.get_pool_types(): raise ValueError, _("Unknown storage pool type: %s" % type) self._type = type if target_path is None: target_path = self._get_default_target_path() self.target_path = target_path # Initialize all optional properties self._host = None self._source_path = None if not uuid: self._uuid = None self._random_uuid = util.uuidToString(util.randomUUID()) # Properties used by all pools def get_type(self): return self._type type = property(get_type) """conn: libvirt connection to check object against/install on""" def get_conn(self): return self._conn def set_conn(self, val): if not isinstance(val, libvirt.virConnect): raise ValueError(_("'conn' must be a libvirt connection object.")) self._conn = val conn = property(get_conn, set_conn) def get_target_path(self): return self._target_path def set_target_path(self, val): self._validate_path(val) self._target_path = val target_path = property(get_target_path, set_target_path) # Get/Set methods for use by some pools. Will be registered when applicable def get_source_path(self): return self._source_path def set_source_path(self, val): self._validate_path(val) self._source_path = val def get_host(self): return self._host def set_host(self, val): if type(val) is not type("str"): raise ValueError(_("Host name must be a string")) self._host = val """uuid: uuid of the storage object. optional: generated if not set""" def get_uuid(self): return self._uuid def set_uuid(self, val): if type(val) is not type("string"): raise ValueError, _("UUID must be a string.") form = re.match("[a-fA-F0-9]{8}[-]([a-fA-F0-9]{4}[-]){3}[a-fA-F0-9]{12}$", val) if form is None: form = re.match("[a-fA-F0-9]{32}$", val) if form is None: raise ValueError, _("UUID must be a 32-digit hexadecimal number. It may take the form XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX or may omit hyphens altogether.") else: # UUID had no dashes, so add them in val=val[0:8] + "-" + val[8:12] + "-" + val[12:16] + \ "-" + val[16:20] + "-" + val[20:32] self._uuid = val uuid = property(get_uuid, set_uuid) # Validation functions def _check_name_collision(self, name): pool = None try: pool = self.conn.storagePoolLookupByName(name) except libvirt.libvirtError: pass if pool: raise ValueError(_("Name '%s' already in use by another pool." % name)) def _get_default_target_path(self): raise RuntimeError, "Must be implemented in subclass" # XML Building def _get_target_xml(self): raise RuntimeError, "Must be implemented in subclass" def _get_source_xml(self): raise RuntimeError, "Must be implemented in subclass" def _get_storage_xml(self): src_xml = "" if self._get_source_xml() != "": src_xml = " <source>\n" + \ "%s" % (self._get_source_xml()) + \ " </source>\n" tar_xml = " <target>\n" + \ "%s" % (self._get_target_xml()) + \ " </target>\n" return " <uuid>%s</uuid>\n" % (self.uuid or self._random_uuid) + \ "%s" % src_xml + \ "%s" % tar_xml def install(self, create=False): xml = self.get_xml_config() logging.debug("Defining storage xml:\n%s" % xml) # Define the pool xml try: pool = self.conn.storagePoolDefineXML(xml, 0) except Exception, e: raise RuntimeError(_("Could not define storage pool: %s" % str(e))) # Build the pool? errmsg = None try: pool.build(libvirt.VIR_STORAGE_POOL_BUILD_NEW) except Exception, e: errmsg = _("Could not build storage pool: %s" % str(e)) if create and not errmsg: try: pool.create(0) except Exception, e: errmsg = _("Could not start storage pool: %s" % str(e)) if errmsg: # Try and clean up the leftover pool try: pool.undefine() except Exception, e: logging.debug("Error cleaning up pool after failure: " + "%s" % str(e)) raise RuntimeError(errmsg) return pool class DirectoryPool(StoragePool): """Class for building a directory based storage pool""" def get_volume_class(): return FileVolume get_volume_class = staticmethod(get_volume_class) # Register applicable property methods from parent class perms = property(StorageObject.get_perms, StorageObject.set_perms) def __init__(self, name, target_path=None, uuid=None, perms=None, uri=None, conn=None): StoragePool.__init__(self, name=name, type=StoragePool.TYPE_DIR, target_path=target_path, uuid=uuid, uri=uri, conn=conn) if perms: self.perms = perms def _get_default_target_path(self): path = (DEFAULT_DIR_TARGET_BASE + self.name) return path def _get_target_xml(self): xml = " <path>%s</path>\n" % escape(self.target_path) + \ "%s" % self._get_perms_xml() return xml def _get_source_xml(self): return "" class FilesystemPool(StoragePool): """Class for building a formatted partition based storage pool""" def get_volume_class(): return FileVolume get_volume_class = staticmethod(get_volume_class) formats = [ "auto", "ext2", "ext3", "ext4", "ufs", "iso9660", "udf", "gfs", "gfs2", "vfat", "hfs+", "xfs" ] # Register applicable property methods from parent class perms = property(StorageObject.get_perms, StorageObject.set_perms) source_path = property(StoragePool.get_source_path, StoragePool.set_source_path) def __init__(self, name, source_path=None, target_path=None, format="auto", uuid=None, perms=None, uri=None, conn=None): StoragePool.__init__(self, name=name, type=StoragePool.TYPE_FS, target_path=target_path, uuid=uuid, uri=uri, conn=conn) self.format = format if source_path: self.source_path = source_path if perms: self.perms = perms def get_format(self): return self._format def set_format(self, val): if not val in self.formats: raise ValueError(_("Unknown Filesystem format: %s" % val)) self._format = val format = property(get_format, set_format) def _get_default_target_path(self): path = (DEFAULT_DIR_TARGET_BASE + self.name) return path def _get_target_xml(self): xml = " <path>%s</path>\n" % escape(self.target_path) + \ "%s" % self._get_perms_xml() return xml def _get_source_xml(self): if not self.source_path: raise RuntimeError(_("Device path is required")) xml = " <format type='%s'/>\n" % self.format + \ " <device path='%s'/>\n" % escape(self.source_path) return xml class NetworkFilesystemPool(StoragePool): """Class for building a Network Filesystem pool xml object""" def get_volume_class(): return FileVolume get_volume_class = staticmethod(get_volume_class) formats = [ "auto", "nfs" ] # Register applicable property methods from parent class source_path = property(StoragePool.get_source_path, StoragePool.set_source_path) host = property(StoragePool.get_host, StoragePool.set_host) def __init__(self, name, source_path=None, host=None, target_path=None, format="auto", uuid=None, uri=None, conn=None): StoragePool.__init__(self, name=name, type=StoragePool.TYPE_NETFS, uuid=None, target_path=target_path, uri=uri, conn=conn) self.format = format if source_path: self.source_path = source_path if host: self.host = host def get_format(self): return self._format def set_format(self, val): if not val in self.formats: raise ValueError(_("Unknown Network Filesystem format: %s" % val)) self._format = val format = property(get_format, set_format) def _get_default_target_path(self): path = (DEFAULT_DIR_TARGET_BASE + self.name) return path def _get_target_xml(self): xml = " <path>%s</path>\n" % escape(self.target_path) return xml def _get_source_xml(self): if not self.host: raise RuntimeError(_("Hostname is required")) if not self.source_path: raise RuntimeError(_("Host path is required")) xml = """ <format type="%s"/>\n""" % self.format + \ """ <host name="%s"/>\n""" % self.host + \ """ <dir path="%s"/>\n""" % escape(self.source_path) return xml class LogicalPool(StoragePool): def __init__(self, *args, **kwargs): raise RuntimeError, "Not implemented" class DiskPool(StoragePool): def __init__(self, *args, **kwargs): raise RuntimeError, "Not implemented" class iSCSIPool(StoragePool): def __init__(self, *args, **kwargs): raise RuntimeError, "Not implemented" class StorageVolume(StorageObject): """Base class for building a libvirt storage volume xml definition""" formats = [] def __init__(self, name, capacity, pool, target_path=None, allocation=None): self.pool = pool StorageObject.__init__(self, object_type=StorageObject.TYPE_VOLUME, name=name) if not target_path: self.target_path = self._get_default_target_path() else: self.target_path = target_path self.capacity = capacity self.allocation = allocation or self.capacity def get_type(self): return None type = property(get_type) # Properties used by all volumes def get_capacity(self): return self._capacity def set_capacity(self, val): if type(val) not in (int, float) or val <= 0: raise ValueError(_("Capacity must be a positive number")) val = int(val) self._capacity = val if self.allocation and (val > self.allocation): self.allocation = val capacity = property(get_capacity, set_capacity) def get_allocation(self): return self._allocation def set_allocation(self, val): if type(val) not in (int, float) and val <= 0: raise ValueError(_("Allocation must be a positive number")) val = int(val) if val > self.capacity: val = self.capacity self._allocation = val allocation = property(get_allocation, set_allocation) def get_target_path(self): return self._target_path def set_target_path(self, val): self._validate_path(val) self._target_path = val target_path = property(get_target_path, set_target_path) def get_pool(self): return self._pool def set_pool(self, newpool): if not isinstance(newpool, libvirt.virStoragePool): raise ValueError, _("'pool' must be a virStoragePool instance.") self._pool = newpool pool = property(get_pool, set_pool) # Property functions used by more than one child class def get_format(self): return self._format def set_format(self, val): if val not in self.formats: raise ValueError(_("'%s' is not a valid format.") % val) self._format = val def _check_name_collision(self, name): vol = None try: vol = self.pool.storageVolLookupByName(name) except libvirt.libvirtError: pass if vol: raise ValueError(_("Name '%s' already in use by another volume." % name)) def _get_default_target_path(self): raise RuntimeError, "Must be implemented in subclass" def _get_xml_path(self, path): doc = None ctx = None try: xml = self.pool.XMLDesc(0) doc = libxml2.parseDoc(xml) try: ctx = doc.xpathNewContext() ret = ctx.xpathEval(path) str = None if ret != None: if type(ret) == list: if len(ret) == 1: str = ret[0].content else: str = ret ctx.xpathFreeContext() return str except: if ctx: ctx.xpathFreeContext() return None finally: if doc is not None: doc.freeDoc() # xml building functions def _get_target_xml(self): raise RuntimeError, "Must be implemented in subclass" def _get_source_xml(self): raise RuntimeError, "Must be implemented in subclass" def _get_storage_xml(self): src_xml = format_xml = "" if self._get_source_xml() != "": src_xml = " <source>\n" + \ "%s" % (self._get_source_xml()) + \ " </source>\n" tar_xml = " <target>\n" + \ "%s" % (self._get_target_xml()) + \ " </target>\n" alloc = self.allocation or self.capacity return " <capacity>%d</capacity>\n" % self.capacity + \ " <allocation>%d</allocation>\n" % alloc + \ "%s" % src_xml + \ "%s" % tar_xml def install(self): try: vol = self.pool.createXML(self.get_xml_config(), 0) except Exception, e: raise RuntimeError("Couldn't create storage volume '%s': '%s'" % (self.name, str(e))) return vol class FileVolume(StorageVolume): formats = ["raw", "bochs", "cloop", "cow", "dmg", "iso", "qcow",\ "qcow2", "vmdk", "vpc"] # Register applicable property methods from parent class perms = property(StorageObject.get_perms, StorageObject.set_perms) format = property(StorageVolume.get_format, StorageVolume.set_format) def __init__(self, name, capacity, pool, target_path=None, format="raw", allocation=None, perms=None): StorageVolume.__init__(self, name=name, target_path=target_path, allocation=allocation, capacity=capacity, pool=pool) self.format = format if perms: self.perms = perms def _get_default_target_path(self): poolpath = self._get_xml_path("/pool/target/path") return poolpath + "/" + self.name def _get_target_xml(self): return " <path>%s</path>\n" % escape(self.target_path) + \ " <format type='%s'/>\n" % self.format + \ "%s" % self._get_perms_xml() def _get_source_xml(self): return ""
_______________________________________________ et-mgmt-tools mailing list et-mgmt-tools@xxxxxxxxxx https://www.redhat.com/mailman/listinfo/et-mgmt-tools