[PATCH 00/22] Refactor Model

From: Aline Manera <alinefm@br.ibm.com> Hi all, This patch set splitted model.py into several model implementations. One for each resource in kimchi. It also includes a mechanism implemented by Zhou Zheng Sheng to automatically load all the models in one. So we don't nee to change tests and controller to work with new models. It is just the first step. I also will send a patch to split mockmodel And finally, a new one to separate common code and specific code from models. I am sending it separately because all that require a lot of work (and tests) and also can conflict with new features. I am planning to merge it on next Wed (when sprint 2 ends) so I hope those changes will not impact so much any developer. PS. it seems to to be a huge patch set but I just move code from one place to another. There is no new implementation/feature in there. Aline Manera (22): refactor model: Separate libvirtconnection from model.py refactor model: Move StoragePooldef from model to libvirtstoragepools.py refactor model: Create a common model builder Create a model to join all model resources implementation refactor model: Create a separated model for task resource refactor model: Create a separated model for plugins resource refactor model: Create a separated model for debug report resource refactor model: Create a separated model for config resource refactor model: Create a separated model for network resource refactor model: Create a separated model for interface resource refactor model: Create a separated model for storage pool resource refactor model: Create a separated model for storage volume resource refactor model: Create a separated model for storage server resource refactor model: Create a separated model for storage target resource refactor model: Create a separated model for template resource refactor model: Create a separated model for vm resource refactor model: Create a separated model for vm interface resource refactor model: Create a separated model for host resource Update server to use the new model Update tests to use the new model Update mockmodel imports Delete former model.py and rename model_ to model Makefile.am | 1 + src/kimchi/basemodel.py | 55 + src/kimchi/control/config.py | 6 +- src/kimchi/control/storagepools.py | 2 +- src/kimchi/control/utils.py | 12 +- src/kimchi/mockmodel.py | 27 +- src/kimchi/model.py | 2025 -------------------------------- src/kimchi/model/__init__.py | 21 + src/kimchi/model/config.py | 87 ++ src/kimchi/model/debugreports.py | 167 +++ src/kimchi/model/host.py | 201 ++++ src/kimchi/model/interfaces.py | 46 + src/kimchi/model/libvirtconnection.py | 122 ++ src/kimchi/model/libvirtstoragepool.py | 257 ++++ src/kimchi/model/model.py | 53 + src/kimchi/model/networks.py | 265 +++++ src/kimchi/model/plugins.py | 31 + src/kimchi/model/storagepools.py | 246 ++++ src/kimchi/model/storageservers.py | 78 ++ src/kimchi/model/storagetargets.py | 86 ++ src/kimchi/model/storagevolumes.py | 176 +++ src/kimchi/model/tasks.py | 39 + src/kimchi/model/templates.py | 172 +++ src/kimchi/model/utils.py | 33 + src/kimchi/model/vmifaces.py | 135 +++ src/kimchi/model/vms.py | 450 +++++++ src/kimchi/server.py | 2 +- src/kimchi/utils.py | 46 +- tests/test_model.py | 91 +- tests/test_storagepool.py | 4 +- tests/utils.py | 4 +- 31 files changed, 2831 insertions(+), 2109 deletions(-) create mode 100644 src/kimchi/basemodel.py delete mode 100644 src/kimchi/model.py create mode 100644 src/kimchi/model/__init__.py create mode 100644 src/kimchi/model/config.py create mode 100644 src/kimchi/model/debugreports.py create mode 100644 src/kimchi/model/host.py create mode 100644 src/kimchi/model/interfaces.py create mode 100644 src/kimchi/model/libvirtconnection.py create mode 100644 src/kimchi/model/libvirtstoragepool.py create mode 100644 src/kimchi/model/model.py create mode 100644 src/kimchi/model/networks.py create mode 100644 src/kimchi/model/plugins.py create mode 100644 src/kimchi/model/storagepools.py create mode 100644 src/kimchi/model/storageservers.py create mode 100644 src/kimchi/model/storagetargets.py create mode 100644 src/kimchi/model/storagevolumes.py create mode 100644 src/kimchi/model/tasks.py create mode 100644 src/kimchi/model/templates.py create mode 100644 src/kimchi/model/utils.py create mode 100644 src/kimchi/model/vmifaces.py create mode 100644 src/kimchi/model/vms.py -- 1.7.10.4

From: Aline Manera <alinefm@br.ibm.com> Libvirt connection is a base facility for kimchi model, so each resource model should be able to access it directly. It can remove the denpendency on class Model for resource models which need connect libvirt. With this change, we can separate different resource model classes from class Model. Signed-off-by: Aline Manera <alinefm@br.ibm.com> Signed-off-by: Mark Wu <wudxw@linux.vnet.ibm.com> --- src/kimchi/model.py | 93 +----------------------- src/kimchi/model_/libvirtconnection.py | 122 ++++++++++++++++++++++++++++++++ 2 files changed, 123 insertions(+), 92 deletions(-) create mode 100644 src/kimchi/model_/libvirtconnection.py diff --git a/src/kimchi/model.py b/src/kimchi/model.py index 81c1507..bc9934d 100644 --- a/src/kimchi/model.py +++ b/src/kimchi/model.py @@ -72,6 +72,7 @@ from kimchi.exception import MissingParameter, NotFoundError, OperationFailed, T from kimchi.featuretests import FeatureTests from kimchi.iscsi import TargetClient from kimchi.isoinfo import IsoImage +from kimchi.model_.libvirtconnection import LibvirtConnection from kimchi.objectstore import ObjectStore from kimchi.rollbackcontext import RollbackContext from kimchi.scan import Scanner @@ -1931,95 +1932,3 @@ def _get_volume_xml(**kwargs): </volume> """ % kwargs return xml - - -class LibvirtConnection(object): - def __init__(self, uri): - self.uri = uri - self._connections = {} - self._connectionLock = threading.Lock() - self.wrappables = self.get_wrappable_objects() - - def get_wrappable_objects(self): - """ - When a wrapped function returns an instance of another libvirt object, - we also want to wrap that object so we can catch errors that happen when - calling its methods. - """ - objs = [] - for name in ('virDomain', 'virDomainSnapshot', 'virInterface', - 'virNWFilter', 'virNetwork', 'virNodeDevice', 'virSecret', - 'virStoragePool', 'virStorageVol', 'virStream'): - try: - attr = getattr(libvirt, name) - except AttributeError: - pass - objs.append(attr) - return tuple(objs) - - def get(self, conn_id=0): - """ - Return current connection to libvirt or open a new one. Wrap all - callable libvirt methods so we can catch connection errors and handle - them by restarting the server. - """ - def wrapMethod(f): - def wrapper(*args, **kwargs): - try: - ret = f(*args, **kwargs) - return ret - except libvirt.libvirtError as e: - edom = e.get_error_domain() - ecode = e.get_error_code() - EDOMAINS = (libvirt.VIR_FROM_REMOTE, - libvirt.VIR_FROM_RPC) - ECODES = (libvirt.VIR_ERR_SYSTEM_ERROR, - libvirt.VIR_ERR_INTERNAL_ERROR, - libvirt.VIR_ERR_NO_CONNECT, - libvirt.VIR_ERR_INVALID_CONN) - if edom in EDOMAINS and ecode in ECODES: - kimchi_log.error('Connection to libvirt broken. ' - 'Recycling. ecode: %d edom: %d' % - (ecode, edom)) - with self._connectionLock: - self._connections[conn_id] = None - raise - wrapper.__name__ = f.__name__ - wrapper.__doc__ = f.__doc__ - return wrapper - - with self._connectionLock: - conn = self._connections.get(conn_id) - if not conn: - retries = 5 - while True: - retries = retries - 1 - try: - conn = libvirt.open(self.uri) - break - except libvirt.libvirtError: - kimchi_log.error('Unable to connect to libvirt.') - if not retries: - kimchi_log.error('Libvirt is not available, exiting.') - cherrypy.engine.stop() - raise - time.sleep(2) - - for name in dir(libvirt.virConnect): - method = getattr(conn, name) - if callable(method) and not name.startswith('_'): - setattr(conn, name, wrapMethod(method)) - - for cls in self.wrappables: - for name in dir(cls): - method = getattr(cls, name) - if callable(method) and not name.startswith('_'): - setattr(cls, name, wrapMethod(method)) - - self._connections[conn_id] = conn - # In case we're running into troubles with keeping the connections - # alive we should place here: - # conn.setKeepAlive(interval=5, count=3) - # However the values need to be considered wisely to not affect - # hosts which are hosting a lot of virtual machines - return conn diff --git a/src/kimchi/model_/libvirtconnection.py b/src/kimchi/model_/libvirtconnection.py new file mode 100644 index 0000000..7bbb668 --- /dev/null +++ b/src/kimchi/model_/libvirtconnection.py @@ -0,0 +1,122 @@ +# +# Project Kimchi +# +# Copyright IBM, Corp. 2013 +# +# Authors: +# Aline Manera <alinefm@linux.vnet.ibm.com> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +import threading +import time + +import cherrypy +import libvirt + +from kimchi.utils import kimchi_log + + +class LibvirtConnection(object): + def __init__(self, uri): + self.uri = uri + self._connections = {} + self._connectionLock = threading.Lock() + self.wrappables = self.get_wrappable_objects() + + def get_wrappable_objects(self): + """ + When a wrapped function returns an instance of another libvirt object, + we also want to wrap that object so we can catch errors that happen + when calling its methods. + """ + objs = [] + for name in ('virDomain', 'virDomainSnapshot', 'virInterface', + 'virNWFilter', 'virNetwork', 'virNodeDevice', 'virSecret', + 'virStoragePool', 'virStorageVol', 'virStream'): + try: + attr = getattr(libvirt, name) + except AttributeError: + pass + objs.append(attr) + return tuple(objs) + + def get(self, conn_id=0): + """ + Return current connection to libvirt or open a new one. Wrap all + callable libvirt methods so we can catch connection errors and handle + them by restarting the server. + """ + def wrapMethod(f): + def wrapper(*args, **kwargs): + try: + ret = f(*args, **kwargs) + return ret + except libvirt.libvirtError as e: + edom = e.get_error_domain() + ecode = e.get_error_code() + EDOMAINS = (libvirt.VIR_FROM_REMOTE, + libvirt.VIR_FROM_RPC) + ECODES = (libvirt.VIR_ERR_SYSTEM_ERROR, + libvirt.VIR_ERR_INTERNAL_ERROR, + libvirt.VIR_ERR_NO_CONNECT, + libvirt.VIR_ERR_INVALID_CONN) + if edom in EDOMAINS and ecode in ECODES: + kimchi_log.error('Connection to libvirt broken. ' + 'Recycling. ecode: %d edom: %d' % + (ecode, edom)) + with self._connectionLock: + self._connections[conn_id] = None + raise + wrapper.__name__ = f.__name__ + wrapper.__doc__ = f.__doc__ + return wrapper + + with self._connectionLock: + conn = self._connections.get(conn_id) + if not conn: + retries = 5 + while True: + retries = retries - 1 + try: + conn = libvirt.open(self.uri) + break + except libvirt.libvirtError: + kimchi_log.error('Unable to connect to libvirt.') + if not retries: + err = 'Libvirt is not available, exiting.' + kimchi_log.error(err) + cherrypy.engine.stop() + raise + time.sleep(2) + + for name in dir(libvirt.virConnect): + method = getattr(conn, name) + if callable(method) and not name.startswith('_'): + setattr(conn, name, wrapMethod(method)) + + for cls in self.wrappables: + for name in dir(cls): + method = getattr(cls, name) + if callable(method) and not name.startswith('_'): + setattr(cls, name, wrapMethod(method)) + + self._connections[conn_id] = conn + # In case we're running into troubles with keeping the + # connections alive we should place here: + # conn.setKeepAlive(interval=5, count=3) + # However the values need to be considered wisely to not affect + # hosts which are hosting a lot of virtual machines + return conn -- 1.7.10.4

From: Aline Manera <alinefm@br.ibm.com> Separate StoragePoolDef (and its sub-classes) from model as it handles the xml generation for libvirt storage pools. Signed-off-by: Aline Manera <alinefm@br.ibm.com> --- src/kimchi/model.py | 232 +--------------------------- src/kimchi/model_/libvirtstoragepool.py | 257 +++++++++++++++++++++++++++++++ 2 files changed, 260 insertions(+), 229 deletions(-) create mode 100644 src/kimchi/model_/libvirtstoragepool.py diff --git a/src/kimchi/model.py b/src/kimchi/model.py index bc9934d..ddb2d95 100644 --- a/src/kimchi/model.py +++ b/src/kimchi/model.py @@ -39,7 +39,6 @@ import re import shutil import subprocess import sys -import tempfile import threading import time import uuid @@ -68,17 +67,16 @@ from kimchi import xmlutils from kimchi.asynctask import AsyncTask from kimchi.distroloader import DistroLoader from kimchi.exception import InvalidOperation, InvalidParameter, IsoFormatError -from kimchi.exception import MissingParameter, NotFoundError, OperationFailed, TimeoutExpired +from kimchi.exception import MissingParameter, NotFoundError, OperationFailed from kimchi.featuretests import FeatureTests -from kimchi.iscsi import TargetClient from kimchi.isoinfo import IsoImage from kimchi.model_.libvirtconnection import LibvirtConnection +from kimchi.model_.libvirtstoragepool import StoragePoolDef from kimchi.objectstore import ObjectStore -from kimchi.rollbackcontext import RollbackContext from kimchi.scan import Scanner from kimchi.screenshot import VMScreenshot from kimchi.utils import get_enabled_plugins, is_digit, kimchi_log -from kimchi.utils import run_command, parse_cmd_output, patch_find_nfs_target +from kimchi.utils import patch_find_nfs_target from kimchi.vmtemplate import VMTemplate @@ -1684,230 +1682,6 @@ def _get_storage_server_spec(**kwargs): return xml -class StoragePoolDef(object): - @classmethod - def create(cls, poolArgs): - for klass in cls.__subclasses__(): - if poolArgs['type'] == klass.poolType: - return klass(poolArgs) - raise OperationFailed('Unsupported pool type: %s' % poolArgs['type']) - - def __init__(self, poolArgs): - self.poolArgs = poolArgs - - def prepare(self, conn): - ''' Validate pool arguments and perform preparations. Operation which - would cause side effect should be put here. Subclasses can optionally - override this method, or it always succeeds by default. ''' - pass - - @property - def xml(self): - ''' Subclasses have to override this method to actually generate the - storage pool XML definition. Should cause no side effect and be - idempotent''' - # TODO: When add new pool type, should also add the related test in - # tests/test_storagepool.py - raise OperationFailed('self.xml is not implemented: %s' % self) - - -class DirPoolDef(StoragePoolDef): - poolType = 'dir' - - @property - def xml(self): - # Required parameters - # name: - # type: - # path: - xml = """ - <pool type='dir'> - <name>{name}</name> - <target> - <path>{path}</path> - </target> - </pool> - """.format(**self.poolArgs) - return xml - - -class NetfsPoolDef(StoragePoolDef): - poolType = 'netfs' - - def __init__(self, poolArgs): - super(NetfsPoolDef, self).__init__(poolArgs) - self.path = '/var/lib/kimchi/nfs_mount/' + self.poolArgs['name'] - - def prepare(self, conn): - mnt_point = tempfile.mkdtemp(dir='/tmp') - export_path = "%s:%s" % ( - self.poolArgs['source']['host'], self.poolArgs['source']['path']) - mount_cmd = ["mount", "-o", 'soft,timeo=100,retrans=3,retry=0', - export_path, mnt_point] - umount_cmd = ["umount", "-f", export_path] - mounted = False - - with RollbackContext() as rollback: - rollback.prependDefer(os.rmdir, mnt_point) - try: - run_command(mount_cmd, 30) - rollback.prependDefer(run_command, umount_cmd) - except TimeoutExpired: - raise InvalidParameter("Export path %s may block during nfs mount" % export_path) - - with open("/proc/mounts" , "rb") as f: - rawMounts = f.read() - output_items = ['dev_path', 'mnt_point', 'type'] - mounts = parse_cmd_output(rawMounts, output_items) - for item in mounts: - if 'dev_path' in item and item['dev_path'] == export_path: - mounted = True - - if not mounted: - raise InvalidParameter( - "Export path %s mount failed during nfs mount" % export_path) - - @property - def xml(self): - # Required parameters - # name: - # type: - # source[host]: - # source[path]: - poolArgs = copy.deepcopy(self.poolArgs) - poolArgs['path'] = self.path - xml = """ - <pool type='netfs'> - <name>{name}</name> - <source> - <host name='{source[host]}'/> - <dir path='{source[path]}'/> - </source> - <target> - <path>{path}</path> - </target> - </pool> - """.format(**poolArgs) - return xml - - -class LogicalPoolDef(StoragePoolDef): - poolType = 'logical' - - def __init__(self, poolArgs): - super(LogicalPoolDef, self).__init__(poolArgs) - self.path = '/var/lib/kimchi/logical_mount/' + self.poolArgs['name'] - - @property - def xml(self): - # Required parameters - # name: - # type: - # source[devices]: - poolArgs = copy.deepcopy(self.poolArgs) - devices = [] - for device_path in poolArgs['source']['devices']: - devices.append('<device path="%s" />' % device_path) - - poolArgs['source']['devices'] = ''.join(devices) - poolArgs['path'] = self.path - - xml = """ - <pool type='logical'> - <name>{name}</name> - <source> - {source[devices]} - </source> - <target> - <path>{path}</path> - </target> - </pool> - """.format(**poolArgs) - return xml - - -class IscsiPoolDef(StoragePoolDef): - poolType = 'iscsi' - - def prepare(self, conn): - source = self.poolArgs['source'] - if not TargetClient(**source).validate(): - raise OperationFailed("Can not login to iSCSI host %s target %s" % - (source['host'], source['target'])) - self._prepare_auth(conn) - - def _prepare_auth(self, conn): - try: - auth = self.poolArgs['source']['auth'] - except KeyError: - return - - try: - virSecret = conn.secretLookupByUsage( - libvirt.VIR_SECRET_USAGE_TYPE_ISCSI, self.poolArgs['name']) - except libvirt.libvirtError: - xml = ''' - <secret ephemeral='no' private='yes'> - <description>Secret for iSCSI storage pool {name}</description> - <auth type='chap' username='{username}'/> - <usage type='iscsi'> - <target>{name}</target> - </usage> - </secret>'''.format(name=self.poolArgs['name'], - username=auth['username']) - virSecret = conn.secretDefineXML(xml) - - virSecret.setValue(auth['password']) - - def _format_port(self, poolArgs): - try: - port = poolArgs['source']['port'] - except KeyError: - return "" - return "port='%s'" % port - - def _format_auth(self, poolArgs): - try: - auth = poolArgs['source']['auth'] - except KeyError: - return "" - - return ''' - <auth type='chap' username='{username}'> - <secret type='iscsi' usage='{name}'/> - </auth>'''.format(name=poolArgs['name'], username=auth['username']) - - @property - def xml(self): - # Required parameters - # name: - # type: - # source[host]: - # source[target]: - # - # Optional parameters - # source[port]: - poolArgs = copy.deepcopy(self.poolArgs) - poolArgs['source'].update({'port': self._format_port(poolArgs), - 'auth': self._format_auth(poolArgs)}) - poolArgs['path'] = '/dev/disk/by-id' - - xml = """ - <pool type='iscsi'> - <name>{name}</name> - <source> - <host name='{source[host]}' {source[port]}/> - <device path='{source[target]}'/> - {source[auth]} - </source> - <target> - <path>{path}</path> - </target> - </pool> - """.format(**poolArgs) - return xml - - def _get_volume_xml(**kwargs): # Required parameters # name: diff --git a/src/kimchi/model_/libvirtstoragepool.py b/src/kimchi/model_/libvirtstoragepool.py new file mode 100644 index 0000000..f4dbf2e --- /dev/null +++ b/src/kimchi/model_/libvirtstoragepool.py @@ -0,0 +1,257 @@ +# +# Project Kimchi +# +# Copyright IBM, Corp. 2013 +# +# Authors: +# Aline Manera <alinefm@linux.vnet.ibm.com> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +import copy +import os +import tempfile + +import libvirt + +from kimchi.exception import InvalidParameter, OperationFailed, TimeoutExpired +from kimchi.iscsi import TargetClient +from kimchi.rollbackcontext import RollbackContext +from kimchi.utils import parse_cmd_output, run_command + + +class StoragePoolDef(object): + @classmethod + def create(cls, poolArgs): + for klass in cls.__subclasses__(): + if poolArgs['type'] == klass.poolType: + return klass(poolArgs) + raise OperationFailed('Unsupported pool type: %s' % poolArgs['type']) + + def __init__(self, poolArgs): + self.poolArgs = poolArgs + + def prepare(self, conn): + ''' Validate pool arguments and perform preparations. Operation which + would cause side effect should be put here. Subclasses can optionally + override this method, or it always succeeds by default. ''' + pass + + @property + def xml(self): + ''' Subclasses have to override this method to actually generate the + storage pool XML definition. Should cause no side effect and be + idempotent''' + # TODO: When add new pool type, should also add the related test in + # tests/test_storagepool.py + raise OperationFailed('self.xml is not implemented: %s' % self) + + +class DirPoolDef(StoragePoolDef): + poolType = 'dir' + + @property + def xml(self): + # Required parameters + # name: + # type: + # path: + xml = """ + <pool type='dir'> + <name>{name}</name> + <target> + <path>{path}</path> + </target> + </pool> + """.format(**self.poolArgs) + return xml + + +class NetfsPoolDef(StoragePoolDef): + poolType = 'netfs' + + def __init__(self, poolArgs): + super(NetfsPoolDef, self).__init__(poolArgs) + self.path = '/var/lib/kimchi/nfs_mount/' + self.poolArgs['name'] + + def prepare(self, conn): + mnt_point = tempfile.mkdtemp(dir='/tmp') + export_path = "%s:%s" % ( + self.poolArgs['source']['host'], self.poolArgs['source']['path']) + mount_cmd = ["mount", "-o", 'soft,timeo=100,retrans=3,retry=0', + export_path, mnt_point] + umount_cmd = ["umount", "-f", export_path] + mounted = False + + with RollbackContext() as rollback: + rollback.prependDefer(os.rmdir, mnt_point) + try: + run_command(mount_cmd, 30) + rollback.prependDefer(run_command, umount_cmd) + except TimeoutExpired: + err = "Export path %s may block during nfs mount" + raise InvalidParameter(err % export_path) + + with open("/proc/mounts", "rb") as f: + rawMounts = f.read() + output_items = ['dev_path', 'mnt_point', 'type'] + mounts = parse_cmd_output(rawMounts, output_items) + for item in mounts: + if 'dev_path' in item and item['dev_path'] == export_path: + mounted = True + + if not mounted: + err = "Export path %s mount failed during nfs mount" + raise InvalidParameter(err % export_path) + + @property + def xml(self): + # Required parameters + # name: + # type: + # source[host]: + # source[path]: + poolArgs = copy.deepcopy(self.poolArgs) + poolArgs['path'] = self.path + xml = """ + <pool type='netfs'> + <name>{name}</name> + <source> + <host name='{source[host]}'/> + <dir path='{source[path]}'/> + </source> + <target> + <path>{path}</path> + </target> + </pool> + """.format(**poolArgs) + return xml + + +class LogicalPoolDef(StoragePoolDef): + poolType = 'logical' + + def __init__(self, poolArgs): + super(LogicalPoolDef, self).__init__(poolArgs) + self.path = '/var/lib/kimchi/logical_mount/' + self.poolArgs['name'] + + @property + def xml(self): + # Required parameters + # name: + # type: + # source[devices]: + poolArgs = copy.deepcopy(self.poolArgs) + devices = [] + for device_path in poolArgs['source']['devices']: + devices.append('<device path="%s" />' % device_path) + + poolArgs['source']['devices'] = ''.join(devices) + poolArgs['path'] = self.path + + xml = """ + <pool type='logical'> + <name>{name}</name> + <source> + {source[devices]} + </source> + <target> + <path>{path}</path> + </target> + </pool> + """.format(**poolArgs) + return xml + + +class IscsiPoolDef(StoragePoolDef): + poolType = 'iscsi' + + def prepare(self, conn): + source = self.poolArgs['source'] + if not TargetClient(**source).validate(): + raise OperationFailed("Can not login to iSCSI host %s target %s" % + (source['host'], source['target'])) + self._prepare_auth(conn) + + def _prepare_auth(self, conn): + try: + auth = self.poolArgs['source']['auth'] + except KeyError: + return + + try: + virSecret = conn.secretLookupByUsage( + libvirt.VIR_SECRET_USAGE_TYPE_ISCSI, self.poolArgs['name']) + except libvirt.libvirtError: + xml = ''' + <secret ephemeral='no' private='yes'> + <description>Secret for iSCSI storage pool {name}</description> + <auth type='chap' username='{username}'/> + <usage type='iscsi'> + <target>{name}</target> + </usage> + </secret>'''.format(name=self.poolArgs['name'], + username=auth['username']) + virSecret = conn.secretDefineXML(xml) + + virSecret.setValue(auth['password']) + + def _format_port(self, poolArgs): + try: + port = poolArgs['source']['port'] + except KeyError: + return "" + return "port='%s'" % port + + def _format_auth(self, poolArgs): + try: + auth = poolArgs['source']['auth'] + except KeyError: + return "" + + return ''' + <auth type='chap' username='{username}'> + <secret type='iscsi' usage='{name}'/> + </auth>'''.format(name=poolArgs['name'], username=auth['username']) + + @property + def xml(self): + # Required parameters + # name: + # type: + # source[host]: + # source[target]: + # + # Optional parameters + # source[port]: + poolArgs = copy.deepcopy(self.poolArgs) + poolArgs['source'].update({'port': self._format_port(poolArgs), + 'auth': self._format_auth(poolArgs)}) + poolArgs['path'] = '/dev/disk/by-id' + + xml = """ + <pool type='iscsi'> + <name>{name}</name> + <source> + <host name='{source[host]}' {source[port]}/> + <device path='{source[target]}'/> + {source[auth]} + </source> + <target> + <path>{path}</path> + </target> + </pool> + """.format(**poolArgs) + return xml -- 1.7.10.4

From: Aline Manera <alinefm@br.ibm.com> It will be useful while splitting model and mockmodel into smaller modules. It will automatically map all sub-model instance methods to a BaseModel instance. Signed-off-by: Aline Manera <alinefm@br.ibm.com> Signed-off-by: Zhou Zheng Sheng <zhshzhou@linux.vnet.ibm.com> --- src/kimchi/basemodel.py | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 src/kimchi/basemodel.py diff --git a/src/kimchi/basemodel.py b/src/kimchi/basemodel.py new file mode 100644 index 0000000..285e86d --- /dev/null +++ b/src/kimchi/basemodel.py @@ -0,0 +1,47 @@ +# +# Project Kimchi +# +# Copyright IBM, Corp. 2014 +# +# Authors: +# Zhou Zheng Sheng <zhshzhou@linux.vnet.ibm.com> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + + +class BaseModel(object): + ''' + This model squashes all sub-model's public callable methods to itself. + + Model methods are not limited to get_list, create, lookup, update, delete. + Controller can call generate_action_handler to generate new actions, which + call the related model methods. So all public callable methods of a + sub-model should be mapped to this model. + ''' + def __init__(self, model_instances): + for model_instance in model_instances: + cls_name = model_instance.__class__.__name__ + if cls_name.endswith('Model'): + method_prefix = cls_name[:-len('Model')].lower() + else: + method_prefix = cls_name.lower() + + callables = [m for m in dir(model_instance) + if not m.startswith('_') and + callable(getattr(model_instance, m))] + + for member_name in callables: + m = getattr(model_instance, member_name, None) + setattr(self, '%s_%s' % (method_prefix, member_name), m) -- 1.7.10.4

From: Aline Manera <alinefm@br.ibm.com> That way we don't need to change tests and controller in order to use the separated model implementations All models will be automatically loaded into Model() which will contain all methods needed by controller. Signed-off-by: Aline Manera <alinefm@br.ibm.com> --- src/kimchi/control/utils.py | 12 +--------- src/kimchi/model_/model.py | 53 +++++++++++++++++++++++++++++++++++++++++++ src/kimchi/utils.py | 9 ++++++++ 3 files changed, 63 insertions(+), 11 deletions(-) create mode 100644 src/kimchi/model_/model.py diff --git a/src/kimchi/control/utils.py b/src/kimchi/control/utils.py index 2dd351c..9c6878b 100644 --- a/src/kimchi/control/utils.py +++ b/src/kimchi/control/utils.py @@ -25,14 +25,13 @@ import cherrypy import json -import os from jsonschema import Draft3Validator, ValidationError, FormatChecker from kimchi.exception import InvalidParameter -from kimchi.utils import import_module +from kimchi.utils import import_module, listPathModules def get_class_name(cls): @@ -118,15 +117,6 @@ class UrlSubNode(object): return fun -def listPathModules(path): - modules = set() - for f in os.listdir(path): - base, ext = os.path.splitext(f) - if ext in ('.py', '.pyc', '.pyo'): - modules.add(base) - return sorted(modules) - - def load_url_sub_node(path, package_name, expect_attr="_url_sub_node_name"): sub_nodes = {} for mod_name in listPathModules(path): diff --git a/src/kimchi/model_/model.py b/src/kimchi/model_/model.py new file mode 100644 index 0000000..709e0bb --- /dev/null +++ b/src/kimchi/model_/model.py @@ -0,0 +1,53 @@ +# +# Project Kimchi +# +# Copyright IBM, Corp. 2013 +# +# Authors: +# Aline Manera <alinefm@linux.vnet.ibm.com> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +import inspect +import os + +from kimchi.basemodel import BaseModel +from kimchi.model_.libvirtconnection import LibvirtConnection +from kimchi.objectstore import ObjectStore +from kimchi.utils import import_module, listPathModules + + +class Model(BaseModel): + def __init__(self, libvirt_uri='qemu:///system', objstore_loc=None): + self.objstore = ObjectStore(objstore_loc) + self.conn = LibvirtConnection(libvirt_uri) + kargs = {'objstore': self.objstore, 'conn': self.conn} + + this = os.path.basename(__file__) + this_mod = os.path.splitext(this)[0] + + models = [] + for mod_name in listPathModules(os.path.dirname(__file__)): + if mod_name.startswith("_") or mod_name == this_mod: + continue + + module = import_module('model_.' + mod_name) + members = inspect.getmembers(module, inspect.isclass) + for cls_name, instance in members: + if inspect.getmodule(instance) == module: + if cls_name.endswith('Model'): + models.append(instance(**kargs)) + + return super(Model, self).__init__(models) diff --git a/src/kimchi/utils.py b/src/kimchi/utils.py index 0e66214..59500dd 100644 --- a/src/kimchi/utils.py +++ b/src/kimchi/utils.py @@ -179,3 +179,12 @@ def patch_find_nfs_target(nfs_server): target['type'] = 'nfs' target['host_name'] = nfs_server return targets + + +def listPathModules(path): + modules = set() + for f in os.listdir(path): + base, ext = os.path.splitext(f) + if ext in ('.py', '.pyc', '.pyo'): + modules.add(base) + return sorted(modules) -- 1.7.10.4

From: Aline Manera <alinefm@br.ibm.com> The model implementation for config and its sub-resources were added to model_/tasks.py Signed-off-by: Aline Manera <alinefm@br.ibm.com> --- Makefile.am | 1 + src/kimchi/model_/__init__.py | 21 +++++++++++++++++++++ src/kimchi/model_/tasks.py | 39 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 61 insertions(+) create mode 100644 src/kimchi/model_/__init__.py create mode 100644 src/kimchi/model_/tasks.py diff --git a/Makefile.am b/Makefile.am index fa7b6b6..1ffe6db 100644 --- a/Makefile.am +++ b/Makefile.am @@ -54,6 +54,7 @@ PEP8_WHITELIST = \ src/kimchi/iscsi.py \ src/kimchi/isoinfo.py \ src/kimchi/kvmusertests.py \ + src/kimchi/model_/*.py \ src/kimchi/rollbackcontext.py \ src/kimchi/root.py \ src/kimchi/server.py \ diff --git a/src/kimchi/model_/__init__.py b/src/kimchi/model_/__init__.py new file mode 100644 index 0000000..8a37cc4 --- /dev/null +++ b/src/kimchi/model_/__init__.py @@ -0,0 +1,21 @@ +# +# Project Kimchi +# +# Copyright IBM, Corp. 2013 +# +# Authors: +# Aline Manera <alinefm@linux.vnet.ibm.com> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA diff --git a/src/kimchi/model_/tasks.py b/src/kimchi/model_/tasks.py new file mode 100644 index 0000000..40ca1d6 --- /dev/null +++ b/src/kimchi/model_/tasks.py @@ -0,0 +1,39 @@ +# +# Project Kimchi +# +# Copyright IBM, Corp. 2013 +# +# Authors: +# Aline Manera <alinefm@linux.vnet.ibm.com> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + + +class TasksModel(object): + def __init__(self, **kargs): + self.objstore = kargs['objstore'] + + def get_list(self): + with self.objstore as session: + return session.get_list('task') + + +class TaskModel(object): + def __init__(self, **kargs): + self.objstore = kargs['objstore'] + + def lookup(self, id): + with self.objstore as session: + return session.get('task', str(id)) -- 1.7.10.4

From: Aline Manera <alinefm@br.ibm.com> The model implementation for config and its sub-resources were added to model_/plugins.py Signed-off-by: Aline Manera <alinefm@br.ibm.com> --- src/kimchi/model_/plugins.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 src/kimchi/model_/plugins.py diff --git a/src/kimchi/model_/plugins.py b/src/kimchi/model_/plugins.py new file mode 100644 index 0000000..d6756d0 --- /dev/null +++ b/src/kimchi/model_/plugins.py @@ -0,0 +1,31 @@ +# +# Project Kimchi +# +# Copyright IBM, Corp. 2013 +# +# Authors: +# Aline Manera <alinefm@linux.vnet.ibm.com> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +from kimchi.utils import get_enabled_plugins + + +class PluginsModel(object): + def __init__(self, **kargs): + pass + + def get_list(self): + return [plugin for (plugin, config) in get_enabled_plugins()] -- 1.7.10.4

From: Aline Manera <alinefm@br.ibm.com> The model implementation for config and its sub-resources were added to model_/debugreports.py The idea is create a separated model implementation for each resource. Signed-off-by: Aline Manera <alinefm@br.ibm.com> --- src/kimchi/model_/debugreports.py | 167 +++++++++++++++++++++++++++++++++++++ src/kimchi/utils.py | 19 ++++- 2 files changed, 182 insertions(+), 4 deletions(-) create mode 100644 src/kimchi/model_/debugreports.py diff --git a/src/kimchi/model_/debugreports.py b/src/kimchi/model_/debugreports.py new file mode 100644 index 0000000..39402aa --- /dev/null +++ b/src/kimchi/model_/debugreports.py @@ -0,0 +1,167 @@ +# +# Project Kimchi +# +# Copyright IBM, Corp. 2013 +# +# Authors: +# Aline Manera <alinefm@linux.vnet.ibm.com> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +import fnmatch +import glob +import logging +import os +import shutil +import subprocess +import time + +from kimchi import config +from kimchi.exception import NotFoundError, OperationFailed +from kimchi.model_.tasks import TaskModel +from kimchi.utils import add_task, kimchi_log + + +class DebugReportsModel(object): + def __init__(self, **kargs): + self.objstore = kargs['objstore'] + self.task = TaskModel(**kargs) + + def create(self, params): + ident = params['name'] + taskid = self._gen_debugreport_file(ident) + return self.task.lookup(taskid) + + def get_list(self): + path = config.get_debugreports_path() + file_pattern = os.path.join(path, '*.*') + file_lists = glob.glob(file_pattern) + file_lists = [os.path.split(file)[1] for file in file_lists] + name_lists = [file.split('.', 1)[0] for file in file_lists] + + return name_lists + + def _gen_debugreport_file(self, name): + gen_cmd = self.get_system_report_tool() + + if gen_cmd is not None: + return add_task('', gen_cmd, self.objstore, name) + + raise OperationFailed("debugreport tool not found") + + @staticmethod + def sosreport_generate(cb, name): + command = 'sosreport --batch --name "%s"' % name + try: + retcode = subprocess.call(command, shell=True, + stdout=subprocess.PIPE) + if retcode < 0: + raise OperationFailed('Command terminated with signal') + elif retcode > 0: + raise OperationFailed('Command failed: rc = %i' % retcode) + pattern = '/tmp/sosreport-%s-*' % name + for reportFile in glob.glob(pattern): + if not fnmatch.fnmatch(reportFile, '*.md5'): + output = reportFile + break + else: + # sosreport tends to change the name mangling rule and + # compression file format between different releases. + # It's possible to fail to match a report file even sosreport + # runs successfully. In future we might have a general name + # mangling function in kimchi to format the name before passing + # it to sosreport. Then we can delete this exception. + raise OperationFailed('Can not find generated debug report ' + 'named by %s' % pattern) + ext = output.split('.', 1)[1] + path = config.get_debugreports_path() + target = os.path.join(path, name) + target_file = '%s.%s' % (target, ext) + shutil.move(output, target_file) + os.remove('%s.md5' % output) + cb('OK', True) + + return + + except OSError: + raise + + except Exception, e: + # No need to call cb to update the task status here. + # The task object will catch the exception rasied here + # and update the task status there + log = logging.getLogger('Model') + log.warning('Exception in generating debug file: %s', e) + raise OperationFailed(e) + + @staticmethod + def get_system_report_tool(): + # Please add new possible debug report command here + # and implement the report generating function + # based on the new report command + report_tools = ({'cmd': 'sosreport --help', + 'fn': DebugReportsModel.sosreport_generate},) + + # check if the command can be found by shell one by one + for helper_tool in report_tools: + try: + retcode = subprocess.call(helper_tool['cmd'], shell=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + if retcode == 0: + return helper_tool['fn'] + except Exception, e: + kimchi_log.info('Exception running command: %s', e) + + return None + + +class DebugReportModel(object): + def __init__(self, **kargs): + pass + + def lookup(self, name): + path = config.get_debugreports_path() + file_pattern = os.path.join(path, name) + file_pattern = file_pattern + '.*' + try: + file_target = glob.glob(file_pattern)[0] + except IndexError: + raise NotFoundError('no such report') + + ctime = os.stat(file_target).st_ctime + ctime = time.strftime("%Y-%m-%d-%H:%M:%S", time.localtime(ctime)) + file_target = os.path.split(file_target)[-1] + file_target = os.path.join("/data/debugreports", file_target) + return {'file': file_target, + 'ctime': ctime} + + def delete(self, name): + path = config.get_debugreports_path() + file_pattern = os.path.join(path, name + '.*') + try: + file_target = glob.glob(file_pattern)[0] + except IndexError: + raise NotFoundError('no such report') + + os.remove(file_target) + + +class DebugReportContentModel(object): + def __init__(self, **kargs): + self._debugreport = DebugReportModel() + + def lookup(self, name): + return self._debugreport.lookup(name) diff --git a/src/kimchi/utils.py b/src/kimchi/utils.py index 59500dd..30c09d4 100644 --- a/src/kimchi/utils.py +++ b/src/kimchi/utils.py @@ -25,17 +25,28 @@ import cherrypy import os import subprocess import urllib2 - +from threading import Timer from cherrypy.lib.reprconf import Parser -from kimchi.exception import TimeoutExpired - from kimchi import config -from threading import Timer +from kimchi.asynctask import AsyncTask kimchi_log = cherrypy.log.error_log +task_id = 0 + + +def get_next_task_id(): + global task_id + task_id += 1 + return task_id + + +def add_task(target_uri, fn, objstore, opaque=None): + id = get_next_task_id() + AsyncTask(id, target_uri, fn, objstore, opaque) + return id def is_digit(value): -- 1.7.10.4

From: Aline Manera <alinefm@br.ibm.com> The model implementation for config and its sub-resources were added to model_/config.py It is needed to create a temporary module named model_ as there is already a model.py in Kimchi code. It will be rename later when removing model.py Signed-off-by: Aline Manera <alinefm@br.ibm.com> --- src/kimchi/basemodel.py | 8 ++++ src/kimchi/model_/config.py | 87 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+) create mode 100644 src/kimchi/model_/config.py diff --git a/src/kimchi/basemodel.py b/src/kimchi/basemodel.py index 285e86d..0058dc2 100644 --- a/src/kimchi/basemodel.py +++ b/src/kimchi/basemodel.py @@ -45,3 +45,11 @@ class BaseModel(object): for member_name in callables: m = getattr(model_instance, member_name, None) setattr(self, '%s_%s' % (method_prefix, member_name), m) + + +class Singleton(type): + _instances = {} + def __call__(cls, *args, **kwargs): + if cls not in cls._instances: + cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs) + return cls._instances[cls] diff --git a/src/kimchi/model_/config.py b/src/kimchi/model_/config.py new file mode 100644 index 0000000..933e37f --- /dev/null +++ b/src/kimchi/model_/config.py @@ -0,0 +1,87 @@ +# +# Project Kimchi +# +# Copyright IBM, Corp. 2013 +# +# Authors: +# Aline Manera <alinefm@linux.vnet.ibm.com> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +import cherrypy + +from kimchi.basemodel import Singleton +from kimchi.distroloader import DistroLoader +from kimchi.exception import NotFoundError +from kimchi.featuretests import FeatureTests +from kimchi.model_.debugreports import DebugReportsModel +from kimchi.screenshot import VMScreenshot +from kimchi.utils import kimchi_log + + +class CapabilitiesModel(object): + __metaclass__ = Singleton + + def __init__(self, **kargs): + self.qemu_stream = False + self.qemu_stream_dns = False + self.libvirt_stream_protocols = [] + + # Subscribe function to set host capabilities to be run when cherrypy + # server is up + # It is needed because some features tests depends on the server + cherrypy.engine.subscribe('start', self._set_capabilities) + + def _set_capabilities(self): + kimchi_log.info("*** Running feature tests ***") + self.qemu_stream = FeatureTests.qemu_supports_iso_stream() + self.qemu_stream_dns = FeatureTests.qemu_iso_stream_dns() + self.nfs_target_probe = FeatureTests.libvirt_support_nfs_probe() + + self.libvirt_stream_protocols = [] + for p in ['http', 'https', 'ftp', 'ftps', 'tftp']: + if FeatureTests.libvirt_supports_iso_stream(p): + self.libvirt_stream_protocols.append(p) + + kimchi_log.info("*** Feature tests completed ***") + _set_capabilities.priority = 90 + + def lookup(self, *ident): + report_tool = DebugReportsModel.get_system_report_tool() + + return {'libvirt_stream_protocols': self.libvirt_stream_protocols, + 'qemu_stream': self.qemu_stream, + 'screenshot': VMScreenshot.get_stream_test_result(), + 'system_report_tool': bool(report_tool)} + + +class DistrosModel(object): + def __init__(self, **kargs): + distroloader = DistroLoader() + self.distros = distroloader.get() + + def get_list(self): + return self.distros.keys() + + +class DistroModel(object): + def __init__(self, **kargs): + self._distros = DistrosModel() + + def lookup(self, name): + try: + return self._distros.distros[name] + except KeyError: + raise NotFoundError("Distro '%s' not found." % name) -- 1.7.10.4

From: Aline Manera <alinefm@br.ibm.com> The model implementation for network and its sub-resources were added to model_/networks.py Signed-off-by: Aline Manera <alinefm@br.ibm.com> --- src/kimchi/model_/networks.py | 265 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 265 insertions(+) create mode 100644 src/kimchi/model_/networks.py diff --git a/src/kimchi/model_/networks.py b/src/kimchi/model_/networks.py new file mode 100644 index 0000000..b164141 --- /dev/null +++ b/src/kimchi/model_/networks.py @@ -0,0 +1,265 @@ +# +# Project Kimchi +# +# Copyright IBM, Corp. 2013 +# +# Authors: +# Aline Manera <alinefm@linux.vnet.ibm.com> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +import ipaddr +import libvirt + +from kimchi import netinfo +from kimchi import network as knetwork +from kimchi import networkxml +from kimchi import xmlutils +from kimchi.exception import InvalidOperation, InvalidParameter +from kimchi.exception import MissingParameter, NotFoundError, OperationFailed + + +class NetworksModel(object): + def __init__(self, **kargs): + self.conn = kargs['conn'] + + def create(self, params): + conn = self.conn.get() + name = params['name'] + if name in self.get_list(): + raise InvalidOperation("Network %s already exists" % name) + + connection = params["connection"] + # set forward mode, isolated do not need forward + if connection != 'isolated': + params['forward'] = {'mode': connection} + + # set subnet, bridge network do not need subnet + if connection in ["nat", 'isolated']: + self._set_network_subnet(params) + + # only bridge network need bridge(linux bridge) or interface(macvtap) + if connection == 'bridge': + self._set_network_bridge(params) + + xml = networkxml.to_network_xml(**params) + + try: + network = conn.networkDefineXML(xml) + network.setAutostart(True) + except libvirt.libvirtError as e: + raise OperationFailed(e.get_error_message()) + + return name + + def get_list(self): + conn = self.conn.get() + return sorted(conn.listNetworks() + conn.listDefinedNetworks()) + + def _set_network_subnet(self, params): + netaddr = params.get('subnet', '') + net_addrs = [] + # lookup a free network address for nat and isolated automatically + if not netaddr: + for net_name in self.get_list(): + network = self._get_network(net_name) + xml = network.XMLDesc(0) + subnet = NetworkModel.get_network_from_xml(xml)['subnet'] + subnet and net_addrs.append(ipaddr.IPNetwork(subnet)) + netaddr = knetwork.get_one_free_network(net_addrs) + if not netaddr: + raise OperationFailed("can not find a free IP address for " + "network '%s'" % params['name']) + + try: + ip = ipaddr.IPNetwork(netaddr) + except ValueError as e: + raise InvalidParameter("%s" % e) + + if ip.ip == ip.network: + ip.ip = ip.ip + 1 + + dhcp_start = str(ip.ip + ip.numhosts / 2) + dhcp_end = str(ip.ip + ip.numhosts - 2) + params.update({'net': str(ip), + 'dhcp': {'range': {'start': dhcp_start, + 'end': dhcp_end}}}) + + def _set_network_bridge(self, params): + try: + iface = params['interface'] + if iface in self.get_all_networks_interfaces(): + raise InvalidParameter("interface '%s' already in use." % + iface) + except KeyError, e: + raise MissingParameter(e) + if netinfo.is_bridge(iface): + params['bridge'] = iface + elif netinfo.is_bare_nic(iface) or netinfo.is_bonding(iface): + if params.get('vlan_id') is None: + params['forward']['dev'] = iface + else: + params['bridge'] = \ + self._create_vlan_tagged_bridge(str(iface), + str(params['vlan_id'])) + else: + raise InvalidParameter("the interface should be bare nic, " + "bonding or bridge device.") + + def get_all_networks_interfaces(self): + net_names = self.get_list() + interfaces = [] + for name in net_names: + conn = self.conn.get() + network = conn.networkLookupByName(name) + xml = network.XMLDesc(0) + net_dict = NetworkModel.get_network_from_xml(xml) + forward = net_dict['forward'] + (forward['mode'] == 'bridge' and forward['interface'] and + interfaces.append(forward['interface'][0]) is None or + interfaces.extend(forward['interface'] + forward['pf'])) + net_dict['bridge'] and interfaces.append(net_dict['bridge']) + return interfaces + + def _create_vlan_tagged_bridge(self, interface, vlan_id): + br_name = '-'.join(('kimchi', interface, vlan_id)) + br_xml = networkxml.create_vlan_tagged_bridge_xml(br_name, interface, + vlan_id) + conn = self.conn.get() + conn.changeBegin() + try: + vlan_tagged_br = conn.interfaceDefineXML(br_xml) + vlan_tagged_br.create() + except libvirt.libvirtError as e: + conn.changeRollback() + raise OperationFailed(e.message) + else: + conn.changeCommit() + return br_name + + +class NetworkModel(object): + def __init__(self, **kargs): + self.conn = kargs['conn'] + + def lookup(self, name): + network = self._get_network(name) + xml = network.XMLDesc(0) + net_dict = self.get_network_from_xml(xml) + subnet = net_dict['subnet'] + dhcp = net_dict['dhcp'] + forward = net_dict['forward'] + interface = net_dict['bridge'] + + connection = forward['mode'] or "isolated" + # FIXME, if we want to support other forward mode well. + if connection == 'bridge': + # macvtap bridge + interface = interface or forward['interface'][0] + # exposing the network on linux bridge or macvtap interface + interface_subnet = knetwork.get_dev_netaddr(interface) + subnet = subnet if subnet else interface_subnet + + # libvirt use format 192.168.0.1/24, standard should be 192.168.0.0/24 + # http://www.ovirt.org/File:Issue3.png + if subnet: + subnet = ipaddr.IPNetwork(subnet) + subnet = "%s/%s" % (subnet.network, subnet.prefixlen) + + return {'connection': connection, + 'interface': interface, + 'subnet': subnet, + 'dhcp': dhcp, + 'vms': self._get_vms_attach_to_a_network(name), + 'autostart': network.autostart() == 1, + 'state': network.isActive() and "active" or "inactive"} + + def _get_vms_attach_to_a_network(self, network): + vms = [] + conn = self.conn.get() + for dom in conn.listAllDomains(0): + networks = self._vm_get_networks(dom) + if network in networks: + vms.append(dom.name()) + return vms + + def _vm_get_networks(self, dom): + xml = dom.XMLDesc(0) + xpath = "/domain/devices/interface[@type='network']/source/@network" + return xmlutils.xpath_get_text(xml, xpath) + + def activate(self, name): + network = self._get_network(name) + network.create() + + def deactivate(self, name): + network = self._get_network(name) + network.destroy() + + def delete(self, name): + network = self._get_network(name) + if network.isActive(): + raise InvalidOperation( + "Unable to delete the active network %s" % name) + self._remove_vlan_tagged_bridge(network) + network.undefine() + + def _get_network(self, name): + conn = self.conn.get() + try: + return conn.networkLookupByName(name) + except libvirt.libvirtError as e: + raise NotFoundError("Network '%s' not found: %s" % + (name, e.get_error_message())) + + @staticmethod + def get_network_from_xml(xml): + address = xmlutils.xpath_get_text(xml, "/network/ip/@address") + address = address and address[0] or '' + netmask = xmlutils.xpath_get_text(xml, "/network/ip/@netmask") + netmask = netmask and netmask[0] or '' + net = address and netmask and "/".join([address, netmask]) or '' + + dhcp_start = xmlutils.xpath_get_text(xml, + "/network/ip/dhcp/range/@start") + dhcp_start = dhcp_start and dhcp_start[0] or '' + dhcp_end = xmlutils.xpath_get_text(xml, "/network/ip/dhcp/range/@end") + dhcp_end = dhcp_end and dhcp_end[0] or '' + dhcp = {'start': dhcp_start, 'end': dhcp_end} + + forward_mode = xmlutils.xpath_get_text(xml, "/network/forward/@mode") + forward_mode = forward_mode and forward_mode[0] or '' + forward_if = xmlutils.xpath_get_text(xml, + "/network/forward/interface/@dev") + forward_pf = xmlutils.xpath_get_text(xml, "/network/forward/pf/@dev") + bridge = xmlutils.xpath_get_text(xml, "/network/bridge/@name") + bridge = bridge and bridge[0] or '' + return {'subnet': net, 'dhcp': dhcp, 'bridge': bridge, + 'forward': {'mode': forward_mode, + 'interface': forward_if, + 'pf': forward_pf}} + + def _remove_vlan_tagged_bridge(self, network): + try: + bridge = network.bridgeName() + except libvirt.libvirtError: + pass + else: + if bridge.startswith('kimchi-'): + conn = self.conn.get() + iface = conn.interfaceLookupByName(bridge) + if iface.isActive(): + iface.destroy() + iface.undefine() -- 1.7.10.4

From: Aline Manera <alinefm@br.ibm.com> The model implementation for config and its sub-resources were added to model_/interfaces.py Signed-off-by: Aline Manera <alinefm@br.ibm.com> --- src/kimchi/model_/interfaces.py | 46 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 src/kimchi/model_/interfaces.py diff --git a/src/kimchi/model_/interfaces.py b/src/kimchi/model_/interfaces.py new file mode 100644 index 0000000..b3b6ccd --- /dev/null +++ b/src/kimchi/model_/interfaces.py @@ -0,0 +1,46 @@ +# +# Project Kimchi +# +# Copyright IBM, Corp. 2013 +# +# Authors: +# Aline Manera <alinefm@linux.vnet.ibm.com> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +from kimchi import netinfo +from kimchi.exception import NotFoundError +from kimchi.model_.networks import NetworksModel + + +class InterfacesModel(object): + def __init__(self, **kargs): + self.conn = kargs['conn'] + self.networks = NetworksModel(**kargs) + + def get_list(self): + return list(set(netinfo.all_favored_interfaces()) - + set(self.networks.get_all_networks_interfaces())) + + +class InterfaceModel(object): + def __init__(self, **kargs): + pass + + def lookup(self, name): + try: + return netinfo.get_interface_info(name) + except ValueError, e: + raise NotFoundError(e) -- 1.7.10.4

From: Aline Manera <alinefm@br.ibm.com> The model implementation for storage pool and its sub-resources were added to model_/storagepools.py Signed-off-by: Aline Manera <alinefm@br.ibm.com> --- src/kimchi/model_/storagepools.py | 246 +++++++++++++++++++++++++++++++++++++ 1 file changed, 246 insertions(+) create mode 100644 src/kimchi/model_/storagepools.py diff --git a/src/kimchi/model_/storagepools.py b/src/kimchi/model_/storagepools.py new file mode 100644 index 0000000..2fca8e4 --- /dev/null +++ b/src/kimchi/model_/storagepools.py @@ -0,0 +1,246 @@ +# +# Project Kimchi +# +# Copyright IBM, Corp. 2013 +# +# Authors: +# Aline Manera <alinefm@linux.vnet.ibm.com> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +import libvirt + +from kimchi import xmlutils +from kimchi.scan import Scanner +from kimchi.exception import InvalidOperation, MissingParameter +from kimchi.exception import NotFoundError, OperationFailed +from kimchi.model_.libvirtstoragepool import StoragePoolDef +from kimchi.utils import add_task, kimchi_log + + +ISO_POOL_NAME = u'kimchi_isos' +POOL_STATE_MAP = {0: 'inactive', + 1: 'initializing', + 2: 'active', + 3: 'degraded', + 4: 'inaccessible'} + +STORAGE_SOURCES = {'netfs': {'addr': '/pool/source/host/@name', + 'path': '/pool/source/dir/@path'}} + + +class StoragePoolsModel(object): + def __init__(self, **kargs): + self.conn = kargs['conn'] + self.objstore = kargs['objstore'] + self.scanner = Scanner(self._clean_scan) + self.scanner.delete() + + def get_list(self): + try: + conn = self.conn.get() + names = conn.listStoragePools() + names += conn.listDefinedStoragePools() + return sorted(names) + except libvirt.libvirtError as e: + raise OperationFailed(e.get_error_message()) + + def create(self, params): + task_id = None + conn = self.conn.get() + try: + name = params['name'] + if name in (ISO_POOL_NAME, ): + raise InvalidOperation("StoragePool already exists") + + if params['type'] == 'kimchi-iso': + task_id = self._do_deep_scan(params) + poolDef = StoragePoolDef.create(params) + poolDef.prepare(conn) + xml = poolDef.xml + except KeyError, key: + raise MissingParameter(key) + + if name in self.get_list(): + err = "The name %s has been used by a pool" + raise InvalidOperation(err % name) + + try: + if task_id: + # Create transient pool for deep scan + conn.storagePoolCreateXML(xml, 0) + return name + + pool = conn.storagePoolDefineXML(xml, 0) + if params['type'] in ['logical', 'dir', 'netfs']: + pool.build(libvirt.VIR_STORAGE_POOL_BUILD_NEW) + # autostart dir and logical storage pool created from kimchi + pool.setAutostart(1) + else: + # disable autostart for others + pool.setAutostart(0) + except libvirt.libvirtError as e: + msg = "Problem creating Storage Pool: %s" + kimchi_log.error(msg, e) + raise OperationFailed(e.get_error_message()) + return name + + def _clean_scan(self, pool_name): + try: + conn = self.conn.get() + pool = conn.storagePoolLookupByName(pool_name) + pool.destroy() + with self.objstore as session: + session.delete('scanning', pool_name) + except Exception, e: + err = "Exception %s occured when cleaning scan result" + kimchi_log.debug(err % e.message) + + def _do_deep_scan(self, params): + scan_params = dict(ignore_list=[]) + scan_params['scan_path'] = params['path'] + params['type'] = 'dir' + + for pool in self.get_list(): + try: + res = self.storagepool_lookup(pool) + if res['state'] == 'active': + scan_params['ignore_list'].append(res['path']) + except Exception, e: + err = "Exception %s occured when get ignore path" + kimchi_log.debug(err % e.message) + + params['path'] = self.scanner.scan_dir_prepare(params['name']) + scan_params['pool_path'] = params['path'] + task_id = add_task('', self.scanner.start_scan, self.objstore, + scan_params) + # Record scanning-task/storagepool mapping for future querying + with self.objstore as session: + session.store('scanning', params['name'], task_id) + return task_id + + +class StoragePoolModel(object): + def __init__(self, **kargs): + self.conn = kargs['conn'] + self.objstore = kargs['objstore'] + + @staticmethod + def get_storagepool(name, conn): + conn = conn.get() + try: + return conn.storagePoolLookupByName(name) + except libvirt.libvirtError as e: + if e.get_error_code() == libvirt.VIR_ERR_NO_STORAGE_POOL: + raise NotFoundError("Storage Pool '%s' not found" % name) + else: + raise + + def _get_storagepool_vols_num(self, pool): + try: + if pool.isActive(): + pool.refresh(0) + return pool.numOfVolumes() + else: + return 0 + except libvirt.libvirtError as e: + raise OperationFailed(e.get_error_message()) + + def _get_storage_source(self, pool_type, pool_xml): + source = {} + if pool_type not in STORAGE_SOURCES: + return source + + for key, val in STORAGE_SOURCES[pool_type].items(): + res = xmlutils.xpath_get_text(pool_xml, val) + source[key] = res[0] if len(res) == 1 else res + + return source + + def lookup(self, name): + pool = self.get_storagepool(name, self.conn) + info = pool.info() + nr_volumes = self._get_storagepool_vols_num(pool) + autostart = True if pool.autostart() else False + xml = pool.XMLDesc(0) + path = xmlutils.xpath_get_text(xml, "/pool/target/path")[0] + pool_type = xmlutils.xpath_get_text(xml, "/pool/@type")[0] + source = self._get_storage_source(pool_type, xml) + res = {'state': POOL_STATE_MAP[info[0]], + 'path': path, + 'source': source, + 'type': pool_type, + 'autostart': autostart, + 'capacity': info[1], + 'allocated': info[2], + 'available': info[3], + 'nr_volumes': nr_volumes} + + if not pool.isPersistent(): + # Deal with deep scan generated pool + try: + with self.objstore as session: + task_id = session.get('scanning', name) + res['task_id'] = str(task_id) + res['type'] = 'kimchi-iso' + except NotFoundError: + # User created normal pool + pass + return res + + def update(self, name, params): + autostart = params['autostart'] + if autostart not in [True, False]: + raise InvalidOperation("Autostart flag must be true or false") + pool = self.get_storagepool(name, self.conn) + if autostart: + pool.setAutostart(1) + else: + pool.setAutostart(0) + ident = pool.name() + return ident + + def activate(self, name): + pool = self.get_storagepool(name, self.conn) + try: + pool.create(0) + except libvirt.libvirtError as e: + raise OperationFailed(e.get_error_message()) + + def deactivate(self, name): + pool = self.get_storagepool(name, self.conn) + try: + pool.destroy() + except libvirt.libvirtError as e: + raise OperationFailed(e.get_error_message()) + + def delete(self, name): + pool = self.get_storagepool(name, self.conn) + if pool.isActive(): + err = "Unable to delete the active storagepool %s" + raise InvalidOperation(err % name) + try: + pool.undefine() + except libvirt.libvirtError as e: + raise OperationFailed(e.get_error_message()) + + +class IsoPoolModel(object): + def __init__(self, **kargs): + pass + + def lookup(self, name): + return {'state': 'active', + 'type': 'kimchi-iso'} -- 1.7.10.4

From: Aline Manera <alinefm@br.ibm.com> The model implementation for storage volume and its sub-resources were added to model_/storagevolumes.py Signed-off-by: Aline Manera <alinefm@br.ibm.com> --- src/kimchi/model_/storagevolumes.py | 176 +++++++++++++++++++++++++++++++++++ 1 file changed, 176 insertions(+) create mode 100644 src/kimchi/model_/storagevolumes.py diff --git a/src/kimchi/model_/storagevolumes.py b/src/kimchi/model_/storagevolumes.py new file mode 100644 index 0000000..0edac52 --- /dev/null +++ b/src/kimchi/model_/storagevolumes.py @@ -0,0 +1,176 @@ +# +# Project Kimchi +# +# Copyright IBM, Corp. 2013 +# +# Authors: +# Aline Manera <alinefm@linux.vnet.ibm.com> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +import os + +import libvirt + +from kimchi import xmlutils +from kimchi.exception import InvalidOperation, IsoFormatError +from kimchi.exception import MissingParameter, NotFoundError, OperationFailed +from kimchi.isoinfo import IsoImage +from kimchi.model_.storagepools import StoragePoolModel + + +VOLUME_TYPE_MAP = {0: 'file', + 1: 'block', + 2: 'directory', + 3: 'network'} + + +class StorageVolumesModel(object): + def __init__(self, **kargs): + self.conn = kargs['conn'] + + def create(self, pool, params): + vol_xml = """ + <volume> + <name>%(name)s</name> + <allocation unit="MiB">%(allocation)s</allocation> + <capacity unit="MiB">%(capacity)s</capacity> + <source> + </source> + <target> + <format type='%(format)s'/> + </target> + </volume> + """ + params.setdefault('allocation', 0) + params.setdefault('format', 'qcow2') + + try: + pool = StoragePoolModel.get_storagepool(pool, self.conn) + name = params['name'] + xml = vol_xml % params + except KeyError, key: + raise MissingParameter(key) + + try: + pool.createXML(xml, 0) + except libvirt.libvirtError as e: + raise OperationFailed(e.get_error_message()) + return name + + def get_list(self, pool): + pool = StoragePoolModel.get_storagepool(pool, self.conn) + if not pool.isActive(): + err = "Unable to list volumes in inactive storagepool %s" + raise InvalidOperation(err % pool.name()) + try: + pool.refresh(0) + return pool.listVolumes() + except libvirt.libvirtError as e: + raise OperationFailed(e.get_error_message()) + + +class StorageVolumeModel(object): + def __init__(self, **kargs): + self.conn = kargs['conn'] + + def _get_storagevolume(self, pool, name): + pool = StoragePoolModel.get_storagepool(pool, self.conn) + if not pool.isActive(): + err = "Unable to list volumes in inactive storagepool %s" + raise InvalidOperation(err % pool.name()) + try: + return pool.storageVolLookupByName(name) + except libvirt.libvirtError as e: + if e.get_error_code() == libvirt.VIR_ERR_NO_STORAGE_VOL: + raise NotFoundError("Storage Volume '%s' not found" % name) + else: + raise + + def lookup(self, pool, name): + vol = self._get_storagevolume(pool, name) + path = vol.path() + info = vol.info() + xml = vol.XMLDesc(0) + fmt = xmlutils.xpath_get_text(xml, "/volume/target/format/@type")[0] + res = dict(type=VOLUME_TYPE_MAP[info[0]], + capacity=info[1], + allocation=info[2], + path=path, + format=fmt) + if fmt == 'iso': + if os.path.islink(path): + path = os.path.join(os.path.dirname(path), os.readlink(path)) + os_distro = os_version = 'unknown' + try: + iso_img = IsoImage(path) + os_distro, os_version = iso_img.probe() + bootable = True + except IsoFormatError: + bootable = False + res.update( + dict(os_distro=os_distro, os_version=os_version, path=path, + bootable=bootable)) + + return res + + def wipe(self, pool, name): + volume = self._get_storagevolume(pool, name) + try: + volume.wipePattern(libvirt.VIR_STORAGE_VOL_WIPE_ALG_ZERO, 0) + except libvirt.libvirtError as e: + raise OperationFailed(e.get_error_message()) + + def delete(self, pool, name): + volume = self._get_storagevolume(pool, name) + try: + volume.delete(0) + except libvirt.libvirtError as e: + raise OperationFailed(e.get_error_message()) + + def resize(self, pool, name, size): + size = size << 20 + volume = self._get_storagevolume(pool, name) + try: + volume.resize(size, 0) + except libvirt.libvirtError as e: + raise OperationFailed(e.get_error_message()) + + +class IsoVolumesModel(object): + def __init__(self, **kargs): + self.conn = kargs['conn'] + self.storagevolume = StorageVolumeModel(**kargs) + + def get_list(self): + iso_volumes = [] + conn = self.conn.get() + pools = conn.listStoragePools() + pools += conn.listDefinedStoragePools() + + for pool in pools: + try: + pool.refresh(0) + volumes = pool.listVolumes() + except InvalidOperation: + # Skip inactive pools + continue + + for volume in volumes: + res = self.storagevolume.lookup(pool, volume) + if res['format'] == 'iso': + res['name'] = '%s' % volume + iso_volumes.append(res) + return iso_volumes -- 1.7.10.4

From: Aline Manera <alinefm@br.ibm.com> The model implementation for storage server and its sub-resources were added to model_/storagesevers.py Signed-off-by: Aline Manera <alinefm@br.ibm.com> --- src/kimchi/model_/storageservers.py | 78 +++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 src/kimchi/model_/storageservers.py diff --git a/src/kimchi/model_/storageservers.py b/src/kimchi/model_/storageservers.py new file mode 100644 index 0000000..1728394 --- /dev/null +++ b/src/kimchi/model_/storageservers.py @@ -0,0 +1,78 @@ +# +# Project Kimchi +# +# Copyright IBM, Corp. 2013 +# +# Authors: +# Aline Manera <alinefm@linux.vnet.ibm.com> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +from kimchi.exception import NotFoundError +from kimchi.model_.storagepools import StoragePoolModel, STORAGE_SOURCES + + +class StorageServersModel(object): + def __init__(self, **kargs): + self.conn = kargs['conn'] + self.pool = StoragePoolModel(**kargs) + + def get_list(self, _target_type=None): + if not _target_type: + target_type = STORAGE_SOURCES.keys() + else: + target_type = [_target_type] + pools = self.pools.get_list() + + conn = self.conn.get() + pools = conn.listStoragePools() + pools += conn.listDefinedStoragePools() + + server_list = [] + for pool in pools: + try: + pool_info = self.pool.lookup(pool) + if (pool_info['type'] in target_type and + pool_info['source']['addr'] not in server_list): + # Avoid to add same server for multiple times + # if it hosts more than one storage type + server_list.append(pool_info['source']['addr']) + except NotFoundError: + pass + + return server_list + + +class StorageServerModel(object): + def __init__(self, **kargs): + self.conn = kargs['conn'] + self.pool = StoragePoolModel(**kargs) + + def lookup(self, server): + conn = self.conn.get() + pools = conn.listStoragePools() + pools += conn.listDefinedStoragePools() + for pool in pools: + try: + pool_info = self.pool.lookup(pool) + if pool_info['source'] and \ + pool_info['source']['addr'] == server: + return dict(host=server) + except NotFoundError: + # Avoid inconsistent pool result because of lease between list + # lookup + pass + + raise NotFoundError('server %s does not used by kimchi' % server) -- 1.7.10.4

From: Aline Manera <alinefm@br.ibm.com> The model implementation for storage target and its sub-resources were added to model_/storagetargets.py Signed-off-by: Aline Manera <alinefm@br.ibm.com> --- src/kimchi/model_/storagetargets.py | 86 +++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 src/kimchi/model_/storagetargets.py diff --git a/src/kimchi/model_/storagetargets.py b/src/kimchi/model_/storagetargets.py new file mode 100644 index 0000000..be73732 --- /dev/null +++ b/src/kimchi/model_/storagetargets.py @@ -0,0 +1,86 @@ +# +# Project Kimchi +# +# Copyright IBM, Corp. 2013 +# +# Authors: +# Aline Manera <alinefm@linux.vnet.ibm.com> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +import libvirt +import lxml.etree as ET +from lxml import objectify +from lxml.builder import E + +from kimchi.model_.config import CapabilitiesModel +from kimchi.model_.storagepools import STORAGE_SOURCES +from kimchi.utils import kimchi_log, patch_find_nfs_target + + +class StorageTargetsModel(object): + def __init__(self, **kargs): + self.conn = kargs['conn'] + self.caps = CapabilitiesModel() + + def get_list(self, storage_server, _target_type=None): + target_list = list() + + if not _target_type: + target_types = STORAGE_SOURCES.keys() + else: + target_types = [_target_type] + + for target_type in target_types: + if not self.caps.nfs_target_probe and target_type == 'netfs': + targets = patch_find_nfs_target(storage_server) + else: + xml = self._get_storage_server_spec(server=storage_server, + target_type=target_type) + conn = self.conn.get() + try: + ret = conn.findStoragePoolSources(target_type, xml, 0) + except libvirt.libvirtError as e: + err = "Query storage pool source fails because of %s" + kimchi_log.warning(err, e.get_error_message()) + continue + + targets = self._parse_target_source_result(target_type, ret) + + target_list.extend(targets) + return target_list + + def _get_storage_server_spec(**kwargs): + # Required parameters: + # server: + # target_type: + extra_args = [] + if kwargs['target_type'] == 'netfs': + extra_args.append(E.format(type='nfs')) + obj = E.source(E.host(name=kwargs['server']), *extra_args) + xml = ET.tostring(obj) + return xml + + def _parse_target_source_result(target_type, xml_str): + root = objectify.fromstring(xml_str) + ret = [] + for source in root.getchildren(): + if target_type == 'netfs': + host_name = source.host.get('name') + target_path = source.dir.get('path') + type = source.format.get('type') + ret.append(dict(host=host_name, target_type=type, + target=target_path)) + return ret -- 1.7.10.4

From: Aline Manera <alinefm@br.ibm.com> The model implementation for template and its sub-resources were added to model_/templates.py Signed-off-by: Aline Manera <alinefm@br.ibm.com> --- src/kimchi/model_/templates.py | 172 ++++++++++++++++++++++++++++++++++++++++ src/kimchi/utils.py | 14 ++++ 2 files changed, 186 insertions(+) create mode 100644 src/kimchi/model_/templates.py diff --git a/src/kimchi/model_/templates.py b/src/kimchi/model_/templates.py new file mode 100644 index 0000000..03632a6 --- /dev/null +++ b/src/kimchi/model_/templates.py @@ -0,0 +1,172 @@ +# +# Project Kimchi +# +# Copyright IBM, Corp. 2013 +# +# Authors: +# Aline Manera <alinefm@linux.vnet.ibm.com> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +import copy + +import libvirt + +from kimchi import xmlutils +from kimchi.exception import InvalidOperation, InvalidParameter, NotFoundError +from kimchi.utils import pool_name_from_uri +from kimchi.vmtemplate import VMTemplate + + +class TemplatesModel(object): + def __init__(self, **kargs): + self.objstore = kargs['objstore'] + self.conn = kargs['conn'] + + def create(self, params): + name = params['name'] + conn = self.conn.get() + + pool_uri = params.get(u'storagepool', '') + if pool_uri: + pool_name = pool_name_from_uri(pool_uri) + try: + conn.storagePoolLookupByName(pool_name) + except Exception as e: + err = "Storagepool specified is not valid: %s." + raise InvalidParameter(err % e.message) + + for net_name in params.get(u'networks', []): + try: + conn.networkLookupByName(net_name) + except Exception, e: + raise InvalidParameter("Network '%s' specified by template " + "does not exist." % net_name) + + with self.objstore as session: + if name in session.get_list('template'): + raise InvalidOperation("Template already exists") + t = LibvirtVMTemplate(params, scan=True) + session.store('template', name, t.info) + return name + + def get_list(self): + with self.objstore as session: + return session.get_list('template') + + +class TemplateModel(object): + def __init__(self, **kargs): + self.objstore = kargs['objstore'] + self.conn = kargs['conn'] + self.templates = TemplatesModel(**kargs) + + @staticmethod + def get_template(name, objstore, conn, overrides=None): + with objstore as session: + params = session.get('template', name) + if overrides: + params.update(overrides) + return LibvirtVMTemplate(params, False, conn) + + def lookup(self, name): + t = self.get_template(name, self.objstore, self.conn) + return t.info + + def delete(self, name): + with self.objstore as session: + session.delete('template', name) + + def update(self, name, params): + old_t = self.lookup(name) + new_t = copy.copy(old_t) + new_t.update(params) + ident = name + + pool_uri = new_t.get(u'storagepool', '') + pool_name = pool_name_from_uri(pool_uri) + try: + conn = self.conn.get() + conn.storagePoolLookupByName(pool_name) + except Exception as e: + err = "Storagepool specified is not valid: %s." + raise InvalidParameter(err % e.message) + + for net_name in params.get(u'networks', []): + try: + conn = self.conn.get() + conn.networkLookupByName(net_name) + except Exception, e: + raise InvalidParameter("Network '%s' specified by template " + "does not exist" % net_name) + + self.delete(name) + try: + ident = self.templates.create(new_t) + except: + ident = self.templates.create(old_t) + raise + return ident + + +class LibvirtVMTemplate(VMTemplate): + def __init__(self, args, scan=False, conn=None): + VMTemplate.__init__(self, args, scan) + self.conn = conn + + def _storage_validate(self): + pool_uri = self.info['storagepool'] + pool_name = pool_name_from_uri(pool_uri) + try: + conn = self.conn.get() + pool = conn.storagePoolLookupByName(pool_name) + except libvirt.libvirtError: + err = 'Storage specified by template does not exist' + raise InvalidParameter(err) + + if not pool.isActive(): + err = 'Storage specified by template is not active' + raise InvalidParameter(err) + + return pool + + def _network_validate(self): + names = self.info['networks'] + for name in names: + try: + conn = self.conn.get() + network = conn.networkLookupByName(name) + except libvirt.libvirtError: + err = 'Network specified by template does not exist' + raise InvalidParameter(err) + + if not network.isActive(): + err = 'Network specified by template is not active' + raise InvalidParameter(err) + + def _get_storage_path(self): + pool = self._storage_validate() + xml = pool.XMLDesc(0) + return xmlutils.xpath_get_text(xml, "/pool/target/path")[0] + + def fork_vm_storage(self, vm_uuid): + # Provision storage: + # TODO: Rebase on the storage API once upstream + pool = self._storage_validate() + vol_list = self.to_volume_list(vm_uuid) + for v in vol_list: + # outgoing text to libvirt, encode('utf-8') + pool.createXML(v['xml'].encode('utf-8'), 0) + return vol_list diff --git a/src/kimchi/utils.py b/src/kimchi/utils.py index 30c09d4..5345d28 100644 --- a/src/kimchi/utils.py +++ b/src/kimchi/utils.py @@ -23,6 +23,7 @@ import cherrypy import os +import re import subprocess import urllib2 from threading import Timer @@ -31,12 +32,25 @@ from cherrypy.lib.reprconf import Parser from kimchi import config from kimchi.asynctask import AsyncTask +from kimchi.exception import InvalidParameter, TimeoutExpired kimchi_log = cherrypy.log.error_log task_id = 0 +def _uri_to_name(collection, uri): + expr = '/%s/(.*?)/?$' % collection + m = re.match(expr, uri) + if not m: + raise InvalidParameter(uri) + return m.group(1) + + +def pool_name_from_uri(uri): + return _uri_to_name('storagepools', uri) + + def get_next_task_id(): global task_id task_id += 1 -- 1.7.10.4

From: Aline Manera <alinefm@br.ibm.com> The model implementation for vm and its sub-resources were added to model_/vms.py Signed-off-by: Aline Manera <alinefm@br.ibm.com> --- src/kimchi/model_/utils.py | 33 ++++ src/kimchi/model_/vms.py | 450 ++++++++++++++++++++++++++++++++++++++++++++ src/kimchi/utils.py | 4 + 3 files changed, 487 insertions(+) create mode 100644 src/kimchi/model_/utils.py create mode 100644 src/kimchi/model_/vms.py diff --git a/src/kimchi/model_/utils.py b/src/kimchi/model_/utils.py new file mode 100644 index 0000000..a27b867 --- /dev/null +++ b/src/kimchi/model_/utils.py @@ -0,0 +1,33 @@ +# +# Project Kimchi +# +# Copyright IBM, Corp. 2013 +# +# Authors: +# Aline Manera <alinefm@linux.vnet.ibm.com> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +from kimchi.exception import OperationFailed + + +def get_vm_name(vm_name, t_name, name_list): + if vm_name: + return vm_name + for i in xrange(1, 1000): + vm_name = "%s-vm-%i" % (t_name, i) + if vm_name not in name_list: + return vm_name + raise OperationFailed("Unable to choose a VM name") diff --git a/src/kimchi/model_/vms.py b/src/kimchi/model_/vms.py new file mode 100644 index 0000000..d2ab292 --- /dev/null +++ b/src/kimchi/model_/vms.py @@ -0,0 +1,450 @@ +# +# Project Kimchi +# +# Copyright IBM, Corp. 2013 +# +# Authors: +# Aline Manera <alinefm@linux.vnet.ibm.com> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +import os +import time +import uuid +from xml.etree import ElementTree + +import libvirt +from cherrypy.process.plugins import BackgroundTask + +from kimchi import vnc +from kimchi import xmlutils +from kimchi.exception import InvalidOperation, InvalidParameter +from kimchi.exception import NotFoundError, OperationFailed +from kimchi.model_.config import CapabilitiesModel +from kimchi.model_.templates import TemplateModel +from kimchi.model_.utils import get_vm_name +from kimchi.screenshot import VMScreenshot +from kimchi.utils import template_name_from_uri + + +DOM_STATE_MAP = {0: 'nostate', + 1: 'running', + 2: 'blocked', + 3: 'paused', + 4: 'shutdown', + 5: 'shutoff', + 6: 'crashed'} + +GUESTS_STATS_INTERVAL = 5 +VM_STATIC_UPDATE_PARAMS = {'name': './name'} +VM_LIVE_UPDATE_PARAMS = {} + +stats = {} + + +class VMsModel(object): + def __init__(self, **kargs): + self.conn = kargs['conn'] + self.objstore = kargs['objstore'] + self.caps = CapabilitiesModel() + self.guests_stats_thread = BackgroundTask(GUESTS_STATS_INTERVAL, + self._update_guests_stats) + self.guests_stats_thread.start() + + def _update_guests_stats(self): + vm_list = self.get_list() + + for name in vm_list: + dom = VMModel.get_vm(name, self.conn) + vm_uuid = dom.UUIDString() + info = dom.info() + state = DOM_STATE_MAP[info[0]] + + if state != 'running': + stats[vm_uuid] = {} + continue + + if stats.get(vm_uuid, None) is None: + stats[vm_uuid] = {} + + timestamp = time.time() + prevStats = stats.get(vm_uuid, {}) + seconds = timestamp - prevStats.get('timestamp', 0) + stats[vm_uuid].update({'timestamp': timestamp}) + + self._get_percentage_cpu_usage(vm_uuid, info, seconds) + self._get_network_io_rate(vm_uuid, dom, seconds) + self._get_disk_io_rate(vm_uuid, dom, seconds) + + def _get_percentage_cpu_usage(self, vm_uuid, info, seconds): + prevCpuTime = stats[vm_uuid].get('cputime', 0) + + cpus = info[3] + cpuTime = info[4] - prevCpuTime + + base = (((cpuTime) * 100.0) / (seconds * 1000.0 * 1000.0 * 1000.0)) + percentage = max(0.0, min(100.0, base / cpus)) + + stats[vm_uuid].update({'cputime': info[4], 'cpu': percentage}) + + def _get_network_io_rate(self, vm_uuid, dom, seconds): + prevNetRxKB = stats[vm_uuid].get('netRxKB', 0) + prevNetTxKB = stats[vm_uuid].get('netTxKB', 0) + currentMaxNetRate = stats[vm_uuid].get('max_net_io', 100) + + rx_bytes = 0 + tx_bytes = 0 + + tree = ElementTree.fromstring(dom.XMLDesc(0)) + for target in tree.findall('devices/interface/target'): + dev = target.get('dev') + io = dom.interfaceStats(dev) + rx_bytes += io[0] + tx_bytes += io[4] + + netRxKB = float(rx_bytes) / 1000 + netTxKB = float(tx_bytes) / 1000 + + rx_stats = (netRxKB - prevNetRxKB) / seconds + tx_stats = (netTxKB - prevNetTxKB) / seconds + + rate = rx_stats + tx_stats + max_net_io = round(max(currentMaxNetRate, int(rate)), 1) + + stats[vm_uuid].update({'net_io': rate, 'max_net_io': max_net_io, + 'netRxKB': netRxKB, 'netTxKB': netTxKB}) + + def _get_disk_io_rate(self, vm_uuid, dom, seconds): + prevDiskRdKB = stats[vm_uuid].get('diskRdKB', 0) + prevDiskWrKB = stats[vm_uuid].get('diskWrKB', 0) + currentMaxDiskRate = stats[vm_uuid].get('max_disk_io', 100) + + rd_bytes = 0 + wr_bytes = 0 + + tree = ElementTree.fromstring(dom.XMLDesc(0)) + for target in tree.findall("devices/disk/target"): + dev = target.get("dev") + io = dom.blockStats(dev) + rd_bytes += io[1] + wr_bytes += io[3] + + diskRdKB = float(rd_bytes) / 1024 + diskWrKB = float(wr_bytes) / 1024 + + rd_stats = (diskRdKB - prevDiskRdKB) / seconds + wr_stats = (diskWrKB - prevDiskWrKB) / seconds + + rate = rd_stats + wr_stats + max_disk_io = round(max(currentMaxDiskRate, int(rate)), 1) + + stats[vm_uuid].update({'disk_io': rate, + 'max_disk_io': max_disk_io, + 'diskRdKB': diskRdKB, + 'diskWrKB': diskWrKB}) + + def create(self, params): + conn = self.conn.get() + t_name = template_name_from_uri(params['template']) + vm_uuid = str(uuid.uuid4()) + vm_list = self.get_list() + name = get_vm_name(params.get('name'), t_name, vm_list) + # incoming text, from js json, is unicode, do not need decode + if name in vm_list: + raise InvalidOperation("VM already exists") + + vm_overrides = dict() + pool_uri = params.get('storagepool') + if pool_uri: + vm_overrides['storagepool'] = pool_uri + t = TemplateModel.get_template(t_name, self.objstore, self.conn, + vm_overrides) + + if not self.caps.qemu_stream and t.info.get('iso_stream', False): + err = "Remote ISO image is not supported by this server." + raise InvalidOperation(err) + + t.validate() + vol_list = t.fork_vm_storage(vm_uuid) + + # Store the icon for displaying later + icon = t.info.get('icon') + if icon: + with self.objstore as session: + session.store('vm', vm_uuid, {'icon': icon}) + + libvirt_stream = False + if len(self.caps.libvirt_stream_protocols) == 0: + libvirt_stream = True + + graphics = params.get('graphics') + xml = t.to_vm_xml(name, vm_uuid, + libvirt_stream=libvirt_stream, + qemu_stream_dns=self.caps.qemu_stream_dns, + graphics=graphics) + + try: + conn.defineXML(xml.encode('utf-8')) + except libvirt.libvirtError as e: + for v in vol_list: + vol = conn.storageVolLookupByPath(v['path']) + vol.delete(0) + raise OperationFailed(e.get_error_message()) + + return name + + def get_list(self): + conn = self.conn.get() + ids = conn.listDomainsID() + names = map(lambda x: conn.lookupByID(x).name(), ids) + names += conn.listDefinedDomains() + names = map(lambda x: x.decode('utf-8'), names) + return sorted(names, key=unicode.lower) + + +class VMModel(object): + def __init__(self, **kargs): + self.conn = kargs['conn'] + self.objstore = kargs['objstore'] + self.vms = VMsModel(**kargs) + self.vmscreenshot = VMScreenshotModel(**kargs) + + def update(self, name, params): + dom = self.get_vm(name, self.conn) + dom = self._static_vm_update(dom, params) + self._live_vm_update(dom, params) + return dom.name() + + def _static_vm_update(self, dom, params): + state = DOM_STATE_MAP[dom.info()[0]] + + old_xml = new_xml = dom.XMLDesc(0) + + for key, val in params.items(): + if key in VM_STATIC_UPDATE_PARAMS: + xpath = VM_STATIC_UPDATE_PARAMS[key] + new_xml = xmlutils.xml_item_update(new_xml, xpath, val) + + try: + if 'name' in params: + if state == 'running': + err = "VM name only can be updated when vm is powered off." + raise InvalidParameter(err) + else: + dom.undefine() + conn = self.conn.get() + dom = conn.defineXML(new_xml) + except libvirt.libvirtError as e: + dom = conn.defineXML(old_xml) + raise OperationFailed(e.get_error_message()) + return dom + + def _live_vm_update(self, dom, params): + pass + + def lookup(self, name): + dom = self.get_vm(name, self.conn) + info = dom.info() + state = DOM_STATE_MAP[info[0]] + screenshot = None + graphics = self._vm_get_graphics(name) + graphics_type, graphics_listen, graphics_port = graphics + graphics_port = graphics_port if state == 'running' else None + try: + if state == 'running': + screenshot = self.vmscreenshot.lookup(name) + elif state == 'shutoff': + # reset vm stats when it is powered off to avoid sending + # incorrect (old) data + stats[dom.UUIDString()] = {} + except NotFoundError: + pass + + with self.objstore as session: + try: + extra_info = session.get('vm', dom.UUIDString()) + except NotFoundError: + extra_info = {} + icon = extra_info.get('icon') + + vm_stats = stats.get(dom.UUIDString(), {}) + res = {} + res['cpu_utilization'] = vm_stats.get('cpu', 0) + res['net_throughput'] = vm_stats.get('net_io', 0) + res['net_throughput_peak'] = vm_stats.get('max_net_io', 100) + res['io_throughput'] = vm_stats.get('disk_io', 0) + res['io_throughput_peak'] = vm_stats.get('max_disk_io', 100) + + return {'state': state, + 'stats': str(res), + 'uuid': dom.UUIDString(), + 'memory': info[2] >> 10, + 'cpus': info[3], + 'screenshot': screenshot, + 'icon': icon, + 'graphics': {"type": graphics_type, + "listen": graphics_listen, + "port": graphics_port} + } + + def _vm_get_disk_paths(self, dom): + xml = dom.XMLDesc(0) + xpath = "/domain/devices/disk[@device='disk']/source/@file" + return xmlutils.xpath_get_text(xml, xpath) + + def _vm_exists(self, name): + try: + self.get_vm(name, self.conn) + return True + except NotFoundError: + return False + except Exception, e: + err = "Unable to retrieve VM '%s': %s" + raise OperationFailed(err % (name, e.message)) + + @staticmethod + def get_vm(name, conn): + conn = conn.get() + try: + # outgoing text to libvirt, encode('utf-8') + return conn.lookupByName(name.encode("utf-8")) + except libvirt.libvirtError as e: + if e.get_error_code() == libvirt.VIR_ERR_NO_DOMAIN: + raise NotFoundError("Virtual Machine '%s' not found" % name) + else: + raise + + def delete(self, name): + if self._vm_exists(name): + conn = self.conn.get() + dom = self.get_vm(name, self.conn) + self._vmscreenshot_delete(dom.UUIDString()) + paths = self._vm_get_disk_paths(dom) + info = self.lookup(name) + + if info['state'] == 'running': + self.stop(name) + + dom.undefine() + + for path in paths: + vol = conn.storageVolLookupByPath(path) + vol.delete(0) + + with self.objstore as session: + session.delete('vm', dom.UUIDString(), ignore_missing=True) + + vnc.remove_proxy_token(name) + + def start(self, name): + dom = self.get_vm(name, self.conn) + dom.create() + + def stop(self, name): + if self._vm_exists(name): + dom = self.get_vm(name, self.conn) + dom.destroy() + + def _vm_get_graphics(self, name): + dom = self.get_vm(name, self.conn) + xml = dom.XMLDesc(0) + expr = "/domain/devices/graphics/@type" + res = xmlutils.xpath_get_text(xml, expr) + graphics_type = res[0] if res else None + expr = "/domain/devices/graphics/@listen" + res = xmlutils.xpath_get_text(xml, expr) + graphics_listen = res[0] if res else None + graphics_port = None + if graphics_type: + expr = "/domain/devices/graphics[@type='%s']/@port" % graphics_type + res = xmlutils.xpath_get_text(xml, expr) + graphics_port = int(res[0]) if res else None + return graphics_type, graphics_listen, graphics_port + + def connect(self, name): + graphics = self._vm_get_graphics(name) + graphics_type, graphics_listen, graphics_port = graphics + if graphics_port is not None: + vnc.add_proxy_token(name, graphics_port) + else: + raise OperationFailed("Only able to connect to running vm's vnc " + "graphics.") + + def _vmscreenshot_delete(self, vm_uuid): + screenshot = VMScreenshotModel.get_screenshot(vm_uuid, self.objstore, + self.conn) + screenshot.delete() + with self.objstore as session: + session.delete('screenshot', vm_uuid) + + +class VMScreenshotModel(object): + def __init__(self, **kargs): + self.objstore = kargs['objstore'] + self.conn = kargs['conn'] + + def lookup(self, name): + dom = VMModel.get_vm(name, self.conn) + d_info = dom.info() + vm_uuid = dom.UUIDString() + if DOM_STATE_MAP[d_info[0]] != 'running': + raise NotFoundError('No screenshot for stopped vm') + + screenshot = self.get_screenshot(vm_uuid, self.objstore, self.conn) + img_path = screenshot.lookup() + # screenshot info changed after scratch generation + with self.objstore as session: + session.store('screenshot', vm_uuid, screenshot.info) + return img_path + + @staticmethod + def get_screenshot(vm_uuid, objstore, conn): + with objstore as session: + try: + params = session.get('screenshot', vm_uuid) + except NotFoundError: + params = {'uuid': vm_uuid} + session.store('screenshot', vm_uuid, params) + return LibvirtVMScreenshot(params, conn) + + +class LibvirtVMScreenshot(VMScreenshot): + def __init__(self, vm_uuid, conn): + VMScreenshot.__init__(self, vm_uuid) + self.conn = conn + + def _generate_scratch(self, thumbnail): + def handler(stream, buf, opaque): + fd = opaque + os.write(fd, buf) + + fd = os.open(thumbnail, os.O_WRONLY | os.O_TRUNC | os.O_CREAT, 0644) + try: + conn = self.conn.get() + dom = conn.lookupByUUIDString(self.vm_uuid) + vm_name = dom.name() + stream = conn.newStream(0) + dom.screenshot(stream, 0, 0) + stream.recvAll(handler, fd) + except libvirt.libvirtError: + try: + stream.abort() + except: + pass + raise NotFoundError("Screenshot not supported for %s" % vm_name) + else: + stream.finish() + finally: + os.close(fd) diff --git a/src/kimchi/utils.py b/src/kimchi/utils.py index 5345d28..854f187 100644 --- a/src/kimchi/utils.py +++ b/src/kimchi/utils.py @@ -47,6 +47,10 @@ def _uri_to_name(collection, uri): return m.group(1) +def template_name_from_uri(uri): + return _uri_to_name('templates', uri) + + def pool_name_from_uri(uri): return _uri_to_name('storagepools', uri) -- 1.7.10.4

From: Aline Manera <alinefm@br.ibm.com> The model implementation for vm interface and its sub-resources were added to model_/vmifaces.py Signed-off-by: Aline Manera <alinefm@br.ibm.com> --- src/kimchi/model_/vmifaces.py | 135 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 src/kimchi/model_/vmifaces.py diff --git a/src/kimchi/model_/vmifaces.py b/src/kimchi/model_/vmifaces.py new file mode 100644 index 0000000..4ec0c7b --- /dev/null +++ b/src/kimchi/model_/vmifaces.py @@ -0,0 +1,135 @@ +# +# Project Kimchi +# +# Copyright IBM, Corp. 2013 +# +# Authors: +# Aline Manera <alinefm@linux.vnet.ibm.com> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +import random + +import libvirt +from lxml import etree, objectify +from lxml.builder import E + +from kimchi.exception import InvalidOperation, InvalidParameter, NotFoundError +from kimchi.model_.vms import DOM_STATE_MAP, VMModel + + +class VMIfacesModel(object): + def __init__(self, **kargs): + self.conn = kargs['conn'] + + def get_list(self, vm): + macs = [] + for iface in self.get_vmifaces(vm, self.conn): + macs.append(iface.mac.get('address')) + return macs + + def create(self, vm, params): + def randomMAC(): + mac = [0x52, 0x54, 0x00, + random.randint(0x00, 0x7f), + random.randint(0x00, 0xff), + random.randint(0x00, 0xff)] + return ':'.join(map(lambda x: "%02x" % x, mac)) + + conn = self.conn.get() + networks = conn.listNetworks() + conn.listDefinedNetworks() + + if params["type"] == "network" and params["network"] not in networks: + raise InvalidParameter("%s is not an available network" % + params["network"]) + + dom = VMModel.get_vm(vm, self.conn) + if DOM_STATE_MAP[dom.info()[0]] != "shutoff": + raise InvalidOperation("do not support hot plugging attach " + "guest interface") + + macs = (iface.mac.get('address') + for iface in self.get_vmifaces(vm, self.conn)) + + mac = randomMAC() + while True: + if mac not in macs: + break + mac = randomMAC() + + children = [E.mac(address=mac)] + ("network" in params.keys() and + children.append(E.source(network=params['network']))) + ("model" in params.keys() and + children.append(E.model(type=params['model']))) + attrib = {"type": params["type"]} + + xml = etree.tostring(E.interface(*children, **attrib)) + + dom.attachDeviceFlags(xml, libvirt.VIR_DOMAIN_AFFECT_CURRENT) + + return mac + + @staticmethod + def get_vmifaces(vm, conn): + dom = VMModel.get_vm(vm, conn) + xml = dom.XMLDesc(0) + root = objectify.fromstring(xml) + + return root.devices.findall("interface") + + +class VMIfaceModel(object): + def __init__(self, **kargs): + self.conn = kargs['conn'] + + def _get_vmiface(self, vm, mac): + ifaces = VMIfacesModel.get_vmifaces(vm, self.conn) + + for iface in ifaces: + if iface.mac.get('address') == mac: + return iface + return None + + def lookup(self, vm, mac): + info = {} + + iface = self._get_vmiface(vm, mac) + if iface is None: + raise NotFoundError('iface: "%s"' % mac) + + info['type'] = iface.attrib['type'] + info['mac'] = iface.mac.get('address') + if iface.find("model") is not None: + info['model'] = iface.model.get('type') + if info['type'] == 'network': + info['network'] = iface.source.get('network') + if info['type'] == 'bridge': + info['bridge'] = iface.source.get('bridge') + + return info + + def delete(self, vm, mac): + dom = VMModel.get_vm(vm, self.conn) + iface = self._get_vmiface(vm, mac) + + if DOM_STATE_MAP[dom.info()[0]] != "shutoff": + raise InvalidOperation("do not support hot plugging detach " + "guest interface") + if iface is None: + raise NotFoundError('iface: "%s"' % mac) + + dom.detachDeviceFlags(etree.tostring(iface), + libvirt.VIR_DOMAIN_AFFECT_CURRENT) -- 1.7.10.4

From: Aline Manera <alinefm@br.ibm.com> The model implementation for config and its sub-resources were added to model_/host.py Signed-off-by: Aline Manera <alinefm@br.ibm.com> --- src/kimchi/model_/host.py | 201 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100644 src/kimchi/model_/host.py diff --git a/src/kimchi/model_/host.py b/src/kimchi/model_/host.py new file mode 100644 index 0000000..d5f92ef --- /dev/null +++ b/src/kimchi/model_/host.py @@ -0,0 +1,201 @@ +# +# Project Kimchi +# +# Copyright IBM, Corp. 2013 +# +# Authors: +# Aline Manera <alinefm@linux.vnet.ibm.com> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +import os +import time +import platform +from collections import defaultdict + +import psutil +from cherrypy.process.plugins import BackgroundTask + +from kimchi import disks +from kimchi import netinfo +from kimchi.basemodel import Singleton +from kimchi.exception import NotFoundError, OperationFailed +from kimchi.model_.vms import DOM_STATE_MAP +from kimchi.utils import kimchi_log + + +HOST_STATS_INTERVAL = 1 + + +class HostModel(object): + def __init__(self, **kargs): + self.host_info = self._get_host_info() + + def _get_host_info(self): + res = {} + with open('/proc/cpuinfo') as f: + for line in f.xreadlines(): + if "model name" in line: + res['cpu'] = line.split(':')[1].strip() + break + + res['memory'] = psutil.TOTAL_PHYMEM + # 'fedora' '17' 'Beefy Miracle' + distro, version, codename = platform.linux_distribution() + res['os_distro'] = distro + res['os_version'] = version + res['os_codename'] = unicode(codename, "utf-8") + + return res + + def lookup(self, *name): + return self.host_info + + def shutdown(self, args=None): + # Check for running vms before shutdown + running_vms = self.vms_get_list_by_state('running') + if len(running_vms) > 0: + raise OperationFailed("Shutdown not allowed: VMs are running!") + kimchi_log.info('Host is going to shutdown.') + os.system('shutdown -h now') + + def reboot(self, args=None): + # Find running VMs + running_vms = self._get_vms_list_by_state('running') + if len(running_vms) > 0: + raise OperationFailed("Reboot not allowed: VMs are running!") + kimchi_log.info('Host is going to reboot.') + os.system('reboot') + + def _get_vms_list_by_state(self, state): + ret_list = [] + for name in self.vms_get_list(): + info = self._get_vm(name).info() + if (DOM_STATE_MAP[info[0]]) == state: + ret_list.append(name) + return ret_list + + +class HostStatsModel(object): + __metaclass__ = Singleton + + def __init__(self, **kargs): + self.host_stats = defaultdict(int) + self.host_stats_thread = BackgroundTask(HOST_STATS_INTERVAL, + self._update_host_stats) + self.host_stats_thread.start() + + def lookup(self, *name): + return {'cpu_utilization': self.host_stats['cpu_utilization'], + 'memory': self.host_stats.get('memory'), + 'disk_read_rate': self.host_stats['disk_read_rate'], + 'disk_write_rate': self.host_stats['disk_write_rate'], + 'net_recv_rate': self.host_stats['net_recv_rate'], + 'net_sent_rate': self.host_stats['net_sent_rate']} + + def _update_host_stats(self): + preTimeStamp = self.host_stats['timestamp'] + timestamp = time.time() + # FIXME when we upgrade psutil, we can get uptime by psutil.uptime + # we get uptime by float(open("/proc/uptime").readline().split()[0]) + # and calculate the first io_rate after the OS started. + seconds = (timestamp - preTimeStamp if preTimeStamp else + float(open("/proc/uptime").readline().split()[0])) + + self.host_stats['timestamp'] = timestamp + self._get_host_disk_io_rate(seconds) + self._get_host_network_io_rate(seconds) + + self._get_percentage_host_cpu_usage() + self._get_host_memory_stats() + + def _get_percentage_host_cpu_usage(self): + # This is cpu usage producer. This producer will calculate the usage + # at an interval of HOST_STATS_INTERVAL. + # The psutil.cpu_percent works as non blocking. + # psutil.cpu_percent maintains a cpu time sample. + # It will update the cpu time sample when it is called. + # So only this producer can call psutil.cpu_percent in kimchi. + self.host_stats['cpu_utilization'] = psutil.cpu_percent(None) + + def _get_host_memory_stats(self): + virt_mem = psutil.virtual_memory() + # available: + # the actual amount of available memory that can be given + # instantly to processes that request more memory in bytes; this + # is calculated by summing different memory values depending on + # the platform (e.g. free + buffers + cached on Linux) + memory_stats = {'total': virt_mem.total, + 'free': virt_mem.free, + 'cached': virt_mem.cached, + 'buffers': virt_mem.buffers, + 'avail': virt_mem.available} + self.host_stats['memory'] = memory_stats + + def _get_host_disk_io_rate(self, seconds): + prev_read_bytes = self.host_stats['disk_read_bytes'] + prev_write_bytes = self.host_stats['disk_write_bytes'] + + disk_io = psutil.disk_io_counters(False) + read_bytes = disk_io.read_bytes + write_bytes = disk_io.write_bytes + + rd_rate = int(float(read_bytes - prev_read_bytes) / seconds + 0.5) + wr_rate = int(float(write_bytes - prev_write_bytes) / seconds + 0.5) + + self.host_stats.update({'disk_read_rate': rd_rate, + 'disk_write_rate': wr_rate, + 'disk_read_bytes': read_bytes, + 'disk_write_bytes': write_bytes}) + + def _get_host_network_io_rate(self, seconds): + prev_recv_bytes = self.host_stats['net_recv_bytes'] + prev_sent_bytes = self.host_stats['net_sent_bytes'] + + net_ios = psutil.network_io_counters(True) + recv_bytes = 0 + sent_bytes = 0 + for key in set(netinfo.nics() + + netinfo.wlans()) & set(net_ios.iterkeys()): + recv_bytes = recv_bytes + net_ios[key].bytes_recv + sent_bytes = sent_bytes + net_ios[key].bytes_sent + + rx_rate = int(float(recv_bytes - prev_recv_bytes) / seconds + 0.5) + tx_rate = int(float(sent_bytes - prev_sent_bytes) / seconds + 0.5) + + self.host_stats.update({'net_recv_rate': rx_rate, + 'net_sent_rate': tx_rate, + 'net_recv_bytes': recv_bytes, + 'net_sent_bytes': sent_bytes}) + + +class PartitionsModel(object): + def __init__(self, **kargs): + pass + + def get_list(self): + result = disks.get_partitions_names() + return result + + +class PartitionModel(object): + def __init__(self, **kargs): + pass + + def lookup(self, name): + if name not in disks.get_partitions_names(): + raise NotFoundError("Partition %s not found in the host" + % name) + return disks.get_partition_details(name) -- 1.7.10.4

From: Aline Manera <alinefm@br.ibm.com> Also update control/config.py to use capabilities_lookup() instead of using the model method directly And correct import in control/storagepools.py Signed-off-by: Aline Manera <alinefm@br.ibm.com> --- src/kimchi/control/config.py | 6 +----- src/kimchi/control/storagepools.py | 2 +- src/kimchi/server.py | 2 +- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/kimchi/control/config.py b/src/kimchi/control/config.py index cc41e8a..2c3b264 100644 --- a/src/kimchi/control/config.py +++ b/src/kimchi/control/config.py @@ -52,11 +52,7 @@ class Capabilities(Resource): @property def data(self): - caps = ['libvirt_stream_protocols', 'qemu_stream', - 'screenshot', 'system_report_tool'] - ret = dict([(x, None) for x in caps]) - ret.update(self.model.get_capabilities()) - return ret + return self.info class Distros(Collection): diff --git a/src/kimchi/control/storagepools.py b/src/kimchi/control/storagepools.py index 3b8ef79..8a6823c 100644 --- a/src/kimchi/control/storagepools.py +++ b/src/kimchi/control/storagepools.py @@ -30,7 +30,7 @@ from kimchi.control.base import Collection, Resource from kimchi.control.storagevolumes import IsoVolumes, StorageVolumes from kimchi.control.utils import get_class_name, model_fn, parse_request from kimchi.control.utils import validate_params -from kimchi.model import ISO_POOL_NAME +from kimchi.model_.storagepools import ISO_POOL_NAME from kimchi.control.utils import UrlSubNode diff --git a/src/kimchi/server.py b/src/kimchi/server.py index 2a9f53f..9cc4c3c 100644 --- a/src/kimchi/server.py +++ b/src/kimchi/server.py @@ -30,7 +30,7 @@ import sslcert from kimchi import auth from kimchi import config -from kimchi import model +from kimchi.model_ import model from kimchi import mockmodel from kimchi import vnc from kimchi.control import sub_nodes -- 1.7.10.4

From: Aline Manera <alinefm@br.ibm.com> Also correct import references to model as model.py will be delete Signed-off-by: Aline Manera <alinefm@br.ibm.com> --- tests/test_model.py | 91 +++++++++++++++++++++++---------------------- tests/test_storagepool.py | 4 +- tests/utils.py | 4 +- 3 files changed, 50 insertions(+), 49 deletions(-) diff --git a/tests/test_model.py b/tests/test_model.py index 547ad6b..74f3dd9 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -32,14 +32,15 @@ import uuid import iso_gen -import kimchi.model import kimchi.objectstore import utils from kimchi import netinfo from kimchi.exception import InvalidOperation, InvalidParameter from kimchi.exception import NotFoundError, OperationFailed from kimchi.iscsi import TargetClient +from kimchi.model_ import model from kimchi.rollbackcontext import RollbackContext +from kimchi.utils import add_task class ModelTests(unittest.TestCase): @@ -50,7 +51,7 @@ class ModelTests(unittest.TestCase): os.unlink(self.tmp_store) def test_vm_info(self): - inst = kimchi.model.Model('test:///default', self.tmp_store) + inst = model.Model('test:///default', self.tmp_store) vms = inst.vms_get_list() self.assertEquals(1, len(vms)) self.assertEquals('test', vms[0]) @@ -71,7 +72,7 @@ class ModelTests(unittest.TestCase): @unittest.skipUnless(utils.running_as_root(), 'Must be run as root') def test_vm_lifecycle(self): - inst = kimchi.model.Model(objstore_loc=self.tmp_store) + inst = model.Model(objstore_loc=self.tmp_store) with RollbackContext() as rollback: params = {'name': 'test', 'disks': []} @@ -96,7 +97,7 @@ class ModelTests(unittest.TestCase): @unittest.skipUnless(utils.running_as_root(), 'Must be run as root') def test_vm_graphics(self): - inst = kimchi.model.Model(objstore_loc=self.tmp_store) + inst = model.Model(objstore_loc=self.tmp_store) params = {'name': 'test', 'disks': []} inst.templates_create(params) with RollbackContext() as rollback: @@ -122,7 +123,7 @@ class ModelTests(unittest.TestCase): @unittest.skipUnless(utils.running_as_root(), 'Must be run as root') def test_vm_ifaces(self): - inst = kimchi.model.Model(objstore_loc=self.tmp_store) + inst = model.Model(objstore_loc=self.tmp_store) with RollbackContext() as rollback: params = {'name': 'test', 'disks': []} inst.templates_create(params) @@ -163,7 +164,7 @@ class ModelTests(unittest.TestCase): @unittest.skipUnless(utils.running_as_root(), 'Must be run as root') def test_vm_storage_provisioning(self): - inst = kimchi.model.Model(objstore_loc=self.tmp_store) + inst = model.Model(objstore_loc=self.tmp_store) with RollbackContext() as rollback: params = {'name': 'test', 'disks': [{'size': 1}]} @@ -181,7 +182,7 @@ class ModelTests(unittest.TestCase): @unittest.skipUnless(utils.running_as_root(), 'Must be run as root') def test_storagepool(self): - inst = kimchi.model.Model('qemu:///system', self.tmp_store) + inst = model.Model('qemu:///system', self.tmp_store) poolDefs = [ {'type': 'dir', @@ -245,7 +246,7 @@ class ModelTests(unittest.TestCase): @unittest.skipUnless(utils.running_as_root(), 'Must be run as root') def test_storagevolume(self): - inst = kimchi.model.Model('qemu:///system', self.tmp_store) + inst = model.Model('qemu:///system', self.tmp_store) with RollbackContext() as rollback: path = '/tmp/kimchi-images' @@ -301,7 +302,7 @@ class ModelTests(unittest.TestCase): @unittest.skipUnless(utils.running_as_root(), 'Must be run as root') def test_template_storage_customise(self): - inst = kimchi.model.Model(objstore_loc=self.tmp_store) + inst = model.Model(objstore_loc=self.tmp_store) with RollbackContext() as rollback: path = '/tmp/kimchi-images' @@ -339,8 +340,8 @@ class ModelTests(unittest.TestCase): @unittest.skipUnless(utils.running_as_root(), 'Must be run as root') def test_template_create(self): - inst = kimchi.model.Model('test:///default', - objstore_loc=self.tmp_store) + inst = model.Model('test:///default', + objstore_loc=self.tmp_store) # Test non-exist path raises InvalidParameter params = {'name': 'test', 'cdrom': '/non-exsitent.iso'} @@ -380,8 +381,8 @@ class ModelTests(unittest.TestCase): @unittest.skipUnless(utils.running_as_root(), 'Must be run as root') def test_template_update(self): - inst = kimchi.model.Model('qemu:///system', - objstore_loc=self.tmp_store) + inst = model.Model('qemu:///system', + objstore_loc=self.tmp_store) with RollbackContext() as rollback: net_name = 'test-network' net_args = {'name': net_name, @@ -419,8 +420,8 @@ class ModelTests(unittest.TestCase): 'new-test', params) def test_vm_edit(self): - inst = kimchi.model.Model('qemu:///system', - objstore_loc=self.tmp_store) + inst = model.Model('qemu:///system', + objstore_loc=self.tmp_store) orig_params = {'name': 'test', 'memory': '1024', 'cpus': '1'} inst.templates_create(orig_params) @@ -456,7 +457,7 @@ class ModelTests(unittest.TestCase): @unittest.skipUnless(utils.running_as_root(), 'Must be run as root') def test_network(self): - inst = kimchi.model.Model('qemu:///system', self.tmp_store) + inst = model.Model('qemu:///system', self.tmp_store) with RollbackContext() as rollback: name = 'test-network' @@ -494,7 +495,7 @@ class ModelTests(unittest.TestCase): ret = inst.vms_get_list() self.assertEquals('test', ret[0]) - inst = kimchi.model.Model('test:///default', self.tmp_store) + inst = model.Model('test:///default', self.tmp_store) threads = [] for i in xrange(100): t = threading.Thread(target=worker) @@ -559,8 +560,8 @@ class ModelTests(unittest.TestCase): self.assertEquals(10, len(store._connections.keys())) def test_get_interfaces(self): - inst = kimchi.model.Model('test:///default', - objstore_loc=self.tmp_store) + inst = model.Model('test:///default', + objstore_loc=self.tmp_store) expected_ifaces = netinfo.all_favored_interfaces() ifaces = inst.interfaces_get_list() self.assertEquals(len(expected_ifaces), len(ifaces)) @@ -589,17 +590,17 @@ class ModelTests(unittest.TestCase): except: cb("Exception raised", False) - inst = kimchi.model.Model('test:///default', - objstore_loc=self.tmp_store) - taskid = inst.add_task('', quick_op, 'Hello') + inst = model.Model('test:///default', + objstore_loc=self.tmp_store) + taskid = add_task('', quick_op, inst.objstore, 'Hello') self._wait_task(inst, taskid) self.assertEquals(1, taskid) self.assertEquals('finished', inst.task_lookup(taskid)['status']) self.assertEquals('Hello', inst.task_lookup(taskid)['message']) - taskid = inst.add_task('', long_op, - {'delay': 3, 'result': False, - 'message': 'It was not meant to be'}) + taskid = add_task('', long_op, inst.objstore, + {'delay': 3, 'result': False, + 'message': 'It was not meant to be'}) self.assertEquals(2, taskid) self.assertEquals('running', inst.task_lookup(taskid)['status']) self.assertEquals('OK', inst.task_lookup(taskid)['message']) @@ -607,7 +608,7 @@ class ModelTests(unittest.TestCase): self.assertEquals('failed', inst.task_lookup(taskid)['status']) self.assertEquals('It was not meant to be', inst.task_lookup(taskid)['message']) - taskid = inst.add_task('', abnormal_op, {}) + taskid = add_task('', abnormal_op, inst.objstore, {}) self._wait_task(inst, taskid) self.assertEquals('Exception raised', inst.task_lookup(taskid)['message']) @@ -615,7 +616,7 @@ class ModelTests(unittest.TestCase): @unittest.skipUnless(utils.running_as_root(), 'Must be run as root') def test_delete_running_vm(self): - inst = kimchi.model.Model(objstore_loc=self.tmp_store) + inst = model.Model(objstore_loc=self.tmp_store) with RollbackContext() as rollback: params = {'name': u'test', 'disks': []} @@ -636,7 +637,7 @@ class ModelTests(unittest.TestCase): @unittest.skipUnless(utils.running_as_root(), 'Must be run as root') def test_vm_list_sorted(self): - inst = kimchi.model.Model(objstore_loc=self.tmp_store) + inst = model.Model(objstore_loc=self.tmp_store) with RollbackContext() as rollback: params = {'name': 'test', 'disks': []} @@ -652,8 +653,8 @@ class ModelTests(unittest.TestCase): self.assertEquals(vms, sorted(vms, key=unicode.lower)) def test_use_test_host(self): - inst = kimchi.model.Model('test:///default', - objstore_loc=self.tmp_store) + inst = model.Model('test:///default', + objstore_loc=self.tmp_store) with RollbackContext() as rollback: params = {'name': 'test', 'disks': [], @@ -675,10 +676,10 @@ class ModelTests(unittest.TestCase): @unittest.skipUnless(utils.running_as_root(), 'Must be run as root') def test_debug_reports(self): - inst = kimchi.model.Model('test:///default', - objstore_loc=self.tmp_store) + inst = model.Model('test:///default', + objstore_loc=self.tmp_store) - if not inst.get_capabilities()['system_report_tool']: + if not inst.capabilities_lookup()['system_report_tool']: raise unittest.SkipTest("Without debug report tool") try: @@ -722,10 +723,11 @@ class ModelTests(unittest.TestCase): time.sleep(1) def test_get_distros(self): - inst = kimchi.model.Model('test:///default', - objstore_loc=self.tmp_store) - distros = inst._get_distros() - for distro in distros.values(): + inst = model.Model('test:///default', + objstore_loc=self.tmp_store) + distros = inst.distros_get_list() + for d in distros: + distro = inst.distro_lookup(d) self.assertIn('name', distro) self.assertIn('os_distro', distro) self.assertIn('os_version', distro) @@ -733,8 +735,8 @@ class ModelTests(unittest.TestCase): @unittest.skipUnless(utils.running_as_root(), 'Must be run as root') def test_get_hostinfo(self): - inst = kimchi.model.Model('qemu:///system', - objstore_loc=self.tmp_store) + inst = model.Model('qemu:///system', + objstore_loc=self.tmp_store) info = inst.host_lookup() distro, version, codename = platform.linux_distribution() self.assertIn('cpu', info) @@ -744,10 +746,9 @@ class ModelTests(unittest.TestCase): self.assertEquals(psutil.TOTAL_PHYMEM, info['memory']) def test_get_hoststats(self): - inst = kimchi.model.Model('test:///default', - objstore_loc=self.tmp_store) - # HOST_STATS_INTERVAL is 1 seconds - time.sleep(kimchi.model.HOST_STATS_INTERVAL + 0.5) + inst = model.Model('test:///default', + objstore_loc=self.tmp_store) + time.sleep(1.5) stats = inst.hoststats_lookup() cpu_utilization = stats['cpu_utilization'] # cpu_utilization is set int 0, after first stats sample @@ -771,8 +772,8 @@ class ModelTests(unittest.TestCase): @unittest.skipUnless(utils.running_as_root(), 'Must be run as root') def test_deep_scan(self): - inst = kimchi.model.Model('qemu:///system', - objstore_loc=self.tmp_store) + inst = model.Model('qemu:///system', + objstore_loc=self.tmp_store) with RollbackContext() as rollback: path = '/tmp/kimchi-images/tmpdir' if not os.path.exists(path): diff --git a/tests/test_storagepool.py b/tests/test_storagepool.py index 8341537..869b608 100644 --- a/tests/test_storagepool.py +++ b/tests/test_storagepool.py @@ -24,7 +24,7 @@ import libxml2 import unittest -import kimchi.model +from kimchi.model_.libvirtstoragepool import StoragePoolDef from kimchi.rollbackcontext import RollbackContext @@ -144,7 +144,7 @@ class storagepoolTests(unittest.TestCase): """}] for poolDef in poolDefs: - defObj = kimchi.model.StoragePoolDef.create(poolDef['def']) + defObj = StoragePoolDef.create(poolDef['def']) xmlStr = defObj.xml with RollbackContext() as rollback: t1 = libxml2.readDoc(xmlStr, URL='', encoding='UTF-8', diff --git a/tests/utils.py b/tests/utils.py index 79fc2e2..40dfae2 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -36,7 +36,7 @@ from lxml import etree import kimchi.server -import kimchi.model +from kimchi.exception import OperationFailed _ports = {} @@ -156,7 +156,7 @@ def patch_auth(): try: return fake_user[username] == password except KeyError: - raise kimchi.model.OperationFailed('Bad login') + raise OperationFailed('Bad login') import kimchi.auth kimchi.auth.authenticate = _authenticate -- 1.7.10.4

From: Aline Manera <alinefm@br.ibm.com> There are some methods from model used in mockmodel.py Adjust imports to get the methods from new model implementation. Signed-off-by: Aline Manera <alinefm@br.ibm.com> --- src/kimchi/mockmodel.py | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/kimchi/mockmodel.py b/src/kimchi/mockmodel.py index 916020a..79f6f79 100644 --- a/src/kimchi/mockmodel.py +++ b/src/kimchi/mockmodel.py @@ -28,7 +28,6 @@ import ipaddr import os import psutil import random -import subprocess import time import uuid @@ -41,16 +40,18 @@ except ImportError: import ImageDraw -import kimchi.model + from kimchi import config -from kimchi import network as knetwork from kimchi.asynctask import AsyncTask from kimchi.distroloader import DistroLoader from kimchi.exception import InvalidOperation, InvalidParameter from kimchi.exception import MissingParameter, NotFoundError, OperationFailed +from kimchi.model_.storagepools import ISO_POOL_NAME, STORAGE_SOURCES +from kimchi.model_.utils import get_vm_name +from kimchi.model_.vms import VM_STATIC_UPDATE_PARAMS from kimchi.objectstore import ObjectStore from kimchi.screenshot import VMScreenshot -from kimchi.utils import is_digit +from kimchi.utils import template_name_from_uri, pool_name_from_uri from kimchi.vmtemplate import VMTemplate @@ -60,7 +61,7 @@ class MockModel(object): self.objstore = ObjectStore(objstore_loc) self.distros = self._get_distros() - def get_capabilities(self): + def capabilities_lookup(self, *ident): return {'libvirt_stream_protocols': ['http', 'https', 'ftp', 'ftps', 'tftp'], 'qemu_stream': True, 'screenshot': True, @@ -88,7 +89,7 @@ class MockModel(object): self._mock_vms[dom.name] = dom for key, val in params.items(): - if key in kimchi.model.VM_STATIC_UPDATE_PARAMS and key in dom.info: + if key in VM_STATIC_UPDATE_PARAMS and key in dom.info: dom.info[key] = val def _live_vm_update(self, dom, params): @@ -128,9 +129,9 @@ class MockModel(object): pass def vms_create(self, params): - t_name = kimchi.model.template_name_from_uri(params['template']) - name = kimchi.model.get_vm_name(params.get('name'), t_name, - self._mock_vms.keys()) + t_name = template_name_from_uri(params['template']) + name = get_vm_name(params.get('name'), t_name, + self._mock_vms.keys()) if name in self._mock_vms: raise InvalidOperation("VM already exists") @@ -209,7 +210,7 @@ class MockModel(object): new_storagepool = new_t.get(u'storagepool', '') try: - self._get_storagepool(kimchi.model.pool_name_from_uri(new_storagepool)) + self._get_storagepool(pool_name_from_uri(new_storagepool)) except Exception as e: raise InvalidParameter("Storagepool specified is not valid: %s." % e.message) @@ -303,7 +304,7 @@ class MockModel(object): pool.info['autostart'] = False except KeyError, item: raise MissingParameter(item) - if name in self._mock_storagepools or name in (kimchi.model.ISO_POOL_NAME,): + if name in self._mock_storagepools or name in (ISO_POOL_NAME,): raise InvalidOperation("StoragePool already exists") self._mock_storagepools[name] = pool return name @@ -410,7 +411,7 @@ class MockModel(object): def storageservers_get_list(self, _target_type=None): # FIXME: When added new storage server support, this needs to be updated - target_type = kimchi.model.STORAGE_SOURCES.keys() \ + target_type = STORAGE_SOURCES.keys() \ if not _target_type else [_target_type] pools = self.storagepools_get_list() server_list = [] @@ -665,7 +666,7 @@ class MockVMTemplate(VMTemplate): def _storage_validate(self): pool_uri = self.info['storagepool'] - pool_name = kimchi.model.pool_name_from_uri(pool_uri) + pool_name = pool_name_from_uri(pool_uri) try: pool = self.model._get_storagepool(pool_name) except NotFoundError: -- 1.7.10.4

From: Aline Manera <alinefm@br.ibm.com> model.py can be deleted as all resources have their own model implementations Also rename model_ to model Signed-off-by: Aline Manera <alinefm@br.ibm.com> --- Makefile.am | 2 +- src/kimchi/control/storagepools.py | 2 +- src/kimchi/mockmodel.py | 6 +- src/kimchi/model.py | 1708 ------------------------------- src/kimchi/model/__init__.py | 21 + src/kimchi/model/config.py | 87 ++ src/kimchi/model/debugreports.py | 167 +++ src/kimchi/model/host.py | 201 ++++ src/kimchi/model/interfaces.py | 46 + src/kimchi/model/libvirtconnection.py | 122 +++ src/kimchi/model/libvirtstoragepool.py | 257 +++++ src/kimchi/model/model.py | 53 + src/kimchi/model/networks.py | 265 +++++ src/kimchi/model/plugins.py | 31 + src/kimchi/model/storagepools.py | 246 +++++ src/kimchi/model/storageservers.py | 78 ++ src/kimchi/model/storagetargets.py | 86 ++ src/kimchi/model/storagevolumes.py | 176 ++++ src/kimchi/model/tasks.py | 39 + src/kimchi/model/templates.py | 172 ++++ src/kimchi/model/utils.py | 33 + src/kimchi/model/vmifaces.py | 135 +++ src/kimchi/model/vms.py | 450 ++++++++ src/kimchi/model_/__init__.py | 21 - src/kimchi/model_/config.py | 87 -- src/kimchi/model_/debugreports.py | 167 --- src/kimchi/model_/host.py | 201 ---- src/kimchi/model_/interfaces.py | 46 - src/kimchi/model_/libvirtconnection.py | 122 --- src/kimchi/model_/libvirtstoragepool.py | 257 ----- src/kimchi/model_/model.py | 53 - src/kimchi/model_/networks.py | 265 ----- src/kimchi/model_/plugins.py | 31 - src/kimchi/model_/storagepools.py | 246 ----- src/kimchi/model_/storageservers.py | 78 -- src/kimchi/model_/storagetargets.py | 86 -- src/kimchi/model_/storagevolumes.py | 176 ---- src/kimchi/model_/tasks.py | 39 - src/kimchi/model_/templates.py | 172 ---- src/kimchi/model_/utils.py | 33 - src/kimchi/model_/vmifaces.py | 135 --- src/kimchi/model_/vms.py | 450 -------- src/kimchi/server.py | 2 +- tests/test_model.py | 2 +- tests/test_storagepool.py | 2 +- 45 files changed, 2673 insertions(+), 4381 deletions(-) delete mode 100644 src/kimchi/model.py create mode 100644 src/kimchi/model/__init__.py create mode 100644 src/kimchi/model/config.py create mode 100644 src/kimchi/model/debugreports.py create mode 100644 src/kimchi/model/host.py create mode 100644 src/kimchi/model/interfaces.py create mode 100644 src/kimchi/model/libvirtconnection.py create mode 100644 src/kimchi/model/libvirtstoragepool.py create mode 100644 src/kimchi/model/model.py create mode 100644 src/kimchi/model/networks.py create mode 100644 src/kimchi/model/plugins.py create mode 100644 src/kimchi/model/storagepools.py create mode 100644 src/kimchi/model/storageservers.py create mode 100644 src/kimchi/model/storagetargets.py create mode 100644 src/kimchi/model/storagevolumes.py create mode 100644 src/kimchi/model/tasks.py create mode 100644 src/kimchi/model/templates.py create mode 100644 src/kimchi/model/utils.py create mode 100644 src/kimchi/model/vmifaces.py create mode 100644 src/kimchi/model/vms.py delete mode 100644 src/kimchi/model_/__init__.py delete mode 100644 src/kimchi/model_/config.py delete mode 100644 src/kimchi/model_/debugreports.py delete mode 100644 src/kimchi/model_/host.py delete mode 100644 src/kimchi/model_/interfaces.py delete mode 100644 src/kimchi/model_/libvirtconnection.py delete mode 100644 src/kimchi/model_/libvirtstoragepool.py delete mode 100644 src/kimchi/model_/model.py delete mode 100644 src/kimchi/model_/networks.py delete mode 100644 src/kimchi/model_/plugins.py delete mode 100644 src/kimchi/model_/storagepools.py delete mode 100644 src/kimchi/model_/storageservers.py delete mode 100644 src/kimchi/model_/storagetargets.py delete mode 100644 src/kimchi/model_/storagevolumes.py delete mode 100644 src/kimchi/model_/tasks.py delete mode 100644 src/kimchi/model_/templates.py delete mode 100644 src/kimchi/model_/utils.py delete mode 100644 src/kimchi/model_/vmifaces.py delete mode 100644 src/kimchi/model_/vms.py diff --git a/Makefile.am b/Makefile.am index 1ffe6db..c092f42 100644 --- a/Makefile.am +++ b/Makefile.am @@ -54,7 +54,7 @@ PEP8_WHITELIST = \ src/kimchi/iscsi.py \ src/kimchi/isoinfo.py \ src/kimchi/kvmusertests.py \ - src/kimchi/model_/*.py \ + src/kimchi/model/*.py \ src/kimchi/rollbackcontext.py \ src/kimchi/root.py \ src/kimchi/server.py \ diff --git a/src/kimchi/control/storagepools.py b/src/kimchi/control/storagepools.py index 8a6823c..fc276cd 100644 --- a/src/kimchi/control/storagepools.py +++ b/src/kimchi/control/storagepools.py @@ -30,7 +30,7 @@ from kimchi.control.base import Collection, Resource from kimchi.control.storagevolumes import IsoVolumes, StorageVolumes from kimchi.control.utils import get_class_name, model_fn, parse_request from kimchi.control.utils import validate_params -from kimchi.model_.storagepools import ISO_POOL_NAME +from kimchi.model.storagepools import ISO_POOL_NAME from kimchi.control.utils import UrlSubNode diff --git a/src/kimchi/mockmodel.py b/src/kimchi/mockmodel.py index 79f6f79..e59d1f5 100644 --- a/src/kimchi/mockmodel.py +++ b/src/kimchi/mockmodel.py @@ -46,9 +46,9 @@ from kimchi.asynctask import AsyncTask from kimchi.distroloader import DistroLoader from kimchi.exception import InvalidOperation, InvalidParameter from kimchi.exception import MissingParameter, NotFoundError, OperationFailed -from kimchi.model_.storagepools import ISO_POOL_NAME, STORAGE_SOURCES -from kimchi.model_.utils import get_vm_name -from kimchi.model_.vms import VM_STATIC_UPDATE_PARAMS +from kimchi.model.storagepools import ISO_POOL_NAME, STORAGE_SOURCES +from kimchi.model.utils import get_vm_name +from kimchi.model.vms import VM_STATIC_UPDATE_PARAMS from kimchi.objectstore import ObjectStore from kimchi.screenshot import VMScreenshot from kimchi.utils import template_name_from_uri, pool_name_from_uri diff --git a/src/kimchi/model.py b/src/kimchi/model.py deleted file mode 100644 index ddb2d95..0000000 --- a/src/kimchi/model.py +++ /dev/null @@ -1,1708 +0,0 @@ -# -# Project Kimchi -# -# Copyright IBM, Corp. 2013 -# -# Authors: -# Adam Litke <agl@linux.vnet.ibm.com> -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library 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 -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - -import cherrypy -import copy -import disks -import fnmatch -import functools -import glob -import ipaddr -import json -import libvirt -import logging -import lxml.etree as ET -import os -import platform -import psutil -import random -import re -import shutil -import subprocess -import sys -import threading -import time -import uuid - - -from cherrypy.process.plugins import BackgroundTask -from cherrypy.process.plugins import SimplePlugin -from collections import defaultdict -from lxml import etree, objectify -from lxml.builder import E -from xml.etree import ElementTree - - -try: - from collections import OrderedDict -except ImportError: - from ordereddict import OrderedDict - - -from kimchi import config -from kimchi import netinfo -from kimchi import network as knetwork -from kimchi import networkxml -from kimchi import vnc -from kimchi import xmlutils -from kimchi.asynctask import AsyncTask -from kimchi.distroloader import DistroLoader -from kimchi.exception import InvalidOperation, InvalidParameter, IsoFormatError -from kimchi.exception import MissingParameter, NotFoundError, OperationFailed -from kimchi.featuretests import FeatureTests -from kimchi.isoinfo import IsoImage -from kimchi.model_.libvirtconnection import LibvirtConnection -from kimchi.model_.libvirtstoragepool import StoragePoolDef -from kimchi.objectstore import ObjectStore -from kimchi.scan import Scanner -from kimchi.screenshot import VMScreenshot -from kimchi.utils import get_enabled_plugins, is_digit, kimchi_log -from kimchi.utils import patch_find_nfs_target -from kimchi.vmtemplate import VMTemplate - - -ISO_POOL_NAME = u'kimchi_isos' -GUESTS_STATS_INTERVAL = 5 -HOST_STATS_INTERVAL = 1 -VM_STATIC_UPDATE_PARAMS = {'name': './name'} -VM_LIVE_UPDATE_PARAMS = {} -STORAGE_SOURCES = {'netfs': {'addr': '/pool/source/host/@name', - 'path': '/pool/source/dir/@path'}} - - -def _uri_to_name(collection, uri): - expr = '/%s/(.*?)/?$' % collection - m = re.match(expr, uri) - if not m: - raise InvalidParameter(uri) - return m.group(1) - -def template_name_from_uri(uri): - return _uri_to_name('templates', uri) - -def pool_name_from_uri(uri): - return _uri_to_name('storagepools', uri) - -def get_vm_name(vm_name, t_name, name_list): - if vm_name: - return vm_name - for i in xrange(1, 1000): - vm_name = "%s-vm-%i" % (t_name, i) - if vm_name not in name_list: - return vm_name - raise OperationFailed("Unable to choose a VM name") - -class Model(object): - dom_state_map = {0: 'nostate', - 1: 'running', - 2: 'blocked', - 3: 'paused', - 4: 'shutdown', - 5: 'shutoff', - 6: 'crashed'} - - pool_state_map = {0: 'inactive', - 1: 'initializing', - 2: 'active', - 3: 'degraded', - 4: 'inaccessible'} - - volume_type_map = {0: 'file', - 1: 'block', - 2: 'directory', - 3: 'network'} - - def __init__(self, libvirt_uri=None, objstore_loc=None): - self.libvirt_uri = libvirt_uri or 'qemu:///system' - self.conn = LibvirtConnection(self.libvirt_uri) - self.objstore = ObjectStore(objstore_loc) - self.next_taskid = 1 - self.stats = {} - self.host_stats = defaultdict(int) - self.host_info = {} - self.qemu_stream = False - self.qemu_stream_dns = False - self.libvirt_stream_protocols = [] - # Subscribe function to set host capabilities to be run when cherrypy - # server is up - # It is needed because some features tests depends on the server - cherrypy.engine.subscribe('start', self._set_capabilities) - self.scanner = Scanner(self._clean_scan) - self.scanner.delete() - self.guests_stats_thread = BackgroundTask(GUESTS_STATS_INTERVAL, - self._update_guests_stats) - self.host_stats_thread = BackgroundTask(HOST_STATS_INTERVAL, - self._update_host_stats) - self.guests_stats_thread.start() - self.host_stats_thread.start() - - # Please add new possible debug report command here - # and implement the report generating function - # based on the new report command - self.report_tools = ({'cmd': 'sosreport --help', 'fn': self._sosreport_generate}, - {'cmd': 'supportconfig -h', 'fn':None}, - {'cmd': 'linuxexplorers --help', 'fn':None}) - - self.distros = self._get_distros() - if 'qemu:///' in self.libvirt_uri: - self.host_info = self._get_host_info() - self._default_pool_check() - self._default_network_check() - - def _default_network_check(self): - conn = self.conn.get() - xml = """ - <network> - <name>default</name> - <forward mode='nat'/> - <bridge name='virbr0' stp='on' delay='0' /> - <ip address='192.168.122.1' netmask='255.255.255.0'> - <dhcp> - <range start='192.168.122.2' end='192.168.122.254' /> - </dhcp> - </ip> - </network> - """ - try: - net = conn.networkLookupByName("default") - except libvirt.libvirtError: - try: - net = conn.networkDefineXML(xml) - except libvirt.libvirtError, e: - cherrypy.log.error( - "Fatal: Cannot create default network because of %s, exit kimchid" % e.message, - severity=logging.ERROR) - sys.exit(1) - - if net.isActive() == 0: - try: - net.create() - except libvirt.libvirtError, e: - cherrypy.log.error( - "Fatal: Cannot activate default network because of %s, exit kimchid" % e.message, - severity=logging.ERROR) - sys.exit(1) - - def _default_pool_check(self): - default_pool = {'name': 'default', - 'path': '/var/lib/libvirt/images', - 'type': 'dir'} - try: - self.storagepools_create(default_pool) - except InvalidOperation: - # ignore error when pool existed - pass - except OperationFailed as e: - # path used by other pool or other reasons of failure, exit - cherrypy.log.error( - "Fatal: Cannot create default pool because of %s, exit kimchid" % e.message, - severity=logging.ERROR) - sys.exit(1) - - if self.storagepool_lookup('default')['state'] == 'inactive': - try: - self.storagepool_activate('default') - except OperationFailed: - cherrypy.log.error( - "Fatal: Default pool cannot be activated, exit kimchid", - severity=logging.ERROR) - sys.exit(1) - - def _set_capabilities(self): - kimchi_log.info("*** Running feature tests ***") - self.qemu_stream = FeatureTests.qemu_supports_iso_stream() - self.qemu_stream_dns = FeatureTests.qemu_iso_stream_dns() - self.nfs_target_probe = FeatureTests.libvirt_support_nfs_probe() - - self.libvirt_stream_protocols = [] - for p in ['http', 'https', 'ftp', 'ftps', 'tftp']: - if FeatureTests.libvirt_supports_iso_stream(p): - self.libvirt_stream_protocols.append(p) - - kimchi_log.info("*** Feature tests completed ***") - _set_capabilities.priority = 90 - - def get_capabilities(self): - report_tool = self._get_system_report_tool() - - return {'libvirt_stream_protocols': self.libvirt_stream_protocols, - 'qemu_stream': self.qemu_stream, - 'screenshot': VMScreenshot.get_stream_test_result(), - 'system_report_tool': bool(report_tool)} - - def _update_guests_stats(self): - vm_list = self.vms_get_list() - - for name in vm_list: - dom = self._get_vm(name) - vm_uuid = dom.UUIDString() - info = dom.info() - state = Model.dom_state_map[info[0]] - - if state != 'running': - self.stats[vm_uuid] = {} - continue - - if self.stats.get(vm_uuid, None) is None: - self.stats[vm_uuid] = {} - - timestamp = time.time() - prevStats = self.stats.get(vm_uuid, {}) - seconds = timestamp - prevStats.get('timestamp', 0) - self.stats[vm_uuid].update({'timestamp': timestamp}) - - self._get_percentage_cpu_usage(vm_uuid, info, seconds) - self._get_network_io_rate(vm_uuid, dom, seconds) - self._get_disk_io_rate(vm_uuid, dom, seconds) - - def _get_host_info(self): - res = {} - with open('/proc/cpuinfo') as f: - for line in f.xreadlines(): - if "model name" in line: - res['cpu'] = line.split(':')[1].strip() - break - - res['memory'] = psutil.TOTAL_PHYMEM - # 'fedora' '17' 'Beefy Miracle' - distro, version, codename = platform.linux_distribution() - res['os_distro'] = distro - res['os_version'] = version - res['os_codename'] = unicode(codename,"utf-8") - - return res - - def _get_percentage_cpu_usage(self, vm_uuid, info, seconds): - prevCpuTime = self.stats[vm_uuid].get('cputime', 0) - - cpus = info[3] - cpuTime = info[4] - prevCpuTime - - base = (((cpuTime) * 100.0) / (seconds * 1000.0 * 1000.0 * 1000.0)) - percentage = max(0.0, min(100.0, base / cpus)) - - self.stats[vm_uuid].update({'cputime': info[4], 'cpu': percentage}) - - def _get_network_io_rate(self, vm_uuid, dom, seconds): - prevNetRxKB = self.stats[vm_uuid].get('netRxKB', 0) - prevNetTxKB = self.stats[vm_uuid].get('netTxKB', 0) - currentMaxNetRate = self.stats[vm_uuid].get('max_net_io', 100) - - rx_bytes = 0 - tx_bytes = 0 - - tree = ElementTree.fromstring(dom.XMLDesc(0)) - for target in tree.findall('devices/interface/target'): - dev = target.get('dev') - io = dom.interfaceStats(dev) - rx_bytes += io[0] - tx_bytes += io[4] - - netRxKB = float(rx_bytes) / 1000 - netTxKB = float(tx_bytes) / 1000 - - rx_stats = (netRxKB - prevNetRxKB) / seconds - tx_stats = (netTxKB - prevNetTxKB) / seconds - - rate = rx_stats + tx_stats - max_net_io = round(max(currentMaxNetRate, int(rate)), 1) - - self.stats[vm_uuid].update({'net_io': rate, 'max_net_io': max_net_io, - 'netRxKB': netRxKB, 'netTxKB': netTxKB}) - - def _get_disk_io_rate(self, vm_uuid, dom, seconds): - prevDiskRdKB = self.stats[vm_uuid].get('diskRdKB', 0) - prevDiskWrKB = self.stats[vm_uuid].get('diskWrKB', 0) - currentMaxDiskRate = self.stats[vm_uuid].get('max_disk_io', 100) - - rd_bytes = 0 - wr_bytes = 0 - - tree = ElementTree.fromstring(dom.XMLDesc(0)) - for target in tree.findall("devices/disk/target"): - dev = target.get("dev") - io = dom.blockStats(dev) - rd_bytes += io[1] - wr_bytes += io[3] - - diskRdKB = float(rd_bytes) / 1024 - diskWrKB = float(wr_bytes) / 1024 - - rd_stats = (diskRdKB - prevDiskRdKB) / seconds - wr_stats = (diskWrKB - prevDiskWrKB) / seconds - - rate = rd_stats + wr_stats - max_disk_io = round(max(currentMaxDiskRate, int(rate)), 1) - - self.stats[vm_uuid].update({'disk_io': rate, 'max_disk_io': max_disk_io, - 'diskRdKB': diskRdKB, 'diskWrKB': diskWrKB}) - - def debugreport_lookup(self, name): - path = config.get_debugreports_path() - file_pattern = os.path.join(path, name) - file_pattern = file_pattern + '.*' - try: - file_target = glob.glob(file_pattern)[0] - except IndexError: - raise NotFoundError('no such report') - - ctime = os.stat(file_target).st_ctime - ctime = time.strftime("%Y-%m-%d-%H:%M:%S", time.localtime(ctime)) - file_target = os.path.split(file_target)[-1] - file_target = os.path.join("/data/debugreports", file_target) - return {'file': file_target, - 'ctime': ctime} - - def debugreportcontent_lookup(self, name): - return self.debugreport_lookup(name) - - def debugreport_delete(self, name): - path = config.get_debugreports_path() - file_pattern = os.path.join(path, name + '.*') - try: - file_target = glob.glob(file_pattern)[0] - except IndexError: - raise NotFoundError('no such report') - - os.remove(file_target) - - def debugreports_create(self, params): - ident = params['name'] - taskid = self._gen_debugreport_file(ident) - return self.task_lookup(taskid) - - def debugreports_get_list(self): - path = config.get_debugreports_path() - file_pattern = os.path.join(path, '*.*') - file_lists = glob.glob(file_pattern) - file_lists = [os.path.split(file)[1] for file in file_lists] - name_lists = [file.split('.', 1)[0] for file in file_lists] - - return name_lists - - def _update_host_stats(self): - preTimeStamp = self.host_stats['timestamp'] - timestamp = time.time() - # FIXME when we upgrade psutil, we can get uptime by psutil.uptime - # we get uptime by float(open("/proc/uptime").readline().split()[0]) - # and calculate the first io_rate after the OS started. - seconds = (timestamp - preTimeStamp if preTimeStamp else - float(open("/proc/uptime").readline().split()[0])) - - self.host_stats['timestamp'] = timestamp - self._get_host_disk_io_rate(seconds) - self._get_host_network_io_rate(seconds) - - self._get_percentage_host_cpu_usage() - self._get_host_memory_stats() - - def _get_percentage_host_cpu_usage(self): - # This is cpu usage producer. This producer will calculate the usage - # at an interval of HOST_STATS_INTERVAL. - # The psutil.cpu_percent works as non blocking. - # psutil.cpu_percent maintains a cpu time sample. - # It will update the cpu time sample when it is called. - # So only this producer can call psutil.cpu_percent in kimchi. - self.host_stats['cpu_utilization'] = psutil.cpu_percent(None) - - def _get_host_memory_stats(self): - virt_mem = psutil.virtual_memory() - # available: - # the actual amount of available memory that can be given - # instantly to processes that request more memory in bytes; this - # is calculated by summing different memory values depending on - # the platform (e.g. free + buffers + cached on Linux) - memory_stats = {'total': virt_mem.total, - 'free': virt_mem.free, - 'cached': virt_mem.cached, - 'buffers': virt_mem.buffers, - 'avail': virt_mem.available} - self.host_stats['memory'] = memory_stats - - def _get_host_disk_io_rate(self, seconds): - prev_read_bytes = self.host_stats['disk_read_bytes'] - prev_write_bytes = self.host_stats['disk_write_bytes'] - - disk_io = psutil.disk_io_counters(False) - read_bytes = disk_io.read_bytes - write_bytes = disk_io.write_bytes - - rd_rate = int(float(read_bytes - prev_read_bytes) / seconds + 0.5) - wr_rate = int(float(write_bytes - prev_write_bytes) / seconds + 0.5) - - self.host_stats.update({'disk_read_rate': rd_rate, - 'disk_write_rate': wr_rate, - 'disk_read_bytes': read_bytes, - 'disk_write_bytes': write_bytes}) - - def _get_host_network_io_rate(self, seconds): - prev_recv_bytes = self.host_stats['net_recv_bytes'] - prev_sent_bytes = self.host_stats['net_sent_bytes'] - - net_ios = psutil.network_io_counters(True) - recv_bytes = 0 - sent_bytes = 0 - for key in set(netinfo.nics() + - netinfo.wlans()) & set(net_ios.iterkeys()): - recv_bytes = recv_bytes + net_ios[key].bytes_recv - sent_bytes = sent_bytes + net_ios[key].bytes_sent - - rx_rate = int(float(recv_bytes - prev_recv_bytes) / seconds + 0.5) - tx_rate = int(float(sent_bytes - prev_sent_bytes) / seconds + 0.5) - - self.host_stats.update({'net_recv_rate': rx_rate, - 'net_sent_rate': tx_rate, - 'net_recv_bytes': recv_bytes, - 'net_sent_bytes': sent_bytes}) - - def _static_vm_update(self, dom, params): - state = Model.dom_state_map[dom.info()[0]] - - old_xml = new_xml = dom.XMLDesc(0) - - for key, val in params.items(): - if key in VM_STATIC_UPDATE_PARAMS: - new_xml = xmlutils.xml_item_update(new_xml, VM_STATIC_UPDATE_PARAMS[key], val) - - try: - if 'name' in params: - if state == 'running': - raise InvalidParameter("vm name can just updated when vm shutoff") - else: - dom.undefine() - conn = self.conn.get() - dom = conn.defineXML(new_xml) - except libvirt.libvirtError as e: - dom = conn.defineXML(old_xml) - raise OperationFailed(e.get_error_message()) - return dom - - def _live_vm_update(self, dom, params): - pass - - def vm_update(self, name, params): - dom = self._get_vm(name) - dom = self._static_vm_update(dom, params) - self._live_vm_update(dom, params) - return dom.name() - - def vm_lookup(self, name): - dom = self._get_vm(name) - info = dom.info() - state = Model.dom_state_map[info[0]] - screenshot = None - graphics_type, graphics_listen, graphics_port = self._vm_get_graphics(name) - graphics_port = graphics_port if state == 'running' else None - try: - if state == 'running': - screenshot = self.vmscreenshot_lookup(name) - elif state == 'shutoff': - # reset vm stats when it is powered off to avoid sending - # incorrect (old) data - self.stats[dom.UUIDString()] = {} - except NotFoundError: - pass - - with self.objstore as session: - try: - extra_info = session.get('vm', dom.UUIDString()) - except NotFoundError: - extra_info = {} - icon = extra_info.get('icon') - - vm_stats = self.stats.get(dom.UUIDString(), {}) - stats = {} - stats['cpu_utilization'] = vm_stats.get('cpu', 0) - stats['net_throughput'] = vm_stats.get('net_io', 0) - stats['net_throughput_peak'] = vm_stats.get('max_net_io', 100) - stats['io_throughput'] = vm_stats.get('disk_io', 0) - stats['io_throughput_peak'] = vm_stats.get('max_disk_io', 100) - - return {'state': state, - 'stats': str(stats), - 'uuid': dom.UUIDString(), - 'memory': info[2] >> 10, - 'cpus': info[3], - 'screenshot': screenshot, - 'icon': icon, - 'graphics': {"type": graphics_type, - "listen": graphics_listen, - "port": graphics_port} - } - - def _vm_get_disk_paths(self, dom): - xml = dom.XMLDesc(0) - xpath = "/domain/devices/disk[@device='disk']/source/@file" - return xmlutils.xpath_get_text(xml, xpath) - - def _vm_get_networks(self, dom): - xml = dom.XMLDesc(0) - xpath = "/domain/devices/interface[@type='network']/source/@network" - return xmlutils.xpath_get_text(xml, xpath) - - def vm_delete(self, name): - if self._vm_exists(name): - conn = self.conn.get() - dom = self._get_vm(name) - self._vmscreenshot_delete(dom.UUIDString()) - paths = self._vm_get_disk_paths(dom) - info = self.vm_lookup(name) - - if info['state'] == 'running': - self.vm_stop(name) - - dom.undefine() - - for path in paths: - vol = conn.storageVolLookupByPath(path) - vol.delete(0) - - with self.objstore as session: - session.delete('vm', dom.UUIDString(), ignore_missing=True) - - vnc.remove_proxy_token(name) - - def vm_start(self, name): - dom = self._get_vm(name) - dom.create() - - def vm_stop(self, name): - if self._vm_exists(name): - dom = self._get_vm(name) - dom.destroy() - - def _vm_get_graphics(self, name): - dom = self._get_vm(name) - xml = dom.XMLDesc(0) - expr = "/domain/devices/graphics/@type" - res = xmlutils.xpath_get_text(xml, expr) - graphics_type = res[0] if res else None - expr = "/domain/devices/graphics/@listen" - res = xmlutils.xpath_get_text(xml, expr) - graphics_listen = res[0] if res else None - graphics_port = None - if graphics_type: - expr = "/domain/devices/graphics[@type='%s']/@port" % graphics_type - res = xmlutils.xpath_get_text(xml, expr) - graphics_port = int(res[0]) if res else None - return graphics_type, graphics_listen, graphics_port - - def vm_connect(self, name): - graphics_type, graphics_listen, graphics_port \ - = self._vm_get_graphics(name) - if graphics_port is not None: - vnc.add_proxy_token(name, graphics_port) - else: - raise OperationFailed("Only able to connect to running vm's vnc " - "graphics.") - - def vms_create(self, params): - conn = self.conn.get() - t_name = template_name_from_uri(params['template']) - vm_uuid = str(uuid.uuid4()) - vm_list = self.vms_get_list() - name = get_vm_name(params.get('name'), t_name, vm_list) - # incoming text, from js json, is unicode, do not need decode - if name in vm_list: - raise InvalidOperation("VM already exists") - - vm_overrides = dict() - pool_uri = params.get('storagepool') - if pool_uri: - vm_overrides['storagepool'] = pool_uri - t = self._get_template(t_name, vm_overrides) - - if not self.qemu_stream and t.info.get('iso_stream', False): - raise InvalidOperation("Remote ISO image is not supported by this server.") - - t.validate() - vol_list = t.fork_vm_storage(vm_uuid) - - # Store the icon for displaying later - icon = t.info.get('icon') - if icon: - with self.objstore as session: - session.store('vm', vm_uuid, {'icon': icon}) - - libvirt_stream = False if len(self.libvirt_stream_protocols) == 0 else True - graphics = params.get('graphics') - - xml = t.to_vm_xml(name, vm_uuid, - libvirt_stream=libvirt_stream, - qemu_stream_dns=self.qemu_stream_dns, - graphics=graphics) - try: - dom = conn.defineXML(xml.encode('utf-8')) - except libvirt.libvirtError as e: - for v in vol_list: - vol = conn.storageVolLookupByPath(v['path']) - vol.delete(0) - raise OperationFailed(e.get_error_message()) - return name - - def vms_get_list(self): - conn = self.conn.get() - ids = conn.listDomainsID() - names = map(lambda x: conn.lookupByID(x).name(), ids) - names += conn.listDefinedDomains() - names = map(lambda x: x.decode('utf-8'), names) - return sorted(names, key=unicode.lower) - - def vmscreenshot_lookup(self, name): - dom = self._get_vm(name) - d_info = dom.info() - vm_uuid = dom.UUIDString() - if Model.dom_state_map[d_info[0]] != 'running': - raise NotFoundError('No screenshot for stopped vm') - - screenshot = self._get_screenshot(vm_uuid) - img_path = screenshot.lookup() - # screenshot info changed after scratch generation - with self.objstore as session: - session.store('screenshot', vm_uuid, screenshot.info) - return img_path - - def _vmscreenshot_delete(self, vm_uuid): - screenshot = self._get_screenshot(vm_uuid) - screenshot.delete() - with self.objstore as session: - session.delete('screenshot', vm_uuid) - - def template_lookup(self, name): - t = self._get_template(name) - return t.info - - def template_delete(self, name): - with self.objstore as session: - session.delete('template', name) - - def templates_create(self, params): - name = params['name'] - for net_name in params.get(u'networks', []): - try: - self._get_network(net_name) - except NotFoundError: - raise InvalidParameter("Network '%s' specified by template " - "does not exist" % net_name) - - with self.objstore as session: - if name in session.get_list('template'): - raise InvalidOperation("Template already exists") - t = LibvirtVMTemplate(params, scan=True) - session.store('template', name, t.info) - return name - - def template_update(self, name, params): - old_t = self.template_lookup(name) - new_t = copy.copy(old_t) - - new_t.update(params) - ident = name - - new_storagepool = new_t.get(u'storagepool', '') - try: - self._get_storagepool(pool_name_from_uri(new_storagepool)) - except Exception as e: - raise InvalidParameter("Storagepool specified is not valid: %s." % e.message) - - for net_name in params.get(u'networks', []): - try: - self._get_network(net_name) - except NotFoundError: - raise InvalidParameter("Network '%s' specified by template " - "does not exist" % net_name) - - self.template_delete(name) - try: - ident = self.templates_create(new_t) - except: - ident = self.templates_create(old_t) - raise - return ident - - def templates_get_list(self): - with self.objstore as session: - return session.get_list('template') - - def interfaces_get_list(self): - return list(set(netinfo.all_favored_interfaces()) - - set(self._get_all_networks_interfaces())) - - def interface_lookup(self, name): - try: - return netinfo.get_interface_info(name) - except ValueError, e: - raise NotFoundError(e) - - def _get_network(self, name): - conn = self.conn.get() - try: - return conn.networkLookupByName(name) - except libvirt.libvirtError as e: - raise NotFoundError("Network '%s' not found: %s" % - (name, e.get_error_message())) - - def _get_network_from_xml(self, xml): - address = xmlutils.xpath_get_text(xml, "/network/ip/@address") - address = address and address[0] or '' - netmask = xmlutils.xpath_get_text(xml, "/network/ip/@netmask") - netmask = netmask and netmask[0] or '' - net = address and netmask and "/".join([address, netmask]) or '' - - dhcp_start = xmlutils.xpath_get_text(xml, - "/network/ip/dhcp/range/@start") - dhcp_start = dhcp_start and dhcp_start[0] or '' - dhcp_end = xmlutils.xpath_get_text(xml, "/network/ip/dhcp/range/@end") - dhcp_end = dhcp_end and dhcp_end[0] or '' - dhcp = {'start': dhcp_start, 'end': dhcp_end} - - forward_mode = xmlutils.xpath_get_text(xml, "/network/forward/@mode") - forward_mode = forward_mode and forward_mode[0] or '' - forward_if = xmlutils.xpath_get_text(xml, - "/network/forward/interface/@dev") - forward_pf = xmlutils.xpath_get_text(xml, "/network/forward/pf/@dev") - bridge = xmlutils.xpath_get_text(xml, "/network/bridge/@name") - bridge = bridge and bridge[0] or '' - return {'subnet': net, 'dhcp': dhcp, 'bridge': bridge, - 'forward': {'mode': forward_mode, - 'interface': forward_if, - 'pf': forward_pf}} - - def _get_all_networks_interfaces(self): - net_names = self.networks_get_list() - interfaces = [] - for name in net_names: - network = self._get_network(name) - xml = network.XMLDesc(0) - net_dict = self._get_network_from_xml(xml) - forward = net_dict['forward'] - (forward['mode'] == 'bridge' and forward['interface'] and - interfaces.append(forward['interface'][0]) is None or - interfaces.extend(forward['interface'] + forward['pf'])) - net_dict['bridge'] and interfaces.append(net_dict['bridge']) - return interfaces - - def _set_network_subnet(self, params): - netaddr = params.get('subnet', '') - net_addrs = [] - # lookup a free network address for nat and isolated automatically - if not netaddr: - for net_name in self.networks_get_list(): - network = self._get_network(net_name) - xml = network.XMLDesc(0) - subnet = self._get_network_from_xml(xml)['subnet'] - subnet and net_addrs.append(ipaddr.IPNetwork(subnet)) - netaddr = knetwork.get_one_free_network(net_addrs) - if not netaddr: - raise OperationFailed("can not find a free IP address " - "for network '%s'" % - params['name']) - try: - ip = ipaddr.IPNetwork(netaddr) - except ValueError as e: - raise InvalidParameter("%s" % e) - if ip.ip == ip.network: - ip.ip = ip.ip + 1 - dhcp_start = str(ip.ip + ip.numhosts / 2) - dhcp_end = str(ip.ip + ip.numhosts - 2) - params.update({'net': str(ip), - 'dhcp': {'range': {'start': dhcp_start, - 'end': dhcp_end}}}) - - def _set_network_bridge(self, params): - try: - iface = params['interface'] - if iface in self._get_all_networks_interfaces(): - raise InvalidParameter("interface '%s' already in use." % - iface) - except KeyError, e: - raise MissingParameter(e) - if netinfo.is_bridge(iface): - params['bridge'] = iface - elif netinfo.is_bare_nic(iface) or netinfo.is_bonding(iface): - if params.get('vlan_id') is None: - params['forward']['dev'] = iface - else: - params['bridge'] = \ - self._create_vlan_tagged_bridge(str(iface), - str(params['vlan_id'])) - else: - raise InvalidParameter("the interface should be bare nic, " - "bonding or bridge device.") - - def networks_create(self, params): - conn = self.conn.get() - name = params['name'] - if name in self.networks_get_list(): - raise InvalidOperation("Network %s already exists" % name) - - connection = params["connection"] - # set forward mode, isolated do not need forward - if connection != 'isolated': - params['forward'] = {'mode': connection} - - # set subnet, bridge network do not need subnet - if connection in ["nat", 'isolated']: - self._set_network_subnet(params) - - # only bridge network need bridge(linux bridge) or interface(macvtap) - if connection == 'bridge': - self._set_network_bridge(params) - - xml = networkxml.to_network_xml(**params) - - try: - network = conn.networkDefineXML(xml) - network.setAutostart(True) - except libvirt.libvirtError as e: - raise OperationFailed(e.get_error_message()) - - return name - - def networks_get_list(self): - conn = self.conn.get() - return sorted(conn.listNetworks() + conn.listDefinedNetworks()) - - def _get_vms_attach_to_a_network(self, network): - vms = [] - conn = self.conn.get() - for dom in conn.listAllDomains(0): - networks = self._vm_get_networks(dom) - if network in networks: - vms.append(dom.name()) - return vms - - def network_lookup(self, name): - network = self._get_network(name) - xml = network.XMLDesc(0) - net_dict = self._get_network_from_xml(xml) - subnet = net_dict['subnet'] - dhcp = net_dict['dhcp'] - forward = net_dict['forward'] - interface = net_dict['bridge'] - - connection = forward['mode'] or "isolated" - # FIXME, if we want to support other forward mode well. - if connection == 'bridge': - # macvtap bridge - interface = interface or forward['interface'][0] - # exposing the network on linux bridge or macvtap interface - interface_subnet = knetwork.get_dev_netaddr(interface) - subnet = subnet if subnet else interface_subnet - - # libvirt use format 192.168.0.1/24, standard should be 192.168.0.0/24 - # http://www.ovirt.org/File:Issue3.png - if subnet: - subnet = ipaddr.IPNetwork(subnet) - subnet = "%s/%s" % (subnet.network, subnet.prefixlen) - - return {'connection': connection, - 'interface': interface, - 'subnet': subnet, - 'dhcp': dhcp, - 'vms': self._get_vms_attach_to_a_network(name), - 'autostart': network.autostart() == 1, - 'state': network.isActive() and "active" or "inactive"} - - def network_activate(self, name): - network = self._get_network(name) - network.create() - - def network_deactivate(self, name): - network = self._get_network(name) - network.destroy() - - def network_delete(self, name): - network = self._get_network(name) - if network.isActive(): - raise InvalidOperation( - "Unable to delete the active network %s" % name) - self._remove_vlan_tagged_bridge(network) - network.undefine() - - def _get_vlan_tagged_bridge_name(self, interface, vlan_id): - return '-'.join(('kimchi', interface, vlan_id)) - - def _is_vlan_tagged_bridge(self, bridge): - return bridge.startswith('kimchi-') - - def _create_vlan_tagged_bridge(self, interface, vlan_id): - br_name = self._get_vlan_tagged_bridge_name(interface, vlan_id) - br_xml = networkxml.create_vlan_tagged_bridge_xml(br_name, interface, - vlan_id) - conn = self.conn.get() - conn.changeBegin() - try: - vlan_tagged_br = conn.interfaceDefineXML(br_xml) - vlan_tagged_br.create() - except libvirt.libvirtError as e: - conn.changeRollback() - raise OperationFailed(e.message) - else: - conn.changeCommit() - return br_name - - def _remove_vlan_tagged_bridge(self, network): - try: - bridge = network.bridgeName() - except libvirt.libvirtError: - pass - else: - if self._is_vlan_tagged_bridge(bridge): - conn = self.conn.get() - iface = conn.interfaceLookupByName(bridge) - if iface.isActive(): - iface.destroy() - iface.undefine() - - def vmifaces_create(self, vm, params): - def randomMAC(): - mac = [0x52, 0x54, 0x00, - random.randint(0x00, 0x7f), - random.randint(0x00, 0xff), - random.randint(0x00, 0xff)] - return ':'.join(map(lambda x: "%02x" % x, mac)) - - if (params["type"] == "network" and - params["network"] not in self.networks_get_list()): - raise InvalidParameter("%s is not an available network" % - params["network"]) - - dom = self._get_vm(vm) - if Model.dom_state_map[dom.info()[0]] != "shutoff": - raise InvalidOperation("do not support hot plugging attach " - "guest interface") - - macs = (iface.mac.get('address') - for iface in self._get_vmifaces(vm)) - - mac = randomMAC() - while True: - if mac not in macs: - break - mac = randomMAC() - - children = [E.mac(address=mac)] - ("network" in params.keys() and - children.append(E.source(network=params['network']))) - ("model" in params.keys() and - children.append(E.model(type=params['model']))) - attrib = {"type": params["type"]} - - xml = etree.tostring(E.interface(*children, **attrib)) - - dom.attachDeviceFlags(xml, libvirt.VIR_DOMAIN_AFFECT_CURRENT) - - return mac - - def _get_vmifaces(self, vm): - dom = self._get_vm(vm) - xml = dom.XMLDesc(0) - root = objectify.fromstring(xml) - - return root.devices.findall("interface") - - def _get_vmiface(self, vm, mac): - ifaces = self._get_vmifaces(vm) - - for iface in ifaces: - if iface.mac.get('address') == mac: - return iface - return None - - def vmifaces_get_list(self, vm): - return [iface.mac.get('address') for iface in self._get_vmifaces(vm)] - - def vmiface_lookup(self, vm, mac): - info = {} - - iface = self._get_vmiface(vm, mac) - if iface is None: - raise NotFoundError('iface: "%s"' % mac) - - info['type'] = iface.attrib['type'] - info['mac'] = iface.mac.get('address') - if iface.find("model") is not None: - info['model'] = iface.model.get('type') - if info['type'] == 'network': - info['network'] = iface.source.get('network') - if info['type'] == 'bridge': - info['bridge'] = iface.source.get('bridge') - - return info - - def vmiface_delete(self, vm, mac): - dom = self._get_vm(vm) - iface = self._get_vmiface(vm, mac) - - if Model.dom_state_map[dom.info()[0]] != "shutoff": - raise InvalidOperation("do not support hot plugging detach " - "guest interface") - if iface is None: - raise NotFoundError('iface: "%s"' % mac) - - dom.detachDeviceFlags(etree.tostring(iface), - libvirt.VIR_DOMAIN_AFFECT_CURRENT) - - def add_task(self, target_uri, fn, opaque=None): - id = self.next_taskid - self.next_taskid = self.next_taskid + 1 - - task = AsyncTask(id, target_uri, fn, self.objstore, opaque) - - return id - - def tasks_get_list(self): - with self.objstore as session: - return session.get_list('task') - - def task_lookup(self, id): - with self.objstore as session: - return session.get('task', str(id)) - - def _vm_exists(self, name): - try: - self._get_vm(name) - return True - except NotFoundError: - return False - except: - raise - - - def _get_vm(self, name): - conn = self.conn.get() - try: - # outgoing text to libvirt, encode('utf-8') - return conn.lookupByName(name.encode("utf-8")) - except libvirt.libvirtError as e: - if e.get_error_code() == libvirt.VIR_ERR_NO_DOMAIN: - raise NotFoundError("Virtual Machine '%s' not found" % name) - else: - raise - - def _get_template(self, name, overrides=None): - with self.objstore as session: - params = session.get('template', name) - if overrides: - params.update(overrides) - return LibvirtVMTemplate(params, False, self.conn) - - def isopool_lookup(self, name): - return {'state': 'active', - 'type': 'kimchi-iso'} - - def isovolumes_get_list(self): - iso_volumes = [] - pools = self.storagepools_get_list() - - for pool in pools: - try: - volumes = self.storagevolumes_get_list(pool) - except InvalidOperation: - # Skip inactive pools - continue - for volume in volumes: - res = self.storagevolume_lookup(pool, volume) - if res['format'] == 'iso': - res['name'] = '%s' % volume - iso_volumes.append(res) - return iso_volumes - - def _clean_scan(self, pool_name): - try: - self.storagepool_deactivate(pool_name) - with self.objstore as session: - session.delete('scanning', pool_name) - except Exception, e: - kimchi_log.debug("Exception %s occured when cleaning scan result" % e.message) - - def _do_deep_scan(self, params): - scan_params = dict(ignore_list=[]) - scan_params['scan_path'] = params['path'] - params['type'] = 'dir' - - for pool in self.storagepools_get_list(): - try: - res = self.storagepool_lookup(pool) - if res['state'] == 'active': - scan_params['ignore_list'].append(res['path']) - except Exception, e: - kimchi_log.debug("Exception %s occured when get ignore path" % e.message) - - params['path'] = scan_params['pool_path'] = self.scanner.scan_dir_prepare( - params['name']) - task_id = self.add_task('', self.scanner.start_scan, scan_params) - # Record scanning-task/storagepool mapping for future querying - with self.objstore as session: - session.store('scanning', params['name'], task_id) - return task_id - - def storagepools_create(self, params): - task_id = None - conn = self.conn.get() - try: - name = params['name'] - if name in (ISO_POOL_NAME, ): - raise InvalidOperation("StoragePool already exists") - - if params['type'] == 'kimchi-iso': - task_id = self._do_deep_scan(params) - poolDef = StoragePoolDef.create(params) - poolDef.prepare(conn) - xml = poolDef.xml - except KeyError, key: - raise MissingParameter(key) - - if name in self.storagepools_get_list(): - raise InvalidOperation( - "The name %s has been used by a pool" % name) - - try: - if task_id: - # Create transient pool for deep scan - conn.storagePoolCreateXML(xml, 0) - return name - - pool = conn.storagePoolDefineXML(xml, 0) - if params['type'] in ['logical', 'dir', 'netfs']: - pool.build(libvirt.VIR_STORAGE_POOL_BUILD_NEW) - # autostart dir and logical storage pool created from kimchi - pool.setAutostart(1) - else: - # disable autostart for others - pool.setAutostart(0) - except libvirt.libvirtError as e: - msg = "Problem creating Storage Pool: %s" - kimchi_log.error(msg, e) - raise OperationFailed(e.get_error_message()) - return name - - def _get_storage_source(self, pool_type, pool_xml): - source = {} - if pool_type not in STORAGE_SOURCES: - return source - - for key, val in STORAGE_SOURCES[pool_type].items(): - res = xmlutils.xpath_get_text(pool_xml, val) - source[key] = res[0] if len(res) == 1 else res - - return source - - def storagepool_lookup(self, name): - pool = self._get_storagepool(name) - info = pool.info() - nr_volumes = self._get_storagepool_vols_num(pool) - autostart = True if pool.autostart() else False - xml = pool.XMLDesc(0) - path = xmlutils.xpath_get_text(xml, "/pool/target/path")[0] - pool_type = xmlutils.xpath_get_text(xml, "/pool/@type")[0] - source = self._get_storage_source(pool_type, xml) - res = {'state': Model.pool_state_map[info[0]], - 'path': path, - 'source': source, - 'type': pool_type, - 'autostart': autostart, - 'capacity': info[1], - 'allocated': info[2], - 'available': info[3], - 'nr_volumes': nr_volumes} - - if not pool.isPersistent(): - # Deal with deep scan generated pool - try: - with self.objstore as session: - task_id = session.get('scanning', name) - res['task_id'] = str(task_id) - res['type'] = 'kimchi-iso' - except NotFoundError: - # User created normal pool - pass - return res - - def storagepool_update(self, name, params): - autostart = params['autostart'] - if autostart not in [True, False]: - raise InvalidOperation("Autostart flag must be true or false") - pool = self._get_storagepool(name) - if autostart: - pool.setAutostart(1) - else: - pool.setAutostart(0) - ident = pool.name() - return ident - - def storagepool_activate(self, name): - pool = self._get_storagepool(name) - try: - pool.create(0) - except libvirt.libvirtError as e: - raise OperationFailed(e.get_error_message()) - - def storagepool_deactivate(self, name): - pool = self._get_storagepool(name) - try: - pool.destroy() - except libvirt.libvirtError as e: - raise OperationFailed(e.get_error_message()) - - def _pool_refresh(self, pool): - try: - pool.refresh(0) - except libvirt.libvirtError as e: - raise OperationFailed(e.get_error_message()) - - def _get_storagepool_vols_num(self, pool): - try: - if pool.isActive(): - self._pool_refresh(pool) - return pool.numOfVolumes() - else: - return 0 - except libvirt.libvirtError as e: - raise OperationFailed(e.get_error_message()) - - def storagepool_delete(self, name): - pool = self._get_storagepool(name) - if pool.isActive(): - raise InvalidOperation( - "Unable to delete the active storagepool %s" % name) - try: - pool.undefine() - except libvirt.libvirtError as e: - raise OperationFailed(e.get_error_message()) - - def storagepools_get_list(self): - try: - conn = self.conn.get() - names = conn.listStoragePools() - names += conn.listDefinedStoragePools() - return sorted(names) - except libvirt.libvirtError as e: - raise OperationFailed(e.get_error_message()) - - def _get_storagepool(self, name): - conn = self.conn.get() - try: - return conn.storagePoolLookupByName(name) - except libvirt.libvirtError as e: - if e.get_error_code() == libvirt.VIR_ERR_NO_STORAGE_POOL: - raise NotFoundError("Storage Pool '%s' not found" % name) - else: - raise - - def storagevolumes_create(self, pool, params): - info = self.storagepool_lookup(pool) - try: - name = params['name'] - xml = _get_volume_xml(**params) - except KeyError, key: - raise MissingParameter(key) - pool = self._get_storagepool(pool) - try: - pool.createXML(xml, 0) - except libvirt.libvirtError as e: - raise OperationFailed(e.get_error_message()) - return name - - def storagevolume_lookup(self, pool, name): - vol = self._get_storagevolume(pool, name) - path = vol.path() - info = vol.info() - xml = vol.XMLDesc(0) - fmt = xmlutils.xpath_get_text(xml, "/volume/target/format/@type")[0] - res = dict(type=Model.volume_type_map[info[0]], - capacity=info[1], - allocation=info[2], - path=path, - format=fmt) - if fmt == 'iso': - if os.path.islink(path): - path = os.path.join(os.path.dirname(path), os.readlink(path)) - os_distro = os_version = 'unknown' - try: - iso_img = IsoImage(path) - os_distro, os_version = iso_img.probe() - bootable = True - except IsoFormatError: - bootable = False - res.update( - dict(os_distro=os_distro, os_version=os_version, path=path, bootable=bootable)) - - return res - - def storagevolume_wipe(self, pool, name): - volume = self._get_storagevolume(pool, name) - try: - volume.wipePattern(libvirt.VIR_STORAGE_VOL_WIPE_ALG_ZERO, 0) - except libvirt.libvirtError as e: - raise OperationFailed(e.get_error_message()) - - def storagevolume_delete(self, pool, name): - volume = self._get_storagevolume(pool, name) - try: - volume.delete(0) - except libvirt.libvirtError as e: - raise OperationFailed(e.get_error_message()) - - def storagevolume_resize(self, pool, name, size): - size = size << 20 - volume = self._get_storagevolume(pool, name) - try: - volume.resize(size, 0) - except libvirt.libvirtError as e: - raise OperationFailed(e.get_error_message()) - - def storagevolumes_get_list(self, pool): - pool = self._get_storagepool(pool) - if not pool.isActive(): - raise InvalidOperation( - "Unable to list volumes in inactive storagepool %s" % pool.name()) - try: - self._pool_refresh(pool) - return pool.listVolumes() - except libvirt.libvirtError as e: - raise OperationFailed(e.get_error_message()) - - def _get_storagevolume(self, pool, name): - pool = self._get_storagepool(pool) - if not pool.isActive(): - raise InvalidOperation( - "Unable to list volumes in inactive storagepool %s" % pool.name()) - try: - return pool.storageVolLookupByName(name) - except libvirt.libvirtError as e: - if e.get_error_code() == libvirt.VIR_ERR_NO_STORAGE_VOL: - raise NotFoundError("Storage Volume '%s' not found" % name) - else: - raise - - def storageservers_get_list(self, _target_type=None): - target_type = STORAGE_SOURCES.keys() if not _target_type else [_target_type] - pools = self.storagepools_get_list() - server_list = [] - for pool in pools: - try: - pool_info = self.storagepool_lookup(pool) - if (pool_info['type'] in target_type and - pool_info['source']['addr'] not in server_list): - # Avoid to add same server for multiple times - # if it hosts more than one storage type - server_list.append(pool_info['source']['addr']) - except NotFoundError: - pass - - return server_list - - def storageserver_lookup(self, server): - pools = self.storagepools_get_list() - for pool in pools: - try: - pool_info = self.storagepool_lookup(pool) - if pool_info['source'] and pool_info['source']['addr'] == server: - return dict(host=server) - except NotFoundError: - # Avoid inconsistent pool result because of lease between list and lookup - pass - - raise NotFoundError('server %s does not used by kimchi' % server) - - def storagetargets_get_list(self, storage_server, _target_type=None): - target_types = STORAGE_SOURCES.keys() if not _target_type else [_target_type] - target_list = list() - - for target_type in target_types: - if not self.nfs_target_probe and target_type == 'netfs': - targets = patch_find_nfs_target(storage_server) - else: - xml = _get_storage_server_spec(server=storage_server, target_type=target_type) - conn = self.conn.get() - - try: - ret = conn.findStoragePoolSources(target_type, xml, 0) - except libvirt.libvirtError as e: - kimchi_log.warning("Query storage pool source fails because of %s", - e.get_error_message()) - continue - targets = _parse_target_source_result(target_type, ret) - - target_list.extend(targets) - return target_list - - def _get_screenshot(self, vm_uuid): - with self.objstore as session: - try: - params = session.get('screenshot', vm_uuid) - except NotFoundError: - params = {'uuid': vm_uuid} - session.store('screenshot', vm_uuid, params) - return LibvirtVMScreenshot(params, self.conn) - - def _sosreport_generate(self, cb, name): - command = 'sosreport --batch --name "%s"' % name - try: - retcode = subprocess.call(command, shell=True, stdout=subprocess.PIPE) - if retcode < 0: - raise OperationFailed('Command terminated with signal') - elif retcode > 0: - raise OperationFailed('Command failed: rc = %i' % retcode) - pattern = '/tmp/sosreport-%s-*' % name - for reportFile in glob.glob(pattern): - if not fnmatch.fnmatch(reportFile, '*.md5'): - output = reportFile - break - else: - # sosreport tends to change the name mangling rule and - # compression file format between different releases. - # It's possible to fail to match a report file even sosreport - # runs successfully. In future we might have a general name - # mangling function in kimchi to format the name before passing - # it to sosreport. Then we can delete this exception. - raise OperationFailed('Can not find generated debug report ' - 'named by %s' % pattern) - ext = output.split('.', 1)[1] - path = config.get_debugreports_path() - target = os.path.join(path, name) - target_file = '%s.%s' % (target, ext) - shutil.move(output, target_file) - os.remove('%s.md5' % output) - cb('OK', True) - - return - - except OSError: - raise - - except Exception, e: - # No need to call cb to update the task status here. - # The task object will catch the exception rasied here - # and update the task status there - log = logging.getLogger('Model') - log.warning('Exception in generating debug file: %s', e) - raise OperationFailed(e) - - def _get_system_report_tool(self): - # check if the command can be found by shell one by one - for helper_tool in self.report_tools: - try: - retcode = subprocess.call(helper_tool['cmd'], shell=True, - stdout=subprocess.PIPE, stderr=subprocess.PIPE) - if retcode == 0: - return helper_tool['fn'] - except Exception, e: - kimchi_log.info('Exception running command: %s', e) - - return None - - def _gen_debugreport_file(self, name): - gen_cmd = self._get_system_report_tool() - - if gen_cmd is not None: - return self.add_task('', gen_cmd, name) - - raise OperationFailed("debugreport tool not found") - - def _get_distros(self): - distroloader = DistroLoader() - return distroloader.get() - - def distros_get_list(self): - return self.distros.keys() - - def distro_lookup(self, name): - try: - return self.distros[name] - except KeyError: - raise NotFoundError("distro '%s' not found" % name) - - def host_lookup(self, *name): - return self.host_info - - def hoststats_lookup(self, *name): - return {'cpu_utilization': self.host_stats['cpu_utilization'], - 'memory': self.host_stats.get('memory'), - 'disk_read_rate': self.host_stats['disk_read_rate'], - 'disk_write_rate': self.host_stats['disk_write_rate'], - 'net_recv_rate': self.host_stats['net_recv_rate'], - 'net_sent_rate': self.host_stats['net_sent_rate']} - - def plugins_get_list(self): - return [plugin for (plugin, config) in get_enabled_plugins()] - - def partitions_get_list(self): - result = disks.get_partitions_names() - return result - - def partition_lookup(self, name): - if name not in disks.get_partitions_names(): - raise NotFoundError("Partition %s not found in the host" - % name) - return disks.get_partition_details(name) - - def vms_get_list_by_state(self, state): - ret_list = [] - for name in self.vms_get_list(): - info = self._get_vm(name).info() - if (Model.dom_state_map[info[0]]) == state: - ret_list.append(name) - return ret_list - - def host_shutdown(self, args=None): - # Check for running vms before shutdown - running_vms = self.vms_get_list_by_state('running') - if len(running_vms) > 0: - raise OperationFailed("Shutdown not allowed: VMs are running!") - kimchi_log.info('Host is going to shutdown.') - os.system('shutdown -h now') - - def host_reboot(self, args=None): - # Find running VMs - running_vms = self.vms_get_list_by_state('running') - if len(running_vms) > 0: - raise OperationFailed("Reboot not allowed: VMs are running!") - kimchi_log.info('Host is going to reboot.') - os.system('reboot') - - -class LibvirtVMTemplate(VMTemplate): - def __init__(self, args, scan=False, conn=None): - VMTemplate.__init__(self, args, scan) - self.conn = conn - - def _storage_validate(self): - pool_uri = self.info['storagepool'] - pool_name = pool_name_from_uri(pool_uri) - try: - conn = self.conn.get() - pool = conn.storagePoolLookupByName(pool_name) - except libvirt.libvirtError: - raise InvalidParameter('Storage specified by template does not exist') - if not pool.isActive(): - raise InvalidParameter('Storage specified by template is not active') - - return pool - - def _network_validate(self): - names = self.info['networks'] - for name in names: - try: - conn = self.conn.get() - network = conn.networkLookupByName(name) - except libvirt.libvirtError: - raise InvalidParameter('Network specified by template does not exist') - if not network.isActive(): - raise InvalidParameter('Network specified by template is not active') - - def _get_storage_path(self): - pool = self._storage_validate() - xml = pool.XMLDesc(0) - return xmlutils.xpath_get_text(xml, "/pool/target/path")[0] - - def fork_vm_storage(self, vm_uuid): - # Provision storage: - # TODO: Rebase on the storage API once upstream - pool = self._storage_validate() - vol_list = self.to_volume_list(vm_uuid) - for v in vol_list: - # outgoing text to libvirt, encode('utf-8') - pool.createXML(v['xml'].encode('utf-8'), 0) - return vol_list - - -class LibvirtVMScreenshot(VMScreenshot): - def __init__(self, vm_uuid, conn): - VMScreenshot.__init__(self, vm_uuid) - self.conn = conn - - def _generate_scratch(self, thumbnail): - def handler(stream, buf, opaque): - fd = opaque - os.write(fd, buf) - - fd = os.open(thumbnail, os.O_WRONLY | os.O_TRUNC | os.O_CREAT, 0644) - try: - conn = self.conn.get() - dom = conn.lookupByUUIDString(self.vm_uuid) - vm_name = dom.name() - stream = conn.newStream(0) - mimetype = dom.screenshot(stream, 0, 0) - stream.recvAll(handler, fd) - except libvirt.libvirtError: - try: - stream.abort() - except: - pass - raise NotFoundError("Screenshot not supported for %s" % vm_name) - else: - stream.finish() - finally: - os.close(fd) - - -def _parse_target_source_result(target_type, xml_str): - root = objectify.fromstring(xml_str) - ret = [] - for source in root.getchildren(): - if target_type == 'netfs': - host_name = source.host.get('name') - target_path = source.dir.get('path') - type = source.format.get('type') - ret.append(dict(host=host_name, target_type=type, target=target_path)) - return ret - - -def _get_storage_server_spec(**kwargs): - # Required parameters: - # server: - # target_type: - extra_args = [] - if kwargs['target_type'] == 'netfs': - extra_args.append(E.format(type='nfs')) - obj = E.source(E.host(name=kwargs['server']), *extra_args) - xml = ET.tostring(obj) - return xml - - -def _get_volume_xml(**kwargs): - # Required parameters - # name: - # capacity: - # - # Optional: - # allocation: - # format: - kwargs.setdefault('allocation', 0) - kwargs.setdefault('format', 'qcow2') - - xml = """ - <volume> - <name>%(name)s</name> - <allocation unit="MiB">%(allocation)s</allocation> - <capacity unit="MiB">%(capacity)s</capacity> - <source> - </source> - <target> - <format type='%(format)s'/> - </target> - </volume> - """ % kwargs - return xml diff --git a/src/kimchi/model/__init__.py b/src/kimchi/model/__init__.py new file mode 100644 index 0000000..8a37cc4 --- /dev/null +++ b/src/kimchi/model/__init__.py @@ -0,0 +1,21 @@ +# +# Project Kimchi +# +# Copyright IBM, Corp. 2013 +# +# Authors: +# Aline Manera <alinefm@linux.vnet.ibm.com> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA diff --git a/src/kimchi/model/config.py b/src/kimchi/model/config.py new file mode 100644 index 0000000..86a1167 --- /dev/null +++ b/src/kimchi/model/config.py @@ -0,0 +1,87 @@ +# +# Project Kimchi +# +# Copyright IBM, Corp. 2013 +# +# Authors: +# Aline Manera <alinefm@linux.vnet.ibm.com> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +import cherrypy + +from kimchi.basemodel import Singleton +from kimchi.distroloader import DistroLoader +from kimchi.exception import NotFoundError +from kimchi.featuretests import FeatureTests +from kimchi.model.debugreports import DebugReportsModel +from kimchi.screenshot import VMScreenshot +from kimchi.utils import kimchi_log + + +class CapabilitiesModel(object): + __metaclass__ = Singleton + + def __init__(self, **kargs): + self.qemu_stream = False + self.qemu_stream_dns = False + self.libvirt_stream_protocols = [] + + # Subscribe function to set host capabilities to be run when cherrypy + # server is up + # It is needed because some features tests depends on the server + cherrypy.engine.subscribe('start', self._set_capabilities) + + def _set_capabilities(self): + kimchi_log.info("*** Running feature tests ***") + self.qemu_stream = FeatureTests.qemu_supports_iso_stream() + self.qemu_stream_dns = FeatureTests.qemu_iso_stream_dns() + self.nfs_target_probe = FeatureTests.libvirt_support_nfs_probe() + + self.libvirt_stream_protocols = [] + for p in ['http', 'https', 'ftp', 'ftps', 'tftp']: + if FeatureTests.libvirt_supports_iso_stream(p): + self.libvirt_stream_protocols.append(p) + + kimchi_log.info("*** Feature tests completed ***") + _set_capabilities.priority = 90 + + def lookup(self, *ident): + report_tool = DebugReportsModel.get_system_report_tool() + + return {'libvirt_stream_protocols': self.libvirt_stream_protocols, + 'qemu_stream': self.qemu_stream, + 'screenshot': VMScreenshot.get_stream_test_result(), + 'system_report_tool': bool(report_tool)} + + +class DistrosModel(object): + def __init__(self, **kargs): + distroloader = DistroLoader() + self.distros = distroloader.get() + + def get_list(self): + return self.distros.keys() + + +class DistroModel(object): + def __init__(self, **kargs): + self._distros = DistrosModel() + + def lookup(self, name): + try: + return self._distros.distros[name] + except KeyError: + raise NotFoundError("Distro '%s' not found." % name) diff --git a/src/kimchi/model/debugreports.py b/src/kimchi/model/debugreports.py new file mode 100644 index 0000000..a1cb19c --- /dev/null +++ b/src/kimchi/model/debugreports.py @@ -0,0 +1,167 @@ +# +# Project Kimchi +# +# Copyright IBM, Corp. 2013 +# +# Authors: +# Aline Manera <alinefm@linux.vnet.ibm.com> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +import fnmatch +import glob +import logging +import os +import shutil +import subprocess +import time + +from kimchi import config +from kimchi.exception import NotFoundError, OperationFailed +from kimchi.model.tasks import TaskModel +from kimchi.utils import add_task, kimchi_log + + +class DebugReportsModel(object): + def __init__(self, **kargs): + self.objstore = kargs['objstore'] + self.task = TaskModel(**kargs) + + def create(self, params): + ident = params['name'] + taskid = self._gen_debugreport_file(ident) + return self.task.lookup(taskid) + + def get_list(self): + path = config.get_debugreports_path() + file_pattern = os.path.join(path, '*.*') + file_lists = glob.glob(file_pattern) + file_lists = [os.path.split(file)[1] for file in file_lists] + name_lists = [file.split('.', 1)[0] for file in file_lists] + + return name_lists + + def _gen_debugreport_file(self, name): + gen_cmd = self.get_system_report_tool() + + if gen_cmd is not None: + return add_task('', gen_cmd, self.objstore, name) + + raise OperationFailed("debugreport tool not found") + + @staticmethod + def sosreport_generate(cb, name): + command = 'sosreport --batch --name "%s"' % name + try: + retcode = subprocess.call(command, shell=True, + stdout=subprocess.PIPE) + if retcode < 0: + raise OperationFailed('Command terminated with signal') + elif retcode > 0: + raise OperationFailed('Command failed: rc = %i' % retcode) + pattern = '/tmp/sosreport-%s-*' % name + for reportFile in glob.glob(pattern): + if not fnmatch.fnmatch(reportFile, '*.md5'): + output = reportFile + break + else: + # sosreport tends to change the name mangling rule and + # compression file format between different releases. + # It's possible to fail to match a report file even sosreport + # runs successfully. In future we might have a general name + # mangling function in kimchi to format the name before passing + # it to sosreport. Then we can delete this exception. + raise OperationFailed('Can not find generated debug report ' + 'named by %s' % pattern) + ext = output.split('.', 1)[1] + path = config.get_debugreports_path() + target = os.path.join(path, name) + target_file = '%s.%s' % (target, ext) + shutil.move(output, target_file) + os.remove('%s.md5' % output) + cb('OK', True) + + return + + except OSError: + raise + + except Exception, e: + # No need to call cb to update the task status here. + # The task object will catch the exception rasied here + # and update the task status there + log = logging.getLogger('Model') + log.warning('Exception in generating debug file: %s', e) + raise OperationFailed(e) + + @staticmethod + def get_system_report_tool(): + # Please add new possible debug report command here + # and implement the report generating function + # based on the new report command + report_tools = ({'cmd': 'sosreport --help', + 'fn': DebugReportsModel.sosreport_generate},) + + # check if the command can be found by shell one by one + for helper_tool in report_tools: + try: + retcode = subprocess.call(helper_tool['cmd'], shell=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + if retcode == 0: + return helper_tool['fn'] + except Exception, e: + kimchi_log.info('Exception running command: %s', e) + + return None + + +class DebugReportModel(object): + def __init__(self, **kargs): + pass + + def lookup(self, name): + path = config.get_debugreports_path() + file_pattern = os.path.join(path, name) + file_pattern = file_pattern + '.*' + try: + file_target = glob.glob(file_pattern)[0] + except IndexError: + raise NotFoundError('no such report') + + ctime = os.stat(file_target).st_ctime + ctime = time.strftime("%Y-%m-%d-%H:%M:%S", time.localtime(ctime)) + file_target = os.path.split(file_target)[-1] + file_target = os.path.join("/data/debugreports", file_target) + return {'file': file_target, + 'ctime': ctime} + + def delete(self, name): + path = config.get_debugreports_path() + file_pattern = os.path.join(path, name + '.*') + try: + file_target = glob.glob(file_pattern)[0] + except IndexError: + raise NotFoundError('no such report') + + os.remove(file_target) + + +class DebugReportContentModel(object): + def __init__(self, **kargs): + self._debugreport = DebugReportModel() + + def lookup(self, name): + return self._debugreport.lookup(name) diff --git a/src/kimchi/model/host.py b/src/kimchi/model/host.py new file mode 100644 index 0000000..d5fc124 --- /dev/null +++ b/src/kimchi/model/host.py @@ -0,0 +1,201 @@ +# +# Project Kimchi +# +# Copyright IBM, Corp. 2013 +# +# Authors: +# Aline Manera <alinefm@linux.vnet.ibm.com> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +import os +import time +import platform +from collections import defaultdict + +import psutil +from cherrypy.process.plugins import BackgroundTask + +from kimchi import disks +from kimchi import netinfo +from kimchi.basemodel import Singleton +from kimchi.exception import NotFoundError, OperationFailed +from kimchi.model.vms import DOM_STATE_MAP +from kimchi.utils import kimchi_log + + +HOST_STATS_INTERVAL = 1 + + +class HostModel(object): + def __init__(self, **kargs): + self.host_info = self._get_host_info() + + def _get_host_info(self): + res = {} + with open('/proc/cpuinfo') as f: + for line in f.xreadlines(): + if "model name" in line: + res['cpu'] = line.split(':')[1].strip() + break + + res['memory'] = psutil.TOTAL_PHYMEM + # 'fedora' '17' 'Beefy Miracle' + distro, version, codename = platform.linux_distribution() + res['os_distro'] = distro + res['os_version'] = version + res['os_codename'] = unicode(codename, "utf-8") + + return res + + def lookup(self, *name): + return self.host_info + + def shutdown(self, args=None): + # Check for running vms before shutdown + running_vms = self.vms_get_list_by_state('running') + if len(running_vms) > 0: + raise OperationFailed("Shutdown not allowed: VMs are running!") + kimchi_log.info('Host is going to shutdown.') + os.system('shutdown -h now') + + def reboot(self, args=None): + # Find running VMs + running_vms = self._get_vms_list_by_state('running') + if len(running_vms) > 0: + raise OperationFailed("Reboot not allowed: VMs are running!") + kimchi_log.info('Host is going to reboot.') + os.system('reboot') + + def _get_vms_list_by_state(self, state): + ret_list = [] + for name in self.vms_get_list(): + info = self._get_vm(name).info() + if (DOM_STATE_MAP[info[0]]) == state: + ret_list.append(name) + return ret_list + + +class HostStatsModel(object): + __metaclass__ = Singleton + + def __init__(self, **kargs): + self.host_stats = defaultdict(int) + self.host_stats_thread = BackgroundTask(HOST_STATS_INTERVAL, + self._update_host_stats) + self.host_stats_thread.start() + + def lookup(self, *name): + return {'cpu_utilization': self.host_stats['cpu_utilization'], + 'memory': self.host_stats.get('memory'), + 'disk_read_rate': self.host_stats['disk_read_rate'], + 'disk_write_rate': self.host_stats['disk_write_rate'], + 'net_recv_rate': self.host_stats['net_recv_rate'], + 'net_sent_rate': self.host_stats['net_sent_rate']} + + def _update_host_stats(self): + preTimeStamp = self.host_stats['timestamp'] + timestamp = time.time() + # FIXME when we upgrade psutil, we can get uptime by psutil.uptime + # we get uptime by float(open("/proc/uptime").readline().split()[0]) + # and calculate the first io_rate after the OS started. + seconds = (timestamp - preTimeStamp if preTimeStamp else + float(open("/proc/uptime").readline().split()[0])) + + self.host_stats['timestamp'] = timestamp + self._get_host_disk_io_rate(seconds) + self._get_host_network_io_rate(seconds) + + self._get_percentage_host_cpu_usage() + self._get_host_memory_stats() + + def _get_percentage_host_cpu_usage(self): + # This is cpu usage producer. This producer will calculate the usage + # at an interval of HOST_STATS_INTERVAL. + # The psutil.cpu_percent works as non blocking. + # psutil.cpu_percent maintains a cpu time sample. + # It will update the cpu time sample when it is called. + # So only this producer can call psutil.cpu_percent in kimchi. + self.host_stats['cpu_utilization'] = psutil.cpu_percent(None) + + def _get_host_memory_stats(self): + virt_mem = psutil.virtual_memory() + # available: + # the actual amount of available memory that can be given + # instantly to processes that request more memory in bytes; this + # is calculated by summing different memory values depending on + # the platform (e.g. free + buffers + cached on Linux) + memory_stats = {'total': virt_mem.total, + 'free': virt_mem.free, + 'cached': virt_mem.cached, + 'buffers': virt_mem.buffers, + 'avail': virt_mem.available} + self.host_stats['memory'] = memory_stats + + def _get_host_disk_io_rate(self, seconds): + prev_read_bytes = self.host_stats['disk_read_bytes'] + prev_write_bytes = self.host_stats['disk_write_bytes'] + + disk_io = psutil.disk_io_counters(False) + read_bytes = disk_io.read_bytes + write_bytes = disk_io.write_bytes + + rd_rate = int(float(read_bytes - prev_read_bytes) / seconds + 0.5) + wr_rate = int(float(write_bytes - prev_write_bytes) / seconds + 0.5) + + self.host_stats.update({'disk_read_rate': rd_rate, + 'disk_write_rate': wr_rate, + 'disk_read_bytes': read_bytes, + 'disk_write_bytes': write_bytes}) + + def _get_host_network_io_rate(self, seconds): + prev_recv_bytes = self.host_stats['net_recv_bytes'] + prev_sent_bytes = self.host_stats['net_sent_bytes'] + + net_ios = psutil.network_io_counters(True) + recv_bytes = 0 + sent_bytes = 0 + for key in set(netinfo.nics() + + netinfo.wlans()) & set(net_ios.iterkeys()): + recv_bytes = recv_bytes + net_ios[key].bytes_recv + sent_bytes = sent_bytes + net_ios[key].bytes_sent + + rx_rate = int(float(recv_bytes - prev_recv_bytes) / seconds + 0.5) + tx_rate = int(float(sent_bytes - prev_sent_bytes) / seconds + 0.5) + + self.host_stats.update({'net_recv_rate': rx_rate, + 'net_sent_rate': tx_rate, + 'net_recv_bytes': recv_bytes, + 'net_sent_bytes': sent_bytes}) + + +class PartitionsModel(object): + def __init__(self, **kargs): + pass + + def get_list(self): + result = disks.get_partitions_names() + return result + + +class PartitionModel(object): + def __init__(self, **kargs): + pass + + def lookup(self, name): + if name not in disks.get_partitions_names(): + raise NotFoundError("Partition %s not found in the host" + % name) + return disks.get_partition_details(name) diff --git a/src/kimchi/model/interfaces.py b/src/kimchi/model/interfaces.py new file mode 100644 index 0000000..52c6bae --- /dev/null +++ b/src/kimchi/model/interfaces.py @@ -0,0 +1,46 @@ +# +# Project Kimchi +# +# Copyright IBM, Corp. 2013 +# +# Authors: +# Aline Manera <alinefm@linux.vnet.ibm.com> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +from kimchi import netinfo +from kimchi.exception import NotFoundError +from kimchi.model.networks import NetworksModel + + +class InterfacesModel(object): + def __init__(self, **kargs): + self.conn = kargs['conn'] + self.networks = NetworksModel(**kargs) + + def get_list(self): + return list(set(netinfo.all_favored_interfaces()) - + set(self.networks.get_all_networks_interfaces())) + + +class InterfaceModel(object): + def __init__(self, **kargs): + pass + + def lookup(self, name): + try: + return netinfo.get_interface_info(name) + except ValueError, e: + raise NotFoundError(e) diff --git a/src/kimchi/model/libvirtconnection.py b/src/kimchi/model/libvirtconnection.py new file mode 100644 index 0000000..7bbb668 --- /dev/null +++ b/src/kimchi/model/libvirtconnection.py @@ -0,0 +1,122 @@ +# +# Project Kimchi +# +# Copyright IBM, Corp. 2013 +# +# Authors: +# Aline Manera <alinefm@linux.vnet.ibm.com> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +import threading +import time + +import cherrypy +import libvirt + +from kimchi.utils import kimchi_log + + +class LibvirtConnection(object): + def __init__(self, uri): + self.uri = uri + self._connections = {} + self._connectionLock = threading.Lock() + self.wrappables = self.get_wrappable_objects() + + def get_wrappable_objects(self): + """ + When a wrapped function returns an instance of another libvirt object, + we also want to wrap that object so we can catch errors that happen + when calling its methods. + """ + objs = [] + for name in ('virDomain', 'virDomainSnapshot', 'virInterface', + 'virNWFilter', 'virNetwork', 'virNodeDevice', 'virSecret', + 'virStoragePool', 'virStorageVol', 'virStream'): + try: + attr = getattr(libvirt, name) + except AttributeError: + pass + objs.append(attr) + return tuple(objs) + + def get(self, conn_id=0): + """ + Return current connection to libvirt or open a new one. Wrap all + callable libvirt methods so we can catch connection errors and handle + them by restarting the server. + """ + def wrapMethod(f): + def wrapper(*args, **kwargs): + try: + ret = f(*args, **kwargs) + return ret + except libvirt.libvirtError as e: + edom = e.get_error_domain() + ecode = e.get_error_code() + EDOMAINS = (libvirt.VIR_FROM_REMOTE, + libvirt.VIR_FROM_RPC) + ECODES = (libvirt.VIR_ERR_SYSTEM_ERROR, + libvirt.VIR_ERR_INTERNAL_ERROR, + libvirt.VIR_ERR_NO_CONNECT, + libvirt.VIR_ERR_INVALID_CONN) + if edom in EDOMAINS and ecode in ECODES: + kimchi_log.error('Connection to libvirt broken. ' + 'Recycling. ecode: %d edom: %d' % + (ecode, edom)) + with self._connectionLock: + self._connections[conn_id] = None + raise + wrapper.__name__ = f.__name__ + wrapper.__doc__ = f.__doc__ + return wrapper + + with self._connectionLock: + conn = self._connections.get(conn_id) + if not conn: + retries = 5 + while True: + retries = retries - 1 + try: + conn = libvirt.open(self.uri) + break + except libvirt.libvirtError: + kimchi_log.error('Unable to connect to libvirt.') + if not retries: + err = 'Libvirt is not available, exiting.' + kimchi_log.error(err) + cherrypy.engine.stop() + raise + time.sleep(2) + + for name in dir(libvirt.virConnect): + method = getattr(conn, name) + if callable(method) and not name.startswith('_'): + setattr(conn, name, wrapMethod(method)) + + for cls in self.wrappables: + for name in dir(cls): + method = getattr(cls, name) + if callable(method) and not name.startswith('_'): + setattr(cls, name, wrapMethod(method)) + + self._connections[conn_id] = conn + # In case we're running into troubles with keeping the + # connections alive we should place here: + # conn.setKeepAlive(interval=5, count=3) + # However the values need to be considered wisely to not affect + # hosts which are hosting a lot of virtual machines + return conn diff --git a/src/kimchi/model/libvirtstoragepool.py b/src/kimchi/model/libvirtstoragepool.py new file mode 100644 index 0000000..f4dbf2e --- /dev/null +++ b/src/kimchi/model/libvirtstoragepool.py @@ -0,0 +1,257 @@ +# +# Project Kimchi +# +# Copyright IBM, Corp. 2013 +# +# Authors: +# Aline Manera <alinefm@linux.vnet.ibm.com> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +import copy +import os +import tempfile + +import libvirt + +from kimchi.exception import InvalidParameter, OperationFailed, TimeoutExpired +from kimchi.iscsi import TargetClient +from kimchi.rollbackcontext import RollbackContext +from kimchi.utils import parse_cmd_output, run_command + + +class StoragePoolDef(object): + @classmethod + def create(cls, poolArgs): + for klass in cls.__subclasses__(): + if poolArgs['type'] == klass.poolType: + return klass(poolArgs) + raise OperationFailed('Unsupported pool type: %s' % poolArgs['type']) + + def __init__(self, poolArgs): + self.poolArgs = poolArgs + + def prepare(self, conn): + ''' Validate pool arguments and perform preparations. Operation which + would cause side effect should be put here. Subclasses can optionally + override this method, or it always succeeds by default. ''' + pass + + @property + def xml(self): + ''' Subclasses have to override this method to actually generate the + storage pool XML definition. Should cause no side effect and be + idempotent''' + # TODO: When add new pool type, should also add the related test in + # tests/test_storagepool.py + raise OperationFailed('self.xml is not implemented: %s' % self) + + +class DirPoolDef(StoragePoolDef): + poolType = 'dir' + + @property + def xml(self): + # Required parameters + # name: + # type: + # path: + xml = """ + <pool type='dir'> + <name>{name}</name> + <target> + <path>{path}</path> + </target> + </pool> + """.format(**self.poolArgs) + return xml + + +class NetfsPoolDef(StoragePoolDef): + poolType = 'netfs' + + def __init__(self, poolArgs): + super(NetfsPoolDef, self).__init__(poolArgs) + self.path = '/var/lib/kimchi/nfs_mount/' + self.poolArgs['name'] + + def prepare(self, conn): + mnt_point = tempfile.mkdtemp(dir='/tmp') + export_path = "%s:%s" % ( + self.poolArgs['source']['host'], self.poolArgs['source']['path']) + mount_cmd = ["mount", "-o", 'soft,timeo=100,retrans=3,retry=0', + export_path, mnt_point] + umount_cmd = ["umount", "-f", export_path] + mounted = False + + with RollbackContext() as rollback: + rollback.prependDefer(os.rmdir, mnt_point) + try: + run_command(mount_cmd, 30) + rollback.prependDefer(run_command, umount_cmd) + except TimeoutExpired: + err = "Export path %s may block during nfs mount" + raise InvalidParameter(err % export_path) + + with open("/proc/mounts", "rb") as f: + rawMounts = f.read() + output_items = ['dev_path', 'mnt_point', 'type'] + mounts = parse_cmd_output(rawMounts, output_items) + for item in mounts: + if 'dev_path' in item and item['dev_path'] == export_path: + mounted = True + + if not mounted: + err = "Export path %s mount failed during nfs mount" + raise InvalidParameter(err % export_path) + + @property + def xml(self): + # Required parameters + # name: + # type: + # source[host]: + # source[path]: + poolArgs = copy.deepcopy(self.poolArgs) + poolArgs['path'] = self.path + xml = """ + <pool type='netfs'> + <name>{name}</name> + <source> + <host name='{source[host]}'/> + <dir path='{source[path]}'/> + </source> + <target> + <path>{path}</path> + </target> + </pool> + """.format(**poolArgs) + return xml + + +class LogicalPoolDef(StoragePoolDef): + poolType = 'logical' + + def __init__(self, poolArgs): + super(LogicalPoolDef, self).__init__(poolArgs) + self.path = '/var/lib/kimchi/logical_mount/' + self.poolArgs['name'] + + @property + def xml(self): + # Required parameters + # name: + # type: + # source[devices]: + poolArgs = copy.deepcopy(self.poolArgs) + devices = [] + for device_path in poolArgs['source']['devices']: + devices.append('<device path="%s" />' % device_path) + + poolArgs['source']['devices'] = ''.join(devices) + poolArgs['path'] = self.path + + xml = """ + <pool type='logical'> + <name>{name}</name> + <source> + {source[devices]} + </source> + <target> + <path>{path}</path> + </target> + </pool> + """.format(**poolArgs) + return xml + + +class IscsiPoolDef(StoragePoolDef): + poolType = 'iscsi' + + def prepare(self, conn): + source = self.poolArgs['source'] + if not TargetClient(**source).validate(): + raise OperationFailed("Can not login to iSCSI host %s target %s" % + (source['host'], source['target'])) + self._prepare_auth(conn) + + def _prepare_auth(self, conn): + try: + auth = self.poolArgs['source']['auth'] + except KeyError: + return + + try: + virSecret = conn.secretLookupByUsage( + libvirt.VIR_SECRET_USAGE_TYPE_ISCSI, self.poolArgs['name']) + except libvirt.libvirtError: + xml = ''' + <secret ephemeral='no' private='yes'> + <description>Secret for iSCSI storage pool {name}</description> + <auth type='chap' username='{username}'/> + <usage type='iscsi'> + <target>{name}</target> + </usage> + </secret>'''.format(name=self.poolArgs['name'], + username=auth['username']) + virSecret = conn.secretDefineXML(xml) + + virSecret.setValue(auth['password']) + + def _format_port(self, poolArgs): + try: + port = poolArgs['source']['port'] + except KeyError: + return "" + return "port='%s'" % port + + def _format_auth(self, poolArgs): + try: + auth = poolArgs['source']['auth'] + except KeyError: + return "" + + return ''' + <auth type='chap' username='{username}'> + <secret type='iscsi' usage='{name}'/> + </auth>'''.format(name=poolArgs['name'], username=auth['username']) + + @property + def xml(self): + # Required parameters + # name: + # type: + # source[host]: + # source[target]: + # + # Optional parameters + # source[port]: + poolArgs = copy.deepcopy(self.poolArgs) + poolArgs['source'].update({'port': self._format_port(poolArgs), + 'auth': self._format_auth(poolArgs)}) + poolArgs['path'] = '/dev/disk/by-id' + + xml = """ + <pool type='iscsi'> + <name>{name}</name> + <source> + <host name='{source[host]}' {source[port]}/> + <device path='{source[target]}'/> + {source[auth]} + </source> + <target> + <path>{path}</path> + </target> + </pool> + """.format(**poolArgs) + return xml diff --git a/src/kimchi/model/model.py b/src/kimchi/model/model.py new file mode 100644 index 0000000..28f8be4 --- /dev/null +++ b/src/kimchi/model/model.py @@ -0,0 +1,53 @@ +# +# Project Kimchi +# +# Copyright IBM, Corp. 2013 +# +# Authors: +# Aline Manera <alinefm@linux.vnet.ibm.com> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +import inspect +import os + +from kimchi.basemodel import BaseModel +from kimchi.model.libvirtconnection import LibvirtConnection +from kimchi.objectstore import ObjectStore +from kimchi.utils import import_module, listPathModules + + +class Model(BaseModel): + def __init__(self, libvirt_uri='qemu:///system', objstore_loc=None): + self.objstore = ObjectStore(objstore_loc) + self.conn = LibvirtConnection(libvirt_uri) + kargs = {'objstore': self.objstore, 'conn': self.conn} + + this = os.path.basename(__file__) + this_mod = os.path.splitext(this)[0] + + models = [] + for mod_name in listPathModules(os.path.dirname(__file__)): + if mod_name.startswith("_") or mod_name == this_mod: + continue + + module = import_module('model.' + mod_name) + members = inspect.getmembers(module, inspect.isclass) + for cls_name, instance in members: + if inspect.getmodule(instance) == module: + if cls_name.endswith('Model'): + models.append(instance(**kargs)) + + return super(Model, self).__init__(models) diff --git a/src/kimchi/model/networks.py b/src/kimchi/model/networks.py new file mode 100644 index 0000000..b164141 --- /dev/null +++ b/src/kimchi/model/networks.py @@ -0,0 +1,265 @@ +# +# Project Kimchi +# +# Copyright IBM, Corp. 2013 +# +# Authors: +# Aline Manera <alinefm@linux.vnet.ibm.com> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +import ipaddr +import libvirt + +from kimchi import netinfo +from kimchi import network as knetwork +from kimchi import networkxml +from kimchi import xmlutils +from kimchi.exception import InvalidOperation, InvalidParameter +from kimchi.exception import MissingParameter, NotFoundError, OperationFailed + + +class NetworksModel(object): + def __init__(self, **kargs): + self.conn = kargs['conn'] + + def create(self, params): + conn = self.conn.get() + name = params['name'] + if name in self.get_list(): + raise InvalidOperation("Network %s already exists" % name) + + connection = params["connection"] + # set forward mode, isolated do not need forward + if connection != 'isolated': + params['forward'] = {'mode': connection} + + # set subnet, bridge network do not need subnet + if connection in ["nat", 'isolated']: + self._set_network_subnet(params) + + # only bridge network need bridge(linux bridge) or interface(macvtap) + if connection == 'bridge': + self._set_network_bridge(params) + + xml = networkxml.to_network_xml(**params) + + try: + network = conn.networkDefineXML(xml) + network.setAutostart(True) + except libvirt.libvirtError as e: + raise OperationFailed(e.get_error_message()) + + return name + + def get_list(self): + conn = self.conn.get() + return sorted(conn.listNetworks() + conn.listDefinedNetworks()) + + def _set_network_subnet(self, params): + netaddr = params.get('subnet', '') + net_addrs = [] + # lookup a free network address for nat and isolated automatically + if not netaddr: + for net_name in self.get_list(): + network = self._get_network(net_name) + xml = network.XMLDesc(0) + subnet = NetworkModel.get_network_from_xml(xml)['subnet'] + subnet and net_addrs.append(ipaddr.IPNetwork(subnet)) + netaddr = knetwork.get_one_free_network(net_addrs) + if not netaddr: + raise OperationFailed("can not find a free IP address for " + "network '%s'" % params['name']) + + try: + ip = ipaddr.IPNetwork(netaddr) + except ValueError as e: + raise InvalidParameter("%s" % e) + + if ip.ip == ip.network: + ip.ip = ip.ip + 1 + + dhcp_start = str(ip.ip + ip.numhosts / 2) + dhcp_end = str(ip.ip + ip.numhosts - 2) + params.update({'net': str(ip), + 'dhcp': {'range': {'start': dhcp_start, + 'end': dhcp_end}}}) + + def _set_network_bridge(self, params): + try: + iface = params['interface'] + if iface in self.get_all_networks_interfaces(): + raise InvalidParameter("interface '%s' already in use." % + iface) + except KeyError, e: + raise MissingParameter(e) + if netinfo.is_bridge(iface): + params['bridge'] = iface + elif netinfo.is_bare_nic(iface) or netinfo.is_bonding(iface): + if params.get('vlan_id') is None: + params['forward']['dev'] = iface + else: + params['bridge'] = \ + self._create_vlan_tagged_bridge(str(iface), + str(params['vlan_id'])) + else: + raise InvalidParameter("the interface should be bare nic, " + "bonding or bridge device.") + + def get_all_networks_interfaces(self): + net_names = self.get_list() + interfaces = [] + for name in net_names: + conn = self.conn.get() + network = conn.networkLookupByName(name) + xml = network.XMLDesc(0) + net_dict = NetworkModel.get_network_from_xml(xml) + forward = net_dict['forward'] + (forward['mode'] == 'bridge' and forward['interface'] and + interfaces.append(forward['interface'][0]) is None or + interfaces.extend(forward['interface'] + forward['pf'])) + net_dict['bridge'] and interfaces.append(net_dict['bridge']) + return interfaces + + def _create_vlan_tagged_bridge(self, interface, vlan_id): + br_name = '-'.join(('kimchi', interface, vlan_id)) + br_xml = networkxml.create_vlan_tagged_bridge_xml(br_name, interface, + vlan_id) + conn = self.conn.get() + conn.changeBegin() + try: + vlan_tagged_br = conn.interfaceDefineXML(br_xml) + vlan_tagged_br.create() + except libvirt.libvirtError as e: + conn.changeRollback() + raise OperationFailed(e.message) + else: + conn.changeCommit() + return br_name + + +class NetworkModel(object): + def __init__(self, **kargs): + self.conn = kargs['conn'] + + def lookup(self, name): + network = self._get_network(name) + xml = network.XMLDesc(0) + net_dict = self.get_network_from_xml(xml) + subnet = net_dict['subnet'] + dhcp = net_dict['dhcp'] + forward = net_dict['forward'] + interface = net_dict['bridge'] + + connection = forward['mode'] or "isolated" + # FIXME, if we want to support other forward mode well. + if connection == 'bridge': + # macvtap bridge + interface = interface or forward['interface'][0] + # exposing the network on linux bridge or macvtap interface + interface_subnet = knetwork.get_dev_netaddr(interface) + subnet = subnet if subnet else interface_subnet + + # libvirt use format 192.168.0.1/24, standard should be 192.168.0.0/24 + # http://www.ovirt.org/File:Issue3.png + if subnet: + subnet = ipaddr.IPNetwork(subnet) + subnet = "%s/%s" % (subnet.network, subnet.prefixlen) + + return {'connection': connection, + 'interface': interface, + 'subnet': subnet, + 'dhcp': dhcp, + 'vms': self._get_vms_attach_to_a_network(name), + 'autostart': network.autostart() == 1, + 'state': network.isActive() and "active" or "inactive"} + + def _get_vms_attach_to_a_network(self, network): + vms = [] + conn = self.conn.get() + for dom in conn.listAllDomains(0): + networks = self._vm_get_networks(dom) + if network in networks: + vms.append(dom.name()) + return vms + + def _vm_get_networks(self, dom): + xml = dom.XMLDesc(0) + xpath = "/domain/devices/interface[@type='network']/source/@network" + return xmlutils.xpath_get_text(xml, xpath) + + def activate(self, name): + network = self._get_network(name) + network.create() + + def deactivate(self, name): + network = self._get_network(name) + network.destroy() + + def delete(self, name): + network = self._get_network(name) + if network.isActive(): + raise InvalidOperation( + "Unable to delete the active network %s" % name) + self._remove_vlan_tagged_bridge(network) + network.undefine() + + def _get_network(self, name): + conn = self.conn.get() + try: + return conn.networkLookupByName(name) + except libvirt.libvirtError as e: + raise NotFoundError("Network '%s' not found: %s" % + (name, e.get_error_message())) + + @staticmethod + def get_network_from_xml(xml): + address = xmlutils.xpath_get_text(xml, "/network/ip/@address") + address = address and address[0] or '' + netmask = xmlutils.xpath_get_text(xml, "/network/ip/@netmask") + netmask = netmask and netmask[0] or '' + net = address and netmask and "/".join([address, netmask]) or '' + + dhcp_start = xmlutils.xpath_get_text(xml, + "/network/ip/dhcp/range/@start") + dhcp_start = dhcp_start and dhcp_start[0] or '' + dhcp_end = xmlutils.xpath_get_text(xml, "/network/ip/dhcp/range/@end") + dhcp_end = dhcp_end and dhcp_end[0] or '' + dhcp = {'start': dhcp_start, 'end': dhcp_end} + + forward_mode = xmlutils.xpath_get_text(xml, "/network/forward/@mode") + forward_mode = forward_mode and forward_mode[0] or '' + forward_if = xmlutils.xpath_get_text(xml, + "/network/forward/interface/@dev") + forward_pf = xmlutils.xpath_get_text(xml, "/network/forward/pf/@dev") + bridge = xmlutils.xpath_get_text(xml, "/network/bridge/@name") + bridge = bridge and bridge[0] or '' + return {'subnet': net, 'dhcp': dhcp, 'bridge': bridge, + 'forward': {'mode': forward_mode, + 'interface': forward_if, + 'pf': forward_pf}} + + def _remove_vlan_tagged_bridge(self, network): + try: + bridge = network.bridgeName() + except libvirt.libvirtError: + pass + else: + if bridge.startswith('kimchi-'): + conn = self.conn.get() + iface = conn.interfaceLookupByName(bridge) + if iface.isActive(): + iface.destroy() + iface.undefine() diff --git a/src/kimchi/model/plugins.py b/src/kimchi/model/plugins.py new file mode 100644 index 0000000..d6756d0 --- /dev/null +++ b/src/kimchi/model/plugins.py @@ -0,0 +1,31 @@ +# +# Project Kimchi +# +# Copyright IBM, Corp. 2013 +# +# Authors: +# Aline Manera <alinefm@linux.vnet.ibm.com> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +from kimchi.utils import get_enabled_plugins + + +class PluginsModel(object): + def __init__(self, **kargs): + pass + + def get_list(self): + return [plugin for (plugin, config) in get_enabled_plugins()] diff --git a/src/kimchi/model/storagepools.py b/src/kimchi/model/storagepools.py new file mode 100644 index 0000000..233a8a7 --- /dev/null +++ b/src/kimchi/model/storagepools.py @@ -0,0 +1,246 @@ +# +# Project Kimchi +# +# Copyright IBM, Corp. 2013 +# +# Authors: +# Aline Manera <alinefm@linux.vnet.ibm.com> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +import libvirt + +from kimchi import xmlutils +from kimchi.scan import Scanner +from kimchi.exception import InvalidOperation, MissingParameter +from kimchi.exception import NotFoundError, OperationFailed +from kimchi.model.libvirtstoragepool import StoragePoolDef +from kimchi.utils import add_task, kimchi_log + + +ISO_POOL_NAME = u'kimchi_isos' +POOL_STATE_MAP = {0: 'inactive', + 1: 'initializing', + 2: 'active', + 3: 'degraded', + 4: 'inaccessible'} + +STORAGE_SOURCES = {'netfs': {'addr': '/pool/source/host/@name', + 'path': '/pool/source/dir/@path'}} + + +class StoragePoolsModel(object): + def __init__(self, **kargs): + self.conn = kargs['conn'] + self.objstore = kargs['objstore'] + self.scanner = Scanner(self._clean_scan) + self.scanner.delete() + + def get_list(self): + try: + conn = self.conn.get() + names = conn.listStoragePools() + names += conn.listDefinedStoragePools() + return sorted(names) + except libvirt.libvirtError as e: + raise OperationFailed(e.get_error_message()) + + def create(self, params): + task_id = None + conn = self.conn.get() + try: + name = params['name'] + if name in (ISO_POOL_NAME, ): + raise InvalidOperation("StoragePool already exists") + + if params['type'] == 'kimchi-iso': + task_id = self._do_deep_scan(params) + poolDef = StoragePoolDef.create(params) + poolDef.prepare(conn) + xml = poolDef.xml + except KeyError, key: + raise MissingParameter(key) + + if name in self.get_list(): + err = "The name %s has been used by a pool" + raise InvalidOperation(err % name) + + try: + if task_id: + # Create transient pool for deep scan + conn.storagePoolCreateXML(xml, 0) + return name + + pool = conn.storagePoolDefineXML(xml, 0) + if params['type'] in ['logical', 'dir', 'netfs']: + pool.build(libvirt.VIR_STORAGE_POOL_BUILD_NEW) + # autostart dir and logical storage pool created from kimchi + pool.setAutostart(1) + else: + # disable autostart for others + pool.setAutostart(0) + except libvirt.libvirtError as e: + msg = "Problem creating Storage Pool: %s" + kimchi_log.error(msg, e) + raise OperationFailed(e.get_error_message()) + return name + + def _clean_scan(self, pool_name): + try: + conn = self.conn.get() + pool = conn.storagePoolLookupByName(pool_name) + pool.destroy() + with self.objstore as session: + session.delete('scanning', pool_name) + except Exception, e: + err = "Exception %s occured when cleaning scan result" + kimchi_log.debug(err % e.message) + + def _do_deep_scan(self, params): + scan_params = dict(ignore_list=[]) + scan_params['scan_path'] = params['path'] + params['type'] = 'dir' + + for pool in self.get_list(): + try: + res = self.storagepool_lookup(pool) + if res['state'] == 'active': + scan_params['ignore_list'].append(res['path']) + except Exception, e: + err = "Exception %s occured when get ignore path" + kimchi_log.debug(err % e.message) + + params['path'] = self.scanner.scan_dir_prepare(params['name']) + scan_params['pool_path'] = params['path'] + task_id = add_task('', self.scanner.start_scan, self.objstore, + scan_params) + # Record scanning-task/storagepool mapping for future querying + with self.objstore as session: + session.store('scanning', params['name'], task_id) + return task_id + + +class StoragePoolModel(object): + def __init__(self, **kargs): + self.conn = kargs['conn'] + self.objstore = kargs['objstore'] + + @staticmethod + def get_storagepool(name, conn): + conn = conn.get() + try: + return conn.storagePoolLookupByName(name) + except libvirt.libvirtError as e: + if e.get_error_code() == libvirt.VIR_ERR_NO_STORAGE_POOL: + raise NotFoundError("Storage Pool '%s' not found" % name) + else: + raise + + def _get_storagepool_vols_num(self, pool): + try: + if pool.isActive(): + pool.refresh(0) + return pool.numOfVolumes() + else: + return 0 + except libvirt.libvirtError as e: + raise OperationFailed(e.get_error_message()) + + def _get_storage_source(self, pool_type, pool_xml): + source = {} + if pool_type not in STORAGE_SOURCES: + return source + + for key, val in STORAGE_SOURCES[pool_type].items(): + res = xmlutils.xpath_get_text(pool_xml, val) + source[key] = res[0] if len(res) == 1 else res + + return source + + def lookup(self, name): + pool = self.get_storagepool(name, self.conn) + info = pool.info() + nr_volumes = self._get_storagepool_vols_num(pool) + autostart = True if pool.autostart() else False + xml = pool.XMLDesc(0) + path = xmlutils.xpath_get_text(xml, "/pool/target/path")[0] + pool_type = xmlutils.xpath_get_text(xml, "/pool/@type")[0] + source = self._get_storage_source(pool_type, xml) + res = {'state': POOL_STATE_MAP[info[0]], + 'path': path, + 'source': source, + 'type': pool_type, + 'autostart': autostart, + 'capacity': info[1], + 'allocated': info[2], + 'available': info[3], + 'nr_volumes': nr_volumes} + + if not pool.isPersistent(): + # Deal with deep scan generated pool + try: + with self.objstore as session: + task_id = session.get('scanning', name) + res['task_id'] = str(task_id) + res['type'] = 'kimchi-iso' + except NotFoundError: + # User created normal pool + pass + return res + + def update(self, name, params): + autostart = params['autostart'] + if autostart not in [True, False]: + raise InvalidOperation("Autostart flag must be true or false") + pool = self.get_storagepool(name, self.conn) + if autostart: + pool.setAutostart(1) + else: + pool.setAutostart(0) + ident = pool.name() + return ident + + def activate(self, name): + pool = self.get_storagepool(name, self.conn) + try: + pool.create(0) + except libvirt.libvirtError as e: + raise OperationFailed(e.get_error_message()) + + def deactivate(self, name): + pool = self.get_storagepool(name, self.conn) + try: + pool.destroy() + except libvirt.libvirtError as e: + raise OperationFailed(e.get_error_message()) + + def delete(self, name): + pool = self.get_storagepool(name, self.conn) + if pool.isActive(): + err = "Unable to delete the active storagepool %s" + raise InvalidOperation(err % name) + try: + pool.undefine() + except libvirt.libvirtError as e: + raise OperationFailed(e.get_error_message()) + + +class IsoPoolModel(object): + def __init__(self, **kargs): + pass + + def lookup(self, name): + return {'state': 'active', + 'type': 'kimchi-iso'} diff --git a/src/kimchi/model/storageservers.py b/src/kimchi/model/storageservers.py new file mode 100644 index 0000000..6a7c14a --- /dev/null +++ b/src/kimchi/model/storageservers.py @@ -0,0 +1,78 @@ +# +# Project Kimchi +# +# Copyright IBM, Corp. 2013 +# +# Authors: +# Aline Manera <alinefm@linux.vnet.ibm.com> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +from kimchi.exception import NotFoundError +from kimchi.model.storagepools import StoragePoolModel, STORAGE_SOURCES + + +class StorageServersModel(object): + def __init__(self, **kargs): + self.conn = kargs['conn'] + self.pool = StoragePoolModel(**kargs) + + def get_list(self, _target_type=None): + if not _target_type: + target_type = STORAGE_SOURCES.keys() + else: + target_type = [_target_type] + pools = self.pools.get_list() + + conn = self.conn.get() + pools = conn.listStoragePools() + pools += conn.listDefinedStoragePools() + + server_list = [] + for pool in pools: + try: + pool_info = self.pool.lookup(pool) + if (pool_info['type'] in target_type and + pool_info['source']['addr'] not in server_list): + # Avoid to add same server for multiple times + # if it hosts more than one storage type + server_list.append(pool_info['source']['addr']) + except NotFoundError: + pass + + return server_list + + +class StorageServerModel(object): + def __init__(self, **kargs): + self.conn = kargs['conn'] + self.pool = StoragePoolModel(**kargs) + + def lookup(self, server): + conn = self.conn.get() + pools = conn.listStoragePools() + pools += conn.listDefinedStoragePools() + for pool in pools: + try: + pool_info = self.pool.lookup(pool) + if pool_info['source'] and \ + pool_info['source']['addr'] == server: + return dict(host=server) + except NotFoundError: + # Avoid inconsistent pool result because of lease between list + # lookup + pass + + raise NotFoundError('server %s does not used by kimchi' % server) diff --git a/src/kimchi/model/storagetargets.py b/src/kimchi/model/storagetargets.py new file mode 100644 index 0000000..be7cdaf --- /dev/null +++ b/src/kimchi/model/storagetargets.py @@ -0,0 +1,86 @@ +# +# Project Kimchi +# +# Copyright IBM, Corp. 2013 +# +# Authors: +# Aline Manera <alinefm@linux.vnet.ibm.com> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +import libvirt +import lxml.etree as ET +from lxml import objectify +from lxml.builder import E + +from kimchi.model.config import CapabilitiesModel +from kimchi.model.storagepools import STORAGE_SOURCES +from kimchi.utils import kimchi_log, patch_find_nfs_target + + +class StorageTargetsModel(object): + def __init__(self, **kargs): + self.conn = kargs['conn'] + self.caps = CapabilitiesModel() + + def get_list(self, storage_server, _target_type=None): + target_list = list() + + if not _target_type: + target_types = STORAGE_SOURCES.keys() + else: + target_types = [_target_type] + + for target_type in target_types: + if not self.caps.nfs_target_probe and target_type == 'netfs': + targets = patch_find_nfs_target(storage_server) + else: + xml = self._get_storage_server_spec(server=storage_server, + target_type=target_type) + conn = self.conn.get() + try: + ret = conn.findStoragePoolSources(target_type, xml, 0) + except libvirt.libvirtError as e: + err = "Query storage pool source fails because of %s" + kimchi_log.warning(err, e.get_error_message()) + continue + + targets = self._parse_target_source_result(target_type, ret) + + target_list.extend(targets) + return target_list + + def _get_storage_server_spec(**kwargs): + # Required parameters: + # server: + # target_type: + extra_args = [] + if kwargs['target_type'] == 'netfs': + extra_args.append(E.format(type='nfs')) + obj = E.source(E.host(name=kwargs['server']), *extra_args) + xml = ET.tostring(obj) + return xml + + def _parse_target_source_result(target_type, xml_str): + root = objectify.fromstring(xml_str) + ret = [] + for source in root.getchildren(): + if target_type == 'netfs': + host_name = source.host.get('name') + target_path = source.dir.get('path') + type = source.format.get('type') + ret.append(dict(host=host_name, target_type=type, + target=target_path)) + return ret diff --git a/src/kimchi/model/storagevolumes.py b/src/kimchi/model/storagevolumes.py new file mode 100644 index 0000000..7ff570b --- /dev/null +++ b/src/kimchi/model/storagevolumes.py @@ -0,0 +1,176 @@ +# +# Project Kimchi +# +# Copyright IBM, Corp. 2013 +# +# Authors: +# Aline Manera <alinefm@linux.vnet.ibm.com> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +import os + +import libvirt + +from kimchi import xmlutils +from kimchi.exception import InvalidOperation, IsoFormatError +from kimchi.exception import MissingParameter, NotFoundError, OperationFailed +from kimchi.isoinfo import IsoImage +from kimchi.model.storagepools import StoragePoolModel + + +VOLUME_TYPE_MAP = {0: 'file', + 1: 'block', + 2: 'directory', + 3: 'network'} + + +class StorageVolumesModel(object): + def __init__(self, **kargs): + self.conn = kargs['conn'] + + def create(self, pool, params): + vol_xml = """ + <volume> + <name>%(name)s</name> + <allocation unit="MiB">%(allocation)s</allocation> + <capacity unit="MiB">%(capacity)s</capacity> + <source> + </source> + <target> + <format type='%(format)s'/> + </target> + </volume> + """ + params.setdefault('allocation', 0) + params.setdefault('format', 'qcow2') + + try: + pool = StoragePoolModel.get_storagepool(pool, self.conn) + name = params['name'] + xml = vol_xml % params + except KeyError, key: + raise MissingParameter(key) + + try: + pool.createXML(xml, 0) + except libvirt.libvirtError as e: + raise OperationFailed(e.get_error_message()) + return name + + def get_list(self, pool): + pool = StoragePoolModel.get_storagepool(pool, self.conn) + if not pool.isActive(): + err = "Unable to list volumes in inactive storagepool %s" + raise InvalidOperation(err % pool.name()) + try: + pool.refresh(0) + return pool.listVolumes() + except libvirt.libvirtError as e: + raise OperationFailed(e.get_error_message()) + + +class StorageVolumeModel(object): + def __init__(self, **kargs): + self.conn = kargs['conn'] + + def _get_storagevolume(self, pool, name): + pool = StoragePoolModel.get_storagepool(pool, self.conn) + if not pool.isActive(): + err = "Unable to list volumes in inactive storagepool %s" + raise InvalidOperation(err % pool.name()) + try: + return pool.storageVolLookupByName(name) + except libvirt.libvirtError as e: + if e.get_error_code() == libvirt.VIR_ERR_NO_STORAGE_VOL: + raise NotFoundError("Storage Volume '%s' not found" % name) + else: + raise + + def lookup(self, pool, name): + vol = self._get_storagevolume(pool, name) + path = vol.path() + info = vol.info() + xml = vol.XMLDesc(0) + fmt = xmlutils.xpath_get_text(xml, "/volume/target/format/@type")[0] + res = dict(type=VOLUME_TYPE_MAP[info[0]], + capacity=info[1], + allocation=info[2], + path=path, + format=fmt) + if fmt == 'iso': + if os.path.islink(path): + path = os.path.join(os.path.dirname(path), os.readlink(path)) + os_distro = os_version = 'unknown' + try: + iso_img = IsoImage(path) + os_distro, os_version = iso_img.probe() + bootable = True + except IsoFormatError: + bootable = False + res.update( + dict(os_distro=os_distro, os_version=os_version, path=path, + bootable=bootable)) + + return res + + def wipe(self, pool, name): + volume = self._get_storagevolume(pool, name) + try: + volume.wipePattern(libvirt.VIR_STORAGE_VOL_WIPE_ALG_ZERO, 0) + except libvirt.libvirtError as e: + raise OperationFailed(e.get_error_message()) + + def delete(self, pool, name): + volume = self._get_storagevolume(pool, name) + try: + volume.delete(0) + except libvirt.libvirtError as e: + raise OperationFailed(e.get_error_message()) + + def resize(self, pool, name, size): + size = size << 20 + volume = self._get_storagevolume(pool, name) + try: + volume.resize(size, 0) + except libvirt.libvirtError as e: + raise OperationFailed(e.get_error_message()) + + +class IsoVolumesModel(object): + def __init__(self, **kargs): + self.conn = kargs['conn'] + self.storagevolume = StorageVolumeModel(**kargs) + + def get_list(self): + iso_volumes = [] + conn = self.conn.get() + pools = conn.listStoragePools() + pools += conn.listDefinedStoragePools() + + for pool in pools: + try: + pool.refresh(0) + volumes = pool.listVolumes() + except InvalidOperation: + # Skip inactive pools + continue + + for volume in volumes: + res = self.storagevolume.lookup(pool, volume) + if res['format'] == 'iso': + res['name'] = '%s' % volume + iso_volumes.append(res) + return iso_volumes diff --git a/src/kimchi/model/tasks.py b/src/kimchi/model/tasks.py new file mode 100644 index 0000000..40ca1d6 --- /dev/null +++ b/src/kimchi/model/tasks.py @@ -0,0 +1,39 @@ +# +# Project Kimchi +# +# Copyright IBM, Corp. 2013 +# +# Authors: +# Aline Manera <alinefm@linux.vnet.ibm.com> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + + +class TasksModel(object): + def __init__(self, **kargs): + self.objstore = kargs['objstore'] + + def get_list(self): + with self.objstore as session: + return session.get_list('task') + + +class TaskModel(object): + def __init__(self, **kargs): + self.objstore = kargs['objstore'] + + def lookup(self, id): + with self.objstore as session: + return session.get('task', str(id)) diff --git a/src/kimchi/model/templates.py b/src/kimchi/model/templates.py new file mode 100644 index 0000000..03632a6 --- /dev/null +++ b/src/kimchi/model/templates.py @@ -0,0 +1,172 @@ +# +# Project Kimchi +# +# Copyright IBM, Corp. 2013 +# +# Authors: +# Aline Manera <alinefm@linux.vnet.ibm.com> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +import copy + +import libvirt + +from kimchi import xmlutils +from kimchi.exception import InvalidOperation, InvalidParameter, NotFoundError +from kimchi.utils import pool_name_from_uri +from kimchi.vmtemplate import VMTemplate + + +class TemplatesModel(object): + def __init__(self, **kargs): + self.objstore = kargs['objstore'] + self.conn = kargs['conn'] + + def create(self, params): + name = params['name'] + conn = self.conn.get() + + pool_uri = params.get(u'storagepool', '') + if pool_uri: + pool_name = pool_name_from_uri(pool_uri) + try: + conn.storagePoolLookupByName(pool_name) + except Exception as e: + err = "Storagepool specified is not valid: %s." + raise InvalidParameter(err % e.message) + + for net_name in params.get(u'networks', []): + try: + conn.networkLookupByName(net_name) + except Exception, e: + raise InvalidParameter("Network '%s' specified by template " + "does not exist." % net_name) + + with self.objstore as session: + if name in session.get_list('template'): + raise InvalidOperation("Template already exists") + t = LibvirtVMTemplate(params, scan=True) + session.store('template', name, t.info) + return name + + def get_list(self): + with self.objstore as session: + return session.get_list('template') + + +class TemplateModel(object): + def __init__(self, **kargs): + self.objstore = kargs['objstore'] + self.conn = kargs['conn'] + self.templates = TemplatesModel(**kargs) + + @staticmethod + def get_template(name, objstore, conn, overrides=None): + with objstore as session: + params = session.get('template', name) + if overrides: + params.update(overrides) + return LibvirtVMTemplate(params, False, conn) + + def lookup(self, name): + t = self.get_template(name, self.objstore, self.conn) + return t.info + + def delete(self, name): + with self.objstore as session: + session.delete('template', name) + + def update(self, name, params): + old_t = self.lookup(name) + new_t = copy.copy(old_t) + new_t.update(params) + ident = name + + pool_uri = new_t.get(u'storagepool', '') + pool_name = pool_name_from_uri(pool_uri) + try: + conn = self.conn.get() + conn.storagePoolLookupByName(pool_name) + except Exception as e: + err = "Storagepool specified is not valid: %s." + raise InvalidParameter(err % e.message) + + for net_name in params.get(u'networks', []): + try: + conn = self.conn.get() + conn.networkLookupByName(net_name) + except Exception, e: + raise InvalidParameter("Network '%s' specified by template " + "does not exist" % net_name) + + self.delete(name) + try: + ident = self.templates.create(new_t) + except: + ident = self.templates.create(old_t) + raise + return ident + + +class LibvirtVMTemplate(VMTemplate): + def __init__(self, args, scan=False, conn=None): + VMTemplate.__init__(self, args, scan) + self.conn = conn + + def _storage_validate(self): + pool_uri = self.info['storagepool'] + pool_name = pool_name_from_uri(pool_uri) + try: + conn = self.conn.get() + pool = conn.storagePoolLookupByName(pool_name) + except libvirt.libvirtError: + err = 'Storage specified by template does not exist' + raise InvalidParameter(err) + + if not pool.isActive(): + err = 'Storage specified by template is not active' + raise InvalidParameter(err) + + return pool + + def _network_validate(self): + names = self.info['networks'] + for name in names: + try: + conn = self.conn.get() + network = conn.networkLookupByName(name) + except libvirt.libvirtError: + err = 'Network specified by template does not exist' + raise InvalidParameter(err) + + if not network.isActive(): + err = 'Network specified by template is not active' + raise InvalidParameter(err) + + def _get_storage_path(self): + pool = self._storage_validate() + xml = pool.XMLDesc(0) + return xmlutils.xpath_get_text(xml, "/pool/target/path")[0] + + def fork_vm_storage(self, vm_uuid): + # Provision storage: + # TODO: Rebase on the storage API once upstream + pool = self._storage_validate() + vol_list = self.to_volume_list(vm_uuid) + for v in vol_list: + # outgoing text to libvirt, encode('utf-8') + pool.createXML(v['xml'].encode('utf-8'), 0) + return vol_list diff --git a/src/kimchi/model/utils.py b/src/kimchi/model/utils.py new file mode 100644 index 0000000..a27b867 --- /dev/null +++ b/src/kimchi/model/utils.py @@ -0,0 +1,33 @@ +# +# Project Kimchi +# +# Copyright IBM, Corp. 2013 +# +# Authors: +# Aline Manera <alinefm@linux.vnet.ibm.com> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +from kimchi.exception import OperationFailed + + +def get_vm_name(vm_name, t_name, name_list): + if vm_name: + return vm_name + for i in xrange(1, 1000): + vm_name = "%s-vm-%i" % (t_name, i) + if vm_name not in name_list: + return vm_name + raise OperationFailed("Unable to choose a VM name") diff --git a/src/kimchi/model/vmifaces.py b/src/kimchi/model/vmifaces.py new file mode 100644 index 0000000..f3eddb2 --- /dev/null +++ b/src/kimchi/model/vmifaces.py @@ -0,0 +1,135 @@ +# +# Project Kimchi +# +# Copyright IBM, Corp. 2013 +# +# Authors: +# Aline Manera <alinefm@linux.vnet.ibm.com> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +import random + +import libvirt +from lxml import etree, objectify +from lxml.builder import E + +from kimchi.exception import InvalidOperation, InvalidParameter, NotFoundError +from kimchi.model.vms import DOM_STATE_MAP, VMModel + + +class VMIfacesModel(object): + def __init__(self, **kargs): + self.conn = kargs['conn'] + + def get_list(self, vm): + macs = [] + for iface in self.get_vmifaces(vm, self.conn): + macs.append(iface.mac.get('address')) + return macs + + def create(self, vm, params): + def randomMAC(): + mac = [0x52, 0x54, 0x00, + random.randint(0x00, 0x7f), + random.randint(0x00, 0xff), + random.randint(0x00, 0xff)] + return ':'.join(map(lambda x: "%02x" % x, mac)) + + conn = self.conn.get() + networks = conn.listNetworks() + conn.listDefinedNetworks() + + if params["type"] == "network" and params["network"] not in networks: + raise InvalidParameter("%s is not an available network" % + params["network"]) + + dom = VMModel.get_vm(vm, self.conn) + if DOM_STATE_MAP[dom.info()[0]] != "shutoff": + raise InvalidOperation("do not support hot plugging attach " + "guest interface") + + macs = (iface.mac.get('address') + for iface in self.get_vmifaces(vm, self.conn)) + + mac = randomMAC() + while True: + if mac not in macs: + break + mac = randomMAC() + + children = [E.mac(address=mac)] + ("network" in params.keys() and + children.append(E.source(network=params['network']))) + ("model" in params.keys() and + children.append(E.model(type=params['model']))) + attrib = {"type": params["type"]} + + xml = etree.tostring(E.interface(*children, **attrib)) + + dom.attachDeviceFlags(xml, libvirt.VIR_DOMAIN_AFFECT_CURRENT) + + return mac + + @staticmethod + def get_vmifaces(vm, conn): + dom = VMModel.get_vm(vm, conn) + xml = dom.XMLDesc(0) + root = objectify.fromstring(xml) + + return root.devices.findall("interface") + + +class VMIfaceModel(object): + def __init__(self, **kargs): + self.conn = kargs['conn'] + + def _get_vmiface(self, vm, mac): + ifaces = VMIfacesModel.get_vmifaces(vm, self.conn) + + for iface in ifaces: + if iface.mac.get('address') == mac: + return iface + return None + + def lookup(self, vm, mac): + info = {} + + iface = self._get_vmiface(vm, mac) + if iface is None: + raise NotFoundError('iface: "%s"' % mac) + + info['type'] = iface.attrib['type'] + info['mac'] = iface.mac.get('address') + if iface.find("model") is not None: + info['model'] = iface.model.get('type') + if info['type'] == 'network': + info['network'] = iface.source.get('network') + if info['type'] == 'bridge': + info['bridge'] = iface.source.get('bridge') + + return info + + def delete(self, vm, mac): + dom = VMModel.get_vm(vm, self.conn) + iface = self._get_vmiface(vm, mac) + + if DOM_STATE_MAP[dom.info()[0]] != "shutoff": + raise InvalidOperation("do not support hot plugging detach " + "guest interface") + if iface is None: + raise NotFoundError('iface: "%s"' % mac) + + dom.detachDeviceFlags(etree.tostring(iface), + libvirt.VIR_DOMAIN_AFFECT_CURRENT) diff --git a/src/kimchi/model/vms.py b/src/kimchi/model/vms.py new file mode 100644 index 0000000..e62f886 --- /dev/null +++ b/src/kimchi/model/vms.py @@ -0,0 +1,450 @@ +# +# Project Kimchi +# +# Copyright IBM, Corp. 2013 +# +# Authors: +# Aline Manera <alinefm@linux.vnet.ibm.com> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +import os +import time +import uuid +from xml.etree import ElementTree + +import libvirt +from cherrypy.process.plugins import BackgroundTask + +from kimchi import vnc +from kimchi import xmlutils +from kimchi.exception import InvalidOperation, InvalidParameter +from kimchi.exception import NotFoundError, OperationFailed +from kimchi.model.config import CapabilitiesModel +from kimchi.model.templates import TemplateModel +from kimchi.model.utils import get_vm_name +from kimchi.screenshot import VMScreenshot +from kimchi.utils import template_name_from_uri + + +DOM_STATE_MAP = {0: 'nostate', + 1: 'running', + 2: 'blocked', + 3: 'paused', + 4: 'shutdown', + 5: 'shutoff', + 6: 'crashed'} + +GUESTS_STATS_INTERVAL = 5 +VM_STATIC_UPDATE_PARAMS = {'name': './name'} +VM_LIVE_UPDATE_PARAMS = {} + +stats = {} + + +class VMsModel(object): + def __init__(self, **kargs): + self.conn = kargs['conn'] + self.objstore = kargs['objstore'] + self.caps = CapabilitiesModel() + self.guests_stats_thread = BackgroundTask(GUESTS_STATS_INTERVAL, + self._update_guests_stats) + self.guests_stats_thread.start() + + def _update_guests_stats(self): + vm_list = self.get_list() + + for name in vm_list: + dom = VMModel.get_vm(name, self.conn) + vm_uuid = dom.UUIDString() + info = dom.info() + state = DOM_STATE_MAP[info[0]] + + if state != 'running': + stats[vm_uuid] = {} + continue + + if stats.get(vm_uuid, None) is None: + stats[vm_uuid] = {} + + timestamp = time.time() + prevStats = stats.get(vm_uuid, {}) + seconds = timestamp - prevStats.get('timestamp', 0) + stats[vm_uuid].update({'timestamp': timestamp}) + + self._get_percentage_cpu_usage(vm_uuid, info, seconds) + self._get_network_io_rate(vm_uuid, dom, seconds) + self._get_disk_io_rate(vm_uuid, dom, seconds) + + def _get_percentage_cpu_usage(self, vm_uuid, info, seconds): + prevCpuTime = stats[vm_uuid].get('cputime', 0) + + cpus = info[3] + cpuTime = info[4] - prevCpuTime + + base = (((cpuTime) * 100.0) / (seconds * 1000.0 * 1000.0 * 1000.0)) + percentage = max(0.0, min(100.0, base / cpus)) + + stats[vm_uuid].update({'cputime': info[4], 'cpu': percentage}) + + def _get_network_io_rate(self, vm_uuid, dom, seconds): + prevNetRxKB = stats[vm_uuid].get('netRxKB', 0) + prevNetTxKB = stats[vm_uuid].get('netTxKB', 0) + currentMaxNetRate = stats[vm_uuid].get('max_net_io', 100) + + rx_bytes = 0 + tx_bytes = 0 + + tree = ElementTree.fromstring(dom.XMLDesc(0)) + for target in tree.findall('devices/interface/target'): + dev = target.get('dev') + io = dom.interfaceStats(dev) + rx_bytes += io[0] + tx_bytes += io[4] + + netRxKB = float(rx_bytes) / 1000 + netTxKB = float(tx_bytes) / 1000 + + rx_stats = (netRxKB - prevNetRxKB) / seconds + tx_stats = (netTxKB - prevNetTxKB) / seconds + + rate = rx_stats + tx_stats + max_net_io = round(max(currentMaxNetRate, int(rate)), 1) + + stats[vm_uuid].update({'net_io': rate, 'max_net_io': max_net_io, + 'netRxKB': netRxKB, 'netTxKB': netTxKB}) + + def _get_disk_io_rate(self, vm_uuid, dom, seconds): + prevDiskRdKB = stats[vm_uuid].get('diskRdKB', 0) + prevDiskWrKB = stats[vm_uuid].get('diskWrKB', 0) + currentMaxDiskRate = stats[vm_uuid].get('max_disk_io', 100) + + rd_bytes = 0 + wr_bytes = 0 + + tree = ElementTree.fromstring(dom.XMLDesc(0)) + for target in tree.findall("devices/disk/target"): + dev = target.get("dev") + io = dom.blockStats(dev) + rd_bytes += io[1] + wr_bytes += io[3] + + diskRdKB = float(rd_bytes) / 1024 + diskWrKB = float(wr_bytes) / 1024 + + rd_stats = (diskRdKB - prevDiskRdKB) / seconds + wr_stats = (diskWrKB - prevDiskWrKB) / seconds + + rate = rd_stats + wr_stats + max_disk_io = round(max(currentMaxDiskRate, int(rate)), 1) + + stats[vm_uuid].update({'disk_io': rate, + 'max_disk_io': max_disk_io, + 'diskRdKB': diskRdKB, + 'diskWrKB': diskWrKB}) + + def create(self, params): + conn = self.conn.get() + t_name = template_name_from_uri(params['template']) + vm_uuid = str(uuid.uuid4()) + vm_list = self.get_list() + name = get_vm_name(params.get('name'), t_name, vm_list) + # incoming text, from js json, is unicode, do not need decode + if name in vm_list: + raise InvalidOperation("VM already exists") + + vm_overrides = dict() + pool_uri = params.get('storagepool') + if pool_uri: + vm_overrides['storagepool'] = pool_uri + t = TemplateModel.get_template(t_name, self.objstore, self.conn, + vm_overrides) + + if not self.caps.qemu_stream and t.info.get('iso_stream', False): + err = "Remote ISO image is not supported by this server." + raise InvalidOperation(err) + + t.validate() + vol_list = t.fork_vm_storage(vm_uuid) + + # Store the icon for displaying later + icon = t.info.get('icon') + if icon: + with self.objstore as session: + session.store('vm', vm_uuid, {'icon': icon}) + + libvirt_stream = False + if len(self.caps.libvirt_stream_protocols) == 0: + libvirt_stream = True + + graphics = params.get('graphics') + xml = t.to_vm_xml(name, vm_uuid, + libvirt_stream=libvirt_stream, + qemu_stream_dns=self.caps.qemu_stream_dns, + graphics=graphics) + + try: + conn.defineXML(xml.encode('utf-8')) + except libvirt.libvirtError as e: + for v in vol_list: + vol = conn.storageVolLookupByPath(v['path']) + vol.delete(0) + raise OperationFailed(e.get_error_message()) + + return name + + def get_list(self): + conn = self.conn.get() + ids = conn.listDomainsID() + names = map(lambda x: conn.lookupByID(x).name(), ids) + names += conn.listDefinedDomains() + names = map(lambda x: x.decode('utf-8'), names) + return sorted(names, key=unicode.lower) + + +class VMModel(object): + def __init__(self, **kargs): + self.conn = kargs['conn'] + self.objstore = kargs['objstore'] + self.vms = VMsModel(**kargs) + self.vmscreenshot = VMScreenshotModel(**kargs) + + def update(self, name, params): + dom = self.get_vm(name, self.conn) + dom = self._static_vm_update(dom, params) + self._live_vm_update(dom, params) + return dom.name() + + def _static_vm_update(self, dom, params): + state = DOM_STATE_MAP[dom.info()[0]] + + old_xml = new_xml = dom.XMLDesc(0) + + for key, val in params.items(): + if key in VM_STATIC_UPDATE_PARAMS: + xpath = VM_STATIC_UPDATE_PARAMS[key] + new_xml = xmlutils.xml_item_update(new_xml, xpath, val) + + try: + if 'name' in params: + if state == 'running': + err = "VM name only can be updated when vm is powered off." + raise InvalidParameter(err) + else: + dom.undefine() + conn = self.conn.get() + dom = conn.defineXML(new_xml) + except libvirt.libvirtError as e: + dom = conn.defineXML(old_xml) + raise OperationFailed(e.get_error_message()) + return dom + + def _live_vm_update(self, dom, params): + pass + + def lookup(self, name): + dom = self.get_vm(name, self.conn) + info = dom.info() + state = DOM_STATE_MAP[info[0]] + screenshot = None + graphics = self._vm_get_graphics(name) + graphics_type, graphics_listen, graphics_port = graphics + graphics_port = graphics_port if state == 'running' else None + try: + if state == 'running': + screenshot = self.vmscreenshot.lookup(name) + elif state == 'shutoff': + # reset vm stats when it is powered off to avoid sending + # incorrect (old) data + stats[dom.UUIDString()] = {} + except NotFoundError: + pass + + with self.objstore as session: + try: + extra_info = session.get('vm', dom.UUIDString()) + except NotFoundError: + extra_info = {} + icon = extra_info.get('icon') + + vm_stats = stats.get(dom.UUIDString(), {}) + res = {} + res['cpu_utilization'] = vm_stats.get('cpu', 0) + res['net_throughput'] = vm_stats.get('net_io', 0) + res['net_throughput_peak'] = vm_stats.get('max_net_io', 100) + res['io_throughput'] = vm_stats.get('disk_io', 0) + res['io_throughput_peak'] = vm_stats.get('max_disk_io', 100) + + return {'state': state, + 'stats': str(res), + 'uuid': dom.UUIDString(), + 'memory': info[2] >> 10, + 'cpus': info[3], + 'screenshot': screenshot, + 'icon': icon, + 'graphics': {"type": graphics_type, + "listen": graphics_listen, + "port": graphics_port} + } + + def _vm_get_disk_paths(self, dom): + xml = dom.XMLDesc(0) + xpath = "/domain/devices/disk[@device='disk']/source/@file" + return xmlutils.xpath_get_text(xml, xpath) + + def _vm_exists(self, name): + try: + self.get_vm(name, self.conn) + return True + except NotFoundError: + return False + except Exception, e: + err = "Unable to retrieve VM '%s': %s" + raise OperationFailed(err % (name, e.message)) + + @staticmethod + def get_vm(name, conn): + conn = conn.get() + try: + # outgoing text to libvirt, encode('utf-8') + return conn.lookupByName(name.encode("utf-8")) + except libvirt.libvirtError as e: + if e.get_error_code() == libvirt.VIR_ERR_NO_DOMAIN: + raise NotFoundError("Virtual Machine '%s' not found" % name) + else: + raise + + def delete(self, name): + if self._vm_exists(name): + conn = self.conn.get() + dom = self.get_vm(name, self.conn) + self._vmscreenshot_delete(dom.UUIDString()) + paths = self._vm_get_disk_paths(dom) + info = self.lookup(name) + + if info['state'] == 'running': + self.stop(name) + + dom.undefine() + + for path in paths: + vol = conn.storageVolLookupByPath(path) + vol.delete(0) + + with self.objstore as session: + session.delete('vm', dom.UUIDString(), ignore_missing=True) + + vnc.remove_proxy_token(name) + + def start(self, name): + dom = self.get_vm(name, self.conn) + dom.create() + + def stop(self, name): + if self._vm_exists(name): + dom = self.get_vm(name, self.conn) + dom.destroy() + + def _vm_get_graphics(self, name): + dom = self.get_vm(name, self.conn) + xml = dom.XMLDesc(0) + expr = "/domain/devices/graphics/@type" + res = xmlutils.xpath_get_text(xml, expr) + graphics_type = res[0] if res else None + expr = "/domain/devices/graphics/@listen" + res = xmlutils.xpath_get_text(xml, expr) + graphics_listen = res[0] if res else None + graphics_port = None + if graphics_type: + expr = "/domain/devices/graphics[@type='%s']/@port" % graphics_type + res = xmlutils.xpath_get_text(xml, expr) + graphics_port = int(res[0]) if res else None + return graphics_type, graphics_listen, graphics_port + + def connect(self, name): + graphics = self._vm_get_graphics(name) + graphics_type, graphics_listen, graphics_port = graphics + if graphics_port is not None: + vnc.add_proxy_token(name, graphics_port) + else: + raise OperationFailed("Only able to connect to running vm's vnc " + "graphics.") + + def _vmscreenshot_delete(self, vm_uuid): + screenshot = VMScreenshotModel.get_screenshot(vm_uuid, self.objstore, + self.conn) + screenshot.delete() + with self.objstore as session: + session.delete('screenshot', vm_uuid) + + +class VMScreenshotModel(object): + def __init__(self, **kargs): + self.objstore = kargs['objstore'] + self.conn = kargs['conn'] + + def lookup(self, name): + dom = VMModel.get_vm(name, self.conn) + d_info = dom.info() + vm_uuid = dom.UUIDString() + if DOM_STATE_MAP[d_info[0]] != 'running': + raise NotFoundError('No screenshot for stopped vm') + + screenshot = self.get_screenshot(vm_uuid, self.objstore, self.conn) + img_path = screenshot.lookup() + # screenshot info changed after scratch generation + with self.objstore as session: + session.store('screenshot', vm_uuid, screenshot.info) + return img_path + + @staticmethod + def get_screenshot(vm_uuid, objstore, conn): + with objstore as session: + try: + params = session.get('screenshot', vm_uuid) + except NotFoundError: + params = {'uuid': vm_uuid} + session.store('screenshot', vm_uuid, params) + return LibvirtVMScreenshot(params, conn) + + +class LibvirtVMScreenshot(VMScreenshot): + def __init__(self, vm_uuid, conn): + VMScreenshot.__init__(self, vm_uuid) + self.conn = conn + + def _generate_scratch(self, thumbnail): + def handler(stream, buf, opaque): + fd = opaque + os.write(fd, buf) + + fd = os.open(thumbnail, os.O_WRONLY | os.O_TRUNC | os.O_CREAT, 0644) + try: + conn = self.conn.get() + dom = conn.lookupByUUIDString(self.vm_uuid) + vm_name = dom.name() + stream = conn.newStream(0) + dom.screenshot(stream, 0, 0) + stream.recvAll(handler, fd) + except libvirt.libvirtError: + try: + stream.abort() + except: + pass + raise NotFoundError("Screenshot not supported for %s" % vm_name) + else: + stream.finish() + finally: + os.close(fd) diff --git a/src/kimchi/model_/__init__.py b/src/kimchi/model_/__init__.py deleted file mode 100644 index 8a37cc4..0000000 --- a/src/kimchi/model_/__init__.py +++ /dev/null @@ -1,21 +0,0 @@ -# -# Project Kimchi -# -# Copyright IBM, Corp. 2013 -# -# Authors: -# Aline Manera <alinefm@linux.vnet.ibm.com> -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library 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 -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA diff --git a/src/kimchi/model_/config.py b/src/kimchi/model_/config.py deleted file mode 100644 index 933e37f..0000000 --- a/src/kimchi/model_/config.py +++ /dev/null @@ -1,87 +0,0 @@ -# -# Project Kimchi -# -# Copyright IBM, Corp. 2013 -# -# Authors: -# Aline Manera <alinefm@linux.vnet.ibm.com> -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library 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 -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - -import cherrypy - -from kimchi.basemodel import Singleton -from kimchi.distroloader import DistroLoader -from kimchi.exception import NotFoundError -from kimchi.featuretests import FeatureTests -from kimchi.model_.debugreports import DebugReportsModel -from kimchi.screenshot import VMScreenshot -from kimchi.utils import kimchi_log - - -class CapabilitiesModel(object): - __metaclass__ = Singleton - - def __init__(self, **kargs): - self.qemu_stream = False - self.qemu_stream_dns = False - self.libvirt_stream_protocols = [] - - # Subscribe function to set host capabilities to be run when cherrypy - # server is up - # It is needed because some features tests depends on the server - cherrypy.engine.subscribe('start', self._set_capabilities) - - def _set_capabilities(self): - kimchi_log.info("*** Running feature tests ***") - self.qemu_stream = FeatureTests.qemu_supports_iso_stream() - self.qemu_stream_dns = FeatureTests.qemu_iso_stream_dns() - self.nfs_target_probe = FeatureTests.libvirt_support_nfs_probe() - - self.libvirt_stream_protocols = [] - for p in ['http', 'https', 'ftp', 'ftps', 'tftp']: - if FeatureTests.libvirt_supports_iso_stream(p): - self.libvirt_stream_protocols.append(p) - - kimchi_log.info("*** Feature tests completed ***") - _set_capabilities.priority = 90 - - def lookup(self, *ident): - report_tool = DebugReportsModel.get_system_report_tool() - - return {'libvirt_stream_protocols': self.libvirt_stream_protocols, - 'qemu_stream': self.qemu_stream, - 'screenshot': VMScreenshot.get_stream_test_result(), - 'system_report_tool': bool(report_tool)} - - -class DistrosModel(object): - def __init__(self, **kargs): - distroloader = DistroLoader() - self.distros = distroloader.get() - - def get_list(self): - return self.distros.keys() - - -class DistroModel(object): - def __init__(self, **kargs): - self._distros = DistrosModel() - - def lookup(self, name): - try: - return self._distros.distros[name] - except KeyError: - raise NotFoundError("Distro '%s' not found." % name) diff --git a/src/kimchi/model_/debugreports.py b/src/kimchi/model_/debugreports.py deleted file mode 100644 index 39402aa..0000000 --- a/src/kimchi/model_/debugreports.py +++ /dev/null @@ -1,167 +0,0 @@ -# -# Project Kimchi -# -# Copyright IBM, Corp. 2013 -# -# Authors: -# Aline Manera <alinefm@linux.vnet.ibm.com> -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library 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 -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - -import fnmatch -import glob -import logging -import os -import shutil -import subprocess -import time - -from kimchi import config -from kimchi.exception import NotFoundError, OperationFailed -from kimchi.model_.tasks import TaskModel -from kimchi.utils import add_task, kimchi_log - - -class DebugReportsModel(object): - def __init__(self, **kargs): - self.objstore = kargs['objstore'] - self.task = TaskModel(**kargs) - - def create(self, params): - ident = params['name'] - taskid = self._gen_debugreport_file(ident) - return self.task.lookup(taskid) - - def get_list(self): - path = config.get_debugreports_path() - file_pattern = os.path.join(path, '*.*') - file_lists = glob.glob(file_pattern) - file_lists = [os.path.split(file)[1] for file in file_lists] - name_lists = [file.split('.', 1)[0] for file in file_lists] - - return name_lists - - def _gen_debugreport_file(self, name): - gen_cmd = self.get_system_report_tool() - - if gen_cmd is not None: - return add_task('', gen_cmd, self.objstore, name) - - raise OperationFailed("debugreport tool not found") - - @staticmethod - def sosreport_generate(cb, name): - command = 'sosreport --batch --name "%s"' % name - try: - retcode = subprocess.call(command, shell=True, - stdout=subprocess.PIPE) - if retcode < 0: - raise OperationFailed('Command terminated with signal') - elif retcode > 0: - raise OperationFailed('Command failed: rc = %i' % retcode) - pattern = '/tmp/sosreport-%s-*' % name - for reportFile in glob.glob(pattern): - if not fnmatch.fnmatch(reportFile, '*.md5'): - output = reportFile - break - else: - # sosreport tends to change the name mangling rule and - # compression file format between different releases. - # It's possible to fail to match a report file even sosreport - # runs successfully. In future we might have a general name - # mangling function in kimchi to format the name before passing - # it to sosreport. Then we can delete this exception. - raise OperationFailed('Can not find generated debug report ' - 'named by %s' % pattern) - ext = output.split('.', 1)[1] - path = config.get_debugreports_path() - target = os.path.join(path, name) - target_file = '%s.%s' % (target, ext) - shutil.move(output, target_file) - os.remove('%s.md5' % output) - cb('OK', True) - - return - - except OSError: - raise - - except Exception, e: - # No need to call cb to update the task status here. - # The task object will catch the exception rasied here - # and update the task status there - log = logging.getLogger('Model') - log.warning('Exception in generating debug file: %s', e) - raise OperationFailed(e) - - @staticmethod - def get_system_report_tool(): - # Please add new possible debug report command here - # and implement the report generating function - # based on the new report command - report_tools = ({'cmd': 'sosreport --help', - 'fn': DebugReportsModel.sosreport_generate},) - - # check if the command can be found by shell one by one - for helper_tool in report_tools: - try: - retcode = subprocess.call(helper_tool['cmd'], shell=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - if retcode == 0: - return helper_tool['fn'] - except Exception, e: - kimchi_log.info('Exception running command: %s', e) - - return None - - -class DebugReportModel(object): - def __init__(self, **kargs): - pass - - def lookup(self, name): - path = config.get_debugreports_path() - file_pattern = os.path.join(path, name) - file_pattern = file_pattern + '.*' - try: - file_target = glob.glob(file_pattern)[0] - except IndexError: - raise NotFoundError('no such report') - - ctime = os.stat(file_target).st_ctime - ctime = time.strftime("%Y-%m-%d-%H:%M:%S", time.localtime(ctime)) - file_target = os.path.split(file_target)[-1] - file_target = os.path.join("/data/debugreports", file_target) - return {'file': file_target, - 'ctime': ctime} - - def delete(self, name): - path = config.get_debugreports_path() - file_pattern = os.path.join(path, name + '.*') - try: - file_target = glob.glob(file_pattern)[0] - except IndexError: - raise NotFoundError('no such report') - - os.remove(file_target) - - -class DebugReportContentModel(object): - def __init__(self, **kargs): - self._debugreport = DebugReportModel() - - def lookup(self, name): - return self._debugreport.lookup(name) diff --git a/src/kimchi/model_/host.py b/src/kimchi/model_/host.py deleted file mode 100644 index d5f92ef..0000000 --- a/src/kimchi/model_/host.py +++ /dev/null @@ -1,201 +0,0 @@ -# -# Project Kimchi -# -# Copyright IBM, Corp. 2013 -# -# Authors: -# Aline Manera <alinefm@linux.vnet.ibm.com> -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library 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 -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - -import os -import time -import platform -from collections import defaultdict - -import psutil -from cherrypy.process.plugins import BackgroundTask - -from kimchi import disks -from kimchi import netinfo -from kimchi.basemodel import Singleton -from kimchi.exception import NotFoundError, OperationFailed -from kimchi.model_.vms import DOM_STATE_MAP -from kimchi.utils import kimchi_log - - -HOST_STATS_INTERVAL = 1 - - -class HostModel(object): - def __init__(self, **kargs): - self.host_info = self._get_host_info() - - def _get_host_info(self): - res = {} - with open('/proc/cpuinfo') as f: - for line in f.xreadlines(): - if "model name" in line: - res['cpu'] = line.split(':')[1].strip() - break - - res['memory'] = psutil.TOTAL_PHYMEM - # 'fedora' '17' 'Beefy Miracle' - distro, version, codename = platform.linux_distribution() - res['os_distro'] = distro - res['os_version'] = version - res['os_codename'] = unicode(codename, "utf-8") - - return res - - def lookup(self, *name): - return self.host_info - - def shutdown(self, args=None): - # Check for running vms before shutdown - running_vms = self.vms_get_list_by_state('running') - if len(running_vms) > 0: - raise OperationFailed("Shutdown not allowed: VMs are running!") - kimchi_log.info('Host is going to shutdown.') - os.system('shutdown -h now') - - def reboot(self, args=None): - # Find running VMs - running_vms = self._get_vms_list_by_state('running') - if len(running_vms) > 0: - raise OperationFailed("Reboot not allowed: VMs are running!") - kimchi_log.info('Host is going to reboot.') - os.system('reboot') - - def _get_vms_list_by_state(self, state): - ret_list = [] - for name in self.vms_get_list(): - info = self._get_vm(name).info() - if (DOM_STATE_MAP[info[0]]) == state: - ret_list.append(name) - return ret_list - - -class HostStatsModel(object): - __metaclass__ = Singleton - - def __init__(self, **kargs): - self.host_stats = defaultdict(int) - self.host_stats_thread = BackgroundTask(HOST_STATS_INTERVAL, - self._update_host_stats) - self.host_stats_thread.start() - - def lookup(self, *name): - return {'cpu_utilization': self.host_stats['cpu_utilization'], - 'memory': self.host_stats.get('memory'), - 'disk_read_rate': self.host_stats['disk_read_rate'], - 'disk_write_rate': self.host_stats['disk_write_rate'], - 'net_recv_rate': self.host_stats['net_recv_rate'], - 'net_sent_rate': self.host_stats['net_sent_rate']} - - def _update_host_stats(self): - preTimeStamp = self.host_stats['timestamp'] - timestamp = time.time() - # FIXME when we upgrade psutil, we can get uptime by psutil.uptime - # we get uptime by float(open("/proc/uptime").readline().split()[0]) - # and calculate the first io_rate after the OS started. - seconds = (timestamp - preTimeStamp if preTimeStamp else - float(open("/proc/uptime").readline().split()[0])) - - self.host_stats['timestamp'] = timestamp - self._get_host_disk_io_rate(seconds) - self._get_host_network_io_rate(seconds) - - self._get_percentage_host_cpu_usage() - self._get_host_memory_stats() - - def _get_percentage_host_cpu_usage(self): - # This is cpu usage producer. This producer will calculate the usage - # at an interval of HOST_STATS_INTERVAL. - # The psutil.cpu_percent works as non blocking. - # psutil.cpu_percent maintains a cpu time sample. - # It will update the cpu time sample when it is called. - # So only this producer can call psutil.cpu_percent in kimchi. - self.host_stats['cpu_utilization'] = psutil.cpu_percent(None) - - def _get_host_memory_stats(self): - virt_mem = psutil.virtual_memory() - # available: - # the actual amount of available memory that can be given - # instantly to processes that request more memory in bytes; this - # is calculated by summing different memory values depending on - # the platform (e.g. free + buffers + cached on Linux) - memory_stats = {'total': virt_mem.total, - 'free': virt_mem.free, - 'cached': virt_mem.cached, - 'buffers': virt_mem.buffers, - 'avail': virt_mem.available} - self.host_stats['memory'] = memory_stats - - def _get_host_disk_io_rate(self, seconds): - prev_read_bytes = self.host_stats['disk_read_bytes'] - prev_write_bytes = self.host_stats['disk_write_bytes'] - - disk_io = psutil.disk_io_counters(False) - read_bytes = disk_io.read_bytes - write_bytes = disk_io.write_bytes - - rd_rate = int(float(read_bytes - prev_read_bytes) / seconds + 0.5) - wr_rate = int(float(write_bytes - prev_write_bytes) / seconds + 0.5) - - self.host_stats.update({'disk_read_rate': rd_rate, - 'disk_write_rate': wr_rate, - 'disk_read_bytes': read_bytes, - 'disk_write_bytes': write_bytes}) - - def _get_host_network_io_rate(self, seconds): - prev_recv_bytes = self.host_stats['net_recv_bytes'] - prev_sent_bytes = self.host_stats['net_sent_bytes'] - - net_ios = psutil.network_io_counters(True) - recv_bytes = 0 - sent_bytes = 0 - for key in set(netinfo.nics() + - netinfo.wlans()) & set(net_ios.iterkeys()): - recv_bytes = recv_bytes + net_ios[key].bytes_recv - sent_bytes = sent_bytes + net_ios[key].bytes_sent - - rx_rate = int(float(recv_bytes - prev_recv_bytes) / seconds + 0.5) - tx_rate = int(float(sent_bytes - prev_sent_bytes) / seconds + 0.5) - - self.host_stats.update({'net_recv_rate': rx_rate, - 'net_sent_rate': tx_rate, - 'net_recv_bytes': recv_bytes, - 'net_sent_bytes': sent_bytes}) - - -class PartitionsModel(object): - def __init__(self, **kargs): - pass - - def get_list(self): - result = disks.get_partitions_names() - return result - - -class PartitionModel(object): - def __init__(self, **kargs): - pass - - def lookup(self, name): - if name not in disks.get_partitions_names(): - raise NotFoundError("Partition %s not found in the host" - % name) - return disks.get_partition_details(name) diff --git a/src/kimchi/model_/interfaces.py b/src/kimchi/model_/interfaces.py deleted file mode 100644 index b3b6ccd..0000000 --- a/src/kimchi/model_/interfaces.py +++ /dev/null @@ -1,46 +0,0 @@ -# -# Project Kimchi -# -# Copyright IBM, Corp. 2013 -# -# Authors: -# Aline Manera <alinefm@linux.vnet.ibm.com> -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library 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 -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - -from kimchi import netinfo -from kimchi.exception import NotFoundError -from kimchi.model_.networks import NetworksModel - - -class InterfacesModel(object): - def __init__(self, **kargs): - self.conn = kargs['conn'] - self.networks = NetworksModel(**kargs) - - def get_list(self): - return list(set(netinfo.all_favored_interfaces()) - - set(self.networks.get_all_networks_interfaces())) - - -class InterfaceModel(object): - def __init__(self, **kargs): - pass - - def lookup(self, name): - try: - return netinfo.get_interface_info(name) - except ValueError, e: - raise NotFoundError(e) diff --git a/src/kimchi/model_/libvirtconnection.py b/src/kimchi/model_/libvirtconnection.py deleted file mode 100644 index 7bbb668..0000000 --- a/src/kimchi/model_/libvirtconnection.py +++ /dev/null @@ -1,122 +0,0 @@ -# -# Project Kimchi -# -# Copyright IBM, Corp. 2013 -# -# Authors: -# Aline Manera <alinefm@linux.vnet.ibm.com> -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library 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 -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - -import threading -import time - -import cherrypy -import libvirt - -from kimchi.utils import kimchi_log - - -class LibvirtConnection(object): - def __init__(self, uri): - self.uri = uri - self._connections = {} - self._connectionLock = threading.Lock() - self.wrappables = self.get_wrappable_objects() - - def get_wrappable_objects(self): - """ - When a wrapped function returns an instance of another libvirt object, - we also want to wrap that object so we can catch errors that happen - when calling its methods. - """ - objs = [] - for name in ('virDomain', 'virDomainSnapshot', 'virInterface', - 'virNWFilter', 'virNetwork', 'virNodeDevice', 'virSecret', - 'virStoragePool', 'virStorageVol', 'virStream'): - try: - attr = getattr(libvirt, name) - except AttributeError: - pass - objs.append(attr) - return tuple(objs) - - def get(self, conn_id=0): - """ - Return current connection to libvirt or open a new one. Wrap all - callable libvirt methods so we can catch connection errors and handle - them by restarting the server. - """ - def wrapMethod(f): - def wrapper(*args, **kwargs): - try: - ret = f(*args, **kwargs) - return ret - except libvirt.libvirtError as e: - edom = e.get_error_domain() - ecode = e.get_error_code() - EDOMAINS = (libvirt.VIR_FROM_REMOTE, - libvirt.VIR_FROM_RPC) - ECODES = (libvirt.VIR_ERR_SYSTEM_ERROR, - libvirt.VIR_ERR_INTERNAL_ERROR, - libvirt.VIR_ERR_NO_CONNECT, - libvirt.VIR_ERR_INVALID_CONN) - if edom in EDOMAINS and ecode in ECODES: - kimchi_log.error('Connection to libvirt broken. ' - 'Recycling. ecode: %d edom: %d' % - (ecode, edom)) - with self._connectionLock: - self._connections[conn_id] = None - raise - wrapper.__name__ = f.__name__ - wrapper.__doc__ = f.__doc__ - return wrapper - - with self._connectionLock: - conn = self._connections.get(conn_id) - if not conn: - retries = 5 - while True: - retries = retries - 1 - try: - conn = libvirt.open(self.uri) - break - except libvirt.libvirtError: - kimchi_log.error('Unable to connect to libvirt.') - if not retries: - err = 'Libvirt is not available, exiting.' - kimchi_log.error(err) - cherrypy.engine.stop() - raise - time.sleep(2) - - for name in dir(libvirt.virConnect): - method = getattr(conn, name) - if callable(method) and not name.startswith('_'): - setattr(conn, name, wrapMethod(method)) - - for cls in self.wrappables: - for name in dir(cls): - method = getattr(cls, name) - if callable(method) and not name.startswith('_'): - setattr(cls, name, wrapMethod(method)) - - self._connections[conn_id] = conn - # In case we're running into troubles with keeping the - # connections alive we should place here: - # conn.setKeepAlive(interval=5, count=3) - # However the values need to be considered wisely to not affect - # hosts which are hosting a lot of virtual machines - return conn diff --git a/src/kimchi/model_/libvirtstoragepool.py b/src/kimchi/model_/libvirtstoragepool.py deleted file mode 100644 index f4dbf2e..0000000 --- a/src/kimchi/model_/libvirtstoragepool.py +++ /dev/null @@ -1,257 +0,0 @@ -# -# Project Kimchi -# -# Copyright IBM, Corp. 2013 -# -# Authors: -# Aline Manera <alinefm@linux.vnet.ibm.com> -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library 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 -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - -import copy -import os -import tempfile - -import libvirt - -from kimchi.exception import InvalidParameter, OperationFailed, TimeoutExpired -from kimchi.iscsi import TargetClient -from kimchi.rollbackcontext import RollbackContext -from kimchi.utils import parse_cmd_output, run_command - - -class StoragePoolDef(object): - @classmethod - def create(cls, poolArgs): - for klass in cls.__subclasses__(): - if poolArgs['type'] == klass.poolType: - return klass(poolArgs) - raise OperationFailed('Unsupported pool type: %s' % poolArgs['type']) - - def __init__(self, poolArgs): - self.poolArgs = poolArgs - - def prepare(self, conn): - ''' Validate pool arguments and perform preparations. Operation which - would cause side effect should be put here. Subclasses can optionally - override this method, or it always succeeds by default. ''' - pass - - @property - def xml(self): - ''' Subclasses have to override this method to actually generate the - storage pool XML definition. Should cause no side effect and be - idempotent''' - # TODO: When add new pool type, should also add the related test in - # tests/test_storagepool.py - raise OperationFailed('self.xml is not implemented: %s' % self) - - -class DirPoolDef(StoragePoolDef): - poolType = 'dir' - - @property - def xml(self): - # Required parameters - # name: - # type: - # path: - xml = """ - <pool type='dir'> - <name>{name}</name> - <target> - <path>{path}</path> - </target> - </pool> - """.format(**self.poolArgs) - return xml - - -class NetfsPoolDef(StoragePoolDef): - poolType = 'netfs' - - def __init__(self, poolArgs): - super(NetfsPoolDef, self).__init__(poolArgs) - self.path = '/var/lib/kimchi/nfs_mount/' + self.poolArgs['name'] - - def prepare(self, conn): - mnt_point = tempfile.mkdtemp(dir='/tmp') - export_path = "%s:%s" % ( - self.poolArgs['source']['host'], self.poolArgs['source']['path']) - mount_cmd = ["mount", "-o", 'soft,timeo=100,retrans=3,retry=0', - export_path, mnt_point] - umount_cmd = ["umount", "-f", export_path] - mounted = False - - with RollbackContext() as rollback: - rollback.prependDefer(os.rmdir, mnt_point) - try: - run_command(mount_cmd, 30) - rollback.prependDefer(run_command, umount_cmd) - except TimeoutExpired: - err = "Export path %s may block during nfs mount" - raise InvalidParameter(err % export_path) - - with open("/proc/mounts", "rb") as f: - rawMounts = f.read() - output_items = ['dev_path', 'mnt_point', 'type'] - mounts = parse_cmd_output(rawMounts, output_items) - for item in mounts: - if 'dev_path' in item and item['dev_path'] == export_path: - mounted = True - - if not mounted: - err = "Export path %s mount failed during nfs mount" - raise InvalidParameter(err % export_path) - - @property - def xml(self): - # Required parameters - # name: - # type: - # source[host]: - # source[path]: - poolArgs = copy.deepcopy(self.poolArgs) - poolArgs['path'] = self.path - xml = """ - <pool type='netfs'> - <name>{name}</name> - <source> - <host name='{source[host]}'/> - <dir path='{source[path]}'/> - </source> - <target> - <path>{path}</path> - </target> - </pool> - """.format(**poolArgs) - return xml - - -class LogicalPoolDef(StoragePoolDef): - poolType = 'logical' - - def __init__(self, poolArgs): - super(LogicalPoolDef, self).__init__(poolArgs) - self.path = '/var/lib/kimchi/logical_mount/' + self.poolArgs['name'] - - @property - def xml(self): - # Required parameters - # name: - # type: - # source[devices]: - poolArgs = copy.deepcopy(self.poolArgs) - devices = [] - for device_path in poolArgs['source']['devices']: - devices.append('<device path="%s" />' % device_path) - - poolArgs['source']['devices'] = ''.join(devices) - poolArgs['path'] = self.path - - xml = """ - <pool type='logical'> - <name>{name}</name> - <source> - {source[devices]} - </source> - <target> - <path>{path}</path> - </target> - </pool> - """.format(**poolArgs) - return xml - - -class IscsiPoolDef(StoragePoolDef): - poolType = 'iscsi' - - def prepare(self, conn): - source = self.poolArgs['source'] - if not TargetClient(**source).validate(): - raise OperationFailed("Can not login to iSCSI host %s target %s" % - (source['host'], source['target'])) - self._prepare_auth(conn) - - def _prepare_auth(self, conn): - try: - auth = self.poolArgs['source']['auth'] - except KeyError: - return - - try: - virSecret = conn.secretLookupByUsage( - libvirt.VIR_SECRET_USAGE_TYPE_ISCSI, self.poolArgs['name']) - except libvirt.libvirtError: - xml = ''' - <secret ephemeral='no' private='yes'> - <description>Secret for iSCSI storage pool {name}</description> - <auth type='chap' username='{username}'/> - <usage type='iscsi'> - <target>{name}</target> - </usage> - </secret>'''.format(name=self.poolArgs['name'], - username=auth['username']) - virSecret = conn.secretDefineXML(xml) - - virSecret.setValue(auth['password']) - - def _format_port(self, poolArgs): - try: - port = poolArgs['source']['port'] - except KeyError: - return "" - return "port='%s'" % port - - def _format_auth(self, poolArgs): - try: - auth = poolArgs['source']['auth'] - except KeyError: - return "" - - return ''' - <auth type='chap' username='{username}'> - <secret type='iscsi' usage='{name}'/> - </auth>'''.format(name=poolArgs['name'], username=auth['username']) - - @property - def xml(self): - # Required parameters - # name: - # type: - # source[host]: - # source[target]: - # - # Optional parameters - # source[port]: - poolArgs = copy.deepcopy(self.poolArgs) - poolArgs['source'].update({'port': self._format_port(poolArgs), - 'auth': self._format_auth(poolArgs)}) - poolArgs['path'] = '/dev/disk/by-id' - - xml = """ - <pool type='iscsi'> - <name>{name}</name> - <source> - <host name='{source[host]}' {source[port]}/> - <device path='{source[target]}'/> - {source[auth]} - </source> - <target> - <path>{path}</path> - </target> - </pool> - """.format(**poolArgs) - return xml diff --git a/src/kimchi/model_/model.py b/src/kimchi/model_/model.py deleted file mode 100644 index 709e0bb..0000000 --- a/src/kimchi/model_/model.py +++ /dev/null @@ -1,53 +0,0 @@ -# -# Project Kimchi -# -# Copyright IBM, Corp. 2013 -# -# Authors: -# Aline Manera <alinefm@linux.vnet.ibm.com> -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library 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 -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - -import inspect -import os - -from kimchi.basemodel import BaseModel -from kimchi.model_.libvirtconnection import LibvirtConnection -from kimchi.objectstore import ObjectStore -from kimchi.utils import import_module, listPathModules - - -class Model(BaseModel): - def __init__(self, libvirt_uri='qemu:///system', objstore_loc=None): - self.objstore = ObjectStore(objstore_loc) - self.conn = LibvirtConnection(libvirt_uri) - kargs = {'objstore': self.objstore, 'conn': self.conn} - - this = os.path.basename(__file__) - this_mod = os.path.splitext(this)[0] - - models = [] - for mod_name in listPathModules(os.path.dirname(__file__)): - if mod_name.startswith("_") or mod_name == this_mod: - continue - - module = import_module('model_.' + mod_name) - members = inspect.getmembers(module, inspect.isclass) - for cls_name, instance in members: - if inspect.getmodule(instance) == module: - if cls_name.endswith('Model'): - models.append(instance(**kargs)) - - return super(Model, self).__init__(models) diff --git a/src/kimchi/model_/networks.py b/src/kimchi/model_/networks.py deleted file mode 100644 index b164141..0000000 --- a/src/kimchi/model_/networks.py +++ /dev/null @@ -1,265 +0,0 @@ -# -# Project Kimchi -# -# Copyright IBM, Corp. 2013 -# -# Authors: -# Aline Manera <alinefm@linux.vnet.ibm.com> -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library 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 -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - -import ipaddr -import libvirt - -from kimchi import netinfo -from kimchi import network as knetwork -from kimchi import networkxml -from kimchi import xmlutils -from kimchi.exception import InvalidOperation, InvalidParameter -from kimchi.exception import MissingParameter, NotFoundError, OperationFailed - - -class NetworksModel(object): - def __init__(self, **kargs): - self.conn = kargs['conn'] - - def create(self, params): - conn = self.conn.get() - name = params['name'] - if name in self.get_list(): - raise InvalidOperation("Network %s already exists" % name) - - connection = params["connection"] - # set forward mode, isolated do not need forward - if connection != 'isolated': - params['forward'] = {'mode': connection} - - # set subnet, bridge network do not need subnet - if connection in ["nat", 'isolated']: - self._set_network_subnet(params) - - # only bridge network need bridge(linux bridge) or interface(macvtap) - if connection == 'bridge': - self._set_network_bridge(params) - - xml = networkxml.to_network_xml(**params) - - try: - network = conn.networkDefineXML(xml) - network.setAutostart(True) - except libvirt.libvirtError as e: - raise OperationFailed(e.get_error_message()) - - return name - - def get_list(self): - conn = self.conn.get() - return sorted(conn.listNetworks() + conn.listDefinedNetworks()) - - def _set_network_subnet(self, params): - netaddr = params.get('subnet', '') - net_addrs = [] - # lookup a free network address for nat and isolated automatically - if not netaddr: - for net_name in self.get_list(): - network = self._get_network(net_name) - xml = network.XMLDesc(0) - subnet = NetworkModel.get_network_from_xml(xml)['subnet'] - subnet and net_addrs.append(ipaddr.IPNetwork(subnet)) - netaddr = knetwork.get_one_free_network(net_addrs) - if not netaddr: - raise OperationFailed("can not find a free IP address for " - "network '%s'" % params['name']) - - try: - ip = ipaddr.IPNetwork(netaddr) - except ValueError as e: - raise InvalidParameter("%s" % e) - - if ip.ip == ip.network: - ip.ip = ip.ip + 1 - - dhcp_start = str(ip.ip + ip.numhosts / 2) - dhcp_end = str(ip.ip + ip.numhosts - 2) - params.update({'net': str(ip), - 'dhcp': {'range': {'start': dhcp_start, - 'end': dhcp_end}}}) - - def _set_network_bridge(self, params): - try: - iface = params['interface'] - if iface in self.get_all_networks_interfaces(): - raise InvalidParameter("interface '%s' already in use." % - iface) - except KeyError, e: - raise MissingParameter(e) - if netinfo.is_bridge(iface): - params['bridge'] = iface - elif netinfo.is_bare_nic(iface) or netinfo.is_bonding(iface): - if params.get('vlan_id') is None: - params['forward']['dev'] = iface - else: - params['bridge'] = \ - self._create_vlan_tagged_bridge(str(iface), - str(params['vlan_id'])) - else: - raise InvalidParameter("the interface should be bare nic, " - "bonding or bridge device.") - - def get_all_networks_interfaces(self): - net_names = self.get_list() - interfaces = [] - for name in net_names: - conn = self.conn.get() - network = conn.networkLookupByName(name) - xml = network.XMLDesc(0) - net_dict = NetworkModel.get_network_from_xml(xml) - forward = net_dict['forward'] - (forward['mode'] == 'bridge' and forward['interface'] and - interfaces.append(forward['interface'][0]) is None or - interfaces.extend(forward['interface'] + forward['pf'])) - net_dict['bridge'] and interfaces.append(net_dict['bridge']) - return interfaces - - def _create_vlan_tagged_bridge(self, interface, vlan_id): - br_name = '-'.join(('kimchi', interface, vlan_id)) - br_xml = networkxml.create_vlan_tagged_bridge_xml(br_name, interface, - vlan_id) - conn = self.conn.get() - conn.changeBegin() - try: - vlan_tagged_br = conn.interfaceDefineXML(br_xml) - vlan_tagged_br.create() - except libvirt.libvirtError as e: - conn.changeRollback() - raise OperationFailed(e.message) - else: - conn.changeCommit() - return br_name - - -class NetworkModel(object): - def __init__(self, **kargs): - self.conn = kargs['conn'] - - def lookup(self, name): - network = self._get_network(name) - xml = network.XMLDesc(0) - net_dict = self.get_network_from_xml(xml) - subnet = net_dict['subnet'] - dhcp = net_dict['dhcp'] - forward = net_dict['forward'] - interface = net_dict['bridge'] - - connection = forward['mode'] or "isolated" - # FIXME, if we want to support other forward mode well. - if connection == 'bridge': - # macvtap bridge - interface = interface or forward['interface'][0] - # exposing the network on linux bridge or macvtap interface - interface_subnet = knetwork.get_dev_netaddr(interface) - subnet = subnet if subnet else interface_subnet - - # libvirt use format 192.168.0.1/24, standard should be 192.168.0.0/24 - # http://www.ovirt.org/File:Issue3.png - if subnet: - subnet = ipaddr.IPNetwork(subnet) - subnet = "%s/%s" % (subnet.network, subnet.prefixlen) - - return {'connection': connection, - 'interface': interface, - 'subnet': subnet, - 'dhcp': dhcp, - 'vms': self._get_vms_attach_to_a_network(name), - 'autostart': network.autostart() == 1, - 'state': network.isActive() and "active" or "inactive"} - - def _get_vms_attach_to_a_network(self, network): - vms = [] - conn = self.conn.get() - for dom in conn.listAllDomains(0): - networks = self._vm_get_networks(dom) - if network in networks: - vms.append(dom.name()) - return vms - - def _vm_get_networks(self, dom): - xml = dom.XMLDesc(0) - xpath = "/domain/devices/interface[@type='network']/source/@network" - return xmlutils.xpath_get_text(xml, xpath) - - def activate(self, name): - network = self._get_network(name) - network.create() - - def deactivate(self, name): - network = self._get_network(name) - network.destroy() - - def delete(self, name): - network = self._get_network(name) - if network.isActive(): - raise InvalidOperation( - "Unable to delete the active network %s" % name) - self._remove_vlan_tagged_bridge(network) - network.undefine() - - def _get_network(self, name): - conn = self.conn.get() - try: - return conn.networkLookupByName(name) - except libvirt.libvirtError as e: - raise NotFoundError("Network '%s' not found: %s" % - (name, e.get_error_message())) - - @staticmethod - def get_network_from_xml(xml): - address = xmlutils.xpath_get_text(xml, "/network/ip/@address") - address = address and address[0] or '' - netmask = xmlutils.xpath_get_text(xml, "/network/ip/@netmask") - netmask = netmask and netmask[0] or '' - net = address and netmask and "/".join([address, netmask]) or '' - - dhcp_start = xmlutils.xpath_get_text(xml, - "/network/ip/dhcp/range/@start") - dhcp_start = dhcp_start and dhcp_start[0] or '' - dhcp_end = xmlutils.xpath_get_text(xml, "/network/ip/dhcp/range/@end") - dhcp_end = dhcp_end and dhcp_end[0] or '' - dhcp = {'start': dhcp_start, 'end': dhcp_end} - - forward_mode = xmlutils.xpath_get_text(xml, "/network/forward/@mode") - forward_mode = forward_mode and forward_mode[0] or '' - forward_if = xmlutils.xpath_get_text(xml, - "/network/forward/interface/@dev") - forward_pf = xmlutils.xpath_get_text(xml, "/network/forward/pf/@dev") - bridge = xmlutils.xpath_get_text(xml, "/network/bridge/@name") - bridge = bridge and bridge[0] or '' - return {'subnet': net, 'dhcp': dhcp, 'bridge': bridge, - 'forward': {'mode': forward_mode, - 'interface': forward_if, - 'pf': forward_pf}} - - def _remove_vlan_tagged_bridge(self, network): - try: - bridge = network.bridgeName() - except libvirt.libvirtError: - pass - else: - if bridge.startswith('kimchi-'): - conn = self.conn.get() - iface = conn.interfaceLookupByName(bridge) - if iface.isActive(): - iface.destroy() - iface.undefine() diff --git a/src/kimchi/model_/plugins.py b/src/kimchi/model_/plugins.py deleted file mode 100644 index d6756d0..0000000 --- a/src/kimchi/model_/plugins.py +++ /dev/null @@ -1,31 +0,0 @@ -# -# Project Kimchi -# -# Copyright IBM, Corp. 2013 -# -# Authors: -# Aline Manera <alinefm@linux.vnet.ibm.com> -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library 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 -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - -from kimchi.utils import get_enabled_plugins - - -class PluginsModel(object): - def __init__(self, **kargs): - pass - - def get_list(self): - return [plugin for (plugin, config) in get_enabled_plugins()] diff --git a/src/kimchi/model_/storagepools.py b/src/kimchi/model_/storagepools.py deleted file mode 100644 index 2fca8e4..0000000 --- a/src/kimchi/model_/storagepools.py +++ /dev/null @@ -1,246 +0,0 @@ -# -# Project Kimchi -# -# Copyright IBM, Corp. 2013 -# -# Authors: -# Aline Manera <alinefm@linux.vnet.ibm.com> -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library 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 -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - -import libvirt - -from kimchi import xmlutils -from kimchi.scan import Scanner -from kimchi.exception import InvalidOperation, MissingParameter -from kimchi.exception import NotFoundError, OperationFailed -from kimchi.model_.libvirtstoragepool import StoragePoolDef -from kimchi.utils import add_task, kimchi_log - - -ISO_POOL_NAME = u'kimchi_isos' -POOL_STATE_MAP = {0: 'inactive', - 1: 'initializing', - 2: 'active', - 3: 'degraded', - 4: 'inaccessible'} - -STORAGE_SOURCES = {'netfs': {'addr': '/pool/source/host/@name', - 'path': '/pool/source/dir/@path'}} - - -class StoragePoolsModel(object): - def __init__(self, **kargs): - self.conn = kargs['conn'] - self.objstore = kargs['objstore'] - self.scanner = Scanner(self._clean_scan) - self.scanner.delete() - - def get_list(self): - try: - conn = self.conn.get() - names = conn.listStoragePools() - names += conn.listDefinedStoragePools() - return sorted(names) - except libvirt.libvirtError as e: - raise OperationFailed(e.get_error_message()) - - def create(self, params): - task_id = None - conn = self.conn.get() - try: - name = params['name'] - if name in (ISO_POOL_NAME, ): - raise InvalidOperation("StoragePool already exists") - - if params['type'] == 'kimchi-iso': - task_id = self._do_deep_scan(params) - poolDef = StoragePoolDef.create(params) - poolDef.prepare(conn) - xml = poolDef.xml - except KeyError, key: - raise MissingParameter(key) - - if name in self.get_list(): - err = "The name %s has been used by a pool" - raise InvalidOperation(err % name) - - try: - if task_id: - # Create transient pool for deep scan - conn.storagePoolCreateXML(xml, 0) - return name - - pool = conn.storagePoolDefineXML(xml, 0) - if params['type'] in ['logical', 'dir', 'netfs']: - pool.build(libvirt.VIR_STORAGE_POOL_BUILD_NEW) - # autostart dir and logical storage pool created from kimchi - pool.setAutostart(1) - else: - # disable autostart for others - pool.setAutostart(0) - except libvirt.libvirtError as e: - msg = "Problem creating Storage Pool: %s" - kimchi_log.error(msg, e) - raise OperationFailed(e.get_error_message()) - return name - - def _clean_scan(self, pool_name): - try: - conn = self.conn.get() - pool = conn.storagePoolLookupByName(pool_name) - pool.destroy() - with self.objstore as session: - session.delete('scanning', pool_name) - except Exception, e: - err = "Exception %s occured when cleaning scan result" - kimchi_log.debug(err % e.message) - - def _do_deep_scan(self, params): - scan_params = dict(ignore_list=[]) - scan_params['scan_path'] = params['path'] - params['type'] = 'dir' - - for pool in self.get_list(): - try: - res = self.storagepool_lookup(pool) - if res['state'] == 'active': - scan_params['ignore_list'].append(res['path']) - except Exception, e: - err = "Exception %s occured when get ignore path" - kimchi_log.debug(err % e.message) - - params['path'] = self.scanner.scan_dir_prepare(params['name']) - scan_params['pool_path'] = params['path'] - task_id = add_task('', self.scanner.start_scan, self.objstore, - scan_params) - # Record scanning-task/storagepool mapping for future querying - with self.objstore as session: - session.store('scanning', params['name'], task_id) - return task_id - - -class StoragePoolModel(object): - def __init__(self, **kargs): - self.conn = kargs['conn'] - self.objstore = kargs['objstore'] - - @staticmethod - def get_storagepool(name, conn): - conn = conn.get() - try: - return conn.storagePoolLookupByName(name) - except libvirt.libvirtError as e: - if e.get_error_code() == libvirt.VIR_ERR_NO_STORAGE_POOL: - raise NotFoundError("Storage Pool '%s' not found" % name) - else: - raise - - def _get_storagepool_vols_num(self, pool): - try: - if pool.isActive(): - pool.refresh(0) - return pool.numOfVolumes() - else: - return 0 - except libvirt.libvirtError as e: - raise OperationFailed(e.get_error_message()) - - def _get_storage_source(self, pool_type, pool_xml): - source = {} - if pool_type not in STORAGE_SOURCES: - return source - - for key, val in STORAGE_SOURCES[pool_type].items(): - res = xmlutils.xpath_get_text(pool_xml, val) - source[key] = res[0] if len(res) == 1 else res - - return source - - def lookup(self, name): - pool = self.get_storagepool(name, self.conn) - info = pool.info() - nr_volumes = self._get_storagepool_vols_num(pool) - autostart = True if pool.autostart() else False - xml = pool.XMLDesc(0) - path = xmlutils.xpath_get_text(xml, "/pool/target/path")[0] - pool_type = xmlutils.xpath_get_text(xml, "/pool/@type")[0] - source = self._get_storage_source(pool_type, xml) - res = {'state': POOL_STATE_MAP[info[0]], - 'path': path, - 'source': source, - 'type': pool_type, - 'autostart': autostart, - 'capacity': info[1], - 'allocated': info[2], - 'available': info[3], - 'nr_volumes': nr_volumes} - - if not pool.isPersistent(): - # Deal with deep scan generated pool - try: - with self.objstore as session: - task_id = session.get('scanning', name) - res['task_id'] = str(task_id) - res['type'] = 'kimchi-iso' - except NotFoundError: - # User created normal pool - pass - return res - - def update(self, name, params): - autostart = params['autostart'] - if autostart not in [True, False]: - raise InvalidOperation("Autostart flag must be true or false") - pool = self.get_storagepool(name, self.conn) - if autostart: - pool.setAutostart(1) - else: - pool.setAutostart(0) - ident = pool.name() - return ident - - def activate(self, name): - pool = self.get_storagepool(name, self.conn) - try: - pool.create(0) - except libvirt.libvirtError as e: - raise OperationFailed(e.get_error_message()) - - def deactivate(self, name): - pool = self.get_storagepool(name, self.conn) - try: - pool.destroy() - except libvirt.libvirtError as e: - raise OperationFailed(e.get_error_message()) - - def delete(self, name): - pool = self.get_storagepool(name, self.conn) - if pool.isActive(): - err = "Unable to delete the active storagepool %s" - raise InvalidOperation(err % name) - try: - pool.undefine() - except libvirt.libvirtError as e: - raise OperationFailed(e.get_error_message()) - - -class IsoPoolModel(object): - def __init__(self, **kargs): - pass - - def lookup(self, name): - return {'state': 'active', - 'type': 'kimchi-iso'} diff --git a/src/kimchi/model_/storageservers.py b/src/kimchi/model_/storageservers.py deleted file mode 100644 index 1728394..0000000 --- a/src/kimchi/model_/storageservers.py +++ /dev/null @@ -1,78 +0,0 @@ -# -# Project Kimchi -# -# Copyright IBM, Corp. 2013 -# -# Authors: -# Aline Manera <alinefm@linux.vnet.ibm.com> -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library 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 -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - -from kimchi.exception import NotFoundError -from kimchi.model_.storagepools import StoragePoolModel, STORAGE_SOURCES - - -class StorageServersModel(object): - def __init__(self, **kargs): - self.conn = kargs['conn'] - self.pool = StoragePoolModel(**kargs) - - def get_list(self, _target_type=None): - if not _target_type: - target_type = STORAGE_SOURCES.keys() - else: - target_type = [_target_type] - pools = self.pools.get_list() - - conn = self.conn.get() - pools = conn.listStoragePools() - pools += conn.listDefinedStoragePools() - - server_list = [] - for pool in pools: - try: - pool_info = self.pool.lookup(pool) - if (pool_info['type'] in target_type and - pool_info['source']['addr'] not in server_list): - # Avoid to add same server for multiple times - # if it hosts more than one storage type - server_list.append(pool_info['source']['addr']) - except NotFoundError: - pass - - return server_list - - -class StorageServerModel(object): - def __init__(self, **kargs): - self.conn = kargs['conn'] - self.pool = StoragePoolModel(**kargs) - - def lookup(self, server): - conn = self.conn.get() - pools = conn.listStoragePools() - pools += conn.listDefinedStoragePools() - for pool in pools: - try: - pool_info = self.pool.lookup(pool) - if pool_info['source'] and \ - pool_info['source']['addr'] == server: - return dict(host=server) - except NotFoundError: - # Avoid inconsistent pool result because of lease between list - # lookup - pass - - raise NotFoundError('server %s does not used by kimchi' % server) diff --git a/src/kimchi/model_/storagetargets.py b/src/kimchi/model_/storagetargets.py deleted file mode 100644 index be73732..0000000 --- a/src/kimchi/model_/storagetargets.py +++ /dev/null @@ -1,86 +0,0 @@ -# -# Project Kimchi -# -# Copyright IBM, Corp. 2013 -# -# Authors: -# Aline Manera <alinefm@linux.vnet.ibm.com> -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library 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 -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - -import libvirt -import lxml.etree as ET -from lxml import objectify -from lxml.builder import E - -from kimchi.model_.config import CapabilitiesModel -from kimchi.model_.storagepools import STORAGE_SOURCES -from kimchi.utils import kimchi_log, patch_find_nfs_target - - -class StorageTargetsModel(object): - def __init__(self, **kargs): - self.conn = kargs['conn'] - self.caps = CapabilitiesModel() - - def get_list(self, storage_server, _target_type=None): - target_list = list() - - if not _target_type: - target_types = STORAGE_SOURCES.keys() - else: - target_types = [_target_type] - - for target_type in target_types: - if not self.caps.nfs_target_probe and target_type == 'netfs': - targets = patch_find_nfs_target(storage_server) - else: - xml = self._get_storage_server_spec(server=storage_server, - target_type=target_type) - conn = self.conn.get() - try: - ret = conn.findStoragePoolSources(target_type, xml, 0) - except libvirt.libvirtError as e: - err = "Query storage pool source fails because of %s" - kimchi_log.warning(err, e.get_error_message()) - continue - - targets = self._parse_target_source_result(target_type, ret) - - target_list.extend(targets) - return target_list - - def _get_storage_server_spec(**kwargs): - # Required parameters: - # server: - # target_type: - extra_args = [] - if kwargs['target_type'] == 'netfs': - extra_args.append(E.format(type='nfs')) - obj = E.source(E.host(name=kwargs['server']), *extra_args) - xml = ET.tostring(obj) - return xml - - def _parse_target_source_result(target_type, xml_str): - root = objectify.fromstring(xml_str) - ret = [] - for source in root.getchildren(): - if target_type == 'netfs': - host_name = source.host.get('name') - target_path = source.dir.get('path') - type = source.format.get('type') - ret.append(dict(host=host_name, target_type=type, - target=target_path)) - return ret diff --git a/src/kimchi/model_/storagevolumes.py b/src/kimchi/model_/storagevolumes.py deleted file mode 100644 index 0edac52..0000000 --- a/src/kimchi/model_/storagevolumes.py +++ /dev/null @@ -1,176 +0,0 @@ -# -# Project Kimchi -# -# Copyright IBM, Corp. 2013 -# -# Authors: -# Aline Manera <alinefm@linux.vnet.ibm.com> -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library 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 -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - -import os - -import libvirt - -from kimchi import xmlutils -from kimchi.exception import InvalidOperation, IsoFormatError -from kimchi.exception import MissingParameter, NotFoundError, OperationFailed -from kimchi.isoinfo import IsoImage -from kimchi.model_.storagepools import StoragePoolModel - - -VOLUME_TYPE_MAP = {0: 'file', - 1: 'block', - 2: 'directory', - 3: 'network'} - - -class StorageVolumesModel(object): - def __init__(self, **kargs): - self.conn = kargs['conn'] - - def create(self, pool, params): - vol_xml = """ - <volume> - <name>%(name)s</name> - <allocation unit="MiB">%(allocation)s</allocation> - <capacity unit="MiB">%(capacity)s</capacity> - <source> - </source> - <target> - <format type='%(format)s'/> - </target> - </volume> - """ - params.setdefault('allocation', 0) - params.setdefault('format', 'qcow2') - - try: - pool = StoragePoolModel.get_storagepool(pool, self.conn) - name = params['name'] - xml = vol_xml % params - except KeyError, key: - raise MissingParameter(key) - - try: - pool.createXML(xml, 0) - except libvirt.libvirtError as e: - raise OperationFailed(e.get_error_message()) - return name - - def get_list(self, pool): - pool = StoragePoolModel.get_storagepool(pool, self.conn) - if not pool.isActive(): - err = "Unable to list volumes in inactive storagepool %s" - raise InvalidOperation(err % pool.name()) - try: - pool.refresh(0) - return pool.listVolumes() - except libvirt.libvirtError as e: - raise OperationFailed(e.get_error_message()) - - -class StorageVolumeModel(object): - def __init__(self, **kargs): - self.conn = kargs['conn'] - - def _get_storagevolume(self, pool, name): - pool = StoragePoolModel.get_storagepool(pool, self.conn) - if not pool.isActive(): - err = "Unable to list volumes in inactive storagepool %s" - raise InvalidOperation(err % pool.name()) - try: - return pool.storageVolLookupByName(name) - except libvirt.libvirtError as e: - if e.get_error_code() == libvirt.VIR_ERR_NO_STORAGE_VOL: - raise NotFoundError("Storage Volume '%s' not found" % name) - else: - raise - - def lookup(self, pool, name): - vol = self._get_storagevolume(pool, name) - path = vol.path() - info = vol.info() - xml = vol.XMLDesc(0) - fmt = xmlutils.xpath_get_text(xml, "/volume/target/format/@type")[0] - res = dict(type=VOLUME_TYPE_MAP[info[0]], - capacity=info[1], - allocation=info[2], - path=path, - format=fmt) - if fmt == 'iso': - if os.path.islink(path): - path = os.path.join(os.path.dirname(path), os.readlink(path)) - os_distro = os_version = 'unknown' - try: - iso_img = IsoImage(path) - os_distro, os_version = iso_img.probe() - bootable = True - except IsoFormatError: - bootable = False - res.update( - dict(os_distro=os_distro, os_version=os_version, path=path, - bootable=bootable)) - - return res - - def wipe(self, pool, name): - volume = self._get_storagevolume(pool, name) - try: - volume.wipePattern(libvirt.VIR_STORAGE_VOL_WIPE_ALG_ZERO, 0) - except libvirt.libvirtError as e: - raise OperationFailed(e.get_error_message()) - - def delete(self, pool, name): - volume = self._get_storagevolume(pool, name) - try: - volume.delete(0) - except libvirt.libvirtError as e: - raise OperationFailed(e.get_error_message()) - - def resize(self, pool, name, size): - size = size << 20 - volume = self._get_storagevolume(pool, name) - try: - volume.resize(size, 0) - except libvirt.libvirtError as e: - raise OperationFailed(e.get_error_message()) - - -class IsoVolumesModel(object): - def __init__(self, **kargs): - self.conn = kargs['conn'] - self.storagevolume = StorageVolumeModel(**kargs) - - def get_list(self): - iso_volumes = [] - conn = self.conn.get() - pools = conn.listStoragePools() - pools += conn.listDefinedStoragePools() - - for pool in pools: - try: - pool.refresh(0) - volumes = pool.listVolumes() - except InvalidOperation: - # Skip inactive pools - continue - - for volume in volumes: - res = self.storagevolume.lookup(pool, volume) - if res['format'] == 'iso': - res['name'] = '%s' % volume - iso_volumes.append(res) - return iso_volumes diff --git a/src/kimchi/model_/tasks.py b/src/kimchi/model_/tasks.py deleted file mode 100644 index 40ca1d6..0000000 --- a/src/kimchi/model_/tasks.py +++ /dev/null @@ -1,39 +0,0 @@ -# -# Project Kimchi -# -# Copyright IBM, Corp. 2013 -# -# Authors: -# Aline Manera <alinefm@linux.vnet.ibm.com> -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library 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 -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - - -class TasksModel(object): - def __init__(self, **kargs): - self.objstore = kargs['objstore'] - - def get_list(self): - with self.objstore as session: - return session.get_list('task') - - -class TaskModel(object): - def __init__(self, **kargs): - self.objstore = kargs['objstore'] - - def lookup(self, id): - with self.objstore as session: - return session.get('task', str(id)) diff --git a/src/kimchi/model_/templates.py b/src/kimchi/model_/templates.py deleted file mode 100644 index 03632a6..0000000 --- a/src/kimchi/model_/templates.py +++ /dev/null @@ -1,172 +0,0 @@ -# -# Project Kimchi -# -# Copyright IBM, Corp. 2013 -# -# Authors: -# Aline Manera <alinefm@linux.vnet.ibm.com> -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library 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 -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - -import copy - -import libvirt - -from kimchi import xmlutils -from kimchi.exception import InvalidOperation, InvalidParameter, NotFoundError -from kimchi.utils import pool_name_from_uri -from kimchi.vmtemplate import VMTemplate - - -class TemplatesModel(object): - def __init__(self, **kargs): - self.objstore = kargs['objstore'] - self.conn = kargs['conn'] - - def create(self, params): - name = params['name'] - conn = self.conn.get() - - pool_uri = params.get(u'storagepool', '') - if pool_uri: - pool_name = pool_name_from_uri(pool_uri) - try: - conn.storagePoolLookupByName(pool_name) - except Exception as e: - err = "Storagepool specified is not valid: %s." - raise InvalidParameter(err % e.message) - - for net_name in params.get(u'networks', []): - try: - conn.networkLookupByName(net_name) - except Exception, e: - raise InvalidParameter("Network '%s' specified by template " - "does not exist." % net_name) - - with self.objstore as session: - if name in session.get_list('template'): - raise InvalidOperation("Template already exists") - t = LibvirtVMTemplate(params, scan=True) - session.store('template', name, t.info) - return name - - def get_list(self): - with self.objstore as session: - return session.get_list('template') - - -class TemplateModel(object): - def __init__(self, **kargs): - self.objstore = kargs['objstore'] - self.conn = kargs['conn'] - self.templates = TemplatesModel(**kargs) - - @staticmethod - def get_template(name, objstore, conn, overrides=None): - with objstore as session: - params = session.get('template', name) - if overrides: - params.update(overrides) - return LibvirtVMTemplate(params, False, conn) - - def lookup(self, name): - t = self.get_template(name, self.objstore, self.conn) - return t.info - - def delete(self, name): - with self.objstore as session: - session.delete('template', name) - - def update(self, name, params): - old_t = self.lookup(name) - new_t = copy.copy(old_t) - new_t.update(params) - ident = name - - pool_uri = new_t.get(u'storagepool', '') - pool_name = pool_name_from_uri(pool_uri) - try: - conn = self.conn.get() - conn.storagePoolLookupByName(pool_name) - except Exception as e: - err = "Storagepool specified is not valid: %s." - raise InvalidParameter(err % e.message) - - for net_name in params.get(u'networks', []): - try: - conn = self.conn.get() - conn.networkLookupByName(net_name) - except Exception, e: - raise InvalidParameter("Network '%s' specified by template " - "does not exist" % net_name) - - self.delete(name) - try: - ident = self.templates.create(new_t) - except: - ident = self.templates.create(old_t) - raise - return ident - - -class LibvirtVMTemplate(VMTemplate): - def __init__(self, args, scan=False, conn=None): - VMTemplate.__init__(self, args, scan) - self.conn = conn - - def _storage_validate(self): - pool_uri = self.info['storagepool'] - pool_name = pool_name_from_uri(pool_uri) - try: - conn = self.conn.get() - pool = conn.storagePoolLookupByName(pool_name) - except libvirt.libvirtError: - err = 'Storage specified by template does not exist' - raise InvalidParameter(err) - - if not pool.isActive(): - err = 'Storage specified by template is not active' - raise InvalidParameter(err) - - return pool - - def _network_validate(self): - names = self.info['networks'] - for name in names: - try: - conn = self.conn.get() - network = conn.networkLookupByName(name) - except libvirt.libvirtError: - err = 'Network specified by template does not exist' - raise InvalidParameter(err) - - if not network.isActive(): - err = 'Network specified by template is not active' - raise InvalidParameter(err) - - def _get_storage_path(self): - pool = self._storage_validate() - xml = pool.XMLDesc(0) - return xmlutils.xpath_get_text(xml, "/pool/target/path")[0] - - def fork_vm_storage(self, vm_uuid): - # Provision storage: - # TODO: Rebase on the storage API once upstream - pool = self._storage_validate() - vol_list = self.to_volume_list(vm_uuid) - for v in vol_list: - # outgoing text to libvirt, encode('utf-8') - pool.createXML(v['xml'].encode('utf-8'), 0) - return vol_list diff --git a/src/kimchi/model_/utils.py b/src/kimchi/model_/utils.py deleted file mode 100644 index a27b867..0000000 --- a/src/kimchi/model_/utils.py +++ /dev/null @@ -1,33 +0,0 @@ -# -# Project Kimchi -# -# Copyright IBM, Corp. 2013 -# -# Authors: -# Aline Manera <alinefm@linux.vnet.ibm.com> -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library 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 -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - -from kimchi.exception import OperationFailed - - -def get_vm_name(vm_name, t_name, name_list): - if vm_name: - return vm_name - for i in xrange(1, 1000): - vm_name = "%s-vm-%i" % (t_name, i) - if vm_name not in name_list: - return vm_name - raise OperationFailed("Unable to choose a VM name") diff --git a/src/kimchi/model_/vmifaces.py b/src/kimchi/model_/vmifaces.py deleted file mode 100644 index 4ec0c7b..0000000 --- a/src/kimchi/model_/vmifaces.py +++ /dev/null @@ -1,135 +0,0 @@ -# -# Project Kimchi -# -# Copyright IBM, Corp. 2013 -# -# Authors: -# Aline Manera <alinefm@linux.vnet.ibm.com> -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library 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 -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - -import random - -import libvirt -from lxml import etree, objectify -from lxml.builder import E - -from kimchi.exception import InvalidOperation, InvalidParameter, NotFoundError -from kimchi.model_.vms import DOM_STATE_MAP, VMModel - - -class VMIfacesModel(object): - def __init__(self, **kargs): - self.conn = kargs['conn'] - - def get_list(self, vm): - macs = [] - for iface in self.get_vmifaces(vm, self.conn): - macs.append(iface.mac.get('address')) - return macs - - def create(self, vm, params): - def randomMAC(): - mac = [0x52, 0x54, 0x00, - random.randint(0x00, 0x7f), - random.randint(0x00, 0xff), - random.randint(0x00, 0xff)] - return ':'.join(map(lambda x: "%02x" % x, mac)) - - conn = self.conn.get() - networks = conn.listNetworks() + conn.listDefinedNetworks() - - if params["type"] == "network" and params["network"] not in networks: - raise InvalidParameter("%s is not an available network" % - params["network"]) - - dom = VMModel.get_vm(vm, self.conn) - if DOM_STATE_MAP[dom.info()[0]] != "shutoff": - raise InvalidOperation("do not support hot plugging attach " - "guest interface") - - macs = (iface.mac.get('address') - for iface in self.get_vmifaces(vm, self.conn)) - - mac = randomMAC() - while True: - if mac not in macs: - break - mac = randomMAC() - - children = [E.mac(address=mac)] - ("network" in params.keys() and - children.append(E.source(network=params['network']))) - ("model" in params.keys() and - children.append(E.model(type=params['model']))) - attrib = {"type": params["type"]} - - xml = etree.tostring(E.interface(*children, **attrib)) - - dom.attachDeviceFlags(xml, libvirt.VIR_DOMAIN_AFFECT_CURRENT) - - return mac - - @staticmethod - def get_vmifaces(vm, conn): - dom = VMModel.get_vm(vm, conn) - xml = dom.XMLDesc(0) - root = objectify.fromstring(xml) - - return root.devices.findall("interface") - - -class VMIfaceModel(object): - def __init__(self, **kargs): - self.conn = kargs['conn'] - - def _get_vmiface(self, vm, mac): - ifaces = VMIfacesModel.get_vmifaces(vm, self.conn) - - for iface in ifaces: - if iface.mac.get('address') == mac: - return iface - return None - - def lookup(self, vm, mac): - info = {} - - iface = self._get_vmiface(vm, mac) - if iface is None: - raise NotFoundError('iface: "%s"' % mac) - - info['type'] = iface.attrib['type'] - info['mac'] = iface.mac.get('address') - if iface.find("model") is not None: - info['model'] = iface.model.get('type') - if info['type'] == 'network': - info['network'] = iface.source.get('network') - if info['type'] == 'bridge': - info['bridge'] = iface.source.get('bridge') - - return info - - def delete(self, vm, mac): - dom = VMModel.get_vm(vm, self.conn) - iface = self._get_vmiface(vm, mac) - - if DOM_STATE_MAP[dom.info()[0]] != "shutoff": - raise InvalidOperation("do not support hot plugging detach " - "guest interface") - if iface is None: - raise NotFoundError('iface: "%s"' % mac) - - dom.detachDeviceFlags(etree.tostring(iface), - libvirt.VIR_DOMAIN_AFFECT_CURRENT) diff --git a/src/kimchi/model_/vms.py b/src/kimchi/model_/vms.py deleted file mode 100644 index d2ab292..0000000 --- a/src/kimchi/model_/vms.py +++ /dev/null @@ -1,450 +0,0 @@ -# -# Project Kimchi -# -# Copyright IBM, Corp. 2013 -# -# Authors: -# Aline Manera <alinefm@linux.vnet.ibm.com> -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library 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 -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - -import os -import time -import uuid -from xml.etree import ElementTree - -import libvirt -from cherrypy.process.plugins import BackgroundTask - -from kimchi import vnc -from kimchi import xmlutils -from kimchi.exception import InvalidOperation, InvalidParameter -from kimchi.exception import NotFoundError, OperationFailed -from kimchi.model_.config import CapabilitiesModel -from kimchi.model_.templates import TemplateModel -from kimchi.model_.utils import get_vm_name -from kimchi.screenshot import VMScreenshot -from kimchi.utils import template_name_from_uri - - -DOM_STATE_MAP = {0: 'nostate', - 1: 'running', - 2: 'blocked', - 3: 'paused', - 4: 'shutdown', - 5: 'shutoff', - 6: 'crashed'} - -GUESTS_STATS_INTERVAL = 5 -VM_STATIC_UPDATE_PARAMS = {'name': './name'} -VM_LIVE_UPDATE_PARAMS = {} - -stats = {} - - -class VMsModel(object): - def __init__(self, **kargs): - self.conn = kargs['conn'] - self.objstore = kargs['objstore'] - self.caps = CapabilitiesModel() - self.guests_stats_thread = BackgroundTask(GUESTS_STATS_INTERVAL, - self._update_guests_stats) - self.guests_stats_thread.start() - - def _update_guests_stats(self): - vm_list = self.get_list() - - for name in vm_list: - dom = VMModel.get_vm(name, self.conn) - vm_uuid = dom.UUIDString() - info = dom.info() - state = DOM_STATE_MAP[info[0]] - - if state != 'running': - stats[vm_uuid] = {} - continue - - if stats.get(vm_uuid, None) is None: - stats[vm_uuid] = {} - - timestamp = time.time() - prevStats = stats.get(vm_uuid, {}) - seconds = timestamp - prevStats.get('timestamp', 0) - stats[vm_uuid].update({'timestamp': timestamp}) - - self._get_percentage_cpu_usage(vm_uuid, info, seconds) - self._get_network_io_rate(vm_uuid, dom, seconds) - self._get_disk_io_rate(vm_uuid, dom, seconds) - - def _get_percentage_cpu_usage(self, vm_uuid, info, seconds): - prevCpuTime = stats[vm_uuid].get('cputime', 0) - - cpus = info[3] - cpuTime = info[4] - prevCpuTime - - base = (((cpuTime) * 100.0) / (seconds * 1000.0 * 1000.0 * 1000.0)) - percentage = max(0.0, min(100.0, base / cpus)) - - stats[vm_uuid].update({'cputime': info[4], 'cpu': percentage}) - - def _get_network_io_rate(self, vm_uuid, dom, seconds): - prevNetRxKB = stats[vm_uuid].get('netRxKB', 0) - prevNetTxKB = stats[vm_uuid].get('netTxKB', 0) - currentMaxNetRate = stats[vm_uuid].get('max_net_io', 100) - - rx_bytes = 0 - tx_bytes = 0 - - tree = ElementTree.fromstring(dom.XMLDesc(0)) - for target in tree.findall('devices/interface/target'): - dev = target.get('dev') - io = dom.interfaceStats(dev) - rx_bytes += io[0] - tx_bytes += io[4] - - netRxKB = float(rx_bytes) / 1000 - netTxKB = float(tx_bytes) / 1000 - - rx_stats = (netRxKB - prevNetRxKB) / seconds - tx_stats = (netTxKB - prevNetTxKB) / seconds - - rate = rx_stats + tx_stats - max_net_io = round(max(currentMaxNetRate, int(rate)), 1) - - stats[vm_uuid].update({'net_io': rate, 'max_net_io': max_net_io, - 'netRxKB': netRxKB, 'netTxKB': netTxKB}) - - def _get_disk_io_rate(self, vm_uuid, dom, seconds): - prevDiskRdKB = stats[vm_uuid].get('diskRdKB', 0) - prevDiskWrKB = stats[vm_uuid].get('diskWrKB', 0) - currentMaxDiskRate = stats[vm_uuid].get('max_disk_io', 100) - - rd_bytes = 0 - wr_bytes = 0 - - tree = ElementTree.fromstring(dom.XMLDesc(0)) - for target in tree.findall("devices/disk/target"): - dev = target.get("dev") - io = dom.blockStats(dev) - rd_bytes += io[1] - wr_bytes += io[3] - - diskRdKB = float(rd_bytes) / 1024 - diskWrKB = float(wr_bytes) / 1024 - - rd_stats = (diskRdKB - prevDiskRdKB) / seconds - wr_stats = (diskWrKB - prevDiskWrKB) / seconds - - rate = rd_stats + wr_stats - max_disk_io = round(max(currentMaxDiskRate, int(rate)), 1) - - stats[vm_uuid].update({'disk_io': rate, - 'max_disk_io': max_disk_io, - 'diskRdKB': diskRdKB, - 'diskWrKB': diskWrKB}) - - def create(self, params): - conn = self.conn.get() - t_name = template_name_from_uri(params['template']) - vm_uuid = str(uuid.uuid4()) - vm_list = self.get_list() - name = get_vm_name(params.get('name'), t_name, vm_list) - # incoming text, from js json, is unicode, do not need decode - if name in vm_list: - raise InvalidOperation("VM already exists") - - vm_overrides = dict() - pool_uri = params.get('storagepool') - if pool_uri: - vm_overrides['storagepool'] = pool_uri - t = TemplateModel.get_template(t_name, self.objstore, self.conn, - vm_overrides) - - if not self.caps.qemu_stream and t.info.get('iso_stream', False): - err = "Remote ISO image is not supported by this server." - raise InvalidOperation(err) - - t.validate() - vol_list = t.fork_vm_storage(vm_uuid) - - # Store the icon for displaying later - icon = t.info.get('icon') - if icon: - with self.objstore as session: - session.store('vm', vm_uuid, {'icon': icon}) - - libvirt_stream = False - if len(self.caps.libvirt_stream_protocols) == 0: - libvirt_stream = True - - graphics = params.get('graphics') - xml = t.to_vm_xml(name, vm_uuid, - libvirt_stream=libvirt_stream, - qemu_stream_dns=self.caps.qemu_stream_dns, - graphics=graphics) - - try: - conn.defineXML(xml.encode('utf-8')) - except libvirt.libvirtError as e: - for v in vol_list: - vol = conn.storageVolLookupByPath(v['path']) - vol.delete(0) - raise OperationFailed(e.get_error_message()) - - return name - - def get_list(self): - conn = self.conn.get() - ids = conn.listDomainsID() - names = map(lambda x: conn.lookupByID(x).name(), ids) - names += conn.listDefinedDomains() - names = map(lambda x: x.decode('utf-8'), names) - return sorted(names, key=unicode.lower) - - -class VMModel(object): - def __init__(self, **kargs): - self.conn = kargs['conn'] - self.objstore = kargs['objstore'] - self.vms = VMsModel(**kargs) - self.vmscreenshot = VMScreenshotModel(**kargs) - - def update(self, name, params): - dom = self.get_vm(name, self.conn) - dom = self._static_vm_update(dom, params) - self._live_vm_update(dom, params) - return dom.name() - - def _static_vm_update(self, dom, params): - state = DOM_STATE_MAP[dom.info()[0]] - - old_xml = new_xml = dom.XMLDesc(0) - - for key, val in params.items(): - if key in VM_STATIC_UPDATE_PARAMS: - xpath = VM_STATIC_UPDATE_PARAMS[key] - new_xml = xmlutils.xml_item_update(new_xml, xpath, val) - - try: - if 'name' in params: - if state == 'running': - err = "VM name only can be updated when vm is powered off." - raise InvalidParameter(err) - else: - dom.undefine() - conn = self.conn.get() - dom = conn.defineXML(new_xml) - except libvirt.libvirtError as e: - dom = conn.defineXML(old_xml) - raise OperationFailed(e.get_error_message()) - return dom - - def _live_vm_update(self, dom, params): - pass - - def lookup(self, name): - dom = self.get_vm(name, self.conn) - info = dom.info() - state = DOM_STATE_MAP[info[0]] - screenshot = None - graphics = self._vm_get_graphics(name) - graphics_type, graphics_listen, graphics_port = graphics - graphics_port = graphics_port if state == 'running' else None - try: - if state == 'running': - screenshot = self.vmscreenshot.lookup(name) - elif state == 'shutoff': - # reset vm stats when it is powered off to avoid sending - # incorrect (old) data - stats[dom.UUIDString()] = {} - except NotFoundError: - pass - - with self.objstore as session: - try: - extra_info = session.get('vm', dom.UUIDString()) - except NotFoundError: - extra_info = {} - icon = extra_info.get('icon') - - vm_stats = stats.get(dom.UUIDString(), {}) - res = {} - res['cpu_utilization'] = vm_stats.get('cpu', 0) - res['net_throughput'] = vm_stats.get('net_io', 0) - res['net_throughput_peak'] = vm_stats.get('max_net_io', 100) - res['io_throughput'] = vm_stats.get('disk_io', 0) - res['io_throughput_peak'] = vm_stats.get('max_disk_io', 100) - - return {'state': state, - 'stats': str(res), - 'uuid': dom.UUIDString(), - 'memory': info[2] >> 10, - 'cpus': info[3], - 'screenshot': screenshot, - 'icon': icon, - 'graphics': {"type": graphics_type, - "listen": graphics_listen, - "port": graphics_port} - } - - def _vm_get_disk_paths(self, dom): - xml = dom.XMLDesc(0) - xpath = "/domain/devices/disk[@device='disk']/source/@file" - return xmlutils.xpath_get_text(xml, xpath) - - def _vm_exists(self, name): - try: - self.get_vm(name, self.conn) - return True - except NotFoundError: - return False - except Exception, e: - err = "Unable to retrieve VM '%s': %s" - raise OperationFailed(err % (name, e.message)) - - @staticmethod - def get_vm(name, conn): - conn = conn.get() - try: - # outgoing text to libvirt, encode('utf-8') - return conn.lookupByName(name.encode("utf-8")) - except libvirt.libvirtError as e: - if e.get_error_code() == libvirt.VIR_ERR_NO_DOMAIN: - raise NotFoundError("Virtual Machine '%s' not found" % name) - else: - raise - - def delete(self, name): - if self._vm_exists(name): - conn = self.conn.get() - dom = self.get_vm(name, self.conn) - self._vmscreenshot_delete(dom.UUIDString()) - paths = self._vm_get_disk_paths(dom) - info = self.lookup(name) - - if info['state'] == 'running': - self.stop(name) - - dom.undefine() - - for path in paths: - vol = conn.storageVolLookupByPath(path) - vol.delete(0) - - with self.objstore as session: - session.delete('vm', dom.UUIDString(), ignore_missing=True) - - vnc.remove_proxy_token(name) - - def start(self, name): - dom = self.get_vm(name, self.conn) - dom.create() - - def stop(self, name): - if self._vm_exists(name): - dom = self.get_vm(name, self.conn) - dom.destroy() - - def _vm_get_graphics(self, name): - dom = self.get_vm(name, self.conn) - xml = dom.XMLDesc(0) - expr = "/domain/devices/graphics/@type" - res = xmlutils.xpath_get_text(xml, expr) - graphics_type = res[0] if res else None - expr = "/domain/devices/graphics/@listen" - res = xmlutils.xpath_get_text(xml, expr) - graphics_listen = res[0] if res else None - graphics_port = None - if graphics_type: - expr = "/domain/devices/graphics[@type='%s']/@port" % graphics_type - res = xmlutils.xpath_get_text(xml, expr) - graphics_port = int(res[0]) if res else None - return graphics_type, graphics_listen, graphics_port - - def connect(self, name): - graphics = self._vm_get_graphics(name) - graphics_type, graphics_listen, graphics_port = graphics - if graphics_port is not None: - vnc.add_proxy_token(name, graphics_port) - else: - raise OperationFailed("Only able to connect to running vm's vnc " - "graphics.") - - def _vmscreenshot_delete(self, vm_uuid): - screenshot = VMScreenshotModel.get_screenshot(vm_uuid, self.objstore, - self.conn) - screenshot.delete() - with self.objstore as session: - session.delete('screenshot', vm_uuid) - - -class VMScreenshotModel(object): - def __init__(self, **kargs): - self.objstore = kargs['objstore'] - self.conn = kargs['conn'] - - def lookup(self, name): - dom = VMModel.get_vm(name, self.conn) - d_info = dom.info() - vm_uuid = dom.UUIDString() - if DOM_STATE_MAP[d_info[0]] != 'running': - raise NotFoundError('No screenshot for stopped vm') - - screenshot = self.get_screenshot(vm_uuid, self.objstore, self.conn) - img_path = screenshot.lookup() - # screenshot info changed after scratch generation - with self.objstore as session: - session.store('screenshot', vm_uuid, screenshot.info) - return img_path - - @staticmethod - def get_screenshot(vm_uuid, objstore, conn): - with objstore as session: - try: - params = session.get('screenshot', vm_uuid) - except NotFoundError: - params = {'uuid': vm_uuid} - session.store('screenshot', vm_uuid, params) - return LibvirtVMScreenshot(params, conn) - - -class LibvirtVMScreenshot(VMScreenshot): - def __init__(self, vm_uuid, conn): - VMScreenshot.__init__(self, vm_uuid) - self.conn = conn - - def _generate_scratch(self, thumbnail): - def handler(stream, buf, opaque): - fd = opaque - os.write(fd, buf) - - fd = os.open(thumbnail, os.O_WRONLY | os.O_TRUNC | os.O_CREAT, 0644) - try: - conn = self.conn.get() - dom = conn.lookupByUUIDString(self.vm_uuid) - vm_name = dom.name() - stream = conn.newStream(0) - dom.screenshot(stream, 0, 0) - stream.recvAll(handler, fd) - except libvirt.libvirtError: - try: - stream.abort() - except: - pass - raise NotFoundError("Screenshot not supported for %s" % vm_name) - else: - stream.finish() - finally: - os.close(fd) diff --git a/src/kimchi/server.py b/src/kimchi/server.py index 9cc4c3c..90b2a27 100644 --- a/src/kimchi/server.py +++ b/src/kimchi/server.py @@ -30,7 +30,7 @@ import sslcert from kimchi import auth from kimchi import config -from kimchi.model_ import model +from kimchi.model import model from kimchi import mockmodel from kimchi import vnc from kimchi.control import sub_nodes diff --git a/tests/test_model.py b/tests/test_model.py index 74f3dd9..51b216c 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -38,7 +38,7 @@ from kimchi import netinfo from kimchi.exception import InvalidOperation, InvalidParameter from kimchi.exception import NotFoundError, OperationFailed from kimchi.iscsi import TargetClient -from kimchi.model_ import model +from kimchi.model import model from kimchi.rollbackcontext import RollbackContext from kimchi.utils import add_task diff --git a/tests/test_storagepool.py b/tests/test_storagepool.py index 869b608..a3f4983 100644 --- a/tests/test_storagepool.py +++ b/tests/test_storagepool.py @@ -24,7 +24,7 @@ import libxml2 import unittest -from kimchi.model_.libvirtstoragepool import StoragePoolDef +from kimchi.model.libvirtstoragepool import StoragePoolDef from kimchi.rollbackcontext import RollbackContext -- 1.7.10.4
participants (2)
-
Aline Manera
-
Ramon Medeiros