
From: chandrureddy <chandra@linux.vnet.ibm.com> --- src/wok/plugins/gingerbase/gingerbase.py | 60 +++++ src/wok/plugins/gingerbase/i18n.py | 96 +++++++ src/wok/plugins/gingerbase/lscpu.py | 126 ++++++++++ src/wok/plugins/gingerbase/mockmodel.py | 220 ++++++++++++++++ src/wok/plugins/gingerbase/swupdate.py | 415 +++++++++++++++++++++++++++++++ src/wok/plugins/gingerbase/utils.py | 82 ++++++ src/wok/plugins/kimchi/swupdate.py | 412 ------------------------------ 7 files changed, 999 insertions(+), 412 deletions(-) create mode 100644 src/wok/plugins/gingerbase/gingerbase.py create mode 100644 src/wok/plugins/gingerbase/i18n.py create mode 100644 src/wok/plugins/gingerbase/lscpu.py create mode 100644 src/wok/plugins/gingerbase/mockmodel.py create mode 100644 src/wok/plugins/gingerbase/swupdate.py create mode 100644 src/wok/plugins/gingerbase/utils.py delete mode 100644 src/wok/plugins/kimchi/swupdate.py diff --git a/src/wok/plugins/gingerbase/gingerbase.py b/src/wok/plugins/gingerbase/gingerbase.py new file mode 100644 index 0000000..0d3709e --- /dev/null +++ b/src/wok/plugins/gingerbase/gingerbase.py @@ -0,0 +1,60 @@ +# +# Project Ginger Base +# +# Copyright IBM, Corp. 2015 +# +# 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 json +import os + +from wok.plugins.gingerbase import config, mockmodel +from wok.plugins.gingerbase.i18n import messages +from wok.plugins.gingerbase.control import sub_nodes +from wok.plugins.gingerbase.model import model as gingerBaseModel +from wok.root import WokRoot + + +class GingerBase(WokRoot): + def __init__(self, wok_options): + if hasattr(wok_options, "model"): + self.model = wok_options.model + elif wok_options.test: + self.model = mockmodel.MockModel() + else: + self.model = gingerBaseModel.Model() + + dev_env = wok_options.environment != 'production' + super(GingerBase, self).__init__(self.model, dev_env) + + for ident, node in sub_nodes.items(): + setattr(self, ident, node(self.model)) + + self.api_schema = json.load(open(os.path.join(os.path.dirname( + os.path.abspath(__file__)), 'API.json'))) + self.paths = config.gingerBasePaths + self.domain = 'gingerbase' + self.messages = messages + + make_dirs = [ + os.path.dirname(os.path.abspath(config.get_object_store())), + os.path.abspath(config.get_debugreports_path()) + ] + for directory in make_dirs: + if not os.path.isdir(directory): + os.makedirs(directory) + + def get_custom_conf(self): + return config.GingerBaseConfig() diff --git a/src/wok/plugins/gingerbase/i18n.py b/src/wok/plugins/gingerbase/i18n.py new file mode 100644 index 0000000..fbc2516 --- /dev/null +++ b/src/wok/plugins/gingerbase/i18n.py @@ -0,0 +1,96 @@ +# +# Project Ginger Base +# +# Copyright IBM, Corp. 2015 +# +# Code derived from Project Kimchi +# +# 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 gettext + +_ = gettext.gettext + + +messages = { + "GGBAPI0001E": _("Unknown parameter %(value)s"), + + "GGBDISKS0001E": _("Error while getting block devices. Details: %(err)s"), + "GGBDISKS0002E": _("Error while getting block device information for %(device)s."), + + "GGBDR0001E": _("Debug report %(name)s does not exist"), + "GGBDR0002E": _("Debug report tool not found in system"), + "GGBDR0003E": _("Unable to create debug report %(name)s. Details: %(err)s."), + "GGBDR0004E": _("Can not find any debug report with the given name %(name)s"), + "GGBDR0005E": _("Unable to generate debug report %(name)s. Details: %(err)s"), + "GGBDR0006E": _("You should give a name for the debug report file."), + "GGBDR0007E": _("Debug report name must be a string. Only letters, digits, underscore ('_') and " + "hyphen ('-') are allowed."), + "GGBDR0008E": _("The debug report with specified name \"%(name)s\" already exists. Please use another one."), + + "GGBPART0001E": _("Partition %(name)s does not exist in the host"), + + "GGBHOST0001E": _("Unable to shutdown host machine as there are running virtual machines"), + "GGBHOST0002E": _("Unable to reboot host machine as there are running virtual machines"), + "GGBHOST0005E": _("When specifying CPU topology, each element must be an integer greater than zero."), + + "GGBPKGUPD0001E": _("No packages marked for update"), + "GGBPKGUPD0002E": _("Package %(name)s is not marked to be updated."), + "GGBPKGUPD0003E": _("Error while getting packages marked to be updated. Details: %(err)s"), + "GGBPKGUPD0004E": _("There is no compatible package manager for this system."), + "GGBPKGUPD0005E": _("There is a package manager instance running in the system."), + + "GGBREPOS0001E": _("YUM Repository ID must be one word only string."), + "GGBREPOS0002E": _("Repository URL must be an http://, ftp:// or file:// URL."), + "GGBREPOS0003E": _("Repository configuration is a dictionary with specific values according to repository type."), + "GGBREPOS0004E": _("Distribution to DEB repository must be a string"), + "GGBREPOS0005E": _("Components to DEB repository must be listed in a array"), + "GGBREPOS0006E": _("Components to DEB repository must be a string"), + "GGBREPOS0007E": _("Mirror list to repository must be a string"), + "GGBREPOS0008E": _("YUM Repository name must be string."), + "GGBREPOS0009E": _("GPG check must be a boolean value."), + "GGBREPOS0010E": _("GPG key must be a URL pointing to the ASCII-armored file."), + "GGBREPOS0011E": _("Could not update repository %(repo_id)s."), + "GGBREPOS0012E": _("Repository %(repo_id)s does not exist."), + "GGBREPOS0013E": _("Specify repository base URL, mirror list or metalink in order to create or " + "update a YUM repository."), + "GGBREPOS0014E": _("Repository management tool was not recognized for your system."), + "GGBREPOS0015E": _("Repository %(repo_id)s is already enabled."), + "GGBREPOS0016E": _("Repository %(repo_id)s is already disabled."), + "GGBREPOS0017E": _("Could not remove repository %(repo_id)s."), + "GGBREPOS0018E": _("Could not write repository configuration file %(repo_file)s"), + "GGBREPOS0019E": _("Specify repository distribution in order to create a DEB repository."), + "GGBREPOS0020E": _("Could not enable repository %(repo_id)s."), + "GGBREPOS0021E": _("Could not disable repository %(repo_id)s."), + "GGBREPOS0022E": _("YUM Repository ID already exists"), + "GGBREPOS0023E": _("YUM Repository name must be a string"), + "GGBREPOS0024E": _("Unable to list repositories. Details: '%(err)s'"), + "GGBREPOS0025E": _("Unable to retrieve repository information. Details: '%(err)s'"), + "GGBREPOS0026E": _("Unable to add repository. Details: '%(err)s'"), + "GGBREPOS0027E": _("Unable to remove repository. Details: '%(err)s'"), + "GGBREPOS0028E": _("Configuration items: '%(items)s' are not supported by repository manager"), + "GGBREPOS0029E": _("Repository metalink must be an http://, ftp:// or file:// URL."), + "GGBREPOS0030E": _("Cannot specify mirrorlist and metalink at the same time."), + + + "GGBCPUINF0001E": _("The number of vCPUs is too large for this system."), + "GGBCPUINF0002E": _("Invalid vCPU/topology combination."), + "GGBCPUINF0003E": _("This host (or current configuration) does not allow CPU topology."), + "GGBCPUINF0004E": _("This host (or current configuration) does not allow to fetch lscpu details."), + "GGBCPUINF0005E": _("This host (or current configuration) does not provide Socket(s) information."), + "GGBCPUINF0006E": _("This host (or current configuration) does not provide Core(s) per socket information."), + "GGBCPUINF0007E": _("This host (or current configuration) does not provide Thread(s) per core information."), + +} diff --git a/src/wok/plugins/gingerbase/lscpu.py b/src/wok/plugins/gingerbase/lscpu.py new file mode 100644 index 0000000..0fc1e72 --- /dev/null +++ b/src/wok/plugins/gingerbase/lscpu.py @@ -0,0 +1,126 @@ +# +# Project Ginger Base +# +# Copyright IBM, Corp. 2015 +# +# 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 logging + +from wok.utils import run_command +from wok.exception import NotFoundError + + +class LsCpu(object): + """ + Get CPU information about a CPU hyper threading/architecture on x86 + """ + def log_error(e): + """ + param e: error details to be logged + """ + log = logging.getLogger('Util') + log.warning('Exception in fetching the CPU architecture details: %s', + e) + + def __init__(self): + self.lsCpuInfo = {} + try: + # lscpu - display information about the CPU architecture + out, error, rc = run_command(['lscpu']) + # Output of lscpu on x86 is expected to be: + # Architecture: x86_64 + # CPU op-mode(s): 32-bit, 64-bit + # Byte Order: Little Endian + # CPU(s): 4 + # On-line CPU(s) list: 0-3 + # Thread(s) per core: 2 + # Core(s) per socket: 2 + # Socket(s): 1 + # NUMA node(s): 1 + # Vendor ID: GenuineIntel + # CPU family: 6 + # Model: 42 + # Model name: Intel(R) Core(TM) i5-2540M CPU @ 2.60GHz + # Stepping: 7 + # CPU MHz: 976.421 + # CPU max MHz: 3300.0000 + # CPU min MHz: 800.0000 + # BogoMIPS: 5182.99 + # Virtualization: VT-x + # L1d cache: 32K + # L1i cache: 32K + # L2 cache: 256K + # L3 cache: 3072K + # NUMA node0 CPU(s): 0-3 + + if not rc and (not out.isspace()): + lscpuout = out.split('\n') + if lscpuout and len(lscpuout) > 0: + for line in lscpuout: + if ":" in line and (len(line.split(':')) == 2): + self.lsCpuInfo[line.split(':')[0].strip()] = \ + line.split(':')[1].strip() + else: + continue + except Exception, e: + self.log_error(e) + raise NotFoundError("GGBCPUINF0004E") + + def get_sockets(self): + """ + param self: object of the class self + return: Socket(s) (information about the CPU architecture) + """ + try: + sockets = "Socket(s)" + if len(self.lsCpuInfo) > 0 and sockets in self.lsCpuInfo.keys(): + return int(self.lsCpuInfo[sockets]) + else: + raise NotFoundError("GGBCPUINF0005E") + except IndexError, e: + self.log_error(e) + raise NotFoundError("GGBCPUINF0005E") + + def get_cores_per_socket(self): + """ + param self: object of the class self + return: Core(s) per socket (information about the CPU architecture) + """ + try: + cores_per_socket = "Core(s) per socket" + if len(self.lsCpuInfo) > 0 and cores_per_socket \ + in self.lsCpuInfo.keys(): + return int(self.lsCpuInfo[cores_per_socket]) + else: + raise NotFoundError("GGBCPUINF0006E") + except IndexError, e: + self.log_error(e) + raise NotFoundError("GGBCPUINF0006E") + + def get_threads_per_core(self): + """ + param self: object of the class self + return: Thread(s) per core (information about the CPU architecture) + """ + try: + threads_per_core = "Thread(s) per core" + if len(self.lsCpuInfo) > 0 and threads_per_core \ + in self.lsCpuInfo.keys(): + return int(self.lsCpuInfo[threads_per_core]) + else: + raise NotFoundError("GGBCPUINF0007E") + except IndexError, e: + self.log_error(e) + raise NotFoundError("GGBCPUINF0007E") diff --git a/src/wok/plugins/gingerbase/mockmodel.py b/src/wok/plugins/gingerbase/mockmodel.py new file mode 100644 index 0000000..0b5ae22 --- /dev/null +++ b/src/wok/plugins/gingerbase/mockmodel.py @@ -0,0 +1,220 @@ +# +# Project Ginger Base +# +# Copyright IBM, Corp. 2015 +# +# Code derived from Project Kimchi +# +# 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 lxml.etree as ET +import os +import random +import time + +from wok.objectstore import ObjectStore +from wok.utils import add_task, wok_log + +from wok.plugins.gingerbase import config +from wok.plugins.gingerbase.model import cpuinfo +from wok.plugins.gingerbase.model.debugreports import DebugReportsModel +from wok.plugins.gingerbase.model.model import Model + +fake_user = {'root': 'letmein!'} +mockmodel_defaults = {'domain': 'test', 'arch': 'i686'} + + +class MockModel(Model): + + def __init__(self, objstore_loc=None): + # Override osinfo.defaults to ajust the values according to + # test:///default driver + + self._mock_partitions = MockPartitions() + self._mock_swupdate = MockSoftwareUpdate() + self._mock_repositories = MockRepositories() + + cpuinfo.get_topo_capabilities = \ + MockModel.get_topo_capabilities + + super(MockModel, self).__init__(objstore_loc) + self.objstore_loc = objstore_loc + self.objstore = ObjectStore(objstore_loc) + + # The MockModel methods are instantiated on runtime according to Model + # and BaseModel + # Because that a normal method override will not work here + # Instead of that we also need to do the override on runtime + for method in dir(self): + if method.startswith('_mock_'): + mock_method = getattr(self, method) + if not callable(mock_method): + continue + + m = method[6:] + model_method = getattr(self, m) + setattr(self, '_model_' + m, model_method) + setattr(self, m, mock_method) + + DebugReportsModel._gen_debugreport_file = self._gen_debugreport_file + + def reset(self): + self._mock_swupdate = MockSoftwareUpdate() + self._mock_repositories = MockRepositories() + + if hasattr(self, 'objstore'): + self.objstore = ObjectStore(self.objstore_loc) + + @staticmethod + def get_topo_capabilities(conn): + # The libvirt test driver doesn't return topology. + xml = "<topology sockets='1' cores='2' threads='2'/>" + return ET.fromstring(xml) + + def _gen_debugreport_file(self, name): + return add_task('/plugins/gingerbase/debugreports/%s' % name, + self._create_log, self.objstore, name) + + 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 _mock_host_shutdown(self, *name): + wok_log.info("The host system will be shutted down") + + def _mock_host_reboot(self, *name): + wok_log.info("The host system will be rebooted") + + def _mock_partitions_get_list(self): + return self._mock_partitions.partitions.keys() + + def _mock_partition_lookup(self, name): + return self._mock_partitions.partitions[name] + + def _mock_packagesupdate_get_list(self): + return self._mock_swupdate.pkgs.keys() + + def _mock_packageupdate_lookup(self, pkg_name): + return self._mock_swupdate.pkgs[pkg_name] + + def _mock_host_swupdate(self, args=None): + task_id = add_task('/plugins/gingerbase/host/swupdate', + self._mock_swupdate.doUpdate, + self.objstore) + return self.task_lookup(task_id) + + def _mock_repositories_get_list(self): + return self._mock_repositories.repos.keys() + + def _mock_repositories_create(self, params): + # Create a repo_id if not given by user. The repo_id will follow + # the format gingerbase_repo_<integer>, where integer is the number of + # seconds since the Epoch (January 1st, 1970), in UTC. + repo_id = params.get('repo_id', None) + if repo_id is None: + repo_id = "gingerbase_repo_%s" % str(int(time.time() * 1000)) + params.update({'repo_id': repo_id}) + + config = params.get('config', {}) + info = {'repo_id': repo_id, + 'baseurl': params['baseurl'], + 'enabled': True, + 'config': {'repo_name': config.get('repo_name', repo_id), + 'gpgkey': config.get('gpgkey', []), + 'gpgcheck': True, + 'mirrorlist': params.get('mirrorlist', '')}} + self._mock_repositories.repos[repo_id] = info + return repo_id + + def _mock_repository_lookup(self, repo_id): + return self._mock_repositories.repos[repo_id] + + def _mock_repository_delete(self, repo_id): + del self._mock_repositories.repos[repo_id] + + def _mock_repository_enable(self, repo_id): + self._mock_repositories.repos[repo_id]['enabled'] = True + + def _mock_repository_disable(self, repo_id): + self._mock_repositories.repos[repo_id]['enabled'] = False + + def _mock_repository_update(self, repo_id, params): + self._mock_repositories.repos[repo_id].update(params) + return repo_id + + +class MockPartitions(object): + def __init__(self): + self.partitions = {"vdx": {"available": True, "name": "vdx", + "fstype": "", "path": "/dev/vdx", + "mountpoint": "", "type": "disk", + "size": "2147483648"}, + "vdz": {"available": True, "name": "vdz", + "fstype": "", "path": "/dev/vdz", + "mountpoint": "", "type": "disk", + "size": "2147483648"}} + + +class MockSoftwareUpdate(object): + def __init__(self): + self.pkgs = { + 'udevmountd': {'repository': 'openSUSE-13.1-Update', + 'version': '0.81.5-14.1', + 'arch': 'x86_64', + 'package_name': 'udevmountd'}, + 'sysconfig-network': {'repository': 'openSUSE-13.1-Extras', + 'version': '0.81.5-14.1', + 'arch': 'x86_64', + 'package_name': 'sysconfig-network'}, + 'libzypp': {'repository': 'openSUSE-13.1-Update', + 'version': '13.9.0-10.1', + 'arch': 'noarch', + 'package_name': 'libzypp'}} + self._num2update = 3 + + def doUpdate(self, cb, params): + msgs = [] + for pkg in self.pkgs.keys(): + msgs.append("Updating package %s" % pkg) + cb('\n'.join(msgs)) + time.sleep(1) + + time.sleep(2) + msgs.append("All packages updated") + cb('\n'.join(msgs), True) + + # After updating all packages any package should be listed to be + # updated, so reset self._packages + self.pkgs = {} + + +class MockRepositories(object): + def __init__(self): + self.repos = {"gingerbase_repo_1392167832": + {"repo_id": "gingerbase_repo_1392167832", + "enabled": True, + "baseurl": "http://www.fedora.org", + "config": {"repo_name": "gingerbase_repo_1392167832", + "gpgkey": [], + "gpgcheck": True, + "mirrorlist": ""}}} diff --git a/src/wok/plugins/gingerbase/swupdate.py b/src/wok/plugins/gingerbase/swupdate.py new file mode 100644 index 0000000..6ac98e2 --- /dev/null +++ b/src/wok/plugins/gingerbase/swupdate.py @@ -0,0 +1,415 @@ +# +# Project Ginger Base +# +# Copyright IBM, Corp. 2014-2015 +# +# Code derived from Project Kimchi +# +# 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 fcntl +import os +import signal +import subprocess +import time +from configobj import ConfigObj, ConfigObjError +from psutil import pid_exists + +from wok.basemodel import Singleton +from wok.exception import NotFoundError, OperationFailed +from wok.utils import run_command, wok_log + +from wok.plugins.gingerbase.config import gingerBaseLock +from wok.plugins.gingerbase.yumparser import get_yum_packages_list_update + + +class SoftwareUpdate(object): + __metaclass__ = Singleton + + """ + Class to represent and operate with OS software update. + """ + def __init__(self): + # This stores all packages to be updated for Ginger Base perspective. + # It's a dictionary of dictionaries, in the format + # {'package_name': package}, + # where: + # package = {'package_name': <string>, 'version': <string>, + # 'arch': <string>, 'repository': <string> + # } + self._packages = {} + + # This stores the number of packages to update + self._num2update = 0 + + # Get the distro of host machine and creates an object related to + # correct package management system + try: + __import__('yum') + wok_log.info("Loading YumUpdate features.") + self._pkg_mnger = YumUpdate() + except ImportError: + try: + __import__('apt') + wok_log.info("Loading AptUpdate features.") + self._pkg_mnger = AptUpdate() + except ImportError: + zypper_help = ["zypper", "--help"] + (stdout, stderr, returncode) = run_command(zypper_help) + if returncode == 0: + wok_log.info("Loading ZypperUpdate features.") + self._pkg_mnger = ZypperUpdate() + else: + raise Exception("There is no compatible package manager " + "for this system.") + + def _scanUpdates(self): + """ + Update self._packages with packages to be updated. + """ + self._packages = {} + self._num2update = 0 + + # Call system pkg_mnger to get the packages as list of dictionaries. + for pkg in self._pkg_mnger.getPackagesList(): + + # Check if already exist a package in self._packages + pkg_id = pkg.get('package_name') + if pkg_id in self._packages.keys(): + # package already listed to update. do nothing + continue + + # Update the self._packages and self._num2update + self._packages[pkg_id] = pkg + self._num2update = self._num2update + 1 + + def getUpdates(self): + """ + Return the self._packages. + """ + self._scanUpdates() + return self._packages + + def getUpdate(self, name): + """ + Return a dictionary with all info from a given package name. + """ + if name not in self._packages.keys(): + raise NotFoundError('GGBPKGUPD0002E', {'name': name}) + + return self._packages[name] + + def getNumOfUpdates(self): + """ + Return the number of packages to be updated. + """ + self._scanUpdates() + return self._num2update + + def preUpdate(self): + """ + Make adjustments before executing the command in + a child process. + """ + os.setsid() + signal.signal(signal.SIGTERM, signal.SIG_IGN) + + def tailUpdateLogs(self, cb, params): + """ + When the package manager is already running (started outside kimchi or + if wokd is restarted) we can only know what's happening by reading the + logfiles. This method acts like a 'tail -f' on the default package + manager logfile. If the logfile is not found, a simple '*' is + displayed to track progress. This will be until the process finishes. + """ + if not self._pkg_mnger.isRunning(): + return + + fd = None + try: + fd = os.open(self._pkg_mnger.logfile, os.O_RDONLY) + + # cannot open logfile, print something to let users know that the + # system is being upgrading until the package manager finishes its + # job + except (TypeError, OSError): + msgs = [] + while self._pkg_mnger.isRunning(): + msgs.append('*') + cb(''.join(msgs)) + time.sleep(1) + msgs.append('\n') + cb(''.join(msgs), True) + return + + # go to the end of logfile and starts reading, if nothing is read or + # a pattern is not found in the message just wait and retry until + # the package manager finishes + os.lseek(fd, 0, os.SEEK_END) + msgs = [] + progress = [] + while True: + read = os.read(fd, 1024) + if not read: + if not self._pkg_mnger.isRunning(): + break + + if not msgs: + progress.append('*') + cb(''.join(progress)) + + time.sleep(1) + continue + + msgs.append(read) + cb(''.join(msgs)) + + os.close(fd) + return cb(''.join(msgs), True) + + def doUpdate(self, cb, params): + """ + Execute the update + """ + # reset messages + cb('') + + cmd = self._pkg_mnger.update_cmd + proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + preexec_fn=self.preUpdate) + msgs = [] + while proc.poll() is None: + msgs.append(proc.stdout.readline()) + cb(''.join(msgs)) + time.sleep(0.5) + + # read the final output lines + msgs.extend(proc.stdout.readlines()) + + retcode = proc.poll() + if retcode == 0: + return cb(''.join(msgs), True) + + msgs.extend(proc.stderr.readlines()) + return cb(''.join(msgs), False) + + +class YumUpdate(object): + """ + Class to represent and operate with YUM software update system. + It's loaded only on those systems listed at YUM_DISTROS and loads necessary + modules in runtime. + """ + def __init__(self): + self._pkgs = {} + self.update_cmd = ["yum", "-y", "update"] + self.logfile = self._get_output_log() + + def _get_output_log(self): + """ + Return the logfile path + """ + yumcfg = None + try: + yumcfg = ConfigObj('/etc/yum.conf') + + except ConfigObjError: + return None + + if 'main' in yumcfg and 'logfile' in yumcfg['main']: + return yumcfg['main']['logfile'] + + return None + + def _refreshUpdateList(self): + """ + Update the list of packages to be updated in the system. + """ + try: + gingerBaseLock.acquire() + self._pkgs = get_yum_packages_list_update() + except Exception, e: + raise OperationFailed('GGBPKGUPD0003E', {'err': str(e)}) + finally: + gingerBaseLock.release() + + def getPackagesList(self): + """ + Return a list of package's dictionaries. Each dictionary contains the + information about a package, in the format: + package = {'package_name': <string>, 'version': <string>, + 'arch': <string>, 'repository': <string>} + """ + if self.isRunning(): + raise OperationFailed('GGBPKGUPD0005E') + + self._refreshUpdateList() + pkg_list = [] + for pkg in self._pkgs: + package = {'package_name': pkg.name, 'version': pkg.version, + 'arch': pkg.arch, 'repository': pkg.ui_from_repo} + pkg_list.append(package) + return pkg_list + + def isRunning(self): + """ + Return True whether the YUM package manager is already running or + False otherwise. + """ + try: + with open('/var/run/yum.pid', 'r') as pidfile: + pid = int(pidfile.read().rstrip('\n')) + + # cannot find pidfile, assumes yum is not running + except (IOError, ValueError): + return False + + # the pidfile exists and it lives in process table + if pid_exists(pid): + return True + + return False + + +class AptUpdate(object): + """ + Class to represent and operate with APT software update system. + It's loaded only on those systems listed at APT_DISTROS and loads necessary + modules in runtime. + """ + def __init__(self): + self._pkgs = {} + self.pkg_lock = getattr(__import__('apt_pkg'), 'SystemLock') + self.update_cmd = ['apt-get', 'upgrade', '-y'] + self.logfile = '/var/log/apt/term.log' + + def _refreshUpdateList(self): + """ + Update the list of packages to be updated in the system. + """ + apt_cache = getattr(__import__('apt'), 'Cache')() + try: + with self.pkg_lock(): + apt_cache.update() + apt_cache.upgrade() + self._pkgs = apt_cache.get_changes() + except Exception, e: + gingerBaseLock.release() + raise OperationFailed('GGBPKGUPD0003E', {'err': e.message}) + + def getPackagesList(self): + """ + Return a list of package's dictionaries. Each dictionary contains the + information about a package, in the format + package = {'package_name': <string>, 'version': <string>, + 'arch': <string>, 'repository': <string>} + """ + if self.isRunning(): + raise OperationFailed('GGBPKGUPD0005E') + + gingerBaseLock.acquire() + self._refreshUpdateList() + gingerBaseLock.release() + pkg_list = [] + for pkg in self._pkgs: + package = {'package_name': pkg.shortname, + 'version': pkg.candidate.version, + 'arch': pkg._pkg.architecture, + 'repository': pkg.candidate.origins[0].label} + pkg_list.append(package) + + return pkg_list + + def isRunning(self): + """ + Return True whether the APT package manager is already running or + False otherwise. + """ + try: + with open('/var/lib/dpkg/lock', 'w') as lockfile: + fcntl.lockf(lockfile, fcntl.LOCK_EX | fcntl.LOCK_NB) + + # cannot open dpkg lock file to write in exclusive mode means the + # apt is currently running + except IOError: + return True + + return False + + +class ZypperUpdate(object): + """ + Class to represent and operate with Zypper software update system. + It's loaded only on those systems listed at ZYPPER_DISTROS and loads + necessary modules in runtime. + """ + def __init__(self): + self._pkgs = {} + self.update_cmd = ["zypper", "--non-interactive", "update", + "--auto-agree-with-licenses"] + self.logfile = '/var/log/zypp/history' + + def _refreshUpdateList(self): + """ + Update the list of packages to be updated in the system. + """ + self._pkgs = [] + cmd = ["zypper", "list-updates"] + (stdout, stderr, returncode) = run_command(cmd) + + if len(stderr) > 0: + raise OperationFailed('GGBPKGUPD0003E', {'err': stderr}) + + for line in stdout.split('\n'): + if line.find('v |') >= 0: + info = line.split(' | ') + package = {'package_name': info[2], 'version': info[4], + 'arch': info[5], 'repository': info[1]} + self._pkgs.append(package) + + def getPackagesList(self): + """ + Return a list of package's dictionaries. Each dictionary contains the + information about a package, in the format + package = {'package_name': <string>, 'version': <string>, + 'arch': <string>, 'repository': <string>} + """ + if self.isRunning(): + raise OperationFailed('GGBPKGUPD0005E') + + gingerBaseLock.acquire() + self._refreshUpdateList() + gingerBaseLock.release() + return self._pkgs + + def isRunning(self): + """ + Return True whether the Zypper package manager is already running or + False otherwise. + """ + try: + with open('/var/run/zypp.pid', 'r') as pidfile: + pid = int(pidfile.read().rstrip('\n')) + + # cannot find pidfile, assumes yum is not running + except (IOError, ValueError): + return False + + # the pidfile exists and it lives in process table + if pid_exists(pid): + return True + + return False diff --git a/src/wok/plugins/gingerbase/utils.py b/src/wok/plugins/gingerbase/utils.py new file mode 100644 index 0000000..9f41967 --- /dev/null +++ b/src/wok/plugins/gingerbase/utils.py @@ -0,0 +1,82 @@ +# +# Project Kimchi +# +# Copyright IBM, Corp. 2013-2015 +# +# Code derived from Project Kimchi +# +# 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 contextlib +import os +import urllib2 +from httplib import HTTPConnection, HTTPException +from urlparse import urlparse + +from wok.exception import InvalidParameter + + +MAX_REDIRECTION_ALLOWED = 5 + + +def check_url_path(path, redirected=0): + if redirected > MAX_REDIRECTION_ALLOWED: + return False + try: + code = '' + parse_result = urlparse(path) + server_name = parse_result.netloc + urlpath = parse_result.path + if not urlpath: + # Just a server, as with a repo. + with contextlib.closing(urllib2.urlopen(path)) as res: + code = res.getcode() + else: + # socket.gaierror could be raised, + # which is a child class of IOError + conn = HTTPConnection(server_name, timeout=15) + # Don't try to get the whole file: + conn.request('HEAD', path) + response = conn.getresponse() + code = response.status + conn.close() + if code == 200: + return True + elif code == 301 or code == 302: + for header in response.getheaders(): + if header[0] == 'location': + return check_url_path(header[1], redirected+1) + else: + return False + except (urllib2.URLError, HTTPException, IOError, ValueError): + return False + return True + + +def validate_repo_url(url): + url_parts = url.split('://') # [0] = prefix, [1] = rest of URL + + if url_parts[0] == '': + raise InvalidParameter("KCHREPOS0002E") + + if url_parts[0] in ['http', 'https', 'ftp']: + if not check_url_path(url): + raise InvalidParameter("WOKUTILS0001E", {'url': url}) + elif url_parts[0] == 'file': + if not os.path.exists(url_parts[1]): + raise InvalidParameter("WOKUTILS0001E", {'url': url}) + else: + raise InvalidParameter("KCHREPOS0002E") diff --git a/src/wok/plugins/kimchi/swupdate.py b/src/wok/plugins/kimchi/swupdate.py deleted file mode 100644 index aba53e6..0000000 --- a/src/wok/plugins/kimchi/swupdate.py +++ /dev/null @@ -1,412 +0,0 @@ -# -# Project Kimchi -# -# Copyright IBM, Corp. 2014-2015 -# -# 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 fcntl -import os -import signal -import subprocess -import time -from configobj import ConfigObj, ConfigObjError -from psutil import pid_exists - -from wok.basemodel import Singleton -from wok.exception import NotFoundError, OperationFailed -from wok.utils import run_command, wok_log - -from wok.plugins.kimchi.config import kimchiLock -from wok.plugins.kimchi.yumparser import get_yum_packages_list_update - - -class SoftwareUpdate(object): - __metaclass__ = Singleton - - """ - Class to represent and operate with OS software update. - """ - def __init__(self): - # This stores all packages to be updated for Kimchi perspective. It's a - # dictionary of dictionaries, in the format {'package_name': package}, - # where: - # package = {'package_name': <string>, 'version': <string>, - # 'arch': <string>, 'repository': <string> - # } - self._packages = {} - - # This stores the number of packages to update - self._num2update = 0 - - # Get the distro of host machine and creates an object related to - # correct package management system - try: - __import__('yum') - wok_log.info("Loading YumUpdate features.") - self._pkg_mnger = YumUpdate() - except ImportError: - try: - __import__('apt') - wok_log.info("Loading AptUpdate features.") - self._pkg_mnger = AptUpdate() - except ImportError: - zypper_help = ["zypper", "--help"] - (stdout, stderr, returncode) = run_command(zypper_help) - if returncode == 0: - wok_log.info("Loading ZypperUpdate features.") - self._pkg_mnger = ZypperUpdate() - else: - raise Exception("There is no compatible package manager " - "for this system.") - - def _scanUpdates(self): - """ - Update self._packages with packages to be updated. - """ - self._packages = {} - self._num2update = 0 - - # Call system pkg_mnger to get the packages as list of dictionaries. - for pkg in self._pkg_mnger.getPackagesList(): - - # Check if already exist a package in self._packages - pkg_id = pkg.get('package_name') - if pkg_id in self._packages.keys(): - # package already listed to update. do nothing - continue - - # Update the self._packages and self._num2update - self._packages[pkg_id] = pkg - self._num2update = self._num2update + 1 - - def getUpdates(self): - """ - Return the self._packages. - """ - self._scanUpdates() - return self._packages - - def getUpdate(self, name): - """ - Return a dictionary with all info from a given package name. - """ - if name not in self._packages.keys(): - raise NotFoundError('KCHPKGUPD0002E', {'name': name}) - - return self._packages[name] - - def getNumOfUpdates(self): - """ - Return the number of packages to be updated. - """ - self._scanUpdates() - return self._num2update - - def preUpdate(self): - """ - Make adjustments before executing the command in - a child process. - """ - os.setsid() - signal.signal(signal.SIGTERM, signal.SIG_IGN) - - def tailUpdateLogs(self, cb, params): - """ - When the package manager is already running (started outside kimchi or - if wokd is restarted) we can only know what's happening by reading the - logfiles. This method acts like a 'tail -f' on the default package - manager logfile. If the logfile is not found, a simple '*' is - displayed to track progress. This will be until the process finishes. - """ - if not self._pkg_mnger.isRunning(): - return - - fd = None - try: - fd = os.open(self._pkg_mnger.logfile, os.O_RDONLY) - - # cannot open logfile, print something to let users know that the - # system is being upgrading until the package manager finishes its - # job - except (TypeError, OSError): - msgs = [] - while self._pkg_mnger.isRunning(): - msgs.append('*') - cb(''.join(msgs)) - time.sleep(1) - msgs.append('\n') - cb(''.join(msgs), True) - return - - # go to the end of logfile and starts reading, if nothing is read or - # a pattern is not found in the message just wait and retry until - # the package manager finishes - os.lseek(fd, 0, os.SEEK_END) - msgs = [] - progress = [] - while True: - read = os.read(fd, 1024) - if not read: - if not self._pkg_mnger.isRunning(): - break - - if not msgs: - progress.append('*') - cb(''.join(progress)) - - time.sleep(1) - continue - - msgs.append(read) - cb(''.join(msgs)) - - os.close(fd) - return cb(''.join(msgs), True) - - def doUpdate(self, cb, params): - """ - Execute the update - """ - # reset messages - cb('') - - cmd = self._pkg_mnger.update_cmd - proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - preexec_fn=self.preUpdate) - msgs = [] - while proc.poll() is None: - msgs.append(proc.stdout.readline()) - cb(''.join(msgs)) - time.sleep(0.5) - - # read the final output lines - msgs.extend(proc.stdout.readlines()) - - retcode = proc.poll() - if retcode == 0: - return cb(''.join(msgs), True) - - msgs.extend(proc.stderr.readlines()) - return cb(''.join(msgs), False) - - -class YumUpdate(object): - """ - Class to represent and operate with YUM software update system. - It's loaded only on those systems listed at YUM_DISTROS and loads necessary - modules in runtime. - """ - def __init__(self): - self._pkgs = {} - self.update_cmd = ["yum", "-y", "update"] - self.logfile = self._get_output_log() - - def _get_output_log(self): - """ - Return the logfile path - """ - yumcfg = None - try: - yumcfg = ConfigObj('/etc/yum.conf') - - except ConfigObjError: - return None - - if 'main' in yumcfg and 'logfile' in yumcfg['main']: - return yumcfg['main']['logfile'] - - return None - - def _refreshUpdateList(self): - """ - Update the list of packages to be updated in the system. - """ - try: - kimchiLock.acquire() - self._pkgs = get_yum_packages_list_update() - except Exception, e: - raise OperationFailed('KCHPKGUPD0003E', {'err': str(e)}) - finally: - kimchiLock.release() - - def getPackagesList(self): - """ - Return a list of package's dictionaries. Each dictionary contains the - information about a package, in the format: - package = {'package_name': <string>, 'version': <string>, - 'arch': <string>, 'repository': <string>} - """ - if self.isRunning(): - raise OperationFailed('KCHPKGUPD0005E') - - self._refreshUpdateList() - pkg_list = [] - for pkg in self._pkgs: - package = {'package_name': pkg.name, 'version': pkg.version, - 'arch': pkg.arch, 'repository': pkg.ui_from_repo} - pkg_list.append(package) - return pkg_list - - def isRunning(self): - """ - Return True whether the YUM package manager is already running or - False otherwise. - """ - try: - with open('/var/run/yum.pid', 'r') as pidfile: - pid = int(pidfile.read().rstrip('\n')) - - # cannot find pidfile, assumes yum is not running - except (IOError, ValueError): - return False - - # the pidfile exists and it lives in process table - if pid_exists(pid): - return True - - return False - - -class AptUpdate(object): - """ - Class to represent and operate with APT software update system. - It's loaded only on those systems listed at APT_DISTROS and loads necessary - modules in runtime. - """ - def __init__(self): - self._pkgs = {} - self.pkg_lock = getattr(__import__('apt_pkg'), 'SystemLock') - self.update_cmd = ['apt-get', 'upgrade', '-y'] - self.logfile = '/var/log/apt/term.log' - - def _refreshUpdateList(self): - """ - Update the list of packages to be updated in the system. - """ - apt_cache = getattr(__import__('apt'), 'Cache')() - try: - with self.pkg_lock(): - apt_cache.update() - apt_cache.upgrade() - self._pkgs = apt_cache.get_changes() - except Exception, e: - kimchiLock.release() - raise OperationFailed('KCHPKGUPD0003E', {'err': e.message}) - - def getPackagesList(self): - """ - Return a list of package's dictionaries. Each dictionary contains the - information about a package, in the format - package = {'package_name': <string>, 'version': <string>, - 'arch': <string>, 'repository': <string>} - """ - if self.isRunning(): - raise OperationFailed('KCHPKGUPD0005E') - - kimchiLock.acquire() - self._refreshUpdateList() - kimchiLock.release() - pkg_list = [] - for pkg in self._pkgs: - package = {'package_name': pkg.shortname, - 'version': pkg.candidate.version, - 'arch': pkg._pkg.architecture, - 'repository': pkg.candidate.origins[0].label} - pkg_list.append(package) - - return pkg_list - - def isRunning(self): - """ - Return True whether the APT package manager is already running or - False otherwise. - """ - try: - with open('/var/lib/dpkg/lock', 'w') as lockfile: - fcntl.lockf(lockfile, fcntl.LOCK_EX | fcntl.LOCK_NB) - - # cannot open dpkg lock file to write in exclusive mode means the - # apt is currently running - except IOError: - return True - - return False - - -class ZypperUpdate(object): - """ - Class to represent and operate with Zypper software update system. - It's loaded only on those systems listed at ZYPPER_DISTROS and loads - necessary modules in runtime. - """ - def __init__(self): - self._pkgs = {} - self.update_cmd = ["zypper", "--non-interactive", "update", - "--auto-agree-with-licenses"] - self.logfile = '/var/log/zypp/history' - - def _refreshUpdateList(self): - """ - Update the list of packages to be updated in the system. - """ - self._pkgs = [] - cmd = ["zypper", "list-updates"] - (stdout, stderr, returncode) = run_command(cmd) - - if len(stderr) > 0: - raise OperationFailed('KCHPKGUPD0003E', {'err': stderr}) - - for line in stdout.split('\n'): - if line.find('v |') >= 0: - info = line.split(' | ') - package = {'package_name': info[2], 'version': info[4], - 'arch': info[5], 'repository': info[1]} - self._pkgs.append(package) - - def getPackagesList(self): - """ - Return a list of package's dictionaries. Each dictionary contains the - information about a package, in the format - package = {'package_name': <string>, 'version': <string>, - 'arch': <string>, 'repository': <string>} - """ - if self.isRunning(): - raise OperationFailed('KCHPKGUPD0005E') - - kimchiLock.acquire() - self._refreshUpdateList() - kimchiLock.release() - return self._pkgs - - def isRunning(self): - """ - Return True whether the Zypper package manager is already running or - False otherwise. - """ - try: - with open('/var/run/zypp.pid', 'r') as pidfile: - pid = int(pidfile.read().rstrip('\n')) - - # cannot find pidfile, assumes yum is not running - except (IOError, ValueError): - return False - - # the pidfile exists and it lives in process table - if pid_exists(pid): - return True - - return False -- 2.1.0