[PATCH 00/13][WIP] Refactor model

From: Aline Manera <alinefm@br.ibm.com> I am sending the first patch set to refactor model It is not completed. I still need to do a lot of tests and so on. But I would like to share it with you to get your suggestion. Basically, the common code between model and mockmodel was placed into model/<resource>.py and the specific code into model/libvirtbackend.py and mockbackend.py *** It still needs a lot of work *** - Tests all functions - Update Makefile and spec files - Update test cases Aline Manera (13): refactor model: Create a separated model for task resource refactor model: Create a separated model for debugreport resource refactor model: Create a separated model for config resource refactor model: Create a separated model for host resource refactor model: Create a separated model for plugin resource refactor model: Create a separated model for libvirt connection refactor model: Move StoragePooldef from model to libvirtstoragepools.py refactor model: Create a separated model for storagepool resource refactor model: Create a separated model for storagevolume resource refactor model: Create a separated model for interface and network resources refactor model: Create a separated model for template resource refactor model: Create a separated model for vm resource refactor model: Update server.py and root.py to use new models 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 | 1827 -------------------------------- 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/networkxml.py | 6 +- src/kimchi/root.py | 24 +- src/kimchi/server.py | 16 +- src/kimchi/vmtemplate.py | 18 +- 35 files changed, 2674 insertions(+), 2757 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 -- 1.7.10.4

From: Aline Manera <alinefm@br.ibm.com> To avoid duplicating code in model and mockmodel, the common code related to task resource was added to model_/tasks.py and the specific code for each backend (libvirt or mock) was added to model_/libvirtbackend.py and model_/mockbackend.py The model_ module is a temporary one, as there already is a module named model in source code. When start using the new code, model_ will be renamed to model and the former model.py deleted. Signed-off-by: Aline Manera <alinefm@br.ibm.com> --- src/kimchi/model_/__init__.py | 21 ++++++++++++++++ src/kimchi/model_/libvirtbackend.py | 28 ++++++++++++++++++++++ src/kimchi/model_/mockbackend.py | 28 ++++++++++++++++++++++ src/kimchi/model_/tasks.py | 45 +++++++++++++++++++++++++++++++++++ 4 files changed, 122 insertions(+) create mode 100644 src/kimchi/model_/__init__.py create mode 100644 src/kimchi/model_/libvirtbackend.py create mode 100644 src/kimchi/model_/mockbackend.py create mode 100644 src/kimchi/model_/tasks.py diff --git a/src/kimchi/model_/__init__.py b/src/kimchi/model_/__init__.py new file mode 100644 index 0000000..8a37cc4 --- /dev/null +++ b/src/kimchi/model_/__init__.py @@ -0,0 +1,21 @@ +# +# Project Kimchi +# +# Copyright IBM, Corp. 2013 +# +# Authors: +# Aline Manera <alinefm@linux.vnet.ibm.com> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA diff --git a/src/kimchi/model_/libvirtbackend.py b/src/kimchi/model_/libvirtbackend.py new file mode 100644 index 0000000..0c70116 --- /dev/null +++ b/src/kimchi/model_/libvirtbackend.py @@ -0,0 +1,28 @@ +# +# 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.objectstore import ObjectStore + +class LibvirtBackend(object): + def __init__(self, objstore_loc=None): + self.objstore = ObjectStore(objstore_loc) diff --git a/src/kimchi/model_/mockbackend.py b/src/kimchi/model_/mockbackend.py new file mode 100644 index 0000000..e3c6ab9 --- /dev/null +++ b/src/kimchi/model_/mockbackend.py @@ -0,0 +1,28 @@ +# +# 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.objectstore import ObjectStore + +class MockBackend(object): + def __init__(self, objstore_loc=None): + self.objstore = ObjectStore(objstore_loc) 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)) -- 1.7.10.4

On 2014年01月17日 10:24, Aline Manera wrote:
From: Aline Manera <alinefm@br.ibm.com>
To avoid duplicating code in model and mockmodel, the common code related to task resource was added to model_/tasks.py and the specific code for each backend (libvirt or mock) was added to model_/libvirtbackend.py and model_/mockbackend.py The model_ module is a temporary one, as there already is a module named model in source code. When start using the new code, model_ will be renamed to model and the former model.py deleted.
Signed-off-by: Aline Manera <alinefm@br.ibm.com> --- src/kimchi/model_/__init__.py | 21 ++++++++++++++++ src/kimchi/model_/libvirtbackend.py | 28 ++++++++++++++++++++++ src/kimchi/model_/mockbackend.py | 28 ++++++++++++++++++++++ src/kimchi/model_/tasks.py | 45 +++++++++++++++++++++++++++++++++++ 4 files changed, 122 insertions(+) create mode 100644 src/kimchi/model_/__init__.py create mode 100644 src/kimchi/model_/libvirtbackend.py create mode 100644 src/kimchi/model_/mockbackend.py create mode 100644 src/kimchi/model_/tasks.py
diff --git a/src/kimchi/model_/__init__.py b/src/kimchi/model_/__init__.py new file mode 100644 index 0000000..8a37cc4 --- /dev/null +++ b/src/kimchi/model_/__init__.py @@ -0,0 +1,21 @@ +# +# Project Kimchi +# +# Copyright IBM, Corp. 2013 +# +# Authors: +# Aline Manera <alinefm@linux.vnet.ibm.com> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA diff --git a/src/kimchi/model_/libvirtbackend.py b/src/kimchi/model_/libvirtbackend.py new file mode 100644 index 0000000..0c70116 --- /dev/null +++ b/src/kimchi/model_/libvirtbackend.py @@ -0,0 +1,28 @@ +# +# 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.objectstore import ObjectStore + +class LibvirtBackend(object): + def __init__(self, objstore_loc=None): + self.objstore = ObjectStore(objstore_loc) diff --git a/src/kimchi/model_/mockbackend.py b/src/kimchi/model_/mockbackend.py new file mode 100644 index 0000000..e3c6ab9 --- /dev/null +++ b/src/kimchi/model_/mockbackend.py @@ -0,0 +1,28 @@ +# +# 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.objectstore import ObjectStore + +class MockBackend(object): + def __init__(self, objstore_loc=None): + self.objstore = ObjectStore(objstore_loc) 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 This makes me want a singleton and call it from get(). + + 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))

From: Aline Manera <alinefm@br.ibm.com> To avoid duplicating code in model and mockmodel, the common code related to debugreport resource was added to model_/debugreports.py and the specific code for each backend (libvirt or mock) was added to model_/libvirtbackend.py and model_/mockbackend.py Signed-off-by: Aline Manera <alinefm@br.ibm.com> --- src/kimchi/model_/debugreports.py | 86 +++++++++++++++++++++++++++++++++++ src/kimchi/model_/libvirtbackend.py | 85 ++++++++++++++++++++++++++++++++++ src/kimchi/model_/mockbackend.py | 27 +++++++++++ 3 files changed, 198 insertions(+) create mode 100644 src/kimchi/model_/debugreports.py diff --git a/src/kimchi/model_/debugreports.py b/src/kimchi/model_/debugreports.py new file mode 100644 index 0000000..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_/libvirtbackend.py b/src/kimchi/model_/libvirtbackend.py index 0c70116..ea46a13 100644 --- a/src/kimchi/model_/libvirtbackend.py +++ b/src/kimchi/model_/libvirtbackend.py @@ -21,8 +21,93 @@ # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +import fnmatch +import glob +import logging +import os +import shutil +import subprocess + +from kimchi import config +from kimchi.asynctask import AsyncTask +from kimchi.exception import OperationFailed from kimchi.objectstore import ObjectStore +from kimchi.utils import kimchi_log class LibvirtBackend(object): def __init__(self, objstore_loc=None): self.objstore = ObjectStore(objstore_loc) + self.next_taskid = 1 + + # 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 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 diff --git a/src/kimchi/model_/mockbackend.py b/src/kimchi/model_/mockbackend.py index e3c6ab9..fa60fc1 100644 --- a/src/kimchi/model_/mockbackend.py +++ b/src/kimchi/model_/mockbackend.py @@ -21,8 +21,35 @@ # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +import os +import random + +from kimchi import config +from kimchi.asynctask import AsyncTask from kimchi.objectstore import ObjectStore class MockBackend(object): def __init__(self, objstore_loc=None): self.objstore = ObjectStore(objstore_loc) + self.next_taskid = 1 + + 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 -- 1.7.10.4

From: Aline Manera <alinefm@br.ibm.com> To avoid duplicating code in model and mockmodel, the common code related to config resource (and its sub-resources) was added to model_/config.py and the specific code for each backend (libvirt or mock) was added to model_/libvirtbackend.py and model_/mockbackend.py Signed-off-by: Aline Manera <alinefm@br.ibm.com> --- src/kimchi/model_/config.py | 52 +++++++++++++++++++++++++++++++++++ src/kimchi/model_/libvirtbackend.py | 28 +++++++++++++++++++ src/kimchi/model_/mockbackend.py | 7 +++++ 3 files changed, 87 insertions(+) create mode 100644 src/kimchi/model_/config.py 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_/libvirtbackend.py b/src/kimchi/model_/libvirtbackend.py index ea46a13..506b8c4 100644 --- a/src/kimchi/model_/libvirtbackend.py +++ b/src/kimchi/model_/libvirtbackend.py @@ -21,6 +21,7 @@ # 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 logging @@ -30,6 +31,7 @@ import subprocess from kimchi import config from kimchi.asynctask import AsyncTask +from kimchi.featuretests import FeatureTests from kimchi.exception import OperationFailed from kimchi.objectstore import ObjectStore from kimchi.utils import kimchi_log @@ -39,12 +41,38 @@ class LibvirtBackend(object): self.objstore = ObjectStore(objstore_loc) self.next_taskid = 1 + # 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 gen_debugreport_file(self, name): gen_cmd = self._get_system_report_tool() if gen_cmd is None: diff --git a/src/kimchi/model_/mockbackend.py b/src/kimchi/model_/mockbackend.py index fa60fc1..d6b8925 100644 --- a/src/kimchi/model_/mockbackend.py +++ b/src/kimchi/model_/mockbackend.py @@ -33,6 +33,13 @@ class MockBackend(object): self.objstore = ObjectStore(objstore_loc) self.next_taskid = 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) -- 1.7.10.4

From: Aline Manera <alinefm@br.ibm.com> To avoid duplicating code in model and mockmodel, the common code related to host resource (and its sub-resources) was added to model_/host.py and the specific code for each backend (libvirt or mock) was added to model_/libvirtbackend.py and model_/mockbackend.py Signed-off-by: Aline Manera <alinefm@br.ibm.com> --- src/kimchi/model_/host.py | 49 +++++++++++++++++++ src/kimchi/model_/libvirtbackend.py | 89 +++++++++++++++++++++++++++++++++++ src/kimchi/model_/mockbackend.py | 25 ++++++++++ 3 files changed, 163 insertions(+) create mode 100644 src/kimchi/model_/host.py diff --git a/src/kimchi/model_/host.py b/src/kimchi/model_/host.py new file mode 100644 index 0000000..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_/libvirtbackend.py b/src/kimchi/model_/libvirtbackend.py index 506b8c4..4ab4db6 100644 --- a/src/kimchi/model_/libvirtbackend.py +++ b/src/kimchi/model_/libvirtbackend.py @@ -26,8 +26,14 @@ import fnmatch import glob 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.asynctask import AsyncTask @@ -36,10 +42,16 @@ from kimchi.exception import OperationFailed from kimchi.objectstore import ObjectStore from kimchi.utils import kimchi_log +HOST_STATS_INTERVAL = 1 + class LibvirtBackend(object): def __init__(self, objstore_loc=None): self.objstore = ObjectStore(objstore_loc) self.next_taskid = 1 + self.host_stats = defaultdict(int) + self.host_stats_thread = BackgroundTask(HOST_STATS_INTERVAL, + self._update_host_stats) + self.host_stats_thread.start() # Subscribe function to set host capabilities to be run when cherrypy # server is up @@ -73,6 +85,67 @@ class LibvirtBackend(object): 'screenshot': VMScreenshot.get_stream_test_result(), 'system_report_tool': bool(report_tool)} + 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: @@ -139,3 +212,19 @@ class LibvirtBackend(object): 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 diff --git a/src/kimchi/model_/mockbackend.py b/src/kimchi/model_/mockbackend.py index d6b8925..5fb332e 100644 --- a/src/kimchi/model_/mockbackend.py +++ b/src/kimchi/model_/mockbackend.py @@ -32,6 +32,21 @@ class MockBackend(object): def __init__(self, objstore_loc=None): self.objstore = ObjectStore(objstore_loc) self.next_taskid = 1 + self.host_stats = self._get_host_stats() + + 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'] @@ -60,3 +75,13 @@ class MockBackend(object): 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 -- 1.7.10.4

From: Aline Manera <alinefm@br.ibm.com> To avoid duplicating code in model and mockmodel, the code related to plugin resource was added to model_/plugins.py Signed-off-by: Aline Manera <alinefm@br.ibm.com> --- src/kimchi/model_/plugins.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 src/kimchi/model_/plugins.py diff --git a/src/kimchi/model_/plugins.py b/src/kimchi/model_/plugins.py new file mode 100644 index 0000000..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()] + -- 1.7.10.4

From: Aline Manera <alinefm@br.ibm.com> Libvirt connection is a base facility for kimchi model, so each resource model should be able to access it directly. Signed-off-by: Aline Manera <alinefm@br.ibm.com> --- src/kimchi/model.py | 93 +----------------------- src/kimchi/model_/libvirtconnection.py | 123 ++++++++++++++++++++++++++++++++ 2 files changed, 124 insertions(+), 92 deletions(-) create mode 100644 src/kimchi/model_/libvirtconnection.py diff --git a/src/kimchi/model.py b/src/kimchi/model.py index 2c6d3a1..0577088 100644 --- a/src/kimchi/model.py +++ b/src/kimchi/model.py @@ -67,6 +67,7 @@ from kimchi.exception import MissingParameter, NotFoundError, OperationFailed from kimchi.featuretests import FeatureTests from kimchi.iscsi import TargetClient from kimchi.isoinfo import IsoImage +from kimchi.model_.libvirtconnection import LibvirtConnection from kimchi.objectstore import ObjectStore from kimchi.scan import Scanner from kimchi.screenshot import VMScreenshot @@ -1733,95 +1734,3 @@ def _get_volume_xml(**kwargs): </volume> """ % kwargs return xml - - -class LibvirtConnection(object): - def __init__(self, uri): - self.uri = uri - self._connections = {} - self._connectionLock = threading.Lock() - self.wrappables = self.get_wrappable_objects() - - def get_wrappable_objects(self): - """ - When a wrapped function returns an instance of another libvirt object, - we also want to wrap that object so we can catch errors that happen when - calling its methods. - """ - objs = [] - for name in ('virDomain', 'virDomainSnapshot', 'virInterface', - 'virNWFilter', 'virNetwork', 'virNodeDevice', 'virSecret', - 'virStoragePool', 'virStorageVol', 'virStream'): - try: - attr = getattr(libvirt, name) - except AttributeError: - pass - objs.append(attr) - return tuple(objs) - - def get(self, conn_id=0): - """ - Return current connection to libvirt or open a new one. Wrap all - callable libvirt methods so we can catch connection errors and handle - them by restarting the server. - """ - def wrapMethod(f): - def wrapper(*args, **kwargs): - try: - ret = f(*args, **kwargs) - return ret - except libvirt.libvirtError as e: - edom = e.get_error_domain() - ecode = e.get_error_code() - EDOMAINS = (libvirt.VIR_FROM_REMOTE, - libvirt.VIR_FROM_RPC) - ECODES = (libvirt.VIR_ERR_SYSTEM_ERROR, - libvirt.VIR_ERR_INTERNAL_ERROR, - libvirt.VIR_ERR_NO_CONNECT, - libvirt.VIR_ERR_INVALID_CONN) - if edom in EDOMAINS and ecode in ECODES: - kimchi_log.error('Connection to libvirt broken. ' - 'Recycling. ecode: %d edom: %d' % - (ecode, edom)) - with self._connectionLock: - self._connections[conn_id] = None - raise - wrapper.__name__ = f.__name__ - wrapper.__doc__ = f.__doc__ - return wrapper - - with self._connectionLock: - conn = self._connections.get(conn_id) - if not conn: - retries = 5 - while True: - retries = retries - 1 - try: - conn = libvirt.open(self.uri) - break - except libvirt.libvirtError: - kimchi_log.error('Unable to connect to libvirt.') - if not retries: - kimchi_log.error('Libvirt is not available, exiting.') - cherrypy.engine.stop() - raise - time.sleep(2) - - for name in dir(libvirt.virConnect): - method = getattr(conn, name) - if callable(method) and not name.startswith('_'): - setattr(conn, name, wrapMethod(method)) - - for cls in self.wrappables: - for name in dir(cls): - method = getattr(cls, name) - if callable(method) and not name.startswith('_'): - setattr(cls, name, wrapMethod(method)) - - self._connections[conn_id] = conn - # In case we're running into troubles with keeping the connections - # alive we should place here: - # conn.setKeepAlive(interval=5, count=3) - # However the values need to be considered wisely to not affect - # hosts which are hosting a lot of virtual machines - return conn diff --git a/src/kimchi/model_/libvirtconnection.py b/src/kimchi/model_/libvirtconnection.py new file mode 100644 index 0000000..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 -- 1.7.10.4

From: Aline Manera <alinefm@br.ibm.com> Separate StoragePoolDef from model as it handles the xml generation for libvirt storage pools. Signed-off-by: Aline Manera <alinefm@br.ibm.com> --- src/kimchi/model.py | 202 +-------------------------- src/kimchi/model_/libvirtstoragepool.py | 225 +++++++++++++++++++++++++++++++ 2 files changed, 226 insertions(+), 201 deletions(-) create mode 100644 src/kimchi/model_/libvirtstoragepool.py diff --git a/src/kimchi/model.py b/src/kimchi/model.py index 0577088..027d6d5 100644 --- a/src/kimchi/model.py +++ b/src/kimchi/model.py @@ -65,9 +65,9 @@ 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.iscsi import TargetClient from kimchi.isoinfo import IsoImage from kimchi.model_.libvirtconnection import LibvirtConnection +from kimchi.model_.libvirtstoragepool import StoragePoolDef from kimchi.objectstore import ObjectStore from kimchi.scan import Scanner from kimchi.screenshot import VMScreenshot @@ -1510,206 +1510,6 @@ class LibvirtVMScreenshot(VMScreenshot): finally: os.close(fd) - -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 - - def _get_volume_xml(**kwargs): # Required parameters # name: diff --git a/src/kimchi/model_/libvirtstoragepool.py b/src/kimchi/model_/libvirtstoragepool.py new file mode 100644 index 0000000..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 -- 1.7.10.4

From: Aline Manera <alinefm@br.ibm.com> To avoid duplicating code in model and mockmodel, the code related to storagepool resource was added to model_/storagepools.py and the specific code for each backend (libvirt or mock) was added to model_/libvirtbackend.py and model_/mockbackend.py Signed-off-by: Aline Manera <alinefm@br.ibm.com> --- src/kimchi/model_/libvirtbackend.py | 171 ++++++++++++++++++++++++++++++++++- src/kimchi/model_/mockbackend.py | 51 +++++++++++ src/kimchi/model_/storagepools.py | 86 ++++++++++++++++++ 3 files changed, 307 insertions(+), 1 deletion(-) create mode 100644 src/kimchi/model_/storagepools.py diff --git a/src/kimchi/model_/libvirtbackend.py b/src/kimchi/model_/libvirtbackend.py index 4ab4db6..28bfc01 100644 --- a/src/kimchi/model_/libvirtbackend.py +++ b/src/kimchi/model_/libvirtbackend.py @@ -36,18 +36,33 @@ from cherrypy.process.plugins import BackgroundTask from collections import defaultdict from kimchi import config +from kimchi import xmlutils from kimchi.asynctask import AsyncTask from kimchi.featuretests import FeatureTests from kimchi.exception import OperationFailed +from kimchi.model_.libvirtconnection import LibvirtConnection +from kimchi.model_.libvirtstoragepool import StoragePoolDef from kimchi.objectstore import ObjectStore +from kimchi.scan import Scanner from kimchi.utils import kimchi_log HOST_STATS_INTERVAL = 1 +STORAGE_SOURCES = {'netfs': {'addr': '/pool/source/host/@name', + 'path': '/pool/source/dir/@path'}} class LibvirtBackend(object): - def __init__(self, objstore_loc=None): + pool_state_map = {0: 'inactive', + 1: 'initializing', + 2: 'active', + 3: 'degraded', + 4: 'inaccessible'} + + 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.host_stats = defaultdict(int) self.host_stats_thread = BackgroundTask(HOST_STATS_INTERVAL, self._update_host_stats) @@ -228,3 +243,157 @@ class LibvirtBackend(object): 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) diff --git a/src/kimchi/model_/mockbackend.py b/src/kimchi/model_/mockbackend.py index 5fb332e..01f4c22 100644 --- a/src/kimchi/model_/mockbackend.py +++ b/src/kimchi/model_/mockbackend.py @@ -21,6 +21,7 @@ # 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 @@ -33,6 +34,7 @@ class MockBackend(object): self.objstore = ObjectStore(objstore_loc) self.next_taskid = 1 self.host_stats = self._get_host_stats() + self._storagepools = {} def _get_host_stats(self): memory_stats = {'total': 3934908416L, @@ -85,3 +87,52 @@ class MockBackend(object): 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 + +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) 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 -- 1.7.10.4

From: Aline Manera <alinefm@br.ibm.com> To avoid duplicating code in model and mockmodel, the code related to storagevolume resource was added to model_/storagevolumes.py and the specific code for each backend (libvirt or mock) was added to model_/libvirtbackend.py and model_/mockbackend.py Signed-off-by: Aline Manera <alinefm@br.ibm.com> --- src/kimchi/model_/libvirtbackend.py | 96 ++++++++++++++++++++++++++++++++++- src/kimchi/model_/mockbackend.py | 42 +++++++++++++++ src/kimchi/model_/storagevolumes.py | 95 ++++++++++++++++++++++++++++++++++ src/kimchi/vmtemplate.py | 12 ----- 4 files changed, 232 insertions(+), 13 deletions(-) create mode 100644 src/kimchi/model_/storagevolumes.py diff --git a/src/kimchi/model_/libvirtbackend.py b/src/kimchi/model_/libvirtbackend.py index 28bfc01..ee263c6 100644 --- a/src/kimchi/model_/libvirtbackend.py +++ b/src/kimchi/model_/libvirtbackend.py @@ -39,7 +39,8 @@ from kimchi import config from kimchi import xmlutils from kimchi.asynctask import AsyncTask from kimchi.featuretests import FeatureTests -from kimchi.exception import OperationFailed +from kimchi.isoinfo import IsoImage +from kimchi.exception import IsoFormatError, OperationFailed from kimchi.model_.libvirtconnection import LibvirtConnection from kimchi.model_.libvirtstoragepool import StoragePoolDef from kimchi.objectstore import ObjectStore @@ -57,6 +58,11 @@ class LibvirtBackend(object): 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) @@ -397,3 +403,91 @@ class LibvirtBackend(object): 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()) diff --git a/src/kimchi/model_/mockbackend.py b/src/kimchi/model_/mockbackend.py index 01f4c22..1f93d36 100644 --- a/src/kimchi/model_/mockbackend.py +++ b/src/kimchi/model_/mockbackend.py @@ -122,6 +122,34 @@ class MockBackend(object): 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] + class MockStoragePool(object): def __init__(self, name): self.name = name @@ -136,3 +164,17 @@ class MockStoragePool(object): 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 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/vmtemplate.py b/src/kimchi/vmtemplate.py index 58147e3..e7f6c81 100644 --- a/src/kimchi/vmtemplate.py +++ b/src/kimchi/vmtemplate.py @@ -192,18 +192,6 @@ class VMTemplate(object): 'type': 'disk', 'format': 'qcow2', 'path': '%s/%s' % (storage_path, volume)} - - info['xml'] = """ - <volume> - <name>%(name)s</name> - <allocation>0</allocation> - <capacity unit="G">%(capacity)s</capacity> - <target> - <format type='%(format)s'/> - <path>%(path)s</path> - </target> - </volume> - """ % info ret.append(info) return ret -- 1.7.10.4

From: Aline Manera <alinefm@br.ibm.com> To avoid duplicating code in model and mockmodel, the code to interface resource was added to model_/interfaces.py. And the code related to network resource was added to model_/networks.py and the specific code for each backend (libvirt or mock) was added to model_/libvirtbackend.py and model_/mockbackend.py Signed-off-by: Aline Manera <alinefm@br.ibm.com> --- src/kimchi/model.py | 2 +- src/kimchi/model_/interfaces.py | 48 ++++++++++++ src/kimchi/model_/libvirtbackend.py | 139 +++++++++++++++++++++++++++++++++++ src/kimchi/model_/mockbackend.py | 31 ++++++++ src/kimchi/model_/networks.py | 115 +++++++++++++++++++++++++++++ src/kimchi/networkxml.py | 6 +- 6 files changed, 337 insertions(+), 4 deletions(-) create mode 100644 src/kimchi/model_/interfaces.py create mode 100644 src/kimchi/model_/networks.py diff --git a/src/kimchi/model.py b/src/kimchi/model.py index 027d6d5..7b0eafc 100644 --- a/src/kimchi/model.py +++ b/src/kimchi/model.py @@ -818,7 +818,7 @@ class Model(object): ip.ip = ip.ip + 1 dhcp_start = str(ip.ip + ip.numhosts / 2) dhcp_end = str(ip.ip + ip.numhosts - 2) - params.update({'net': str(ip), + params.update({'subnet': str(ip), 'dhcp': {'range': {'start': dhcp_start, 'end': dhcp_end}}}) 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 index ee263c6..630204b 100644 --- a/src/kimchi/model_/libvirtbackend.py +++ b/src/kimchi/model_/libvirtbackend.py @@ -24,6 +24,7 @@ import cherrypy import fnmatch import glob +import ipaddr import logging import os import platform @@ -36,6 +37,9 @@ 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.featuretests import FeatureTests @@ -491,3 +495,138 @@ class LibvirtBackend(object): 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): + return [] + + 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() diff --git a/src/kimchi/model_/mockbackend.py b/src/kimchi/model_/mockbackend.py index 1f93d36..4c0fdf8 100644 --- a/src/kimchi/model_/mockbackend.py +++ b/src/kimchi/model_/mockbackend.py @@ -30,11 +30,16 @@ from kimchi.asynctask import AsyncTask from kimchi.objectstore import ObjectStore class MockBackend(object): + _network_info = {'state': 'inactive', 'autostart': True, 'connection': '', + 'interface': '', 'subnet': '', + 'dhcp': {'start': '', 'stop': ''}} + 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 = {} def _get_host_stats(self): memory_stats = {'total': 3934908416L, @@ -150,6 +155,32 @@ class MockBackend(object): 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_a_network(name) + return info + + def _get_vms_attach_to_a_network(self, network): + return [] + + 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] + class MockStoragePool(object): def __init__(self, name): self.name = name 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/networkxml.py b/src/kimchi/networkxml.py index 63cb210..212b0ff 100644 --- a/src/kimchi/networkxml.py +++ b/src/kimchi/networkxml.py @@ -59,8 +59,8 @@ def _get_ip_xml(**kwargs): </ip> """ xml = "" - if 'net' in kwargs.keys(): - net = ipaddr.IPNetwork(kwargs['net']) + if 'subnet' in kwargs.keys(): + net = ipaddr.IPNetwork(kwargs['subnet']) address = str(net.ip) netmask = str(net.netmask) dhcp_params = kwargs.get('dhcp', {}) @@ -96,7 +96,7 @@ def to_network_xml(**kwargs): params = {'name': kwargs['name']} # None means is Isolated network, {} means default mode nat forward = kwargs.get('forward', {"mode": None}) - ip = {'net': kwargs['net']} if 'net' in kwargs else {} + ip = {'subnet': kwargs['subnet']} if 'subnet' in kwargs else {} ip['dhcp'] = kwargs.get('dhcp', {}) bridge = kwargs.get('bridge') params = {'name': kwargs['name'], -- 1.7.10.4

From: Aline Manera <alinefm@br.ibm.com> To avoid duplicating code in model and mockmodel, the code related to template resource was added to model_/templates.py and the specific code for each backend (libvirt or mock) was added to model_/libvirtbackend.py and model_/mockbackend.py Signed-off-by: Aline Manera <alinefm@br.ibm.com> --- src/kimchi/model_/libvirtbackend.py | 18 +++++++ src/kimchi/model_/mockbackend.py | 16 +++++++ src/kimchi/model_/templates.py | 89 +++++++++++++++++++++++++++++++++++ 3 files changed, 123 insertions(+) create mode 100644 src/kimchi/model_/templates.py diff --git a/src/kimchi/model_/libvirtbackend.py b/src/kimchi/model_/libvirtbackend.py index 630204b..41f1b06 100644 --- a/src/kimchi/model_/libvirtbackend.py +++ b/src/kimchi/model_/libvirtbackend.py @@ -67,6 +67,8 @@ class LibvirtBackend(object): 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) @@ -630,3 +632,19 @@ class LibvirtBackend(object): 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) diff --git a/src/kimchi/model_/mockbackend.py b/src/kimchi/model_/mockbackend.py index 4c0fdf8..a598582 100644 --- a/src/kimchi/model_/mockbackend.py +++ b/src/kimchi/model_/mockbackend.py @@ -34,12 +34,15 @@ class MockBackend(object): '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 = {} def _get_host_stats(self): memory_stats = {'total': 3934908416L, @@ -181,6 +184,18 @@ class MockBackend(object): 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] + class MockStoragePool(object): def __init__(self, name): self.name = name @@ -209,3 +224,4 @@ class MockStorageVolume(object): self.info['os_version'] = '19' self.info['os_distro'] = 'fedora' self.info['bootable'] = True + 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 -- 1.7.10.4

From: Aline Manera <alinefm@br.ibm.com> To avoid duplicating code in model and mockmodel, the code related to vm resource was added to model_/vms.py and the specific code for each backend (libvirt or mock) was added to model_/libvirtbackend.py and model_/mockbackend.py Signed-off-by: Aline Manera <alinefm@br.ibm.com> --- src/kimchi/model_/libvirtbackend.py | 309 ++++++++++++++++++++++++++++++++++- src/kimchi/model_/mockbackend.py | 117 ++++++++++++- src/kimchi/model_/vms.py | 164 +++++++++++++++++++ src/kimchi/vmtemplate.py | 6 +- 4 files changed, 587 insertions(+), 9 deletions(-) create mode 100644 src/kimchi/model_/vms.py diff --git a/src/kimchi/model_/libvirtbackend.py b/src/kimchi/model_/libvirtbackend.py index 41f1b06..bf29c78 100644 --- a/src/kimchi/model_/libvirtbackend.py +++ b/src/kimchi/model_/libvirtbackend.py @@ -42,20 +42,30 @@ 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.exception import IsoFormatError, OperationFailed 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', @@ -75,9 +85,13 @@ class LibvirtBackend(object): 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 @@ -112,6 +126,96 @@ class LibvirtBackend(object): '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() @@ -602,7 +706,15 @@ class LibvirtBackend(object): 'pf': forward_pf}} def _get_vms_attach_to_network(self, network): - return [] + 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() @@ -648,3 +760,196 @@ class LibvirtBackend(object): 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_/mockbackend.py b/src/kimchi/model_/mockbackend.py index a598582..d4314ca 100644 --- a/src/kimchi/model_/mockbackend.py +++ b/src/kimchi/model_/mockbackend.py @@ -28,6 +28,7 @@ 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': '', @@ -43,6 +44,8 @@ class MockBackend(object): self._storagepools = {} self._networks = {} self._templates = {} + self._vms = {} + self._screenshots = {} def _get_host_stats(self): memory_stats = {'total': 3934908416L, @@ -169,11 +172,15 @@ class MockBackend(object): def get_network_by_name(self, name): info = self._networks[name] - info['vms'] = self._get_vms_attach_to_a_network(name) + info['vms'] = self._get_vms_attach_to_network(name) return info - def _get_vms_attach_to_a_network(self, network): - return [] + 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' @@ -196,6 +203,69 @@ class MockBackend(object): 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 @@ -225,3 +295,44 @@ class MockStorageVolume(object): 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_/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/vmtemplate.py b/src/kimchi/vmtemplate.py index e7f6c81..c0e5982 100644 --- a/src/kimchi/vmtemplate.py +++ b/src/kimchi/vmtemplate.py @@ -162,7 +162,7 @@ class VMTemplate(object): """ % params return ret - def _get_graphics_xml(self, params): + def _get_graphics_xml(self): graphics_xml = """ <graphics type='%(type)s' autoport='yes' listen='%(listen)s'> </graphics> @@ -173,8 +173,6 @@ class VMTemplate(object): </channel> """ graphics = dict(self.info['graphics']) - if params: - graphics.update(params) graphics_xml = graphics_xml % graphics if graphics['type'] == 'spice': graphics_xml = graphics_xml + spicevmc_xml @@ -219,7 +217,7 @@ class VMTemplate(object): params['cdroms'] = '' params['qemu-stream-cmdline'] = '' graphics = kwargs.get('graphics') - params['graphics'] = self._get_graphics_xml(graphics) + params['graphics'] = self._get_graphics_xml() qemu_stream_dns = kwargs.get('qemu_stream_dns', False) libvirt_stream = kwargs.get('libvirt_stream', False) -- 1.7.10.4

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

Looks good, but "looks" does not guarantee that it's functional hehehe The only way to ensure that it works properly is to apply these changes to the master and then do a full feature test in Kimchi to see if nothing trivial was broken. IMHO these changes (after you complete them, of course) should be applied ASAP so we can deal with conflicts and possible bugs right away. Daniel On 01/17/2014 12:24 AM, Aline Manera wrote:
From: Aline Manera <alinefm@br.ibm.com>
I am sending the first patch set to refactor model It is not completed. I still need to do a lot of tests and so on. But I would like to share it with you to get your suggestion.
Basically, the common code between model and mockmodel was placed into model/<resource>.py and the specific code into model/libvirtbackend.py and mockbackend.py
*** It still needs a lot of work *** - Tests all functions - Update Makefile and spec files - Update test cases
Aline Manera (13): refactor model: Create a separated model for task resource refactor model: Create a separated model for debugreport resource refactor model: Create a separated model for config resource refactor model: Create a separated model for host resource refactor model: Create a separated model for plugin resource refactor model: Create a separated model for libvirt connection refactor model: Move StoragePooldef from model to libvirtstoragepools.py refactor model: Create a separated model for storagepool resource refactor model: Create a separated model for storagevolume resource refactor model: Create a separated model for interface and network resources refactor model: Create a separated model for template resource refactor model: Create a separated model for vm resource refactor model: Update server.py and root.py to use new models
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 | 1827 -------------------------------- 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/networkxml.py | 6 +- src/kimchi/root.py | 24 +- src/kimchi/server.py | 16 +- src/kimchi/vmtemplate.py | 18 +- 35 files changed, 2674 insertions(+), 2757 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

on 2014/01/17 10:24, Aline Manera wrote:
From: Aline Manera <alinefm@br.ibm.com>
I am sending the first patch set to refactor model It is not completed. I still need to do a lot of tests and so on. But I would like to share it with you to get your suggestion.
Basically, the common code between model and mockmodel was placed into model/<resource>.py and the specific code into model/libvirtbackend.py and mockbackend.py
*** It still needs a lot of work *** - Tests all functions - Update Makefile and spec files - Update test cases
Aline Manera (13): refactor model: Create a separated model for task resource refactor model: Create a separated model for debugreport resource refactor model: Create a separated model for config resource refactor model: Create a separated model for host resource refactor model: Create a separated model for plugin resource refactor model: Create a separated model for libvirt connection refactor model: Move StoragePooldef from model to libvirtstoragepools.py refactor model: Create a separated model for storagepool resource refactor model: Create a separated model for storagevolume resource refactor model: Create a separated model for interface and network resources refactor model: Create a separated model for template resource refactor model: Create a separated model for vm resource refactor model: Update server.py and root.py to use new models
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 | 1827 -------------------------------- 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/networkxml.py | 6 +- src/kimchi/root.py | 24 +- src/kimchi/server.py | 16 +- src/kimchi/vmtemplate.py | 18 +- 35 files changed, 2674 insertions(+), 2757 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
I do not like this approach. It just put everything into the libvirtbackend instead of in the current monolithic model class. We still get monolithic libvirtbackend module. While the change is big, but we do not benefit much in terms of logic splitting and modulization. I'm proposing another mechanism. Maybe I'll send the patches later. -- Thanks and best regards! Zhou Zheng Sheng / 周征晟 E-mail: zhshzhou@linux.vnet.ibm.com Telephone: 86-10-82454397

on 2014/01/21 11:31, Zhou Zheng Sheng wrote:
on 2014/01/17 10:24, Aline Manera wrote:
From: Aline Manera <alinefm@br.ibm.com>
I am sending the first patch set to refactor model It is not completed. I still need to do a lot of tests and so on. But I would like to share it with you to get your suggestion.
Basically, the common code between model and mockmodel was placed into model/<resource>.py and the specific code into model/libvirtbackend.py and mockbackend.py
*** It still needs a lot of work *** - Tests all functions - Update Makefile and spec files - Update test cases
Aline Manera (13): refactor model: Create a separated model for task resource refactor model: Create a separated model for debugreport resource refactor model: Create a separated model for config resource refactor model: Create a separated model for host resource refactor model: Create a separated model for plugin resource refactor model: Create a separated model for libvirt connection refactor model: Move StoragePooldef from model to libvirtstoragepools.py refactor model: Create a separated model for storagepool resource refactor model: Create a separated model for storagevolume resource refactor model: Create a separated model for interface and network resources refactor model: Create a separated model for template resource refactor model: Create a separated model for vm resource refactor model: Update server.py and root.py to use new models
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 | 1827 -------------------------------- 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/networkxml.py | 6 +- src/kimchi/root.py | 24 +- src/kimchi/server.py | 16 +- src/kimchi/vmtemplate.py | 18 +- 35 files changed, 2674 insertions(+), 2757 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
I do not like this approach. It just put everything into the libvirtbackend instead of in the current monolithic model class. We still get monolithic libvirtbackend module. While the change is big, but we do not benefit much in terms of logic splitting and modulization.
I'm proposing another mechanism. Maybe I'll send the patches later.
The libvirtbackend in "Refactor model" patches is still a monolithic class. This is due to the following reasons. 1. Before the split, all controllers use the same model instance, and we can feed the root controller a mock model instance to have all controllers work on the mock one. 2. After the controller split and model split, we want concrete controllers call concrete models, for example, VM controller calls VM model. This seems OK but there is no way to switch all concrete controllers to call mock models. 3. So comes a new layer of abstraction. A "backend" concept was introduced and the "backend" does the actual work. The models share the same "backend", and we just feed a real "backend" or mock "backend" to switch between production mode and test mode. The key problem of "share one thing" v.s. "modulization" is still not solved. I think we have many ways to solve this problem. The key problem is after split how we enable the concrete controllers call concrete models while we can easily switch between real models and mock models. The first way is that we can have it automatically synthesize a single module instance from all the concrete instances, and all the controllers still call this single model instance. This is feasible because the synthesis is automatic, the logic and responsibilities are still split to each concrete model. The second way is to introduce an abstract model factory. We can have a "real" model factory instance and a "mock" one to produce only "mock" model instance. We let the concrete controller request the model factory instance to construct a concrete model instance. Then we can easily switch between the production mode and test mode by feeding the root controller with a real model factory instance or a mock one. This is feasible because the factory just cares the creation of a concrete model, the inner model responsibilities are still wrapped within the concrete model. I'm proposing the first way because the change will be minimum and non-intrusive. I'm trying to send the patches later... -- Thanks and best regards! Zhou Zheng Sheng / 周征晟 E-mail: zhshzhou@linux.vnet.ibm.com Telephone: 86-10-82454397

On 2014年01月21日 13:09, Zhou Zheng Sheng wrote:
on 2014/01/21 11:31, Zhou Zheng Sheng wrote:
on 2014/01/17 10:24, Aline Manera wrote:
From: Aline Manera <alinefm@br.ibm.com>
I am sending the first patch set to refactor model It is not completed. I still need to do a lot of tests and so on. But I would like to share it with you to get your suggestion.
Basically, the common code between model and mockmodel was placed into model/<resource>.py and the specific code into model/libvirtbackend.py and mockbackend.py
*** It still needs a lot of work *** - Tests all functions - Update Makefile and spec files - Update test cases
Aline Manera (13): refactor model: Create a separated model for task resource refactor model: Create a separated model for debugreport resource refactor model: Create a separated model for config resource refactor model: Create a separated model for host resource refactor model: Create a separated model for plugin resource refactor model: Create a separated model for libvirt connection refactor model: Move StoragePooldef from model to libvirtstoragepools.py refactor model: Create a separated model for storagepool resource refactor model: Create a separated model for storagevolume resource refactor model: Create a separated model for interface and network resources refactor model: Create a separated model for template resource refactor model: Create a separated model for vm resource refactor model: Update server.py and root.py to use new models
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 | 1827 -------------------------------- 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/networkxml.py | 6 +- src/kimchi/root.py | 24 +- src/kimchi/server.py | 16 +- src/kimchi/vmtemplate.py | 18 +- 35 files changed, 2674 insertions(+), 2757 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
I do not like this approach. It just put everything into the libvirtbackend instead of in the current monolithic model class. We still get monolithic libvirtbackend module. While the change is big, but we do not benefit much in terms of logic splitting and modulization.
I'm proposing another mechanism. Maybe I'll send the patches later.
The libvirtbackend in "Refactor model" patches is still a monolithic class. This is due to the following reasons.
1. Before the split, all controllers use the same model instance, and we can feed the root controller a mock model instance to have all controllers work on the mock one.
2. After the controller split and model split, we want concrete controllers call concrete models, for example, VM controller calls VM model. This seems OK but there is no way to switch all concrete controllers to call mock models.
3. So comes a new layer of abstraction. A "backend" concept was introduced and the "backend" does the actual work. The models share the same "backend", and we just feed a real "backend" or mock "backend" to switch between production mode and test mode. The key problem of "share one thing" v.s. "modulization" is still not solved.
I think we have many ways to solve this problem. The key problem is after split how we enable the concrete controllers call concrete models while we can easily switch between real models and mock models.
The first way is that we can have it automatically synthesize a single module instance from all the concrete instances, and all the controllers still call this single model instance. This is feasible because the synthesis is automatic, the logic and responsibilities are still split to each concrete model. I think common logic abstraction is good. While Zhengsheng's point here is to maintain the consistency between old model api and new model api(e.g. vms_get_list), So that testcases in test_model.py can switch to newest seamless. What about combine 1st and 2nd proposal?
Our aim is: 1. reuse common logic between mockmodel implementation and model implementation. 2. make libvirt or mock implementation confined to there own class and transparent to caller. 3. keep consistency between old model api and refactored api to make test_model.py testcases reusable. To address 1 and 2 we can adopt Zhengsheng's proposal of factory pattern. 1. Abstract common logic in a parent class, like we did in VMScreenshot(we need to limit the lookup inside) 2. libvirt and mock reload their own logic as we do in LibvirtVMScreenshot and MockVMScreenshot. To address 3 we can use a synthesize single module together with Sheldon's module function probe: (REF: [Kimchi-devel] [PATCH 1/4] improve controller: add a method to load root sub collections/resouces automatically) Under model directory: there are vm.py, storagepool.py, etc in __init__.py: class LibvirtModel(object): model_type = 'real' def __init__(self): modules = listPathModules('./') for module in modules: # probe libvirt vm modules and classes, see sheldon's patch for module probe implementation module = imp.load_module() cls_names = self._get_classes(module) # assgin these class methods to Model class, see zhengsheng's patch for synthesize: .... m =getattr(model_instance,member_name,None) setattr(self,'%s_%s'%(method_prefix,member_name),m) def _get_classes(module): klasses = [] for name, obj in inspect.getmembers(module): if inspect.isclass(obj): klasses.append(name) The whole refactor thing is huge, I suggest we do it one by one, starting from parts which is easy to decouple, like distro, host and things depend less on each other, not swallow it all at once. Also Mark has proposal to make the db and libvirt connection to be a singleton so that class does not need to keep instance of it. He will follow up with this thread.|
The second way is to introduce an abstract model factory. We can have a "real" model factory instance and a "mock" one to produce only "mock" model instance. We let the concrete controller request the model factory instance to construct a concrete model instance. Then we can easily switch between the production mode and test mode by feeding the root controller with a real model factory instance or a mock one. This is feasible because the factory just cares the creation of a concrete model, the inner model responsibilities are still wrapped within the concrete model.
I'm proposing the first way because the change will be minimum and non-intrusive. I'm trying to send the patches later...

On 01/21/2014 06:44 AM, Royce Lv wrote:
on 2014/01/21 11:31, Zhou Zheng Sheng wrote:
on 2014/01/17 10:24, Aline Manera wrote:
From: Aline Manera <alinefm@br.ibm.com>
I am sending the first patch set to refactor model It is not completed. I still need to do a lot of tests and so on. But I would like to share it with you to get your suggestion.
Basically, the common code between model and mockmodel was placed into model/<resource>.py and the specific code into model/libvirtbackend.py and mockbackend.py
*** It still needs a lot of work *** - Tests all functions - Update Makefile and spec files - Update test cases
Aline Manera (13): refactor model: Create a separated model for task resource refactor model: Create a separated model for debugreport resource refactor model: Create a separated model for config resource refactor model: Create a separated model for host resource refactor model: Create a separated model for plugin resource refactor model: Create a separated model for libvirt connection refactor model: Move StoragePooldef from model to libvirtstoragepools.py refactor model: Create a separated model for storagepool resource refactor model: Create a separated model for storagevolume resource refactor model: Create a separated model for interface and network resources refactor model: Create a separated model for template resource refactor model: Create a separated model for vm resource refactor model: Update server.py and root.py to use new models
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 | 1827 -------------------------------- 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/networkxml.py | 6 +- src/kimchi/root.py | 24 +- src/kimchi/server.py | 16 +- src/kimchi/vmtemplate.py | 18 +- 35 files changed, 2674 insertions(+), 2757 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
I do not like this approach. It just put everything into the libvirtbackend instead of in the current monolithic model class. We still get monolithic libvirtbackend module. While the change is big, but we do not benefit much in terms of logic splitting and modulization.
I'm proposing another mechanism. Maybe I'll send the patches later.
The libvirtbackend in "Refactor model" patches is still a monolithic class. This is due to the following reasons.
1. Before the split, all controllers use the same model instance, and we can feed the root controller a mock model instance to have all controllers work on the mock one.
2. After the controller split and model split, we want concrete controllers call concrete models, for example, VM controller calls VM model. This seems OK but there is no way to switch all concrete controllers to call mock models.
3. So comes a new layer of abstraction. A "backend" concept was introduced and the "backend" does the actual work. The models share the same "backend", and we just feed a real "backend" or mock "backend" to switch between production mode and test mode. The key problem of "share one thing" v.s. "modulization" is still not solved.
I think we have many ways to solve this problem. The key problem is after split how we enable the concrete controllers call concrete models while we can easily switch between real models and mock models.
The first way is that we can have it automatically synthesize a single module instance from all the concrete instances, and all the controllers still call this single model instance. This is feasible because the synthesis is automatic, the logic and responsibilities are still split to each concrete model. I think common logic abstraction is good. While Zhengsheng's point here is to maintain the consistency between
On 2014年01月21日 13:09, Zhou Zheng Sheng wrote: old model api and new model api(e.g. vms_get_list), So that testcases in test_model.py can switch to newest seamless. What about combine 1st and 2nd proposal?
Yeap! I think that way we will have a more modularized and reusable code
Our aim is: 1. reuse common logic between mockmodel implementation and model implementation. 2. make libvirt or mock implementation confined to there own class and transparent to caller. 3. keep consistency between old model api and refactored api to make test_model.py testcases reusable.
To address 1 and 2 we can adopt Zhengsheng's proposal of factory pattern. 1. Abstract common logic in a parent class, like we did in VMScreenshot(we need to limit the lookup inside) 2. libvirt and mock reload their own logic as we do in LibvirtVMScreenshot and MockVMScreenshot.
To address 3 we can use a synthesize single module together with Sheldon's module function probe: (REF: [Kimchi-devel] [PATCH 1/4] improve controller: add a method to load root sub collections/resouces automatically)
Under model directory: there are vm.py, storagepool.py, etc
in __init__.py:
In fact, in __init__.py I was thinking to add the code made by Zhengsheng (in plugins/__init__.py) and libvirtmodel.py will have class LibvirtModel(Model): self.vms = VMsModel() self.templates = TemplatesModel() .... The same way, Zhengsheng did for RootModel() in "[PATCH] Break the 'sample' plugin's monolithic model into several smaller ones"
class LibvirtModel(object): model_type = 'real'
def __init__(self):
modules = listPathModules('./') for module in modules: # probe libvirt vm modules and classes, see sheldon's patch for module probe implementation module = imp.load_module() cls_names = self._get_classes(module)
# assgin these class methods to Model class, see zhengsheng's patch for synthesize: .... m =getattr(model_instance,member_name,None) setattr(self,'%s_%s'%(method_prefix,member_name),m)
def _get_classes(module): klasses = [] for name, obj in inspect.getmembers(module): if inspect.isclass(obj): klasses.append(name)
The whole refactor thing is huge, I suggest we do it one by one, starting from parts which is easy to decouple, like distro, host and things depend less on each other, not swallow it all at once. Also Mark has proposal to make the db and libvirt connection to be a singleton so that class does not need to keep instance of it. He will follow up with this thread.|
The second way is to introduce an abstract model factory. We can have a "real" model factory instance and a "mock" one to produce only "mock" model instance. We let the concrete controller request the model factory instance to construct a concrete model instance. Then we can easily switch between the production mode and test mode by feeding the root controller with a real model factory instance or a mock one. This is feasible because the factory just cares the creation of a concrete model, the inner model responsibilities are still wrapped within the concrete model.
I'm proposing the first way because the change will be minimum and non-intrusive. I'm trying to send the patches later...

On 01/21/2014 06:44 AM, Royce Lv wrote:
on 2014/01/21 11:31, Zhou Zheng Sheng wrote:
on 2014/01/17 10:24, Aline Manera wrote:
From: Aline Manera <alinefm@br.ibm.com>
I am sending the first patch set to refactor model It is not completed. I still need to do a lot of tests and so on. But I would like to share it with you to get your suggestion.
Basically, the common code between model and mockmodel was placed into model/<resource>.py and the specific code into model/libvirtbackend.py and mockbackend.py
*** It still needs a lot of work *** - Tests all functions - Update Makefile and spec files - Update test cases
Aline Manera (13): refactor model: Create a separated model for task resource refactor model: Create a separated model for debugreport resource refactor model: Create a separated model for config resource refactor model: Create a separated model for host resource refactor model: Create a separated model for plugin resource refactor model: Create a separated model for libvirt connection refactor model: Move StoragePooldef from model to libvirtstoragepools.py refactor model: Create a separated model for storagepool resource refactor model: Create a separated model for storagevolume resource refactor model: Create a separated model for interface and network resources refactor model: Create a separated model for template resource refactor model: Create a separated model for vm resource refactor model: Update server.py and root.py to use new models
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 | 1827 -------------------------------- 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/networkxml.py | 6 +- src/kimchi/root.py | 24 +- src/kimchi/server.py | 16 +- src/kimchi/vmtemplate.py | 18 +- 35 files changed, 2674 insertions(+), 2757 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
I do not like this approach. It just put everything into the libvirtbackend instead of in the current monolithic model class. We still get monolithic libvirtbackend module. While the change is big, but we do not benefit much in terms of logic splitting and modulization.
I'm proposing another mechanism. Maybe I'll send the patches later.
The libvirtbackend in "Refactor model" patches is still a monolithic class. This is due to the following reasons.
1. Before the split, all controllers use the same model instance, and we can feed the root controller a mock model instance to have all controllers work on the mock one.
2. After the controller split and model split, we want concrete controllers call concrete models, for example, VM controller calls VM model. This seems OK but there is no way to switch all concrete controllers to call mock models.
3. So comes a new layer of abstraction. A "backend" concept was introduced and the "backend" does the actual work. The models share the same "backend", and we just feed a real "backend" or mock "backend" to switch between production mode and test mode. The key problem of "share one thing" v.s. "modulization" is still not solved.
I think we have many ways to solve this problem. The key problem is after split how we enable the concrete controllers call concrete models while we can easily switch between real models and mock models.
The first way is that we can have it automatically synthesize a single module instance from all the concrete instances, and all the controllers still call this single model instance. This is feasible because the synthesis is automatic, the logic and responsibilities are still split to each concrete model. I think common logic abstraction is good. While Zhengsheng's point here is to maintain the consistency between
On 2014年01月21日 13:09, Zhou Zheng Sheng wrote: old model api and new model api(e.g. vms_get_list), So that testcases in test_model.py can switch to newest seamless. What about combine 1st and 2nd proposal?
Yeap! I think that way we will have a more modularized and reusable code
Our aim is: 1. reuse common logic between mockmodel implementation and model implementation. 2. make libvirt or mock implementation confined to there own class and transparent to caller. 3. keep consistency between old model api and refactored api to make test_model.py testcases reusable.
To address 1 and 2 we can adopt Zhengsheng's proposal of factory pattern. 1. Abstract common logic in a parent class, like we did in VMScreenshot(we need to limit the lookup inside) 2. libvirt and mock reload their own logic as we do in LibvirtVMScreenshot and MockVMScreenshot.
To address 3 we can use a synthesize single module together with Sheldon's module function probe: (REF: [Kimchi-devel] [PATCH 1/4] improve controller: add a method to load root sub collections/resouces automatically)
Under model directory: there are vm.py, storagepool.py, etc
in __init__.py:
In fact, in __init__.py I was thinking to add the code made by Zhengsheng (in plugins/__init__.py) and libvirtmodel.py will have
class LibvirtModel(Model): self.vms = VMsModel() self.templates = TemplatesModel() ....
The same way, Zhengsheng did for RootModel() in "[PATCH] Break the 'sample' plugin's monolithic model into several smaller ones" See, this is hard code "vms,templates,storagepools" by "registering" the class to the model, actually we can "probe" what classes are there and
On 2014年01月22日 01:15, Aline Manera wrote: the root model does not need to know the details in each model. So that once we add a new model, we don't need to worry to update the root one. Idea's from Sheldon's controller patchset, I just steal it:P
class LibvirtModel(object): model_type = 'real'
def __init__(self):
modules = listPathModules('./') for module in modules: # probe libvirt vm modules and classes, see sheldon's patch for module probe implementation module = imp.load_module() cls_names = self._get_classes(module)
# assgin these class methods to Model class, see zhengsheng's patch for synthesize: .... m =getattr(model_instance,member_name,None) setattr(self,'%s_%s'%(method_prefix,member_name),m)
def _get_classes(module): klasses = [] for name, obj in inspect.getmembers(module): if inspect.isclass(obj): klasses.append(name)
The whole refactor thing is huge, I suggest we do it one by one, starting from parts which is easy to decouple, like distro, host and things depend less on each other, not swallow it all at once. Also Mark has proposal to make the db and libvirt connection to be a singleton so that class does not need to keep instance of it. He will follow up with this thread.|
The second way is to introduce an abstract model factory. We can have a "real" model factory instance and a "mock" one to produce only "mock" model instance. We let the concrete controller request the model factory instance to construct a concrete model instance. Then we can easily switch between the production mode and test mode by feeding the root controller with a real model factory instance or a mock one. This is feasible because the factory just cares the creation of a concrete model, the inner model responsibilities are still wrapped within the concrete model.
I'm proposing the first way because the change will be minimum and non-intrusive. I'm trying to send the patches later...

On 01/21/2014 03:09 AM, Zhou Zheng Sheng wrote:
on 2014/01/21 11:31, Zhou Zheng Sheng wrote:
on 2014/01/17 10:24, Aline Manera wrote:
From: Aline Manera <alinefm@br.ibm.com>
I am sending the first patch set to refactor model It is not completed. I still need to do a lot of tests and so on. But I would like to share it with you to get your suggestion.
Basically, the common code between model and mockmodel was placed into model/<resource>.py and the specific code into model/libvirtbackend.py and mockbackend.py
*** It still needs a lot of work *** - Tests all functions - Update Makefile and spec files - Update test cases
Aline Manera (13): refactor model: Create a separated model for task resource refactor model: Create a separated model for debugreport resource refactor model: Create a separated model for config resource refactor model: Create a separated model for host resource refactor model: Create a separated model for plugin resource refactor model: Create a separated model for libvirt connection refactor model: Move StoragePooldef from model to libvirtstoragepools.py refactor model: Create a separated model for storagepool resource refactor model: Create a separated model for storagevolume resource refactor model: Create a separated model for interface and network resources refactor model: Create a separated model for template resource refactor model: Create a separated model for vm resource refactor model: Update server.py and root.py to use new models
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 | 1827 -------------------------------- 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/networkxml.py | 6 +- src/kimchi/root.py | 24 +- src/kimchi/server.py | 16 +- src/kimchi/vmtemplate.py | 18 +- 35 files changed, 2674 insertions(+), 2757 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
I do not like this approach. It just put everything into the libvirtbackend instead of in the current monolithic model class. We still get monolithic libvirtbackend module. While the change is big, but we do not benefit much in terms of logic splitting and modulization.
I'm proposing another mechanism. Maybe I'll send the patches later.
The libvirtbackend in "Refactor model" patches is still a monolithic class. This is due to the following reasons.
1. Before the split, all controllers use the same model instance, and we can feed the root controller a mock model instance to have all controllers work on the mock one.
2. After the controller split and model split, we want concrete controllers call concrete models, for example, VM controller calls VM model. This seems OK but there is no way to switch all concrete controllers to call mock models.
3. So comes a new layer of abstraction. A "backend" concept was introduced and the "backend" does the actual work. The models share the same "backend", and we just feed a real "backend" or mock "backend" to switch between production mode and test mode. The key problem of "share one thing" v.s. "modulization" is still not solved.
I am not sure I completely understood this point. Do you mean I should also split libvirtbackend.py into libvirt/vms.py, libvirt/templates.py ? And the some for mockbackend.py? I've already thought about that and I don't have a solid conclusion. But I am OK in splitting it too. Could I do it after getting this patch set merged? So we will not block it for a long time.
I think we have many ways to solve this problem. The key problem is after split how we enable the concrete controllers call concrete models while we can easily switch between real models and mock models.
The first way is that we can have it automatically synthesize a single module instance from all the concrete instances, and all the controllers still call this single model instance. This is feasible because the synthesis is automatic, the logic and responsibilities are still split to each concrete model.
Yeap. Thanks! I liked the way you did and I will also add it to this patch set. So I don't need to change control and tests.
The second way is to introduce an abstract model factory. We can have a "real" model factory instance and a "mock" one to produce only "mock" model instance. We let the concrete controller request the model factory instance to construct a concrete model instance. Then we can easily switch between the production mode and test mode by feeding the root controller with a real model factory instance or a mock one. This is feasible because the factory just cares the creation of a concrete model, the inner model responsibilities are still wrapped within the concrete model.
I'm proposing the first way because the change will be minimum and non-intrusive. I'm trying to send the patches later...

On 01/21/2014 03:09 AM, Zhou Zheng Sheng wrote:
on 2014/01/21 11:31, Zhou Zheng Sheng wrote:
on 2014/01/17 10:24, Aline Manera wrote:
From: Aline Manera <alinefm@br.ibm.com>
I am sending the first patch set to refactor model It is not completed. I still need to do a lot of tests and so on. But I would like to share it with you to get your suggestion.
Basically, the common code between model and mockmodel was placed into model/<resource>.py and the specific code into model/libvirtbackend.py and mockbackend.py
*** It still needs a lot of work *** - Tests all functions - Update Makefile and spec files - Update test cases
Aline Manera (13): refactor model: Create a separated model for task resource refactor model: Create a separated model for debugreport resource refactor model: Create a separated model for config resource refactor model: Create a separated model for host resource refactor model: Create a separated model for plugin resource refactor model: Create a separated model for libvirt connection refactor model: Move StoragePooldef from model to libvirtstoragepools.py refactor model: Create a separated model for storagepool resource refactor model: Create a separated model for storagevolume resource refactor model: Create a separated model for interface and network resources refactor model: Create a separated model for template resource refactor model: Create a separated model for vm resource refactor model: Update server.py and root.py to use new models
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 | 1827 -------------------------------- 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/networkxml.py | 6 +- src/kimchi/root.py | 24 +- src/kimchi/server.py | 16 +- src/kimchi/vmtemplate.py | 18 +- 35 files changed, 2674 insertions(+), 2757 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
I do not like this approach. It just put everything into the libvirtbackend instead of in the current monolithic model class. We still get monolithic libvirtbackend module. While the change is big, but we do not benefit much in terms of logic splitting and modulization.
I'm proposing another mechanism. Maybe I'll send the patches later.
The libvirtbackend in "Refactor model" patches is still a monolithic class. This is due to the following reasons.
1. Before the split, all controllers use the same model instance, and we can feed the root controller a mock model instance to have all controllers work on the mock one.
2. After the controller split and model split, we want concrete controllers call concrete models, for example, VM controller calls VM model. This seems OK but there is no way to switch all concrete controllers to call mock models.
3. So comes a new layer of abstraction. A "backend" concept was introduced and the "backend" does the actual work. The models share the same "backend", and we just feed a real "backend" or mock "backend" to switch between production mode and test mode. The key problem of "share one thing" v.s. "modulization" is still not solved.
I think we have many ways to solve this problem. The key problem is after split how we enable the concrete controllers call concrete models while we can easily switch between real models and mock models.
The first way is that we can have it automatically synthesize a single module instance from all the concrete instances, and all the controllers still call this single model instance. This is feasible because the synthesis is automatic, the logic and responsibilities are still split to each concrete model.
The second way is to introduce an abstract model factory. We can have a "real" model factory instance and a "mock" one to produce only "mock" model instance. We let the concrete controller request the model factory instance to construct a concrete model instance. Then we can easily switch between the production mode and test mode by feeding the root controller with a real model factory instance or a mock one. This is feasible because the factory just cares the creation of a concrete model, the inner model responsibilities are still wrapped within the concrete model.
I'm proposing the first way because the change will be minimum and non-intrusive. I'm trying to send the patches later...
Perhaps this change could be done in a second round of refactoring? My point is that Aline have already changed *a lot* of code and it would be nice to test these changes ASAP for possible bugs and errors. When we're confident that these changes were successful then we can start further rounds of refactoring.

on 2014/01/22 02:22, Daniel H Barboza wrote:
On 01/21/2014 03:09 AM, Zhou Zheng Sheng wrote:
on 2014/01/21 11:31, Zhou Zheng Sheng wrote:
on 2014/01/17 10:24, Aline Manera wrote:
From: Aline Manera <alinefm@br.ibm.com>
I am sending the first patch set to refactor model It is not completed. I still need to do a lot of tests and so on. But I would like to share it with you to get your suggestion.
Basically, the common code between model and mockmodel was placed into model/<resource>.py and the specific code into model/libvirtbackend.py and mockbackend.py
*** It still needs a lot of work *** - Tests all functions - Update Makefile and spec files - Update test cases
Aline Manera (13): refactor model: Create a separated model for task resource refactor model: Create a separated model for debugreport resource refactor model: Create a separated model for config resource refactor model: Create a separated model for host resource refactor model: Create a separated model for plugin resource refactor model: Create a separated model for libvirt connection refactor model: Move StoragePooldef from model to libvirtstoragepools.py refactor model: Create a separated model for storagepool resource refactor model: Create a separated model for storagevolume resource refactor model: Create a separated model for interface and network resources refactor model: Create a separated model for template resource refactor model: Create a separated model for vm resource refactor model: Update server.py and root.py to use new models
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 | 1827 -------------------------------- 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/networkxml.py | 6 +- src/kimchi/root.py | 24 +- src/kimchi/server.py | 16 +- src/kimchi/vmtemplate.py | 18 +- 35 files changed, 2674 insertions(+), 2757 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
I do not like this approach. It just put everything into the libvirtbackend instead of in the current monolithic model class. We still get monolithic libvirtbackend module. While the change is big, but we do not benefit much in terms of logic splitting and modulization.
I'm proposing another mechanism. Maybe I'll send the patches later.
The libvirtbackend in "Refactor model" patches is still a monolithic class. This is due to the following reasons.
1. Before the split, all controllers use the same model instance, and we can feed the root controller a mock model instance to have all controllers work on the mock one.
2. After the controller split and model split, we want concrete controllers call concrete models, for example, VM controller calls VM model. This seems OK but there is no way to switch all concrete controllers to call mock models.
3. So comes a new layer of abstraction. A "backend" concept was introduced and the "backend" does the actual work. The models share the same "backend", and we just feed a real "backend" or mock "backend" to switch between production mode and test mode. The key problem of "share one thing" v.s. "modulization" is still not solved.
I think we have many ways to solve this problem. The key problem is after split how we enable the concrete controllers call concrete models while we can easily switch between real models and mock models.
The first way is that we can have it automatically synthesize a single module instance from all the concrete instances, and all the controllers still call this single model instance. This is feasible because the synthesis is automatic, the logic and responsibilities are still split to each concrete model.
The second way is to introduce an abstract model factory. We can have a "real" model factory instance and a "mock" one to produce only "mock" model instance. We let the concrete controller request the model factory instance to construct a concrete model instance. Then we can easily switch between the production mode and test mode by feeding the root controller with a real model factory instance or a mock one. This is feasible because the factory just cares the creation of a concrete model, the inner model responsibilities are still wrapped within the concrete model.
I'm proposing the first way because the change will be minimum and non-intrusive. I'm trying to send the patches later...
Perhaps this change could be done in a second round of refactoring? My point is that Aline have already changed *a lot* of code and it would be nice to test these changes ASAP for possible bugs and errors. When we're confident that these changes were successful then we can start further rounds of refactoring.
I like most of Aline's change except the backend idea, and this monolithic libvirtbackend makes the whole refactor not useful at all. It introduces extra layer of abstraction but does not solve the problem. We should not merge it, otherwise we will have to run another round of big change to solve the new problem, and this new round of change has to work with the complexity added in the previous run, so it makes the things worse. I am not against merging a half solution. We can always merge a half solution when the solution is towards the better way. The current situation is different. The monolithic backend is a bad idea, and the solution is not towards a better way, so I'm against it. Maybe you think we can merge the current patches and break the monolithic backend later. I think why don't we break the monolithic model class now, just as the same way we would have to break the backend later? It's a technical debt we have to bear. We should not make new debt at this point even if we have to delay some tasks. Slowing down and solving this problem cleanly would make benefit for us in the long run. Maybe we can just drop the idea of introducing a "backend", and apply my proposed mechanism in the patch "Break the 'sample' plugin's monolithic model into several smaller ones" into Aline's patches, and see if it solves the problem. -- Thanks and best regards! Zhou Zheng Sheng / 周征晟 E-mail: zhshzhou@linux.vnet.ibm.com Telephone: 86-10-82454397
participants (4)
-
Aline Manera
-
Daniel H Barboza
-
Royce Lv
-
Zhou Zheng Sheng