
From: Aline Manera <alinefm@br.ibm.com> Also remove former model.py and mockmodel.py files. And rename model_ to model Signed-off-by: Aline Manera <alinefm@br.ibm.com> --- src/kimchi/control/base.py | 32 +- src/kimchi/control/config.py | 30 +- src/kimchi/control/debugreports.py | 18 +- src/kimchi/control/host.py | 24 +- src/kimchi/control/interfaces.py | 11 +- src/kimchi/control/networks.py | 11 +- src/kimchi/control/plugins.py | 13 +- src/kimchi/control/storagepools.py | 32 +- src/kimchi/control/storagevolumes.py | 24 +- src/kimchi/control/tasks.py | 11 +- src/kimchi/control/templates.py | 11 +- src/kimchi/control/utils.py | 2 +- src/kimchi/control/vms.py | 17 +- src/kimchi/mockmodel.py | 784 ---------------- src/kimchi/model.py | 1536 ------------------------------- src/kimchi/model/__init__.py | 21 + src/kimchi/model/config.py | 52 ++ src/kimchi/model/debugreports.py | 86 ++ src/kimchi/model/host.py | 49 + src/kimchi/model/interfaces.py | 48 + src/kimchi/model/libvirtbackend.py | 955 +++++++++++++++++++ src/kimchi/model/libvirtconnection.py | 123 +++ src/kimchi/model/libvirtstoragepool.py | 225 +++++ src/kimchi/model/mockbackend.py | 338 +++++++ src/kimchi/model/networks.py | 115 +++ src/kimchi/model/plugins.py | 29 + src/kimchi/model/storagepools.py | 86 ++ src/kimchi/model/storagevolumes.py | 95 ++ src/kimchi/model/tasks.py | 45 + src/kimchi/model/templates.py | 89 ++ src/kimchi/model/vms.py | 164 ++++ src/kimchi/model_/__init__.py | 21 - src/kimchi/model_/config.py | 52 -- src/kimchi/model_/debugreports.py | 86 -- src/kimchi/model_/host.py | 49 - src/kimchi/model_/interfaces.py | 48 - src/kimchi/model_/libvirtbackend.py | 955 ------------------- src/kimchi/model_/libvirtconnection.py | 123 --- src/kimchi/model_/libvirtstoragepool.py | 225 ----- src/kimchi/model_/mockbackend.py | 338 ------- src/kimchi/model_/networks.py | 115 --- src/kimchi/model_/plugins.py | 29 - src/kimchi/model_/storagepools.py | 86 -- src/kimchi/model_/storagevolumes.py | 95 -- src/kimchi/model_/tasks.py | 45 - src/kimchi/model_/templates.py | 89 -- src/kimchi/model_/vms.py | 164 ---- src/kimchi/root.py | 24 +- src/kimchi/server.py | 16 +- 49 files changed, 2669 insertions(+), 4967 deletions(-) delete mode 100644 src/kimchi/mockmodel.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/libvirtbackend.py create mode 100644 src/kimchi/model/libvirtconnection.py create mode 100644 src/kimchi/model/libvirtstoragepool.py create mode 100644 src/kimchi/model/mockbackend.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/storagevolumes.py create mode 100644 src/kimchi/model/tasks.py create mode 100644 src/kimchi/model/templates.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_/libvirtbackend.py delete mode 100644 src/kimchi/model_/libvirtconnection.py delete mode 100644 src/kimchi/model_/libvirtstoragepool.py delete mode 100644 src/kimchi/model_/mockbackend.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_/storagevolumes.py delete mode 100644 src/kimchi/model_/tasks.py delete mode 100644 src/kimchi/model_/templates.py delete mode 100644 src/kimchi/model_/vms.py diff --git a/src/kimchi/control/base.py b/src/kimchi/control/base.py index 185c8d8..8ee9f89 100644 --- a/src/kimchi/control/base.py +++ b/src/kimchi/control/base.py @@ -52,8 +52,8 @@ class Resource(object): - Set the 'data' property to a JSON-serializable representation of the Resource. """ - def __init__(self, model, ident=None): - self.model = model + def __init__(self, backend, ident=None): + self.backend = backend self.ident = ident self.model_args = (ident,) self.update_params = [] @@ -66,7 +66,7 @@ class Resource(object): if action_args is not None: model_args.extend(parse_request()[key] for key in action_args) - fn = getattr(self.model, model_fn(self, action_name)) + fn = getattr(self.model, action_name) fn(*model_args) uri_params = tuple(self.model_args) raise internal_redirect(self.uri_fmt % uri_params) @@ -89,15 +89,13 @@ class Resource(object): def lookup(self): try: - lookup = getattr(self.model, model_fn(self, 'lookup')) - self.info = lookup(*self.model_args) + self.info = self.model.lookup(*self.model_args) except AttributeError: self.info = {} def delete(self): try: - fn = getattr(self.model, model_fn(self, 'delete')) - fn(*self.model_args) + self.model.delete(*self.model_args) cherrypy.response.status = 204 except AttributeError: error = "Delete is not allowed for %s" % get_class_name(self) @@ -136,7 +134,7 @@ class Resource(object): def update(self): try: - update = getattr(self.model, model_fn(self, 'update')) + update = getattr(self.model, 'update') except AttributeError: error = "%s does not implement update method" raise cherrypy.HTTPError(405, error % get_class_name(self)) @@ -189,15 +187,15 @@ class Collection(object): - Implement the base operations of 'create' and 'get_list' in the model. """ - def __init__(self, model): - self.model = model + def __init__(self, backend): + self.backend = backend self.resource = Resource self.resource_args = [] self.model_args = [] def create(self, *args): try: - create = getattr(self.model, model_fn(self, 'create')) + create = getattr(self.model, 'create') except AttributeError: error = 'Create is not allowed for %s' % get_class_name(self) raise cherrypy.HTTPError(405, error) @@ -208,19 +206,19 @@ class Collection(object): name = create(*args) cherrypy.response.status = 201 args = self.resource_args + [name] - res = self.resource(self.model, *args) + res = self.resource(self.backend, *args) return res.get() def _get_resources(self): try: - get_list = getattr(self.model, model_fn(self, 'get_list')) - idents = get_list(*self.model_args) + get_list = getattr(self.model, 'get_list') + idents = self.model.get_list(*self.model_args) res_list = [] for ident in idents: # internal text, get_list changes ident to unicode for sorted args = self.resource_args + [ident] - res = self.resource(self.model, *args) + res = self.resource(self.backend, *args) res.lookup() res_list.append(res) return res_list @@ -232,7 +230,7 @@ class Collection(object): ident = vpath.pop(0) # incoming text, from URL, is not unicode, need decode args = self.resource_args + [ident.decode("utf-8")] - return self.resource(self.model, *args) + return self.resource(self.backend, *args) def get(self): resources = self._get_resources() @@ -280,7 +278,7 @@ class AsyncCollection(Collection): def create(self, *args): try: - create = getattr(self.model, model_fn(self, 'create')) + create = getattr(self.model, 'create') except AttributeError: error = 'Create is not allowed for %s' % get_class_name(self) raise cherrypy.HTTPError(405, error) diff --git a/src/kimchi/control/config.py b/src/kimchi/control/config.py index 5186ddd..6720c1a 100644 --- a/src/kimchi/control/config.py +++ b/src/kimchi/control/config.py @@ -25,16 +25,17 @@ import cherrypy +from kimchi.model import config as model_config from kimchi.config import config from kimchi.control.base import Collection, Resource class Config(Resource): - def __init__(self, model, id=None): - super(Config, self).__init__(model, id) - self.capabilities = Capabilities(self.model) + def __init__(self, backend, id=None): + super(Config, self).__init__(backend, id) + self.capabilities = Capabilities(backend) self.capabilities.exposed = True - self.distros = Distros(model) + self.distros = Distros(backend) self.distros.exposed = True @property @@ -44,27 +45,26 @@ class Config(Resource): class Capabilities(Resource): - def __init__(self, model, id=None): - super(Capabilities, self).__init__(model, id) + def __init__(self, backend, id=None): + super(Capabilities, self).__init__(backend, id) + self.model = model_config.Capabilities(backend) @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): - def __init__(self, model): - super(Distros, self).__init__(model) + def __init__(self, backend): + super(Distros, self).__init__(backend) + self.model = model_config.Distros() self.resource = Distro class Distro(Resource): - def __init__(self, model, ident): - super(Distro, self).__init__(model, ident) + def __init__(self, backend, ident): + super(Distro, self).__init__(backend, ident) + self.model = model_config.Distro() @property def data(self): diff --git a/src/kimchi/control/debugreports.py b/src/kimchi/control/debugreports.py index a55ba38..ef3c597 100644 --- a/src/kimchi/control/debugreports.py +++ b/src/kimchi/control/debugreports.py @@ -21,20 +21,23 @@ # 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.model import debugreports from kimchi.control.base import AsyncCollection, Resource from kimchi.control.utils import internal_redirect class DebugReports(AsyncCollection): - def __init__(self, model): - super(DebugReports, self).__init__(model) + def __init__(self, backend): + super(DebugReports, self).__init__(backend) + self.model = debugreports.DebugReports(backend) self.resource = DebugReport class DebugReport(Resource): - def __init__(self, model, ident): - super(DebugReport, self).__init__(model, ident) - self.content = DebugReportContent(model, ident) + def __init__(self, backend, ident): + super(DebugReport, self).__init__(backend, ident) + self.model = debugreports.DebugReport(backend) + self.content = DebugReportContent(backend, ident) @property def data(self): @@ -44,8 +47,9 @@ class DebugReport(Resource): class DebugReportContent(Resource): - def __init__(self, model, ident): - super(DebugReportContent, self).__init__(model, ident) + def __init__(self, backend, ident): + super(DebugReportContent, self).__init__(backend, ident) + self.model = debugreports.DebugReportContent(backend) def get(self): self.lookup() diff --git a/src/kimchi/control/host.py b/src/kimchi/control/host.py index 9b19577..c638fc0 100644 --- a/src/kimchi/control/host.py +++ b/src/kimchi/control/host.py @@ -23,18 +23,20 @@ # 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.model import host from kimchi.control.base import Collection, Resource class Host(Resource): - def __init__(self, model, id=None): - super(Host, self).__init__(model, id) + def __init__(self, backend, id=None): + super(Host, self).__init__(backend, id) + self.model = host.Host(backend) self.uri_fmt = '/host/%s' self.reboot = self.generate_action_handler('reboot') self.shutdown = self.generate_action_handler('shutdown') - self.stats = HostStats(self.model) + self.stats = HostStats(backend) self.stats.exposed = True - self.partitions = Partitions(self.model) + self.partitions = Partitions(backend) self.partitions.exposed = True @property @@ -43,20 +45,26 @@ class Host(Resource): class HostStats(Resource): + def __init__(self, backend, id=None): + super(HostStats, self).__init__(backend, id) + self.model = host.HostStats(backend) + @property def data(self): return self.info class Partitions(Collection): - def __init__(self, model): - super(Partitions, self).__init__(model) + def __init__(self, backend): + super(Partitions, self).__init__(backend) + self.model = host.Partitions() self.resource = Partition class Partition(Resource): - def __init__(self, model, id): - super(Partition, self).__init__(model, id) + def __init__(self, backend, id): + super(Partition, self).__init__(backend, id) + self.model = host.Partition() @property def data(self): diff --git a/src/kimchi/control/interfaces.py b/src/kimchi/control/interfaces.py index 28be26e..319b6a9 100644 --- a/src/kimchi/control/interfaces.py +++ b/src/kimchi/control/interfaces.py @@ -22,18 +22,21 @@ # 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.model import interfaces from kimchi.control.base import Collection, Resource class Interfaces(Collection): - def __init__(self, model): - super(Interfaces, self).__init__(model) + def __init__(self, backend): + super(Interfaces, self).__init__(backend) + self.model = interfaces.Interfaces(backend) self.resource = Interface class Interface(Resource): - def __init__(self, model, ident): - super(Interface, self).__init__(model, ident) + def __init__(self, backend, ident): + super(Interface, self).__init__(backend, ident) + self.model = interfaces.Interface() self.uri_fmt = "/interfaces/%s" @property diff --git a/src/kimchi/control/networks.py b/src/kimchi/control/networks.py index f3f0b41..2fae93e 100644 --- a/src/kimchi/control/networks.py +++ b/src/kimchi/control/networks.py @@ -21,18 +21,21 @@ # 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.model import networks from kimchi.control.base import Collection, Resource class Networks(Collection): - def __init__(self, model): - super(Networks, self).__init__(model) + def __init__(self, backend): + super(Networks, self).__init__(backend) + self.model = networks.Networks(backend) self.resource = Network class Network(Resource): - def __init__(self, model, ident): - super(Network, self).__init__(model, ident) + def __init__(self, backend, ident): + super(Network, self).__init__(backend, ident) + self.model = networks.Network(backend) self.uri_fmt = "/networks/%s" self.activate = self.generate_action_handler('activate') self.deactivate = self.generate_action_handler('deactivate') diff --git a/src/kimchi/control/plugins.py b/src/kimchi/control/plugins.py index af32709..24903ac 100644 --- a/src/kimchi/control/plugins.py +++ b/src/kimchi/control/plugins.py @@ -23,23 +23,20 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA import kimchi.template +from kimchi.model import plugins from kimchi.control.base import Collection, Resource from kimchi.control.utils import get_class_name, model_fn class Plugins(Collection): - def __init__(self, model): - super(Plugins, self).__init__(model) + def __init__(self, backend): + super(Plugins, self).__init__(backend) + self.model = plugins.Plugins() @property def data(self): return self.info def get(self): - res_list = [] - try: - get_list = getattr(self.model, model_fn(self, 'get_list')) - res_list = get_list(*self.model_args) - except AttributeError: - pass + res_list = self.model.get_list() return kimchi.template.render(get_class_name(self), res_list) diff --git a/src/kimchi/control/storagepools.py b/src/kimchi/control/storagepools.py index 782f5a6..fc847bb 100644 --- a/src/kimchi/control/storagepools.py +++ b/src/kimchi/control/storagepools.py @@ -25,25 +25,26 @@ import cherrypy - +from kimchi.model import storagepools 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 class StoragePools(Collection): - def __init__(self, model): - super(StoragePools, self).__init__(model) + def __init__(self, backend): + super(StoragePools, self).__init__(backend) + self.model = storagepools.StoragePools(backend) self.resource = StoragePool - isos = IsoPool(model) + isos = IsoPool(backend) isos.exposed = True setattr(self, ISO_POOL_NAME, isos) def create(self, *args): try: - create = getattr(self.model, model_fn(self, 'create')) + create = self.model.create except AttributeError: error = 'Create is not allowed for %s' % get_class_name(self) raise cherrypy.HTTPError(405, error) @@ -53,7 +54,7 @@ class StoragePools(Collection): args = self.model_args + [params] name = create(*args) args = self.resource_args + [name] - res = self.resource(self.model, *args) + res = self.resource(self.backend, *args) resp = res.get() if 'task_id' in res.data: @@ -77,8 +78,9 @@ class StoragePools(Collection): class StoragePool(Resource): - def __init__(self, model, ident): - super(StoragePool, self).__init__(model, ident) + def __init__(self, backend, ident): + super(StoragePool, self).__init__(backend, ident) + self.model = storagepools.StoragePool(backend) self.update_params = ["autostart"] self.uri_fmt = "/storagepools/%s" self.activate = self.generate_action_handler('activate') @@ -108,22 +110,22 @@ class StoragePool(Resource): subcollection = vpath.pop(0) if subcollection == 'storagevolumes': # incoming text, from URL, is not unicode, need decode - return StorageVolumes(self.model, self.ident.decode("utf-8")) + return StorageVolumes(self.backend, self.ident.decode("utf-8")) class IsoPool(Resource): - def __init__(self, model): - super(IsoPool, self).__init__(model, ISO_POOL_NAME) + def __init__(self, backend): + super(IsoPool, self).__init__(backend, ISO_POOL_NAME) @property def data(self): return {'name': self.ident, - 'state': self.info['state'], - 'type': self.info['type']} + 'state': 'active', + 'type': 'kimchi-iso'} def _cp_dispatch(self, vpath): if vpath: subcollection = vpath.pop(0) if subcollection == 'storagevolumes': # incoming text, from URL, is not unicode, need decode - return IsoVolumes(self.model, self.ident.decode("utf-8")) + return IsoVolumes(self.backend, self.ident.decode("utf-8")) diff --git a/src/kimchi/control/storagevolumes.py b/src/kimchi/control/storagevolumes.py index d541807..f07dfb9 100644 --- a/src/kimchi/control/storagevolumes.py +++ b/src/kimchi/control/storagevolumes.py @@ -24,13 +24,15 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA import kimchi.template +from kimchi.model import storagevolumes from kimchi.control.base import Collection, Resource from kimchi.control.utils import get_class_name, model_fn class StorageVolumes(Collection): - def __init__(self, model, pool): - super(StorageVolumes, self).__init__(model) + def __init__(self, backend, pool): + super(StorageVolumes, self).__init__(backend) + self.model = storagevolumes.StorageVolumes(backend) self.resource = StorageVolume self.pool = pool self.resource_args = [self.pool, ] @@ -38,8 +40,9 @@ class StorageVolumes(Collection): class StorageVolume(Resource): - def __init__(self, model, pool, ident): - super(StorageVolume, self).__init__(model, ident) + def __init__(self, backend, pool, ident): + super(StorageVolume, self).__init__(backend, ident) + self.model = storagevolumes.StorageVolume(backend) self.pool = pool self.ident = ident self.info = {} @@ -66,16 +69,11 @@ class StorageVolume(Resource): class IsoVolumes(Collection): - def __init__(self, model, pool): - super(IsoVolumes, self).__init__(model) + def __init__(self, backend, pool): + super(IsoVolumes, self).__init__(backend) + self.model = storagevolumes.IsoVolumes(backend) self.pool = pool def get(self): - res_list = [] - try: - get_list = getattr(self.model, model_fn(self, 'get_list')) - res_list = get_list(*self.model_args) - except AttributeError: - pass - + res_list = self.model.get_list(self.pool) return kimchi.template.render(get_class_name(self), res_list) diff --git a/src/kimchi/control/tasks.py b/src/kimchi/control/tasks.py index b799422..3bd63f9 100644 --- a/src/kimchi/control/tasks.py +++ b/src/kimchi/control/tasks.py @@ -21,18 +21,21 @@ # 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.model import tasks from kimchi.control.base import Collection, Resource class Tasks(Collection): - def __init__(self, model): - super(Tasks, self).__init__(model) + def __init__(self, backend): + super(Tasks, self).__init__(backend) + self.model = tasks.Tasks(backend) self.resource = Task class Task(Resource): - def __init__(self, model, id): - super(Task, self).__init__(model, id) + def __init__(self, backend, id): + super(Task, self).__init__(backend, id) + self.model = tasks.Task(backend) @property def data(self): diff --git a/src/kimchi/control/templates.py b/src/kimchi/control/templates.py index a77936e..bc65eb7 100644 --- a/src/kimchi/control/templates.py +++ b/src/kimchi/control/templates.py @@ -21,18 +21,21 @@ # 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.model import templates from kimchi.control.base import Collection, Resource class Templates(Collection): - def __init__(self, model): - super(Templates, self).__init__(model) + def __init__(self, backend): + super(Templates, self).__init__(backend) + self.model = templates.Templates(backend) self.resource = Template class Template(Resource): - def __init__(self, model, ident): - super(Template, self).__init__(model, ident) + def __init__(self, backend, ident): + super(Template, self).__init__(backend, ident) + self.model = templates.Template(backend) self.update_params = ["name", "folder", "icon", "os_distro", "storagepool", "os_version", "cpus", "memory", "cdrom", "disks", "networks", diff --git a/src/kimchi/control/utils.py b/src/kimchi/control/utils.py index 814ba20..ee3ecce 100644 --- a/src/kimchi/control/utils.py +++ b/src/kimchi/control/utils.py @@ -94,7 +94,7 @@ def validate_params(params, instance, action): else: return - operation = model_fn(instance, action) + operation = "%s.%s" % (instance.__class__.__name__, action) validator = Draft3Validator(api_schema, format_checker=FormatChecker()) request = {operation: params} diff --git a/src/kimchi/control/vms.py b/src/kimchi/control/vms.py index 7843be7..c50b6b1 100644 --- a/src/kimchi/control/vms.py +++ b/src/kimchi/control/vms.py @@ -22,21 +22,24 @@ # 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.model import vms from kimchi.control.base import Collection, Resource from kimchi.control.utils import internal_redirect class VMs(Collection): - def __init__(self, model): - super(VMs, self).__init__(model) + def __init__(self, backend): + super(VMs, self).__init__(backend) + self.model = vms.VMs(backend) self.resource = VM class VM(Resource): - def __init__(self, model, ident): - super(VM, self).__init__(model, ident) + def __init__(self, backend, ident): + super(VM, self).__init__(backend, ident) + self.model = vms.VM(self.backend) self.update_params = ["name"] - self.screenshot = VMScreenShot(model, ident) + self.screenshot = VMScreenShot(backend, ident) self.uri_fmt = '/vms/%s' self.start = self.generate_action_handler('start') self.stop = self.generate_action_handler('stop') @@ -59,8 +62,8 @@ class VM(Resource): class VMScreenShot(Resource): - def __init__(self, model, ident): - super(VMScreenShot, self).__init__(model, ident) + def __init__(self, backend, ident): + super(VMScreenShot, self).__init__(backend, ident) def get(self): self.lookup() diff --git a/src/kimchi/mockmodel.py b/src/kimchi/mockmodel.py deleted file mode 100644 index 4ef3fa6..0000000 --- a/src/kimchi/mockmodel.py +++ /dev/null @@ -1,784 +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 glob -import ipaddr -import os -import psutil -import random -import subprocess -import time -import uuid - - -try: - from PIL import Image - from PIL import ImageDraw -except ImportError: - import Image - 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.objectstore import ObjectStore -from kimchi.screenshot import VMScreenshot -from kimchi.utils import is_digit -from kimchi.vmtemplate import VMTemplate - - -class MockModel(object): - def __init__(self, objstore_loc=None): - self.reset() - self.objstore = ObjectStore(objstore_loc) - self.distros = self._get_distros() - - def get_capabilities(self): - return {'libvirt_stream_protocols': ['http', 'https', 'ftp', 'ftps', 'tftp'], - 'qemu_stream': True, - 'screenshot': True, - 'system_report_tool': True} - - def reset(self): - self._mock_vms = {} - self._mock_screenshots = {} - self._mock_templates = {} - self._mock_storagepools = {'default': MockStoragePool('default')} - self._mock_networks = {'default': MockNetwork('default')} - self._mock_interfaces = self.dummy_interfaces() - self.next_taskid = 1 - self.storagepool_activate('default') - - def _static_vm_update(self, dom, params): - state = dom.info['state'] - - if 'name' in params: - if state == 'running' or params['name'] in self.vms_get_list(): - raise InvalidParameter("VM name existed or vm not shutoff.") - else: - del self._mock_vms[dom.name] - dom.name = params['name'] - 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: - dom.info[key] = val - - def _live_vm_update(self, dom, params): - pass - - def vm_update(self, name, params): - dom = self._get_vm(name) - self._static_vm_update(dom, params) - self._live_vm_update(dom, params) - - return dom.name - - def vm_lookup(self, name): - vm = self._get_vm(name) - if vm.info['state'] == 'running': - vm.info['screenshot'] = self.vmscreenshot_lookup(name) - else: - vm.info['screenshot'] = None - return vm.info - - def vm_delete(self, name): - vm = self._get_vm(name) - self._vmscreenshot_delete(vm.uuid) - for disk in vm.disk_paths: - self.storagevolume_delete(disk['pool'], disk['volume']) - - del self._mock_vms[vm.name] - - def vm_start(self, name): - self._get_vm(name).info['state'] = 'running' - info = self._get_vm(name).info - - def vm_stop(self, name): - self._get_vm(name).info['state'] = 'shutoff' - - def vm_connect(self, name): - 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()) - if name in self._mock_vms: - raise InvalidOperation("VM already exists") - - vm_uuid = str(uuid.uuid4()) - vm_overrides = dict() - pool_uri = params.get('storagepool') - if pool_uri: - vm_overrides['storagepool'] = pool_uri - - t = self._get_template(t_name, vm_overrides) - t.validate() - - t_info = copy.deepcopy(t.info) - graphics = params.get('graphics') - if graphics: - t_info.update({'graphics': graphics}) - - vm = MockVM(vm_uuid, name, t_info) - icon = t_info.get('icon') - if icon: - vm.info['icon'] = icon - - vm.disk_paths = t.fork_vm_storage(vm_uuid) - self._mock_vms[name] = vm - return name - - def vms_get_list(self): - names = self._mock_vms.keys() - return sorted(names, key=unicode.lower) - - def vmscreenshot_lookup(self, name): - vm = self._get_vm(name) - if vm.info['state'] != 'running': - raise NotFoundError('No screenshot for stopped vm') - screenshot = self._mock_screenshots.setdefault( - vm.uuid, MockVMScreenshot({'uuid': vm.uuid})) - return screenshot.lookup() - - def _vmscreenshot_delete(self, vm_uuid): - screenshot = self._mock_screenshots.get(vm_uuid) - if screenshot: - screenshot.delete() - del self._mock_screenshots[vm_uuid] - - def template_lookup(self, name): - t = self._get_template(name) - return t.info - - def template_delete(self, name): - try: - del self._mock_templates[name] - except KeyError: - raise NotFoundError() - - def templates_create(self, params): - name = params['name'] - if name in self._mock_templates: - raise InvalidOperation("Template already exists") - 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) - - t = MockVMTemplate(params, self) - self._mock_templates[name] = t - 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(kimchi.model.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): - return self._mock_templates.keys() - - def _get_template(self, name, overrides=None): - try: - t = self._mock_templates[name] - if overrides: - args = copy.copy(t.info) - args.update(overrides) - return MockVMTemplate(args, self) - else: - return t - except KeyError: - raise NotFoundError() - - def debugreport_lookup(self, name): - path = config.get_debugreports_path() - file_pattern = os.path.join(path, name + '.txt') - 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 + '.txt') - 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, '*.txt') - 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 _get_vm(self, name): - try: - return self._mock_vms[name] - except KeyError: - raise NotFoundError() - - def storagepools_create(self, params): - try: - name = params['name'] - pool = MockStoragePool(name) - pool.info['type'] = params['type'] - pool.info['path'] = params['path'] - if params['type'] == 'dir': - pool.info['autostart'] = True - else: - pool.info['autostart'] = False - except KeyError, item: - raise MissingParameter(item) - if name in self._mock_storagepools or name in (kimchi.model.ISO_POOL_NAME,): - raise InvalidOperation("StoragePool already exists") - self._mock_storagepools[name] = pool - return name - - def storagepool_lookup(self, name): - storagepool = self._get_storagepool(name) - storagepool.refresh() - return storagepool.info - - def storagepool_update(self, name, params): - autostart = params['autostart'] - if autostart not in [True, False]: - raise InvalidOperation("Autostart flag must be true or false") - storagepool = self._get_storagepool(name) - storagepool.info['autostart'] = autostart - ident = storagepool.name - return ident - - def storagepool_activate(self, name): - self._get_storagepool(name).info['state'] = 'active' - - def storagepool_deactivate(self, name): - self._get_storagepool(name).info['state'] = 'inactive' - - def storagepool_delete(self, name): - # firstly, we should check the pool actually exists - pool = self._get_storagepool(name) - del self._mock_storagepools[pool.name] - - def storagepools_get_list(self): - return sorted(self._mock_storagepools.keys()) - - def _get_storagepool(self, name): - try: - return self._mock_storagepools[name] - except KeyError: - raise NotFoundError() - - def storagevolumes_create(self, pool_name, params): - pool = self._get_storagepool(pool_name) - if pool.info['state'] == 'inactive': - raise InvalidOperation("StoragePool not active") - try: - name = params['name'] - volume = MockStorageVolume(pool, name, params) - volume.info['type'] = params['type'] - volume.info['format'] = params['format'] - volume.info['path'] = os.path.join( - pool.info['path'], name) - except KeyError, item: - raise MissingParameter(item) - if name in pool._volumes: - raise InvalidOperation("StorageVolume already exists") - pool._volumes[name] = volume - return name - - def storagevolume_lookup(self, pool, name): - if self._get_storagepool(pool).info['state'] != 'active': - raise InvalidOperation("StoragePool %s is not active" % pool) - storagevolume = self._get_storagevolume(pool, name) - return storagevolume.info - - def storagevolume_wipe(self, pool, name): - volume = self._get_storagevolume(pool, name) - volume.info['allocation'] = 0 - - def storagevolume_delete(self, pool, name): - # firstly, we should check the pool actually exists - volume = self._get_storagevolume(pool, name) - del self._get_storagepool(pool)._volumes[volume.name] - - def storagevolume_resize(self, pool, name, size): - volume = self._get_storagevolume(pool, name) - volume.info['capacity'] = size - - def storagevolumes_get_list(self, pool): - res = self._get_storagepool(pool) - if res.info['state'] == 'inactive': - raise InvalidOperation( - "Unable to list volumes of inactive storagepool %s" % pool) - return res._volumes.keys() - - 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': - # prevent iso from different pool having same volume name - res['name'] = '%s-%s' % (pool, volume) - iso_volumes.append(res) - return iso_volumes - - def dummy_interfaces(self): - interfaces = {} - ifaces = {"eth1": "nic", "bond0": "bonding", - "eth1.10": "vlan", "bridge0": "bridge"} - for i, name in enumerate(ifaces.iterkeys()): - iface = Interface(name) - iface.info['type'] = ifaces[name] - iface.info['ipaddr'] = '192.168.%s.101' % (i + 1) - interfaces[name] = iface - interfaces['eth1'].info['ipaddr'] = '192.168.0.101' - return interfaces - - def interfaces_get_list(self): - return self._mock_interfaces.keys() - - def interface_lookup(self, name): - return self._mock_interfaces[name].info - - def networks_create(self, params): - name = params['name'] - if name in self.networks_get_list(): - raise InvalidOperation("Network %s already exists" % name) - network = MockNetwork(name) - connection = params['connection'] - network.info['connection'] = connection - if connection == "bridge": - try: - interface = params['interface'] - network.info['interface'] = interface - except KeyError, key: - raise MissingParameter(key) - - subnet = params.get('subnet', '') - if subnet: - network.info['subnet'] = subnet - try: - net = ipaddr.IPNetwork(subnet) - except ValueError, e: - raise InvalidParameter(e) - - network.info['dhcp'] = { - 'start': str(net.network + net.numhosts / 2), - 'stop': str(net.network + net.numhosts - 2)} - if name in self._mock_networks: - raise InvalidOperation("Network already exists") - self._mock_networks[name] = network - return name - - def _get_network(self, name): - try: - return self._mock_networks[name] - except KeyError: - raise NotFoundError("Network '%s'" % name) - - def _get_vms_attach_to_a_network(self, network): - vms = [] - for name, dom in self._mock_vms.iteritems(): - if network in dom.networks: - vms.append(name) - return vms - - def network_lookup(self, name): - network = self._get_network(name) - network.info['vms'] = self._get_vms_attach_to_a_network(name) - return network.info - - def network_activate(self, name): - self._get_network(name).info['state'] = 'active' - - def network_deactivate(self, name): - self._get_network(name).info['state'] = 'inactive' - - def network_delete(self, name): - # firstly, we should check the network actually exists - network = self._get_network(name) - del self._mock_networks[network.name] - - def networks_get_list(self): - return sorted(self._mock_networks.keys()) - - 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 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 _get_storagevolume(self, pool, name): - try: - return self._get_storagepool(pool)._volumes[name] - except KeyError: - raise NotFoundError() - - 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 _gen_debugreport_file(self, ident): - return self.add_task('', self._create_log, ident) - - def _create_log(self, cb, name): - path = config.get_debugreports_path() - tmpf = os.path.join(path, name + '.tmp') - realf = os.path.join(path, name + '.txt') - length = random.randint(1000, 10000) - with open(tmpf, 'w') as fd: - while length: - fd.write('I am logged') - length = length - 1 - os.rename(tmpf, realf) - cb("OK", True) - - def host_lookup(self, *name): - res = {} - res['memory'] = 6114058240 - res['cpu'] = 'Intel(R) Core(TM) i5 CPU M 560 @ 2.67GHz' - res['os_distro'] = 'Red Hat Enterprise Linux Server' - res['os_version'] = '6.4' - res['os_codename'] = 'Santiago' - - return res - - def hoststats_lookup(self, *name): - virt_mem = psutil.virtual_memory() - memory_stats = {'total': virt_mem.total, - 'free': virt_mem.free, - 'cached': virt_mem.cached, - 'buffers': virt_mem.buffers, - 'avail': virt_mem.available} - return {'cpu_utilization': round(random.uniform(0, 100), 1), - 'memory': memory_stats, - 'disk_read_rate': round(random.uniform(0, 4000), 1), - 'disk_write_rate': round(random.uniform(0, 4000), 1), - 'net_recv_rate': round(random.uniform(0, 4000), 1), - 'net_sent_rate': round(random.uniform(0, 4000), 1)} - - def vms_get_list_by_state(self, state): - ret_list = [] - for name in self.vms_get_list(): - if (self._mock_vms[name].info['state']) == 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!") - cherrypy.engine.exit() - - 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!") - cherrypy.engine.stop() - time.sleep(10) - cherrypy.engine.start() - - 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) - -class MockVMTemplate(VMTemplate): - def __init__(self, args, mockmodel_inst=None): - VMTemplate.__init__(self, args) - self.model = mockmodel_inst - - def _storage_validate(self): - pool_uri = self.info['storagepool'] - pool_name = kimchi.model.pool_name_from_uri(pool_uri) - try: - pool = self.model._get_storagepool(pool_name) - except NotFoundError: - raise InvalidParameter('Storage specified by template does not exist') - if pool.info['state'] != 'active': - raise InvalidParameter('Storage specified by template is not active') - - return pool - - def _get_storage_path(self): - pool = self._storage_validate() - return pool.info['path'] - - def fork_vm_storage(self, vm_name): - pool = self._storage_validate() - volumes = self.to_volume_list(vm_name) - disk_paths = [] - for vol_info in volumes: - vol_info['capacity'] = vol_info['capacity'] << 10 - self.model.storagevolumes_create(pool.name, vol_info) - disk_paths.append({'pool': pool.name, 'volume': vol_info['name']}) - return disk_paths - - -class MockVM(object): - def __init__(self, uuid, name, template_info): - self.uuid = uuid - self.name = name - self.disk_paths = [] - self.networks = template_info['networks'] - self.info = {'state': 'shutoff', - 'stats': "{'cpu_utilization': 20, 'net_throughput' : 35, \ - 'net_throughput_peak': 100, 'io_throughput': 45, \ - 'io_throughput_peak': 100}", - 'uuid': self.uuid, - 'memory': template_info['memory'], - 'cpus': template_info['cpus'], - 'icon': None, - 'graphics': {'type': 'vnc', 'listen': '0.0.0.0', 'port': None} - } - self.info['graphics'].update(template_info['graphics']) - - -class MockStoragePool(object): - def __init__(self, name): - self.name = name - self.info = {'state': 'inactive', - 'capacity': 1024 << 20, - 'allocated': 512 << 20, - 'available': 512 << 20, - 'path': '/var/lib/libvirt/images', - 'source': {}, - 'type': 'dir', - 'nr_volumes': 0, - 'autostart': 0} - self._volumes = {} - - def refresh(self): - state = self.info['state'] - self.info['nr_volumes'] = len(self._volumes) \ - if state == 'active' else 0 - - -class Interface(object): - def __init__(self, name): - self.name = name - self.info = {'type': 'nic', - 'ipaddr': '192.168.0.101', - 'netmask': '255.255.255.0', - 'status': 'active'} - - -class MockNetwork(object): - def __init__(self, name): - self.name = name - self.info = {'state': 'inactive', - 'autostart': True, - 'connection': 'nat', - 'interface': 'virbr0', - 'subnet': '192.168.122.0/24', - 'dhcp': {'start': '192.168.122.128', - 'stop': '192.168.122.254'}, - } - - -class MockTask(object): - def __init__(self, id): - self.id = id - -class MockStorageVolume(object): - def __init__(self, pool, name, params={}): - self.name = name - self.pool = pool - fmt = params.get('format', 'raw') - capacity = params.get('capacity', 1024) - self.info = {'type': 'disk', - 'capacity': capacity << 20, - 'allocation': 512, - 'format': fmt} - if fmt == 'iso': - self.info['allocation'] = self.info['capacity'] - self.info['os_version'] = '17' - self.info['os_distro'] = 'fedora' - self.info['bootable'] = True - - -class MockVMScreenshot(VMScreenshot): - OUTDATED_SECS = 5 - BACKGROUND_COLOR = ['blue', 'green', 'purple', 'red', 'yellow'] - BOX_COORD = (50, 115, 206, 141) - BAR_COORD = (50, 115, 50, 141) - - def __init__(self, vm_name): - VMScreenshot.__init__(self, vm_name) - self.coord = MockVMScreenshot.BAR_COORD - self.background = random.choice(MockVMScreenshot.BACKGROUND_COLOR) - - def _generate_scratch(self, thumbnail): - self.coord = (self.coord[0], - self.coord[1], - min(MockVMScreenshot.BOX_COORD[2], - self.coord[2]+random.randrange(50)), - self.coord[3]) - - image = Image.new("RGB", (256, 256), self.background) - d = ImageDraw.Draw(image) - d.rectangle(MockVMScreenshot.BOX_COORD, outline='black') - d.rectangle(self.coord, outline='black', fill='black') - image.save(thumbnail) - - -def get_mock_environment(): - model = MockModel() - for i in xrange(5): - name = 'test-template-%i' % i - params = {'name': name} - t = MockVMTemplate(params, model) - model._mock_templates[name] = t - - for name in ('test-template-1', 'test-template-3'): - model._mock_templates[name].info.update({'folder': ['rhel', '6']}) - - for i in xrange(10): - name = u'test-vm-%i' % i - vm_uuid = str(uuid.uuid4()) - vm = MockVM(vm_uuid, name, model.template_lookup('test-template-0')) - model._mock_vms[name] = vm - - #mock storagepool - for i in xrange(5): - name = 'default-pool-%i' % i - defaultstoragepool = MockStoragePool(name) - defaultstoragepool.info['path'] += '/%i' % i - model._mock_storagepools[name] = defaultstoragepool - for j in xrange(5): - vol_name = 'volume-%i' % j - defaultstoragevolume = MockStorageVolume(name, vol_name) - defaultstoragevolume.info['path'] = '%s/%s' % ( - defaultstoragepool.info['path'], vol_name) - mockpool = model._mock_storagepools[name] - mockpool._volumes[vol_name] = defaultstoragevolume - vol_name = 'Fedora17.iso' - defaultstoragevolume = MockStorageVolume(name, vol_name, - {'format': 'iso'}) - defaultstoragevolume.info['path'] = '%s/%s' % ( - defaultstoragepool.info['path'], vol_name) - mockpool = model._mock_storagepools[name] - mockpool._volumes[vol_name] = defaultstoragevolume - - #mock network - for i in xrange(5): - name = 'test-network-%i' % i - testnetwork = MockNetwork(name) - testnetwork.info['interface'] = 'virbr%i' % (i + 1) - testnetwork.info['subnet'] = '192.168.%s.0/24' % (i + 1) - testnetwork.info['dhcp']['start'] = '192.168.%s.128' % (i + 1) - testnetwork.info['dhcp']['end'] = '192.168.%s.254' % (i + 1) - model._mock_networks[name] = testnetwork - - return model diff --git a/src/kimchi/model.py b/src/kimchi/model.py deleted file mode 100644 index 7b0eafc..0000000 --- a/src/kimchi/model.py +++ /dev/null @@ -1,1536 +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 os -import platform -import psutil -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 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.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.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({'subnet': 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 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 _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 _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..7c0a5d6 --- /dev/null +++ b/src/kimchi/model/config.py @@ -0,0 +1,52 @@ +# +# Project Kimchi +# +# Copyright IBM, Corp. 2013 +# +# Authors: +# Adam Litke <agl@linux.vnet.ibm.com> +# 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.distroloader import DistroLoader +from kimchi.exception import NotFoundError + +ERR_DISTRO_NOT_FOUND = "Distro '%s' not found." + +class Config(object): + pass + +class Capabilities(object): + def __init__(self, backend): + self.backend = backend + + def lookup(self, ident): + return self.backend.get_capabilities() + +class Distros(object): + def __init__(self): + self._distroloader = DistroLoader() + self._distros = self._distroloader.get() + + def get_list(self): + return self._distros.keys() + +class Distro(Distros): + def lookup(self, ident): + if ident not in self.get_list(): + raise NotFoundError(ERR_DISTRO_NOT_FOUND % ident) + + return self._distros[ident] diff --git a/src/kimchi/model/debugreports.py b/src/kimchi/model/debugreports.py new file mode 100644 index 0000000..4db1ace --- /dev/null +++ b/src/kimchi/model/debugreports.py @@ -0,0 +1,86 @@ +# +# Project Kimchi +# +# Copyright IBM, Corp. 2013 +# +# Authors: +# Adam Litke <agl@linux.vnet.ibm.com> +# 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 glob +import os +import time + +from kimchi import config +from kimchi.exception import NotFoundError + +class DebugReports(object): + def __init__(self, backend): + pass + + 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 + +class DebugReport(object): + def __init__(self, backend): + self.backend = backend + + def lookup(self, name): + file_target = self._get_debugreport(name) + 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 create(self, params): + ident = params['name'] + taskid = self.backend.gen_debugreport_file(ident) + if taskid is None: + raise OperationFailed("Debug report tool not found.") + + with self.backend.objstore as session: + return session.get('task', str(taskid)) + + def delete(self, name): + file_target = self._get_debugreports(name) + os.remove(file_target) + + def _get_debugreport(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("Debug report '%s' not found.") + + return file_target + +class DebugReportContent(object): + def __init__(self, backend): + self.backend = backend + + def lookup(self, name): + debugreport = DebugReports(backend) + 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..f5ef0d7 --- /dev/null +++ b/src/kimchi/model/host.py @@ -0,0 +1,49 @@ +# +# Project Kimchi +# +# Copyright IBM, Corp. 2013 +# +# Authors: +# Adam Litke <agl@linux.vnet.ibm.com> +# 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 disks + +class Host(object): + def __init__(self, backend): + self.info = backend.get_host() + + def lookup(self, ident): + return self.info + +class HostStats(object): + def __init__(self, backend): + self.backend = backend + + def lookup(self, ident): + return self.backend.host_stats + +class Partitions(object): + def get_list(self): + return disks.get_partitions_names() + +class Partition(object): + def lookup(self, ident): + if ident not in disks.get_partitions_names(): + raise NotFoundError("Partition %s not found in the host" % name) + + return disks.get_partition_details(ident) diff --git a/src/kimchi/model/interfaces.py b/src/kimchi/model/interfaces.py new file mode 100644 index 0000000..7b1d156 --- /dev/null +++ b/src/kimchi/model/interfaces.py @@ -0,0 +1,48 @@ +# +# Project Kimchi +# +# Copyright IBM, Corp. 2013 +# +# Authors: +# Adam Litke <agl@linux.vnet.ibm.com> +# 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 + +class Interfaces(object): + def __init__(self, backend): + self.backend = backend + + def get_list(self): + return list(set(netinfo.all_favored_interfaces()) - + set(self.get_used_ifaces())) + + def get_used_ifaces(self): + net_names = self.backend.get_networks() + interfaces = [] + for name in net_names: + net_dict = self.backend.get_network_by_name(name) + net_dict['interface'] and interfaces.append(net_dict['interface']) + + return interfaces + +class Interface(object): + def lookup(self, name): + try: + return netinfo.get_interface_info(name) + except ValueError, e: + raise NotFoundError(e) diff --git a/src/kimchi/model/libvirtbackend.py b/src/kimchi/model/libvirtbackend.py new file mode 100644 index 0000000..3fb1de2 --- /dev/null +++ b/src/kimchi/model/libvirtbackend.py @@ -0,0 +1,955 @@ +# +# Project Kimchi +# +# Copyright IBM, Corp. 2013 +# +# Authors: +# Adam Litke <agl@linux.vnet.ibm.com> +# 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 +import fnmatch +import glob +import ipaddr +import logging +import os +import platform +import psutil +import shutil +import subprocess +import time + +from cherrypy.process.plugins import BackgroundTask +from collections import defaultdict + +from kimchi import config +from kimchi import network +from kimchi import netinfo +from kimchi import networkxml +from kimchi import xmlutils +from kimchi.asynctask import AsyncTask +from kimchi.exception import IsoFormatError, 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 kimchi_log + +GUESTS_STATS_INTERVAL = 5 +HOST_STATS_INTERVAL = 1 +STORAGE_SOURCES = {'netfs': {'addr': '/pool/source/host/@name', + 'path': '/pool/source/dir/@path'}} + +class LibvirtBackend(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'} + + TEMPLATE_SCAN = True + + 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.scanner = Scanner(self._clean_scan) + self.stats = {} + self.host_stats = defaultdict(int) + 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() + + # 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) + + # 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},) + + 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.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): + conn = self.conn.get() + vm_list = self.get_vms() + + for name in vm_list: + info = self.get_vm_by_name(name) + vm_uuid = info['uuid'] + state = info['state'] + 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}) + + dom = conn.lookupByName(name.encode("utf-8")) + self._get_percentage_cpu_usage(vm_uuid, dom.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 = 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 _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) + + # get cpu utilization + self.host_stats['cpu_utilization'] = psutil.cpu_percent(None) + + # get memory stats + virt_mem = psutil.virtual_memory() + self.host_stats['memory'] = {'total': virt_mem.total, + 'free': virt_mem.free, + 'cached': virt_mem.cached, + 'buffers': virt_mem.buffers, + 'avail': virt_mem.available} + + 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 + + ifaces = set(netinfo.nics() + netinfo.wlans()) & set(net_ios.iterkeys()) + for key in ifaces: + 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 gen_debugreport_file(self, name): + gen_cmd = self._get_system_report_tool() + if gen_cmd is None: + return None + + return self.add_task('', gen_cmd, name) + + 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 _sosreport_generate(self, cb, name): + command = 'sosreport --batch --name "%s"' % name + try: + retcode = subprocess.call(command, shell=True, + stdout=subprocess.PIPE, + stderr=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) + 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 add_task(self, target_uri, fn, opaque=None): + id = self.next_taskid + self.next_taskid += 1 + task = AsyncTask(id, target_uri, fn, self.objstore, opaque) + return id + + def get_host(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 + 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_storagepools(self): + try: + conn = self.conn.get() + names = conn.listStoragePools() + names += conn.listDefinedStoragePools() + return names + except libvirt.libvirtError as e: + raise OperationFailed(e.get_error_message()) + + def _clean_scan(self, pool_name): + try: + self.deactivate_storagepool(pool_name) + with self.objstore as session: + session.delete('scanning', pool_name) + except Exception, e: + kimchi_log.debug("Error while cleaning deep scan results" % + 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_storagepools(): + try: + res = self.get_storagepool_by_name(pool) + if res['state'] == 'active': + scan_params['ignore_list'].append(res['path']) + except Exception, e: + kimchi_log.debug("Error while preparing for deep scan: %s" % + e.message) + + params['path'] = self.scanner.scan_dir_prepare(params['name']) + scan_params['pool_path'] = params['path'] + 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 create_storagepool(self, params): + conn = self.conn.get() + try: + poolDef = StoragePoolDef.create(params) + poolDef.prepare(conn) + xml = poolDef.xml + except KeyError, key: + raise MissingParameter("You need to specify '%s' in order to " + "create storage pool" % key) + + 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()) + + def get_storagepool_by_name(self, name): + conn = self.conn.get() + pool = conn.storagePoolLookupByName(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': self.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 _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 activate_storagepool(self, name): + try: + conn = self.conn.get() + pool = conn.storagePoolLookupByName(name) + pool.create(0) + except libvirt.libvirtError as e: + raise OperationFailed(e.get_error_message()) + + def deactivate_storagepool(self, name): + try: + conn = self.conn.get() + pool = conn.storagePoolLookupByName(name) + pool.destroy() + except libvirt.libvirtError as e: + raise OperationFailed(e.get_error_message()) + + def delete_storagepool(self, name): + try: + conn = self.conn.get() + pool = conn.storagePoolLookupByName(name) + pool.undefine() + except libvirt.libvirtError as e: + raise OperationFailed(e.get_error_message()) + + def autostart_storagepool(self, name, value): + conn = self.conn.get() + pool = conn.storagePoolLookupByName(name) + if autostart: + pool.setAutostart(1) + else: + pool.setAutostart(0) + + def create_storagevolume(self, pool, params): + storagevol_xml = """ + <volume> + <name>%(name)s</name> + <allocation unit="MiB">%(allocation)s</allocation> + <capacity unit="MiB">%(capacity)s</capacity> + <target> + <format type='%(format)s'/> + <path>%(path)s</path> + </target> + </volume>""" + + params.setdefault('allocation', 0) + params.setdefault('format', 'qcow2') + try: + xml = storagevol_xml % params + except KeyError, key: + raise MissingParameter("You need to specify '%s' in order to " + "create the storage volume." % key) + + conn = self.conn.get() + pool = conn.storagePoolLookupByName(pool) + try: + pool.createXML(xml, 0) + except libvirt.libvirtError as e: + raise OperationFailed(e.get_error_message()) + + def _get_storagevolume(self, pool, name): + conn = self.conn.get() + pool = conn.storagePoolLookupByName(pool) + return pool.storageVolLookupByName(name) + + def get_storagevolumes_by_pool(self, pool): + try: + conn = self.conn.get() + pool = conn.storagePoolLookupByName(pool) + pool.refresh(0) + return pool.listVolumes() + except libvirt.libvirtError as e: + raise OperationFailed(e.get_error_message()) + + def get_storagevolume(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=self.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)) + + 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_storagevolume(self, pool, name): + try: + vol = self._get_storagevolume(pool, name) + vol.wipePattern(libvirt.VIR_STORAGE_VOL_WIPE_ALG_ZERO, 0) + except libvirt.libvirtError as e: + raise OperationFailed(e.get_error_message()) + + def resize_storagevolume(self, pool, name, size): + size = size << 20 + try: + vol = pool.storageVolLookupByName(name) + vol.resize(size, 0) + except libvirt.libvirtError as e: + raise OperationFailed(e.get_error_message()) + + def delete_storagevolume(self, pool, name): + try: + vol = pool.storageVolLookupByName(name) + volume.delete(0) + except libvirt.libvirtError as e: + raise OperationFailed(e.get_error_message()) + + def create_network(self, params): + connection = params["connection"] + # set forward mode, isolated do not need forward + if connection != 'isolated': + params['forward'] = {'mode': connection} + + if connection == 'bridge': + 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'])) + + xml = networkxml.to_network_xml(**params) + try: + conn = self.conn.get() + network = conn.networkDefineXML(xml) + network.setAutostart(True) + except libvirt.libvirtError as e: + raise OperationFailed(e.get_error_message()) + + 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 + + def get_networks(self): + conn = self.conn.get() + return conn.listNetworks() + conn.listDefinedNetworks() + + def get_network_by_name(self, name): + conn = self.conn.get() + net = conn.networkLookupByName(name) + xml = net.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 = network.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_network(name), + 'autostart': net.autostart() == 1, + 'state': net.isActive() and "active" or "inactive"} + + 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_vms_attach_to_network(self, network): + vms = [] + xpath = "/domain/devices/interface[@type='network']/source/@network" + conn = self.conn.get() + for dom in conn.listAllDomains(0): + xml = dom.XMLDesc(0) + networks = xmlutils.xpath_get_text(xml, xpath) + if network in networks: + vms.append(dom.name()) + return vms + + def activate_network(self, name): + conn = self.conn.get() + net = conn.networkLookupByName(name) + net.create() + + def deactivate_network(self, name): + conn = self.conn.get() + net = conn.networkLookupByName(name) + net.destroy() + + def delete_network(self, name): + conn = self.conn.get() + net = conn.networkLookupByName(name) + self._remove_vlan_tagged_bridge(net) + net.undefine() + + 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 create_template(self, name, tmpl): + with self.objstore as session: + session.store('template', name, tmpl.info) + + def get_templates(self): + with self.objstore as session: + return session.get_list('template') + + def get_template_by_name(self, name): + with self.objstore as session: + return session.get('template', name) + + def delete_template(self, name): + with self.objstore as session: + session.delete('template', name) + + def create_vm(self, name, uuid, tmpl, vol_list): + # Store the icon for displaying later + icon = tmpl.info.get('icon', None) + if icon is not None: + with self.objstore as session: + session.store('vm', vm_uuid, {'icon': icon}) + + libvirt_stream = False + if len(self.libvirt_stream_protocols) != 0: + libvirt_stream = True + + xml = tmpl.to_vm_xml(name, vm_uuid, libvirt_stream, + self.qemu_stream_dns) + 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()) + + def get_vms(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 get_screenshot_by_name(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) + + screenshot = LibvirtVMScreenshot(params, 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 + + def delete_screenshot(self, vm_uuid): + os.remove(self.get_screenshot_by_name(vm_uuid)) + with self.objstore as session: + session.delete('screenshot', vm_uuid) + + def get_vm_by_name(self, name): + conn = self.conn.get() + dom = conn.lookupByName(name.encode("utf-8")) + info = dom.info() + state = self.dom_state_map[info[0]] + screenshot = None + graphics = self._get_vm_graphics(dom) + graphics_type, graphics_listen, graphics_port = graphics + graphics_port = graphics_port if state == 'running' else None + if state == 'running': + screenshot = self.get_screenshot_by_name(name) + elif state == 'shutoff': + # reset vm stats when it is powered off to avoid sending + # incorrect (old) data + self.stats[dom.UUIDString()] = {} + + 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], 'icon': icon, + 'screenshot': screenshot, + 'graphics': {'type': graphics_type, 'listen': graphics_listen, + 'port': graphics_port} + } + + def _get_vm_graphics(self, dom): + 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 static_vm_update(self, name, params): + conn = self.conn.get() + dom = conn.lookupByName(name.encode("utf-8")) + 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: + dom.undefine() + conn.defineXML(new_xml) + except libvirt.libvirtError as e: + conn.defineXML(old_xml) + raise OperationFailed(e.get_error_message()) + + def live_vm_update(self, name, params): + pass + + def delete_vm(self, name): + info = self.get_vm_by_name(name) + if info['state'] == 'running': + self.stop_vm(name) + + conn = self.conn.get() + dom = conn.lookupByName(name.encode("utf-8")) + dom.undefine() + + xml = dom.XMLDesc(0) + xpath = "/domain/devices/disk[@device='disk']/source/@file" + paths = xmlutils.xpath_get_text(xml, xpath) + for path in paths: + vol = conn.storageVolLookupByPath(path) + vol.delete(0) + + with self.objstore as session: + session.delete('vm', dom.UUIDString(), ignore_missing=True) + + self.delete_screenshot(dom.UUIDString()) + vnc.remove_proxy_token(name) + + def start_vm(self, name): + conn = self.conn.get() + dom = conn.lookupByName(name.encode("utf-8")) + dom.create() + + def stop_vm(self, name): + conn = self.conn.get() + dom = conn.lookupByName(name.encode("utf-8")) + dom.destroy() + + def connect_vm(self, name): + graphics = self._get_vm_graphics(name) + graphics_type, graphics_listen, graphics_port = get_graphics + if graphics_port is None: + raise OperationFailed("Only able to connect to running vm's vnc " + "graphics.") + vnc.add_proxy_token(name, graphics_port) + +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) diff --git a/src/kimchi/model/libvirtconnection.py b/src/kimchi/model/libvirtconnection.py new file mode 100644 index 0000000..9276acc --- /dev/null +++ b/src/kimchi/model/libvirtconnection.py @@ -0,0 +1,123 @@ +# +# Project Kimchi +# +# Copyright IBM, Corp. 2013 +# +# Authors: +# Adam Litke <agl@linux.vnet.ibm.com> +# 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 +import libvirt +import threading +import time + + +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: + msg = 'Libvirt is not available, exiting.' + kimchi_log.error(msg) + 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..e9b9aa8 --- /dev/null +++ b/src/kimchi/model/libvirtstoragepool.py @@ -0,0 +1,225 @@ +# +# 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.exception import OperationFailed +from kimchi.iscsi import TargetClient + +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): + # TODO: Verify the NFS export can be actually mounted. + pass + + @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/mockbackend.py b/src/kimchi/model/mockbackend.py new file mode 100644 index 0000000..d4314ca --- /dev/null +++ b/src/kimchi/model/mockbackend.py @@ -0,0 +1,338 @@ +# +# Project Kimchi +# +# Copyright IBM, Corp. 2013 +# +# Authors: +# Adam Litke <agl@linux.vnet.ibm.com> +# 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 random + +from kimchi import config +from kimchi.asynctask import AsyncTask +from kimchi.objectstore import ObjectStore +from kimchi.screenshot import VMScreenshot + +class MockBackend(object): + _network_info = {'state': 'inactive', 'autostart': True, 'connection': '', + 'interface': '', 'subnet': '', + 'dhcp': {'start': '', 'stop': ''}} + + TEMPLATE_SCAN = False + + def __init__(self, objstore_loc=None): + self.objstore = ObjectStore(objstore_loc) + self.next_taskid = 1 + self.host_stats = self._get_host_stats() + self._storagepools = {} + self._networks = {} + self._templates = {} + self._vms = {} + self._screenshots = {} + + def _get_host_stats(self): + memory_stats = {'total': 3934908416L, + 'free': round(random.uniform(0, 3934908416L), 1), + 'cached': round(random.uniform(0, 3934908416L), 1), + 'buffers': round(random.uniform(0, 3934908416L), 1), + 'avail': round(random.uniform(0, 3934908416L), 1)} + + return {'cpu_utilization': round(random.uniform(0, 100), 1), + 'memory': memory_stats, + 'disk_read_rate': round(random.uniform(0, 4000), 1), + 'disk_write_rate': round(random.uniform(0, 4000), 1), + 'net_recv_rate': round(random.uniform(0, 4000), 1), + 'net_sent_rate': round(random.uniform(0, 4000), 1)} + + def get_capabilities(self): + protocols = ['http', 'https', 'ftp', 'ftps', 'tftp'] + return {'libvirt_stream_protocols': protocols, + 'qemu_stream': True, + 'screenshot': True, + 'system_report_tool': True} + + def gen_debugreport_file(self, ident): + return self.add_task('', self._create_debugreport, ident) + + def _create_debugreport(self, cb, name): + path = config.get_debugreports_path() + tmpf = os.path.join(path, name + '.tmp') + realf = os.path.join(path, name + '.txt') + length = random.randint(1000, 10000) + with open(tmpf, 'w') as fd: + while length: + fd.write('mock debug report\n') + length = length - 1 + os.rename(tmpf, realf) + cb("OK", True) + + def add_task(self, target_uri, fn, opaque=None): + id = self.next_taskid + self.next_taskid += 1 + task = AsyncTask(id, target_uri, fn, self.objstore, opaque) + return id + + def get_host(self): + res = {} + res['memory'] = 6114058240 + res['cpu'] = 'Intel(R) Core(TM) i5 CPU M 560 @ 2.67GHz' + res['os_distro'] = 'Red Hat Enterprise Linux Server' + res['os_version'] = '6.4' + res['os_codename'] = 'Santiago' + + return res + + def get_storagepools(self): + return self._storagepools.keys() + + def do_deep_scan(self): + return self.add_task('', time.sleep, 25) + + def create_storagepool(self, params): + name = params['name'] + pool = MockStoragePool(name) + pool.info.update(params) + if params['type'] == 'dir': + pool.info['autostart'] = True + else: + pool.info['autostart'] = False + + self._storagepools[name] = pool + + def get_storagepool_by_name(self, name): + pool = self._storagepools[name] + pool.refresh() + return pool.info + + def activate_storagepool(self, name): + self._storagepools[name].info['state'] = 'active' + + def deactivate_storagepool(self, name): + self._storagepools[name].info['state'] = 'inactive' + + def delete_storagepool(self, name): + del self._storagepools[name] + + def autostart_storagepool(self, name, value): + self._storagepools[name].info['autostart'] = value + + def create_storagevolume(self, pool, params): + try: + name = params['name'] + volume = MockStorageVolume(pool, name, params) + volume.info['type'] = params['type'] + volume.info['format'] = params['format'] + volume.info['path'] = os.path.join(pool.info['path'], name) + except KeyError, item: + raise MissingParameter(item) + + pool._volumes[name] = volume + + def get_storagevolumes_by_pool(self, pool): + return self._storagepools[pool]._volumes.keys() + + def get_storagevolume(self, pool, name): + vol = self._storagevolumes[pool]._volumes[name] + return vol.info + + def wipe_storagevolume(self, pool, name): + self._storagepools[pool]._volumes[name].info['allocation'] = 0 + + def resize_storagevolume(self, pool, name, size): + self._storagepools[pool]._volumes[name].info['capacity'] = size + + def delete_storagevolume(self, pool, name): + del self._storagepools[pool]._volumes[name] + + def create_network(self, params): + name = params['name'] + info = copy.deepcopy(self._network_info) + info.update(params) + self._networks[name] = info + + def get_networks(self): + return self._networks.keys() + + def get_network_by_name(self, name): + info = self._networks[name] + info['vms'] = self._get_vms_attach_to_network(name) + return info + + def _get_vms_attach_to_network(self, network): + vms = [] + for name, dom in self._vms.iteritems(): + if network in dom.networks: + vms.append(name) + return vms + + def activate_network(self, name): + self._networks[name]['state'] = 'active' + + def deactivate_network(self, name): + self._networks[name]['state'] = 'inactive' + + def delete_network(self, name): + del self._networks[name] + + def create_template(self, name, tmpl): + self._templates[name] = tmpl.info + + def get_templates(self): + return self._templates.keys() + + def get_template_by_name(self, name): + return self._templates[name] + + def delete_template(self, name): + del self._templates[name] + + def create_vm(self, name, uuid, tmpl, vol_list): + vm = MockVM(vm_uuid, name, tmpl.info) + icon = tmpl.info.get('icon', None) + if icon is not None: + vm.info['icon'] = icon + + disk_paths = [] + for vol in vol_list: + disk_paths.append({'pool': pool.name, 'volume': vol_info['name']}) + + vm.disk_paths = disk_paths + self._vms[name] = vm + + def get_vms(self): + return self._vms.keys() + + def get_screenshot_by_name(self, vm_uuid): + mockscreenshot = MockVMScreenshot({'uuid': vm_uuid}) + screenshot = self._screenshots.setdefault(vm_uuid, mockscreenshot) + return screenshot.lookup() + + def get_vm_by_name(self, name): + vm = self._vms[name] + if vm.info['state'] == 'running': + vm.info['screenshot'] = self.get_screenshot_by_name(name) + else: + vm.info['screenshot'] = None + return vm.info + + def static_vm_update(self, name, params): + vm_info = copy.copy(self._vms[name]) + for key, val in params.items(): + if key in VM_STATIC_UPDATE_PARAMS and key in vm_info: + vm_info[key] = val + + if 'name' in params: + del self._vms[name] + self._vms[params['name']] = vm_info + + def live_vm_update(self, name, params): + pass + + def delete_vm(self, name): + vm = self._vms[name] + screenshot = self._screenshots.get(vm.uuid, None) + if screenshot is not None: + screenshot.delete() + del self._screenshots[vm_uuid] + + for disk in vm.disk_paths: + self.delete_storagevolume(disk['pool'], disk['volume']) + + del self._vms[name] + + def start_vm(self, name): + self._vms[name].info['state'] = 'running' + + def stop_vm(self, name): + self._vms[name].info['state'] = 'shutoff' + + def connect_vm(self, name): + pass + +class MockStoragePool(object): + def __init__(self, name): + self.name = name + self._volumes = {} + self.info = {'state': 'inactive', 'capacity': 1024 << 20, + 'allocated': 512 << 20, 'available': 512 << 20, + 'path': '/var/lib/libvirt/images', 'source': {}, + 'type': 'dir', 'nr_volumes': 0, 'autostart': 0} + + def refresh(self): + state = self.info['state'] + self.info['nr_volumes'] = 0 + if state == 'active': + self.info['nr_volumes'] = len(self._volumes) + +class MockStorageVolume(object): + def __init__(self, pool, name, params={}): + self.name = name + self.pool = pool + self.info = {'type': 'disk', 'allocation': 512, + 'capacity': params.get('capacity', 1024) << 20, + 'format': params.get('format', 'raw')} + + if fmt == 'iso': + self.info['allocation'] = self.info['capacity'] + self.info['os_version'] = '19' + self.info['os_distro'] = 'fedora' + self.info['bootable'] = True + +class MockVM(object): + def __init__(self, uuid, name, template_info): + self.uuid = uuid + self.name = name + self.disk_paths = [] + self.networks = template_info['networks'] + self.info = {'state': 'shutoff', + 'stats': "{'cpu_utilization': 20, 'net_throughput' : 35, \ + 'net_throughput_peak': 100, 'io_throughput': 45, \ + 'io_throughput_peak': 100}", + 'uuid': self.uuid, + 'memory': template_info['memory'], + 'cpus': template_info['cpus'], + 'icon': None, + 'graphics': {'type': 'vnc', 'listen': '0.0.0.0', 'port': None} + } + self.info['graphics'].update(template_info['graphics']) + +class MockVMScreenshot(VMScreenshot): + OUTDATED_SECS = 5 + BACKGROUND_COLOR = ['blue', 'green', 'purple', 'red', 'yellow'] + BOX_COORD = (50, 115, 206, 141) + BAR_COORD = (50, 115, 50, 141) + + def __init__(self, vm_name): + VMScreenshot.__init__(self, vm_name) + self.coord = MockVMScreenshot.BAR_COORD + self.background = random.choice(MockVMScreenshot.BACKGROUND_COLOR) + + def _generate_scratch(self, thumbnail): + self.coord = (self.coord[0], + self.coord[1], + min(MockVMScreenshot.BOX_COORD[2], + self.coord[2]+random.randrange(50)), + self.coord[3]) + + image = Image.new("RGB", (256, 256), self.background) + d = ImageDraw.Draw(image) + d.rectangle(MockVMScreenshot.BOX_COORD, outline='black') + d.rectangle(self.coord, outline='black', fill='black') + image.save(thumbnail) diff --git a/src/kimchi/model/networks.py b/src/kimchi/model/networks.py new file mode 100644 index 0000000..dfefa81 --- /dev/null +++ b/src/kimchi/model/networks.py @@ -0,0 +1,115 @@ +# +# Project Kimchi +# +# Copyright IBM, Corp. 2013 +# +# Authors: +# Adam Litke <agl@linux.vnet.ibm.com> +# 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.model import interfaces + +class Networks(object): + def __init__(self, backend): + self.backend = backend + self.ifaces = interfaces.Interfaces(backend) + + def create(self, params): + name = params['name'] + if name in self.get_list(): + raise InvalidOperation("Network %s already exists" % name) + + connection = params['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': + iface = params.get('interface', None) + if iface is None: + raise MissingParameter("You need to specify interface to create" + " a bridged network.") + + if iface in self.ifaces.get_used_ifaces(): + raise InvalidParameter("interface '%s' already in use." % iface) + + if not (netinfo.is_bridge(iface) or netinfo.is_bare_nic(iface) or + netinfo.is_bonding(iface)): + raise InvalidParameter("The interface should be bare nic, bonding " + "or bridge device.") + + self.backend.create_network(params) + return name + + def get_list(self): + return sorted(self.backend.get_networks()) + + 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(): + net = self.backend.get_network_by_name(net_name) + subnet = net['subnet'] + subnet and net_addrs.append(ipaddr.IPNetwork(subnet)) + + netaddr = network.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({'subnet': str(ip), + 'dhcp': {'range': {'start': dhcp_start, + 'end': dhcp_end}}}) + +class Network(Networks): + def _network_exist(self, name): + if name not in self.get_list(): + raise NotFoundError("Network '%s' not found.") + + return True + + def lookup(self, name): + if self._network_exist(name): + return self.backend.get_network_by_name(name) + + def activate(self, name): + if self._network_exist(name): + return self.backend.activate_network(name) + + def deactivate(self, name): + if self._network_exist(name): + return self.backend.deactivate_network(name) + + def delete(self, name): + if self.lookup(name)['state'] == 'active': + raise InvalidOperation("Unable to delete the active network %s" % + name) + + return self.backend.delete_network(name) diff --git a/src/kimchi/model/plugins.py b/src/kimchi/model/plugins.py new file mode 100644 index 0000000..3cbae77 --- /dev/null +++ b/src/kimchi/model/plugins.py @@ -0,0 +1,29 @@ +# +# Project Kimchi +# +# Copyright IBM, Corp. 2013 +# +# Authors: +# Adam Litke <agl@linux.vnet.ibm.com> +# 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 utils + +class Plugins(object): + def get_list(self): + return [plugin for (plugin, config) in utils.get_enabled_plugins()] + diff --git a/src/kimchi/model/storagepools.py b/src/kimchi/model/storagepools.py new file mode 100644 index 0000000..abdebd8 --- /dev/null +++ b/src/kimchi/model/storagepools.py @@ -0,0 +1,86 @@ +# +# Project Kimchi +# +# Copyright IBM, Corp. 2013 +# +# Authors: +# Adam Litke <agl@linux.vnet.ibm.com> +# 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 + +ISO_POOL_NAME = u'kimchi_isos' + +from kimchi.exception import InvalidParameter + +class StoragePools(object): + def __init__(self, backend): + self.backend = backend + + def get_list(self): + return sorted(self.backend.get_storagepools()) + + def create(self, params): + name = params['name'] + if name in self.get_list() or name in (ISO_POOL_NAME,): + raise InvalidParameter("Storage pool '%s' already exists" % name) + + task_id = None + if params['type'] == 'kimchi-iso': + task_id = self.backend.do_deep_scan(params) + + self.backend.create_storagepool(params) + return name + +class StoragePool(StoragePools): + def lookup(self, name): + if name not in self.get_list(): + raise NotFoundError("Storage pool '%s' not found.") + + return self.backend.get_storagepool_by_name(name) + + def activate(self, name): + if name not in self.get_list(): + raise NotFoundError("Storage pool '%s' not found.") + + self.backend.activate_storagepool() + + def deactivate(self, name): + if name not in self.get_list(): + raise NotFoundError("Storage pool '%s' not found.") + + self.backend.deactivate_storagepool() + + def delete(self, name): + if name not in self.get_list(): + raise NotFoundError("Storage pool '%s' not found.") + + if self.get_storagepool_by_name(name)['state'] == 'active': + raise InvalidOperation("Unable to delete active storage pool '%s'" % + name) + + self.backend.delete_storagepool() + + def update(self, name, params): + if name not in self.get_list(): + raise NotFoundError("Storage pool '%s' not found.") + + autostart = params['autostart'] + if autostart not in [True, False]: + raise InvalidOperation("Autostart flag must be true or false") + + self.backend.autostart_storagepool(name, autostart) + + return name diff --git a/src/kimchi/model/storagevolumes.py b/src/kimchi/model/storagevolumes.py new file mode 100644 index 0000000..9f3c93b --- /dev/null +++ b/src/kimchi/model/storagevolumes.py @@ -0,0 +1,95 @@ +# +# Project Kimchi +# +# Copyright IBM, Corp. 2013 +# +# Authors: +# Adam Litke <agl@linux.vnet.ibm.com> +# 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 InvalidOperation, InvalidParameter, NotFoundError +from kimchi.model import storagepools + +class StorageVolumes(object): + def __init__(self, backend): + self.backend = backend + + def create(self, pool, params): + if name in self.get_list(pool): + raise InvalidParameter("Storage volume '%s' already exists.") + + self.backend.create_storagevolume(pool, params) + return name + + def get_list(self, pool): + info = self.backend.get_storagepool_by_name(pool) + if info['state'] != 'active': + raise InvalidOperation("Unable to list volumes in inactive " + "storagepool %s" % pool) + + return self.backend.get_storagevolumes_by_pool(pool) + +class StorageVolume(StorageVolumes): + def __init__(self, backend): + self.backend = backend + + def _storagevolume_exist(self, pool, name): + if name not in self.get_list(pool): + raise NotFoundError("Storage volume '%' not found in '%' pool" % + (name, pool)) + return True + + def lookup(self, pool, name): + if self._storagevolume_exist(pool, name): + return self.backend.get_storagevolume(pool, name) + + def resize(self, pool, name, size): + if self._storagevolume_exist(pool, name): + self.backend.resize_storagevolume(pool, name, size) + + def wipe(self, pool, name): + if self._storagevolume_exist(pool, name): + self.backend.wipe_storagevolume(pool, name) + + def delete(self, pool, name): + if self._storagevolume_exist(pool, name): + self.backend.delete_storagevolume(pool, name) + +class IsoVolumes(StorageVolumes): + def __init__(self, backend): + self.backend = backend + self.storagepools = storagepools.StoragePools(self.backend) + + def get_list(self, pool): + iso_volumes = [] + + for pool in self.storagepools.get_list(): + try: + volumes = self.get_list(pool) + except InvalidOperation: + # Skip inactive pools + continue + + for volume in volumes: + res = self.lookup(pool, volume) + if res['format'] == 'iso': + # prevent iso from different pool having same volume name + res['name'] = '%s-%s' % (pool, 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..29eaddf --- /dev/null +++ b/src/kimchi/model/tasks.py @@ -0,0 +1,45 @@ +# +# Project Kimchi +# +# Copyright IBM, Corp. 2013 +# +# Authors: +# Adam Litke <agl@linux.vnet.ibm.com> +# 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 + +ERROR_TASK_NOT_FOUND = "Task id '%s' not found." + +class Tasks(object): + def __init__(self, backend): + self.objstore = backend.objstore + + def get_list(self): + with self.objstore as session: + return session.get_list('task') + +class Task(object): + def __init__(self, backend): + self.objstore = backend.objstore + + def lookup(self, ident): + if ident not in self.get_list(): + raise NotFoundError(ERROR_TASK_NOT_FOUND % ident) + + with self.objstore as session: + return session.get('task', str(ident)) diff --git a/src/kimchi/model/templates.py b/src/kimchi/model/templates.py new file mode 100644 index 0000000..86cd54f --- /dev/null +++ b/src/kimchi/model/templates.py @@ -0,0 +1,89 @@ +# +# Project Kimchi +# +# Copyright IBM, Corp. 2013 +# +# Authors: +# Adam Litke <agl@linux.vnet.ibm.com> +# 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.vmtemplate import VMTemplate +from kimchi.exception import InvalidOperation, InvalidParameter, NotFoundError +from kimchi.exception import OperationFailed + +class Templates(object): + def __init__(self, backend): + self.backend = backend + + def create(self, params): + name = params['name'] + if name in self.get_list: + raise InvalidOperation("Template '%s' already exists." % name) + + for net_name in params.get(u'networks', []): + if net_name not in self.backend.get_networks(): + raise InvalidParameter("Network '%s' not found," % net_name) + + try: + tmpl = VMTemplate(params, self.backend.TEMPLATE_SCAN) + self.backend.create_template(name, tmpl) + except Exception, e: + raise OperationFailed("Unable to create template '%s': %s" % + (name, e.message)) + + return name + + def get_list(self): + return sorted(self.backend.get_templates()) + +class Template(Templates): + def lookup(self, name): + if name not in self.get_list(): + raise NotFoundError("Template '%s' not found." % name) + + params = self.backend.get_template_by_name(name) + tmpl = VMTemplate(params, False) + return tmpl.info + + def delete(self, name): + if name not in self.get_list(): + raise NotFoundError("Template '%s' not found." % name) + + self.backend.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 + + new_storagepool = new_t.get(u'storagepool', '') + if new_storagepool not in self.backend.get_storagepools(): + raise InvalidParameter("Storage pool '%s' not found." % name) + + for net_name in params.get(u'networks', []): + if net_name not in self.backend.get_networks(): + raise InvalidParameter("Network '%s' not found." % net_name) + + self.delete(name) + try: + ident = self.create(new_t) + except: + ident = self.create(old_t) + + return ident diff --git a/src/kimchi/model/vms.py b/src/kimchi/model/vms.py new file mode 100644 index 0000000..3f1b6c3 --- /dev/null +++ b/src/kimchi/model/vms.py @@ -0,0 +1,164 @@ +# +# Project Kimchi +# +# Copyright IBM, Corp. 2013 +# +# Authors: +# Adam Litke <agl@linux.vnet.ibm.com> +# 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 uuid + +VM_STATIC_UPDATE_PARAMS = {'name': './name'} + +class VMs(object): + def __init__(self, backend): + self.backend = backend + + def create(self, params): + vm_uuid = str(uuid.uuid4()) + vm_list = self.get_list() + name = self._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 '%s' already exists" % name) + + vm_overrides = dict() + override_params = ['storagepool', 'graphics'] + for param in override_params: + value = params.get(param, None) + if value is not None: + vm_overrides[param] = value + + t_name = self._uri_to_name('templates', params['template']) + t_params = self.backend.get_template_by_name(t_name) + t_params.update(vm_overrides) + tmpl = VMTemplate(t_params, False) + + caps = self.backend.get_capabilities() + if not caps.qemu_stream and t.info.get('iso_stream', False): + raise InvalidOperation("Remote ISO image is not supported by this" + " server.") + + pool_name = self._uri_to_name('storagepools', pool_uri) + self._validate_storagepool(pool_name) + self._validate_network(tmpl) + vol_list = tmpl.to_volume_list(vm_uuid) + for vol_info in vol_list: + self.backend.create_storagevolume(pool_name, vol_info) + + self.backend.create_vm(name, vm_uuid, tmpl, vol_list) + return name + + def _validate_storagepool(self, pool_name): + try: + pool_info = self.backend.get_storagepool_by_name(pool_name) + except Exception: + raise InvalidParameter("Storage pool '%s' specified by template " + "does not exist" % pool_name) + + if not pool_info['state'] != 'active': + raise InvalidParameter("Storage pool '%s' specified by template " + "is not active" % pool_name) + + def _validate_network(self, tmpl): + names = tmpl.info['networks'] + for name in names: + if name not in self.backend.get_networks(): + raise InvalidParameter("Network '%s' specified by template " + "does not exist.") + + net_info = self.backend.get_network_by_name(name) + if net_info['state'] != 'active': + raise InvalidParameter("Network '%s' specified by template is " + "not active.") + + def _uri_to_name(self, collection, uri): + expr = '/%s/(.*?)/?$' % collection + m = re.match(expr, uri) + if not m: + raise InvalidParameter(uri) + return m.group(1) + + def _get_vm_name(self, 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") + + def get_list(self): + return sorted(self.backend.get_vms()) + +class VM(VMs): + def _vm_exists(self, name): + if name not in self.backend.get_vms(): + raise NotFoundError("VM '%s' not found." % name) + + return True + + def lookup(self, name): + if self._vm_exists(name): + return self.backend.get_vm_by_name(name) + + def update(self, name, params): + if self._vm_exists(name): + if 'name' in params: + state = self.get_vm_by_name(name)['state'] + if state == 'running': + raise InvalidParameter("The VM needs to be shutted off for" + "renaming.") + + if params['name'] in self.get_list(): + raise InvalidParameter("VM name '%s' already exists" % + params['name']) + + self.backend.static_vm_update(name, params) + self.backend.live_vm_update(name, params) + + return params.get('name', None) or name + + def delete(self, name): + if self._vm_exists(name): + self.backend.delete_vm(name) + + def start(self, name): + if self._vm_exists(name): + self.backend.start_vm(self, name) + + def stop(self, name): + if self._vm_exists(name): + self.backend.stop_vm(self, name) + + def connect(self, name): + if self._vm_exists(name): + self.backend.connect_vm(self, name) + +class VMScreenshot(object): + def __init__(self, backend): + self.backend = backend + + def lookup(self, name): + vm_info = self.backend.get_vm_by_name(name) + if vm_info['state'] != 'running': + raise NotFoundError('No screenshot for stopped vm') + + return self.backend.get_screenshot_by_name(vm_info['uuid']) 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 7c0a5d6..0000000 --- a/src/kimchi/model_/config.py +++ /dev/null @@ -1,52 +0,0 @@ -# -# Project Kimchi -# -# Copyright IBM, Corp. 2013 -# -# Authors: -# Adam Litke <agl@linux.vnet.ibm.com> -# 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.distroloader import DistroLoader -from kimchi.exception import NotFoundError - -ERR_DISTRO_NOT_FOUND = "Distro '%s' not found." - -class Config(object): - pass - -class Capabilities(object): - def __init__(self, backend): - self.backend = backend - - def lookup(self, ident): - return self.backend.get_capabilities() - -class Distros(object): - def __init__(self): - self._distroloader = DistroLoader() - self._distros = self._distroloader.get() - - def get_list(self): - return self._distros.keys() - -class Distro(Distros): - def lookup(self, ident): - if ident not in self.get_list(): - raise NotFoundError(ERR_DISTRO_NOT_FOUND % ident) - - return self._distros[ident] diff --git a/src/kimchi/model_/debugreports.py b/src/kimchi/model_/debugreports.py deleted file mode 100644 index 4db1ace..0000000 --- a/src/kimchi/model_/debugreports.py +++ /dev/null @@ -1,86 +0,0 @@ -# -# Project Kimchi -# -# Copyright IBM, Corp. 2013 -# -# Authors: -# Adam Litke <agl@linux.vnet.ibm.com> -# 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 glob -import os -import time - -from kimchi import config -from kimchi.exception import NotFoundError - -class DebugReports(object): - def __init__(self, backend): - pass - - 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 - -class DebugReport(object): - def __init__(self, backend): - self.backend = backend - - def lookup(self, name): - file_target = self._get_debugreport(name) - 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 create(self, params): - ident = params['name'] - taskid = self.backend.gen_debugreport_file(ident) - if taskid is None: - raise OperationFailed("Debug report tool not found.") - - with self.backend.objstore as session: - return session.get('task', str(taskid)) - - def delete(self, name): - file_target = self._get_debugreports(name) - os.remove(file_target) - - def _get_debugreport(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("Debug report '%s' not found.") - - return file_target - -class DebugReportContent(object): - def __init__(self, backend): - self.backend = backend - - def lookup(self, name): - debugreport = DebugReports(backend) - return self.debugreport.lookup(name) diff --git a/src/kimchi/model_/host.py b/src/kimchi/model_/host.py deleted file mode 100644 index f5ef0d7..0000000 --- a/src/kimchi/model_/host.py +++ /dev/null @@ -1,49 +0,0 @@ -# -# Project Kimchi -# -# Copyright IBM, Corp. 2013 -# -# Authors: -# Adam Litke <agl@linux.vnet.ibm.com> -# 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 disks - -class Host(object): - def __init__(self, backend): - self.info = backend.get_host() - - def lookup(self, ident): - return self.info - -class HostStats(object): - def __init__(self, backend): - self.backend = backend - - def lookup(self, ident): - return self.backend.host_stats - -class Partitions(object): - def get_list(self): - return disks.get_partitions_names() - -class Partition(object): - def lookup(self, ident): - if ident not in disks.get_partitions_names(): - raise NotFoundError("Partition %s not found in the host" % name) - - return disks.get_partition_details(ident) diff --git a/src/kimchi/model_/interfaces.py b/src/kimchi/model_/interfaces.py deleted file mode 100644 index 7b1d156..0000000 --- a/src/kimchi/model_/interfaces.py +++ /dev/null @@ -1,48 +0,0 @@ -# -# Project Kimchi -# -# Copyright IBM, Corp. 2013 -# -# Authors: -# Adam Litke <agl@linux.vnet.ibm.com> -# 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 - -class Interfaces(object): - def __init__(self, backend): - self.backend = backend - - def get_list(self): - return list(set(netinfo.all_favored_interfaces()) - - set(self.get_used_ifaces())) - - def get_used_ifaces(self): - net_names = self.backend.get_networks() - interfaces = [] - for name in net_names: - net_dict = self.backend.get_network_by_name(name) - net_dict['interface'] and interfaces.append(net_dict['interface']) - - return interfaces - -class Interface(object): - def lookup(self, name): - try: - return netinfo.get_interface_info(name) - except ValueError, e: - raise NotFoundError(e) diff --git a/src/kimchi/model_/libvirtbackend.py b/src/kimchi/model_/libvirtbackend.py deleted file mode 100644 index bf29c78..0000000 --- a/src/kimchi/model_/libvirtbackend.py +++ /dev/null @@ -1,955 +0,0 @@ -# -# Project Kimchi -# -# Copyright IBM, Corp. 2013 -# -# Authors: -# Adam Litke <agl@linux.vnet.ibm.com> -# 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 -import fnmatch -import glob -import ipaddr -import logging -import os -import platform -import psutil -import shutil -import subprocess -import time - -from cherrypy.process.plugins import BackgroundTask -from collections import defaultdict - -from kimchi import config -from kimchi import network -from kimchi import netinfo -from kimchi import networkxml -from kimchi import xmlutils -from kimchi.asynctask import AsyncTask -from kimchi.exception import IsoFormatError, 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 kimchi_log - -GUESTS_STATS_INTERVAL = 5 -HOST_STATS_INTERVAL = 1 -STORAGE_SOURCES = {'netfs': {'addr': '/pool/source/host/@name', - 'path': '/pool/source/dir/@path'}} - -class LibvirtBackend(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'} - - TEMPLATE_SCAN = True - - 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.scanner = Scanner(self._clean_scan) - self.stats = {} - self.host_stats = defaultdict(int) - 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() - - # 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) - - # 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},) - - 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.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): - conn = self.conn.get() - vm_list = self.get_vms() - - for name in vm_list: - info = self.get_vm_by_name(name) - vm_uuid = info['uuid'] - state = info['state'] - 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}) - - dom = conn.lookupByName(name.encode("utf-8")) - self._get_percentage_cpu_usage(vm_uuid, dom.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 = 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 _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) - - # get cpu utilization - self.host_stats['cpu_utilization'] = psutil.cpu_percent(None) - - # get memory stats - virt_mem = psutil.virtual_memory() - self.host_stats['memory'] = {'total': virt_mem.total, - 'free': virt_mem.free, - 'cached': virt_mem.cached, - 'buffers': virt_mem.buffers, - 'avail': virt_mem.available} - - 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 - - ifaces = set(netinfo.nics() + netinfo.wlans()) & set(net_ios.iterkeys()) - for key in ifaces: - 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 gen_debugreport_file(self, name): - gen_cmd = self._get_system_report_tool() - if gen_cmd is None: - return None - - return self.add_task('', gen_cmd, name) - - 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 _sosreport_generate(self, cb, name): - command = 'sosreport --batch --name "%s"' % name - try: - retcode = subprocess.call(command, shell=True, - stdout=subprocess.PIPE, - stderr=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) - 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 add_task(self, target_uri, fn, opaque=None): - id = self.next_taskid - self.next_taskid += 1 - task = AsyncTask(id, target_uri, fn, self.objstore, opaque) - return id - - def get_host(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 - 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_storagepools(self): - try: - conn = self.conn.get() - names = conn.listStoragePools() - names += conn.listDefinedStoragePools() - return names - except libvirt.libvirtError as e: - raise OperationFailed(e.get_error_message()) - - def _clean_scan(self, pool_name): - try: - self.deactivate_storagepool(pool_name) - with self.objstore as session: - session.delete('scanning', pool_name) - except Exception, e: - kimchi_log.debug("Error while cleaning deep scan results" % - 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_storagepools(): - try: - res = self.get_storagepool_by_name(pool) - if res['state'] == 'active': - scan_params['ignore_list'].append(res['path']) - except Exception, e: - kimchi_log.debug("Error while preparing for deep scan: %s" % - e.message) - - params['path'] = self.scanner.scan_dir_prepare(params['name']) - scan_params['pool_path'] = params['path'] - 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 create_storagepool(self, params): - conn = self.conn.get() - try: - poolDef = StoragePoolDef.create(params) - poolDef.prepare(conn) - xml = poolDef.xml - except KeyError, key: - raise MissingParameter("You need to specify '%s' in order to " - "create storage pool" % key) - - 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()) - - def get_storagepool_by_name(self, name): - conn = self.conn.get() - pool = conn.storagePoolLookupByName(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': self.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 _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 activate_storagepool(self, name): - try: - conn = self.conn.get() - pool = conn.storagePoolLookupByName(name) - pool.create(0) - except libvirt.libvirtError as e: - raise OperationFailed(e.get_error_message()) - - def deactivate_storagepool(self, name): - try: - conn = self.conn.get() - pool = conn.storagePoolLookupByName(name) - pool.destroy() - except libvirt.libvirtError as e: - raise OperationFailed(e.get_error_message()) - - def delete_storagepool(self, name): - try: - conn = self.conn.get() - pool = conn.storagePoolLookupByName(name) - pool.undefine() - except libvirt.libvirtError as e: - raise OperationFailed(e.get_error_message()) - - def autostart_storagepool(self, name, value): - conn = self.conn.get() - pool = conn.storagePoolLookupByName(name) - if autostart: - pool.setAutostart(1) - else: - pool.setAutostart(0) - - def create_storagevolume(self, pool, params): - storagevol_xml = """ - <volume> - <name>%(name)s</name> - <allocation unit="MiB">%(allocation)s</allocation> - <capacity unit="MiB">%(capacity)s</capacity> - <target> - <format type='%(format)s'/> - <path>%(path)s</path> - </target> - </volume>""" - - params.setdefault('allocation', 0) - params.setdefault('format', 'qcow2') - try: - xml = storagevol_xml % params - except KeyError, key: - raise MissingParameter("You need to specify '%s' in order to " - "create the storage volume." % key) - - conn = self.conn.get() - pool = conn.storagePoolLookupByName(pool) - try: - pool.createXML(xml, 0) - except libvirt.libvirtError as e: - raise OperationFailed(e.get_error_message()) - - def _get_storagevolume(self, pool, name): - conn = self.conn.get() - pool = conn.storagePoolLookupByName(pool) - return pool.storageVolLookupByName(name) - - def get_storagevolumes_by_pool(self, pool): - try: - conn = self.conn.get() - pool = conn.storagePoolLookupByName(pool) - pool.refresh(0) - return pool.listVolumes() - except libvirt.libvirtError as e: - raise OperationFailed(e.get_error_message()) - - def get_storagevolume(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=self.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)) - - 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_storagevolume(self, pool, name): - try: - vol = self._get_storagevolume(pool, name) - vol.wipePattern(libvirt.VIR_STORAGE_VOL_WIPE_ALG_ZERO, 0) - except libvirt.libvirtError as e: - raise OperationFailed(e.get_error_message()) - - def resize_storagevolume(self, pool, name, size): - size = size << 20 - try: - vol = pool.storageVolLookupByName(name) - vol.resize(size, 0) - except libvirt.libvirtError as e: - raise OperationFailed(e.get_error_message()) - - def delete_storagevolume(self, pool, name): - try: - vol = pool.storageVolLookupByName(name) - volume.delete(0) - except libvirt.libvirtError as e: - raise OperationFailed(e.get_error_message()) - - def create_network(self, params): - connection = params["connection"] - # set forward mode, isolated do not need forward - if connection != 'isolated': - params['forward'] = {'mode': connection} - - if connection == 'bridge': - 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'])) - - xml = networkxml.to_network_xml(**params) - try: - conn = self.conn.get() - network = conn.networkDefineXML(xml) - network.setAutostart(True) - except libvirt.libvirtError as e: - raise OperationFailed(e.get_error_message()) - - 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 - - def get_networks(self): - conn = self.conn.get() - return conn.listNetworks() + conn.listDefinedNetworks() - - def get_network_by_name(self, name): - conn = self.conn.get() - net = conn.networkLookupByName(name) - xml = net.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 = network.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_network(name), - 'autostart': net.autostart() == 1, - 'state': net.isActive() and "active" or "inactive"} - - 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_vms_attach_to_network(self, network): - vms = [] - xpath = "/domain/devices/interface[@type='network']/source/@network" - conn = self.conn.get() - for dom in conn.listAllDomains(0): - xml = dom.XMLDesc(0) - networks = xmlutils.xpath_get_text(xml, xpath) - if network in networks: - vms.append(dom.name()) - return vms - - def activate_network(self, name): - conn = self.conn.get() - net = conn.networkLookupByName(name) - net.create() - - def deactivate_network(self, name): - conn = self.conn.get() - net = conn.networkLookupByName(name) - net.destroy() - - def delete_network(self, name): - conn = self.conn.get() - net = conn.networkLookupByName(name) - self._remove_vlan_tagged_bridge(net) - net.undefine() - - 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 create_template(self, name, tmpl): - with self.objstore as session: - session.store('template', name, tmpl.info) - - def get_templates(self): - with self.objstore as session: - return session.get_list('template') - - def get_template_by_name(self, name): - with self.objstore as session: - return session.get('template', name) - - def delete_template(self, name): - with self.objstore as session: - session.delete('template', name) - - def create_vm(self, name, uuid, tmpl, vol_list): - # Store the icon for displaying later - icon = tmpl.info.get('icon', None) - if icon is not None: - with self.objstore as session: - session.store('vm', vm_uuid, {'icon': icon}) - - libvirt_stream = False - if len(self.libvirt_stream_protocols) != 0: - libvirt_stream = True - - xml = tmpl.to_vm_xml(name, vm_uuid, libvirt_stream, - self.qemu_stream_dns) - 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()) - - def get_vms(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 get_screenshot_by_name(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) - - screenshot = LibvirtVMScreenshot(params, 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 - - def delete_screenshot(self, vm_uuid): - os.remove(self.get_screenshot_by_name(vm_uuid)) - with self.objstore as session: - session.delete('screenshot', vm_uuid) - - def get_vm_by_name(self, name): - conn = self.conn.get() - dom = conn.lookupByName(name.encode("utf-8")) - info = dom.info() - state = self.dom_state_map[info[0]] - screenshot = None - graphics = self._get_vm_graphics(dom) - graphics_type, graphics_listen, graphics_port = graphics - graphics_port = graphics_port if state == 'running' else None - if state == 'running': - screenshot = self.get_screenshot_by_name(name) - elif state == 'shutoff': - # reset vm stats when it is powered off to avoid sending - # incorrect (old) data - self.stats[dom.UUIDString()] = {} - - 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], 'icon': icon, - 'screenshot': screenshot, - 'graphics': {'type': graphics_type, 'listen': graphics_listen, - 'port': graphics_port} - } - - def _get_vm_graphics(self, dom): - 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 static_vm_update(self, name, params): - conn = self.conn.get() - dom = conn.lookupByName(name.encode("utf-8")) - 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: - dom.undefine() - conn.defineXML(new_xml) - except libvirt.libvirtError as e: - conn.defineXML(old_xml) - raise OperationFailed(e.get_error_message()) - - def live_vm_update(self, name, params): - pass - - def delete_vm(self, name): - info = self.get_vm_by_name(name) - if info['state'] == 'running': - self.stop_vm(name) - - conn = self.conn.get() - dom = conn.lookupByName(name.encode("utf-8")) - dom.undefine() - - xml = dom.XMLDesc(0) - xpath = "/domain/devices/disk[@device='disk']/source/@file" - paths = xmlutils.xpath_get_text(xml, xpath) - for path in paths: - vol = conn.storageVolLookupByPath(path) - vol.delete(0) - - with self.objstore as session: - session.delete('vm', dom.UUIDString(), ignore_missing=True) - - self.delete_screenshot(dom.UUIDString()) - vnc.remove_proxy_token(name) - - def start_vm(self, name): - conn = self.conn.get() - dom = conn.lookupByName(name.encode("utf-8")) - dom.create() - - def stop_vm(self, name): - conn = self.conn.get() - dom = conn.lookupByName(name.encode("utf-8")) - dom.destroy() - - def connect_vm(self, name): - graphics = self._get_vm_graphics(name) - graphics_type, graphics_listen, graphics_port = get_graphics - if graphics_port is None: - raise OperationFailed("Only able to connect to running vm's vnc " - "graphics.") - vnc.add_proxy_token(name, graphics_port) - -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) diff --git a/src/kimchi/model_/libvirtconnection.py b/src/kimchi/model_/libvirtconnection.py deleted file mode 100644 index 9276acc..0000000 --- a/src/kimchi/model_/libvirtconnection.py +++ /dev/null @@ -1,123 +0,0 @@ -# -# Project Kimchi -# -# Copyright IBM, Corp. 2013 -# -# Authors: -# Adam Litke <agl@linux.vnet.ibm.com> -# 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 -import libvirt -import threading -import time - - -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: - msg = 'Libvirt is not available, exiting.' - kimchi_log.error(msg) - 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 e9b9aa8..0000000 --- a/src/kimchi/model_/libvirtstoragepool.py +++ /dev/null @@ -1,225 +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.exception import OperationFailed -from kimchi.iscsi import TargetClient - -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): - # TODO: Verify the NFS export can be actually mounted. - pass - - @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_/mockbackend.py b/src/kimchi/model_/mockbackend.py deleted file mode 100644 index d4314ca..0000000 --- a/src/kimchi/model_/mockbackend.py +++ /dev/null @@ -1,338 +0,0 @@ -# -# Project Kimchi -# -# Copyright IBM, Corp. 2013 -# -# Authors: -# Adam Litke <agl@linux.vnet.ibm.com> -# 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 random - -from kimchi import config -from kimchi.asynctask import AsyncTask -from kimchi.objectstore import ObjectStore -from kimchi.screenshot import VMScreenshot - -class MockBackend(object): - _network_info = {'state': 'inactive', 'autostart': True, 'connection': '', - 'interface': '', 'subnet': '', - 'dhcp': {'start': '', 'stop': ''}} - - TEMPLATE_SCAN = False - - def __init__(self, objstore_loc=None): - self.objstore = ObjectStore(objstore_loc) - self.next_taskid = 1 - self.host_stats = self._get_host_stats() - self._storagepools = {} - self._networks = {} - self._templates = {} - self._vms = {} - self._screenshots = {} - - def _get_host_stats(self): - memory_stats = {'total': 3934908416L, - 'free': round(random.uniform(0, 3934908416L), 1), - 'cached': round(random.uniform(0, 3934908416L), 1), - 'buffers': round(random.uniform(0, 3934908416L), 1), - 'avail': round(random.uniform(0, 3934908416L), 1)} - - return {'cpu_utilization': round(random.uniform(0, 100), 1), - 'memory': memory_stats, - 'disk_read_rate': round(random.uniform(0, 4000), 1), - 'disk_write_rate': round(random.uniform(0, 4000), 1), - 'net_recv_rate': round(random.uniform(0, 4000), 1), - 'net_sent_rate': round(random.uniform(0, 4000), 1)} - - def get_capabilities(self): - protocols = ['http', 'https', 'ftp', 'ftps', 'tftp'] - return {'libvirt_stream_protocols': protocols, - 'qemu_stream': True, - 'screenshot': True, - 'system_report_tool': True} - - def gen_debugreport_file(self, ident): - return self.add_task('', self._create_debugreport, ident) - - def _create_debugreport(self, cb, name): - path = config.get_debugreports_path() - tmpf = os.path.join(path, name + '.tmp') - realf = os.path.join(path, name + '.txt') - length = random.randint(1000, 10000) - with open(tmpf, 'w') as fd: - while length: - fd.write('mock debug report\n') - length = length - 1 - os.rename(tmpf, realf) - cb("OK", True) - - def add_task(self, target_uri, fn, opaque=None): - id = self.next_taskid - self.next_taskid += 1 - task = AsyncTask(id, target_uri, fn, self.objstore, opaque) - return id - - def get_host(self): - res = {} - res['memory'] = 6114058240 - res['cpu'] = 'Intel(R) Core(TM) i5 CPU M 560 @ 2.67GHz' - res['os_distro'] = 'Red Hat Enterprise Linux Server' - res['os_version'] = '6.4' - res['os_codename'] = 'Santiago' - - return res - - def get_storagepools(self): - return self._storagepools.keys() - - def do_deep_scan(self): - return self.add_task('', time.sleep, 25) - - def create_storagepool(self, params): - name = params['name'] - pool = MockStoragePool(name) - pool.info.update(params) - if params['type'] == 'dir': - pool.info['autostart'] = True - else: - pool.info['autostart'] = False - - self._storagepools[name] = pool - - def get_storagepool_by_name(self, name): - pool = self._storagepools[name] - pool.refresh() - return pool.info - - def activate_storagepool(self, name): - self._storagepools[name].info['state'] = 'active' - - def deactivate_storagepool(self, name): - self._storagepools[name].info['state'] = 'inactive' - - def delete_storagepool(self, name): - del self._storagepools[name] - - def autostart_storagepool(self, name, value): - self._storagepools[name].info['autostart'] = value - - def create_storagevolume(self, pool, params): - try: - name = params['name'] - volume = MockStorageVolume(pool, name, params) - volume.info['type'] = params['type'] - volume.info['format'] = params['format'] - volume.info['path'] = os.path.join(pool.info['path'], name) - except KeyError, item: - raise MissingParameter(item) - - pool._volumes[name] = volume - - def get_storagevolumes_by_pool(self, pool): - return self._storagepools[pool]._volumes.keys() - - def get_storagevolume(self, pool, name): - vol = self._storagevolumes[pool]._volumes[name] - return vol.info - - def wipe_storagevolume(self, pool, name): - self._storagepools[pool]._volumes[name].info['allocation'] = 0 - - def resize_storagevolume(self, pool, name, size): - self._storagepools[pool]._volumes[name].info['capacity'] = size - - def delete_storagevolume(self, pool, name): - del self._storagepools[pool]._volumes[name] - - def create_network(self, params): - name = params['name'] - info = copy.deepcopy(self._network_info) - info.update(params) - self._networks[name] = info - - def get_networks(self): - return self._networks.keys() - - def get_network_by_name(self, name): - info = self._networks[name] - info['vms'] = self._get_vms_attach_to_network(name) - return info - - def _get_vms_attach_to_network(self, network): - vms = [] - for name, dom in self._vms.iteritems(): - if network in dom.networks: - vms.append(name) - return vms - - def activate_network(self, name): - self._networks[name]['state'] = 'active' - - def deactivate_network(self, name): - self._networks[name]['state'] = 'inactive' - - def delete_network(self, name): - del self._networks[name] - - def create_template(self, name, tmpl): - self._templates[name] = tmpl.info - - def get_templates(self): - return self._templates.keys() - - def get_template_by_name(self, name): - return self._templates[name] - - def delete_template(self, name): - del self._templates[name] - - def create_vm(self, name, uuid, tmpl, vol_list): - vm = MockVM(vm_uuid, name, tmpl.info) - icon = tmpl.info.get('icon', None) - if icon is not None: - vm.info['icon'] = icon - - disk_paths = [] - for vol in vol_list: - disk_paths.append({'pool': pool.name, 'volume': vol_info['name']}) - - vm.disk_paths = disk_paths - self._vms[name] = vm - - def get_vms(self): - return self._vms.keys() - - def get_screenshot_by_name(self, vm_uuid): - mockscreenshot = MockVMScreenshot({'uuid': vm_uuid}) - screenshot = self._screenshots.setdefault(vm_uuid, mockscreenshot) - return screenshot.lookup() - - def get_vm_by_name(self, name): - vm = self._vms[name] - if vm.info['state'] == 'running': - vm.info['screenshot'] = self.get_screenshot_by_name(name) - else: - vm.info['screenshot'] = None - return vm.info - - def static_vm_update(self, name, params): - vm_info = copy.copy(self._vms[name]) - for key, val in params.items(): - if key in VM_STATIC_UPDATE_PARAMS and key in vm_info: - vm_info[key] = val - - if 'name' in params: - del self._vms[name] - self._vms[params['name']] = vm_info - - def live_vm_update(self, name, params): - pass - - def delete_vm(self, name): - vm = self._vms[name] - screenshot = self._screenshots.get(vm.uuid, None) - if screenshot is not None: - screenshot.delete() - del self._screenshots[vm_uuid] - - for disk in vm.disk_paths: - self.delete_storagevolume(disk['pool'], disk['volume']) - - del self._vms[name] - - def start_vm(self, name): - self._vms[name].info['state'] = 'running' - - def stop_vm(self, name): - self._vms[name].info['state'] = 'shutoff' - - def connect_vm(self, name): - pass - -class MockStoragePool(object): - def __init__(self, name): - self.name = name - self._volumes = {} - self.info = {'state': 'inactive', 'capacity': 1024 << 20, - 'allocated': 512 << 20, 'available': 512 << 20, - 'path': '/var/lib/libvirt/images', 'source': {}, - 'type': 'dir', 'nr_volumes': 0, 'autostart': 0} - - def refresh(self): - state = self.info['state'] - self.info['nr_volumes'] = 0 - if state == 'active': - self.info['nr_volumes'] = len(self._volumes) - -class MockStorageVolume(object): - def __init__(self, pool, name, params={}): - self.name = name - self.pool = pool - self.info = {'type': 'disk', 'allocation': 512, - 'capacity': params.get('capacity', 1024) << 20, - 'format': params.get('format', 'raw')} - - if fmt == 'iso': - self.info['allocation'] = self.info['capacity'] - self.info['os_version'] = '19' - self.info['os_distro'] = 'fedora' - self.info['bootable'] = True - -class MockVM(object): - def __init__(self, uuid, name, template_info): - self.uuid = uuid - self.name = name - self.disk_paths = [] - self.networks = template_info['networks'] - self.info = {'state': 'shutoff', - 'stats': "{'cpu_utilization': 20, 'net_throughput' : 35, \ - 'net_throughput_peak': 100, 'io_throughput': 45, \ - 'io_throughput_peak': 100}", - 'uuid': self.uuid, - 'memory': template_info['memory'], - 'cpus': template_info['cpus'], - 'icon': None, - 'graphics': {'type': 'vnc', 'listen': '0.0.0.0', 'port': None} - } - self.info['graphics'].update(template_info['graphics']) - -class MockVMScreenshot(VMScreenshot): - OUTDATED_SECS = 5 - BACKGROUND_COLOR = ['blue', 'green', 'purple', 'red', 'yellow'] - BOX_COORD = (50, 115, 206, 141) - BAR_COORD = (50, 115, 50, 141) - - def __init__(self, vm_name): - VMScreenshot.__init__(self, vm_name) - self.coord = MockVMScreenshot.BAR_COORD - self.background = random.choice(MockVMScreenshot.BACKGROUND_COLOR) - - def _generate_scratch(self, thumbnail): - self.coord = (self.coord[0], - self.coord[1], - min(MockVMScreenshot.BOX_COORD[2], - self.coord[2]+random.randrange(50)), - self.coord[3]) - - image = Image.new("RGB", (256, 256), self.background) - d = ImageDraw.Draw(image) - d.rectangle(MockVMScreenshot.BOX_COORD, outline='black') - d.rectangle(self.coord, outline='black', fill='black') - image.save(thumbnail) diff --git a/src/kimchi/model_/networks.py b/src/kimchi/model_/networks.py deleted file mode 100644 index dfefa81..0000000 --- a/src/kimchi/model_/networks.py +++ /dev/null @@ -1,115 +0,0 @@ -# -# Project Kimchi -# -# Copyright IBM, Corp. 2013 -# -# Authors: -# Adam Litke <agl@linux.vnet.ibm.com> -# 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.model import interfaces - -class Networks(object): - def __init__(self, backend): - self.backend = backend - self.ifaces = interfaces.Interfaces(backend) - - def create(self, params): - name = params['name'] - if name in self.get_list(): - raise InvalidOperation("Network %s already exists" % name) - - connection = params['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': - iface = params.get('interface', None) - if iface is None: - raise MissingParameter("You need to specify interface to create" - " a bridged network.") - - if iface in self.ifaces.get_used_ifaces(): - raise InvalidParameter("interface '%s' already in use." % iface) - - if not (netinfo.is_bridge(iface) or netinfo.is_bare_nic(iface) or - netinfo.is_bonding(iface)): - raise InvalidParameter("The interface should be bare nic, bonding " - "or bridge device.") - - self.backend.create_network(params) - return name - - def get_list(self): - return sorted(self.backend.get_networks()) - - 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(): - net = self.backend.get_network_by_name(net_name) - subnet = net['subnet'] - subnet and net_addrs.append(ipaddr.IPNetwork(subnet)) - - netaddr = network.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({'subnet': str(ip), - 'dhcp': {'range': {'start': dhcp_start, - 'end': dhcp_end}}}) - -class Network(Networks): - def _network_exist(self, name): - if name not in self.get_list(): - raise NotFoundError("Network '%s' not found.") - - return True - - def lookup(self, name): - if self._network_exist(name): - return self.backend.get_network_by_name(name) - - def activate(self, name): - if self._network_exist(name): - return self.backend.activate_network(name) - - def deactivate(self, name): - if self._network_exist(name): - return self.backend.deactivate_network(name) - - def delete(self, name): - if self.lookup(name)['state'] == 'active': - raise InvalidOperation("Unable to delete the active network %s" % - name) - - return self.backend.delete_network(name) diff --git a/src/kimchi/model_/plugins.py b/src/kimchi/model_/plugins.py deleted file mode 100644 index 3cbae77..0000000 --- a/src/kimchi/model_/plugins.py +++ /dev/null @@ -1,29 +0,0 @@ -# -# Project Kimchi -# -# Copyright IBM, Corp. 2013 -# -# Authors: -# Adam Litke <agl@linux.vnet.ibm.com> -# 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 utils - -class Plugins(object): - def get_list(self): - return [plugin for (plugin, config) in utils.get_enabled_plugins()] - diff --git a/src/kimchi/model_/storagepools.py b/src/kimchi/model_/storagepools.py deleted file mode 100644 index abdebd8..0000000 --- a/src/kimchi/model_/storagepools.py +++ /dev/null @@ -1,86 +0,0 @@ -# -# Project Kimchi -# -# Copyright IBM, Corp. 2013 -# -# Authors: -# Adam Litke <agl@linux.vnet.ibm.com> -# 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 - -ISO_POOL_NAME = u'kimchi_isos' - -from kimchi.exception import InvalidParameter - -class StoragePools(object): - def __init__(self, backend): - self.backend = backend - - def get_list(self): - return sorted(self.backend.get_storagepools()) - - def create(self, params): - name = params['name'] - if name in self.get_list() or name in (ISO_POOL_NAME,): - raise InvalidParameter("Storage pool '%s' already exists" % name) - - task_id = None - if params['type'] == 'kimchi-iso': - task_id = self.backend.do_deep_scan(params) - - self.backend.create_storagepool(params) - return name - -class StoragePool(StoragePools): - def lookup(self, name): - if name not in self.get_list(): - raise NotFoundError("Storage pool '%s' not found.") - - return self.backend.get_storagepool_by_name(name) - - def activate(self, name): - if name not in self.get_list(): - raise NotFoundError("Storage pool '%s' not found.") - - self.backend.activate_storagepool() - - def deactivate(self, name): - if name not in self.get_list(): - raise NotFoundError("Storage pool '%s' not found.") - - self.backend.deactivate_storagepool() - - def delete(self, name): - if name not in self.get_list(): - raise NotFoundError("Storage pool '%s' not found.") - - if self.get_storagepool_by_name(name)['state'] == 'active': - raise InvalidOperation("Unable to delete active storage pool '%s'" % - name) - - self.backend.delete_storagepool() - - def update(self, name, params): - if name not in self.get_list(): - raise NotFoundError("Storage pool '%s' not found.") - - autostart = params['autostart'] - if autostart not in [True, False]: - raise InvalidOperation("Autostart flag must be true or false") - - self.backend.autostart_storagepool(name, autostart) - - return name diff --git a/src/kimchi/model_/storagevolumes.py b/src/kimchi/model_/storagevolumes.py deleted file mode 100644 index 9f3c93b..0000000 --- a/src/kimchi/model_/storagevolumes.py +++ /dev/null @@ -1,95 +0,0 @@ -# -# Project Kimchi -# -# Copyright IBM, Corp. 2013 -# -# Authors: -# Adam Litke <agl@linux.vnet.ibm.com> -# 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 InvalidOperation, InvalidParameter, NotFoundError -from kimchi.model import storagepools - -class StorageVolumes(object): - def __init__(self, backend): - self.backend = backend - - def create(self, pool, params): - if name in self.get_list(pool): - raise InvalidParameter("Storage volume '%s' already exists.") - - self.backend.create_storagevolume(pool, params) - return name - - def get_list(self, pool): - info = self.backend.get_storagepool_by_name(pool) - if info['state'] != 'active': - raise InvalidOperation("Unable to list volumes in inactive " - "storagepool %s" % pool) - - return self.backend.get_storagevolumes_by_pool(pool) - -class StorageVolume(StorageVolumes): - def __init__(self, backend): - self.backend = backend - - def _storagevolume_exist(self, pool, name): - if name not in self.get_list(pool): - raise NotFoundError("Storage volume '%' not found in '%' pool" % - (name, pool)) - return True - - def lookup(self, pool, name): - if self._storagevolume_exist(pool, name): - return self.backend.get_storagevolume(pool, name) - - def resize(self, pool, name, size): - if self._storagevolume_exist(pool, name): - self.backend.resize_storagevolume(pool, name, size) - - def wipe(self, pool, name): - if self._storagevolume_exist(pool, name): - self.backend.wipe_storagevolume(pool, name) - - def delete(self, pool, name): - if self._storagevolume_exist(pool, name): - self.backend.delete_storagevolume(pool, name) - -class IsoVolumes(StorageVolumes): - def __init__(self, backend): - self.backend = backend - self.storagepools = storagepools.StoragePools(self.backend) - - def get_list(self, pool): - iso_volumes = [] - - for pool in self.storagepools.get_list(): - try: - volumes = self.get_list(pool) - except InvalidOperation: - # Skip inactive pools - continue - - for volume in volumes: - res = self.lookup(pool, volume) - if res['format'] == 'iso': - # prevent iso from different pool having same volume name - res['name'] = '%s-%s' % (pool, 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 29eaddf..0000000 --- a/src/kimchi/model_/tasks.py +++ /dev/null @@ -1,45 +0,0 @@ -# -# Project Kimchi -# -# Copyright IBM, Corp. 2013 -# -# Authors: -# Adam Litke <agl@linux.vnet.ibm.com> -# 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 - -ERROR_TASK_NOT_FOUND = "Task id '%s' not found." - -class Tasks(object): - def __init__(self, backend): - self.objstore = backend.objstore - - def get_list(self): - with self.objstore as session: - return session.get_list('task') - -class Task(object): - def __init__(self, backend): - self.objstore = backend.objstore - - def lookup(self, ident): - if ident not in self.get_list(): - raise NotFoundError(ERROR_TASK_NOT_FOUND % ident) - - with self.objstore as session: - return session.get('task', str(ident)) diff --git a/src/kimchi/model_/templates.py b/src/kimchi/model_/templates.py deleted file mode 100644 index 86cd54f..0000000 --- a/src/kimchi/model_/templates.py +++ /dev/null @@ -1,89 +0,0 @@ -# -# Project Kimchi -# -# Copyright IBM, Corp. 2013 -# -# Authors: -# Adam Litke <agl@linux.vnet.ibm.com> -# 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.vmtemplate import VMTemplate -from kimchi.exception import InvalidOperation, InvalidParameter, NotFoundError -from kimchi.exception import OperationFailed - -class Templates(object): - def __init__(self, backend): - self.backend = backend - - def create(self, params): - name = params['name'] - if name in self.get_list: - raise InvalidOperation("Template '%s' already exists." % name) - - for net_name in params.get(u'networks', []): - if net_name not in self.backend.get_networks(): - raise InvalidParameter("Network '%s' not found," % net_name) - - try: - tmpl = VMTemplate(params, self.backend.TEMPLATE_SCAN) - self.backend.create_template(name, tmpl) - except Exception, e: - raise OperationFailed("Unable to create template '%s': %s" % - (name, e.message)) - - return name - - def get_list(self): - return sorted(self.backend.get_templates()) - -class Template(Templates): - def lookup(self, name): - if name not in self.get_list(): - raise NotFoundError("Template '%s' not found." % name) - - params = self.backend.get_template_by_name(name) - tmpl = VMTemplate(params, False) - return tmpl.info - - def delete(self, name): - if name not in self.get_list(): - raise NotFoundError("Template '%s' not found." % name) - - self.backend.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 - - new_storagepool = new_t.get(u'storagepool', '') - if new_storagepool not in self.backend.get_storagepools(): - raise InvalidParameter("Storage pool '%s' not found." % name) - - for net_name in params.get(u'networks', []): - if net_name not in self.backend.get_networks(): - raise InvalidParameter("Network '%s' not found." % net_name) - - self.delete(name) - try: - ident = self.create(new_t) - except: - ident = self.create(old_t) - - return ident diff --git a/src/kimchi/model_/vms.py b/src/kimchi/model_/vms.py deleted file mode 100644 index 3f1b6c3..0000000 --- a/src/kimchi/model_/vms.py +++ /dev/null @@ -1,164 +0,0 @@ -# -# Project Kimchi -# -# Copyright IBM, Corp. 2013 -# -# Authors: -# Adam Litke <agl@linux.vnet.ibm.com> -# 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 uuid - -VM_STATIC_UPDATE_PARAMS = {'name': './name'} - -class VMs(object): - def __init__(self, backend): - self.backend = backend - - def create(self, params): - vm_uuid = str(uuid.uuid4()) - vm_list = self.get_list() - name = self._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 '%s' already exists" % name) - - vm_overrides = dict() - override_params = ['storagepool', 'graphics'] - for param in override_params: - value = params.get(param, None) - if value is not None: - vm_overrides[param] = value - - t_name = self._uri_to_name('templates', params['template']) - t_params = self.backend.get_template_by_name(t_name) - t_params.update(vm_overrides) - tmpl = VMTemplate(t_params, False) - - caps = self.backend.get_capabilities() - if not caps.qemu_stream and t.info.get('iso_stream', False): - raise InvalidOperation("Remote ISO image is not supported by this" - " server.") - - pool_name = self._uri_to_name('storagepools', pool_uri) - self._validate_storagepool(pool_name) - self._validate_network(tmpl) - vol_list = tmpl.to_volume_list(vm_uuid) - for vol_info in vol_list: - self.backend.create_storagevolume(pool_name, vol_info) - - self.backend.create_vm(name, vm_uuid, tmpl, vol_list) - return name - - def _validate_storagepool(self, pool_name): - try: - pool_info = self.backend.get_storagepool_by_name(pool_name) - except Exception: - raise InvalidParameter("Storage pool '%s' specified by template " - "does not exist" % pool_name) - - if not pool_info['state'] != 'active': - raise InvalidParameter("Storage pool '%s' specified by template " - "is not active" % pool_name) - - def _validate_network(self, tmpl): - names = tmpl.info['networks'] - for name in names: - if name not in self.backend.get_networks(): - raise InvalidParameter("Network '%s' specified by template " - "does not exist.") - - net_info = self.backend.get_network_by_name(name) - if net_info['state'] != 'active': - raise InvalidParameter("Network '%s' specified by template is " - "not active.") - - def _uri_to_name(self, collection, uri): - expr = '/%s/(.*?)/?$' % collection - m = re.match(expr, uri) - if not m: - raise InvalidParameter(uri) - return m.group(1) - - def _get_vm_name(self, 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") - - def get_list(self): - return sorted(self.backend.get_vms()) - -class VM(VMs): - def _vm_exists(self, name): - if name not in self.backend.get_vms(): - raise NotFoundError("VM '%s' not found." % name) - - return True - - def lookup(self, name): - if self._vm_exists(name): - return self.backend.get_vm_by_name(name) - - def update(self, name, params): - if self._vm_exists(name): - if 'name' in params: - state = self.get_vm_by_name(name)['state'] - if state == 'running': - raise InvalidParameter("The VM needs to be shutted off for" - "renaming.") - - if params['name'] in self.get_list(): - raise InvalidParameter("VM name '%s' already exists" % - params['name']) - - self.backend.static_vm_update(name, params) - self.backend.live_vm_update(name, params) - - return params.get('name', None) or name - - def delete(self, name): - if self._vm_exists(name): - self.backend.delete_vm(name) - - def start(self, name): - if self._vm_exists(name): - self.backend.start_vm(self, name) - - def stop(self, name): - if self._vm_exists(name): - self.backend.stop_vm(self, name) - - def connect(self, name): - if self._vm_exists(name): - self.backend.connect_vm(self, name) - -class VMScreenshot(object): - def __init__(self, backend): - self.backend = backend - - def lookup(self, name): - vm_info = self.backend.get_vm_by_name(name) - if vm_info['state'] != 'running': - raise NotFoundError('No screenshot for stopped vm') - - return self.backend.get_screenshot_by_name(vm_info['uuid']) diff --git a/src/kimchi/root.py b/src/kimchi/root.py index 3cc6321..a064ccd 100644 --- a/src/kimchi/root.py +++ b/src/kimchi/root.py @@ -44,7 +44,7 @@ from kimchi.exception import OperationFailed class Root(Resource): - def __init__(self, model, dev_env): + def __init__(self, backend, dev_env): self._handled_error = ['error_page.400', 'error_page.404', 'error_page.405', 'error_page.406', 'error_page.415', 'error_page.500'] @@ -56,17 +56,17 @@ class Root(Resource): self._cp_config = dict([(key, self.error_development_handler) for key in self._handled_error]) - Resource.__init__(self, model) - self.vms = VMs(model) - self.templates = Templates(model) - self.storagepools = StoragePools(model) - self.interfaces = Interfaces(model) - self.networks = Networks(model) - self.tasks = Tasks(model) - self.config = Config(model) - self.host = Host(model) - self.debugreports = DebugReports(model) - self.plugins = Plugins(model) + Resource.__init__(self, backend) + self.vms = VMs(backend) + self.templates = Templates(backend) + self.storagepools = StoragePools(backend) + self.interfaces = Interfaces(backend) + self.networks = Networks(backend) + self.tasks = Tasks(backend) + self.config = Config(backend) + self.host = Host(backend) + self.debugreports = DebugReports(backend) + self.plugins = Plugins(backend) self.api_schema = json.load(open(get_api_schema_file())) def error_production_handler(self, status, message, traceback, version): diff --git a/src/kimchi/server.py b/src/kimchi/server.py index b820263..ffa9324 100644 --- a/src/kimchi/server.py +++ b/src/kimchi/server.py @@ -30,9 +30,9 @@ import sslcert from kimchi import auth from kimchi import config -from kimchi import model -from kimchi import mockmodel from kimchi import vnc +from kimchi.model.libvirtbackend import LibvirtBackend +from kimchi.model.mockbackend import MockBackend from kimchi.root import Root from kimchi.utils import get_enabled_plugins, import_class @@ -182,18 +182,18 @@ class Server(object): if not dev_env: cherrypy.config.update({'environment': 'production'}) - if hasattr(options, 'model'): - model_instance = options.model + if hasattr(options, 'backend'): + backend_instance = options.backend elif options.test: - model_instance = mockmodel.get_mock_environment() + backend_instance = MockBackend() else: - model_instance = model.Model() + backend_instance = LibvirtBackend() - if isinstance(model_instance, model.Model): + if isinstance(backend_instance, LibvirtBackend): vnc_ws_proxy = vnc.new_ws_proxy() cherrypy.engine.subscribe('exit', vnc_ws_proxy.kill) - self.app = cherrypy.tree.mount(Root(model_instance, dev_env), + self.app = cherrypy.tree.mount(Root(backend_instance, dev_env), config=self.configObj) self._load_plugins() -- 1.7.10.4