[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]

[et-mgmt-tools] [RFC] virtinst: build libvirt storage xml



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 redhat com>
#
# 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 ""

[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]