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 <[EMAIL PROTECTED]>
#
# 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
[email protected]
https://www.redhat.com/mailman/listinfo/et-mgmt-tools