# Copyright (c) 2011 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """ Manages packages on the Guest VM. """ import os import re import subprocess from tempfile import NamedTemporaryFile from oslo_log import log as logging from oslo_utils import encodeutils import pexpect from trove.common import exception from trove.common.exception import ProcessExecutionError from trove.common.i18n import _ from trove.common import utils from trove.guestagent.common import operating_system LOG = logging.getLogger(__name__) OK = 0 RUN_DPKG_FIRST = 1 REINSTALL_FIRST = 2 CONFLICT_REMOVED = 3 def getoutput(*cmd): """Get the stdout+stderr of a command, ignore errors. Similar to commands.getstatusoutput(cmd)[1] of Python 2. """ try: proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) except OSError: # ignore errors like program not found return str("") stdout = proc.communicate()[0] return encodeutils.safe_decode(stdout) class PkgAdminLockError(exception.TroveError): pass class PkgPermissionError(exception.TroveError): pass class PkgPackageStateError(exception.TroveError): pass class PkgNotFoundError(exception.NotFound): pass class PkgTimeout(exception.TroveError): pass class PkgScriptletError(exception.TroveError): pass class PkgDownloadError(exception.TroveError): pass class PkgSignError(exception.TroveError): pass class PkgBrokenError(exception.TroveError): pass class PkgConfigureError(exception.TroveError): pass class BasePackagerMixin(object): def pexpect_kill_proc(self, child): child.delayafterclose = 1 child.delayafterterminate = 1 try: child.close(force=True) except pexpect.ExceptionPexpect: # Close fails to terminate a sudo process on some OSes. subprocess.call(['sudo', 'kill', str(child.pid)]) def pexpect_wait_and_close_proc(self, child): child.expect(pexpect.EOF) child.close() def pexpect_run(self, cmd, output_expects, time_out): child = pexpect.spawn(cmd, timeout=time_out) try: i = child.expect(output_expects) match = child.match self.pexpect_wait_and_close_proc(child) except pexpect.TIMEOUT: self.pexpect_kill_proc(child) raise PkgTimeout(_("Process timeout after %i seconds.") % time_out) return (i, match) class RPMPackagerMixin(BasePackagerMixin): def _rpm_remove_nodeps(self, package_name): """ Sometimes transaction errors happens, easy way is to remove conflicted package without dependencies and hope it will replaced by another package """ try: utils.execute("rpm", "-e", "--nodeps", package_name, run_as_root=True, root_helper="sudo") except ProcessExecutionError: LOG.exception("Error removing conflict %(package)s", package_name) def _install(self, packages, time_out): """must be overridden by an RPM based PackagerMixin""" raise NotImplementedError() def _remove(self, package_name, time_out): """must be overridden by an RPM based PackagerMixin""" raise NotImplementedError() def pkg_install(self, packages, config_opts, time_out): result = self._install(packages, time_out) if result != OK: while result == CONFLICT_REMOVED: result = self._install(packages, time_out) if result != OK: raise PkgPackageStateError(_("Cannot install packages.")) def pkg_is_installed(self, packages): packages = packages if isinstance(packages, list) else packages.split() std_out = getoutput("rpm", "-qa") for pkg in packages: found = False for line in std_out.split("\n"): if line.find(pkg) != -1: found = True break if not found: return False return True def pkg_version(self, package_name): std_out = getoutput("rpm", "-qa", "--qf", "'%{VERSION}-%{RELEASE}\n'", package_name) # Need to capture the version string # check the command output for line in std_out.split("\n"): regex = re.compile("[0-9.]+-.*") matches = regex.match(line) if matches: line = matches.group() return line LOG.error("Unexpected output from rpm command. (%(output)s)", {'output': std_out}) def pkg_remove(self, package_name, time_out): """Removes a package.""" if self.pkg_version(package_name) is None: return result = self._remove(package_name, time_out) if result != OK: raise PkgPackageStateError(_("Package %s is in a bad state.") % package_name) class RedhatPackagerMixin(RPMPackagerMixin): def _install(self, packages, time_out): """Attempts to install packages. Returns OK if the packages are installed or a result code if a recoverable-error occurred. Raises an exception if a non-recoverable error or timeout occurs. """ cmd = "sudo yum --color=never -y install %s" % " ".join(packages) output_expects = [r'\[sudo\] password for .*:', 'No package (.*) available.', ('file .* from install of .* conflicts with file' ' from package (.*?)\r\n'), 'Error: (.*?) conflicts with .*?\r\n', 'Processing Conflict: .* conflicts (.*?)\r\n', '.*scriptlet failed*', 'HTTP Error', 'No more mirrors to try.', 'GPG key retrieval failed:', '.*already installed and latest version', 'Updated:', 'Installed:'] LOG.debug("Running package install command: %s", cmd) i, match = self.pexpect_run(cmd, output_expects, time_out) if i == 0: raise PkgPermissionError(_("Invalid permissions.")) elif i == 1: raise PkgNotFoundError(_("Could not find package %s") % match.group(1)) elif i == 2 or i == 3 or i == 4: self._rpm_remove_nodeps(match.group(1)) return CONFLICT_REMOVED elif i == 5: raise PkgScriptletError(_("Package scriptlet failed")) elif i == 6 or i == 7: raise PkgDownloadError(_("Package download problem")) elif i == 8: raise PkgSignError(_("GPG key retrieval failed")) return OK def _remove(self, package_name, time_out): """Removes a package. Returns OK if the package is removed successfully or a result code if a recoverable-error occurs. Raises an exception if a non-recoverable error or timeout occurs. """ cmd = "sudo yum --color=never -y remove %s" % package_name LOG.debug("Running package remove command: %s", cmd) output_expects = [r'\[sudo\] password for .*:', 'No Packages marked for removal', 'Removed:'] i, match = self.pexpect_run(cmd, output_expects, time_out) if i == 0: raise PkgPermissionError(_("Invalid permissions.")) elif i == 1: raise PkgNotFoundError(_("Could not find package %s") % package_name) return OK class DebianPackagerMixin(BasePackagerMixin): def _fix(self, time_out): """Sometimes you have to run this command before a package will install. """ try: utils.execute("dpkg", "--configure", "-a", run_as_root=True, root_helper="sudo") except ProcessExecutionError: LOG.exception("Error fixing dpkg") def _fix_package_selections(self, packages, config_opts): """ Sometimes you have to run this command before a package will install. This command sets package selections to configure package. """ selections = "" for package in packages: m = re.match('(.+)=(.+)', package) if m: package_name = m.group(1) else: package_name = package std_out = getoutput("sudo", "debconf-show", package_name) for line in std_out.split("\n"): for selection, value in config_opts.items(): m = re.match(".* (.*/%s):.*" % selection, line) if m: selections += ("%s %s string '%s'\n" % (package_name, m.group(1), value)) if selections: with NamedTemporaryFile(delete=False) as f: fname = f.name f.write(encodeutils.safe_encode(selections)) try: utils.execute("debconf-set-selections", fname, run_as_root=True, root_helper="sudo") utils.execute("dpkg", "--configure", "-a", run_as_root=True, root_helper="sudo") except ProcessExecutionError: raise PkgConfigureError(_("Error configuring package.")) finally: os.remove(fname) def _install(self, packages, time_out): """Attempts to install packages. Returns OK if the packages are installed or a result code if a recoverable-error occurred. Raises an exception if a non-recoverable error or timeout occurs. """ cmd = "sudo -E DEBIAN_FRONTEND=noninteractive apt-get -y " \ "--force-yes --allow-unauthenticated -o " \ "DPkg::options::=--force-confmiss --reinstall " \ "install %s" % " ".join(packages) output_expects = ['.*password*', 'E: Unable to locate package (.*)', "Couldn't find package (.*)", "E: Version '.*' for '(.*)' was not found", ("dpkg was interrupted, you must manually run " "'sudo dpkg --configure -a'"), "Unable to lock the administration directory", ("E: Unable to correct problems, you have held " "broken packages."), "Setting up (.*)", "is already the newest version"] LOG.debug("Running package install command: %s", cmd) i, match = self.pexpect_run(cmd, output_expects, time_out) if i == 0: raise PkgPermissionError(_("Invalid permissions.")) elif i == 1 or i == 2 or i == 3: raise PkgNotFoundError(_("Could not find package %s") % match.group(1)) elif i == 4: return RUN_DPKG_FIRST elif i == 5: raise PkgAdminLockError() elif i == 6: raise PkgBrokenError() return OK def _remove(self, package_name, time_out): """Removes a package. Returns OK if the package is removed successfully or a result code if a recoverable-error occurs. Raises an exception if a non-recoverable error or timeout occurs. """ cmd = "sudo -E apt-get -y --allow-unauthenticated remove %s" \ % package_name output_expects = ['.*password*', 'E: Unable to locate package %s' % package_name, 'Package is in a very bad inconsistent state', 'Sub-process /usr/bin/dpkg returned an error code', ("dpkg was interrupted, you must manually run " "'sudo dpkg --configure -a'"), "Unable to lock the administration directory", "Removing %s*" % package_name] LOG.debug("Running remove package command %s", cmd) i, match = self.pexpect_run(cmd, output_expects, time_out) if i == 0: raise PkgPermissionError(_("Invalid permissions.")) elif i == 1: raise PkgNotFoundError(_("Could not find package %s") % package_name) elif i == 2 or i == 3: return REINSTALL_FIRST elif i == 4: return RUN_DPKG_FIRST elif i == 5: raise PkgAdminLockError() return OK def pkg_install(self, packages, config_opts, time_out): """Installs packages.""" try: utils.execute("apt-get", "update", run_as_root=True, root_helper="sudo") except ProcessExecutionError: LOG.exception("Error updating the apt sources") result = self._install(packages, time_out) if result != OK: if result == RUN_DPKG_FIRST: self._fix(time_out) result = self._install(packages, time_out) if result != OK: raise PkgPackageStateError(_("Packages are in a bad state.")) # even after successful install, packages can stay unconfigured # config_opts - is dict with name/value for questions asked by # interactive configure script if config_opts: self._fix_package_selections(packages, config_opts) def pkg_version(self, package_name): std_out = getoutput("apt-cache", "policy", package_name) for line in std_out.split("\n"): m = re.match(r"\s+Installed: (.*)", line) if m: version = m.group(1) if version == "(none)": version = None return version def pkg_is_installed(self, packages): packages = packages if isinstance(packages, list) else packages.split() for pkg in packages: m = re.match('(.+)=(.+)', pkg) if m: package_name = m.group(1) package_version = m.group(2) else: package_name = pkg package_version = None installed_version = self.pkg_version(package_name) if ((package_version and installed_version == package_version) or (installed_version and not package_version)): LOG.debug("Package %s already installed.", package_name) else: return False return True def pkg_remove(self, package_name, time_out): """Removes a package.""" if self.pkg_version(package_name) is None: return result = self._remove(package_name, time_out) if result != OK: if result == REINSTALL_FIRST: self._install(package_name, time_out) elif result == RUN_DPKG_FIRST: self._fix(time_out) result = self._remove(package_name, time_out) if result != OK: raise PkgPackageStateError(_("Package %s is in a bad state.") % package_name) if operating_system.get_os() == operating_system.REDHAT: class Package(RedhatPackagerMixin): pass else: class Package(DebianPackagerMixin): pass