332 lines
12 KiB
Python
332 lines
12 KiB
Python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
|
|
|
# Copyright (c) 2011 OpenStack, LLC.
|
|
# 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 commands
|
|
import os
|
|
import pexpect
|
|
import re
|
|
|
|
from trove.common import exception
|
|
from trove.common import utils
|
|
from trove.common.exception import ProcessExecutionError
|
|
from trove.openstack.common import log as logging
|
|
from trove.guestagent import system
|
|
from trove.openstack.common.gettextutils import _
|
|
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
OK = 0
|
|
RUN_DPKG_FIRST = 1
|
|
REINSTALL_FIRST = 2
|
|
|
|
|
|
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 PkgTransactionCheckError(exception.TroveError):
|
|
pass
|
|
|
|
|
|
class PkgDownloadError(exception.TroveError):
|
|
pass
|
|
|
|
|
|
class BasePackagerMixin:
|
|
|
|
def pexpect_kill_proc(self, child):
|
|
child.delayafterclose = 1
|
|
child.delayafterterminate = 1
|
|
child.close(force=True)
|
|
|
|
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)
|
|
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
|
|
|
|
|
|
class RedhatPackagerMixin(BasePackagerMixin):
|
|
|
|
def _install(self, package_name, time_out):
|
|
"""Attempts to install a package.
|
|
|
|
Returns OK if the package installs fine or a result code if a
|
|
recoverable-error occurred.
|
|
Raises an exception if a non-recoverable error or time out occurs.
|
|
|
|
"""
|
|
cmd = "sudo yum --color=never -y install %s" % package_name
|
|
output_expects = ['\[sudo\] password for .*:',
|
|
'No package %s available.' % package_name,
|
|
'Transaction Check Error:',
|
|
'.*scriptlet failed*',
|
|
'HTTP Error',
|
|
'No more mirrors to try.',
|
|
'.*already installed and latest version',
|
|
'Updated:',
|
|
'Installed:']
|
|
i = self.pexpect_run(cmd, output_expects, time_out)
|
|
if i == 0:
|
|
raise PkgPermissionError("Invalid permissions.")
|
|
elif i == 1:
|
|
raise PkgNotFoundError("Could not find pkg %s" % package_name)
|
|
elif i == 2:
|
|
raise PkgTransactionCheckError("Transaction Check Error")
|
|
elif i == 3:
|
|
raise PkgScriptletError("Package scriptlet failed")
|
|
elif i == 4 or i == 5:
|
|
raise PkgDownloadError("Package download problem")
|
|
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 time out occurs.
|
|
|
|
"""
|
|
cmd = "sudo yum --color=never -y remove %s" % package_name
|
|
output_expects = ['\[sudo\] password for .*:',
|
|
'No Packages marked for removal',
|
|
'Removed:']
|
|
i = self.pexpect_run(cmd, output_expects, time_out)
|
|
if i == 0:
|
|
raise PkgPermissionError("Invalid permissions.")
|
|
elif i == 1:
|
|
raise PkgNotFoundError("Could not find pkg %s" % package_name)
|
|
return OK
|
|
|
|
def pkg_install(self, package_name, time_out):
|
|
result = self._install(package_name, time_out)
|
|
if result != OK:
|
|
raise PkgPackageStateError("Package %s is in a bad state."
|
|
% package_name)
|
|
|
|
def pkg_version(self, package_name):
|
|
cmd_list = ["rpm", "-qa", "--qf", "'%{VERSION}-%{RELEASE}\n'",
|
|
package_name]
|
|
p = commands.getstatusoutput(' '.join(cmd_list))
|
|
# Need to capture the version string
|
|
# check the command output
|
|
std_out = p[1]
|
|
for line in std_out.split("\n"):
|
|
regex = re.compile("[0-9.]+-.*")
|
|
matches = regex.match(line)
|
|
if matches:
|
|
line = matches.group()
|
|
return line
|
|
msg = _("version() saw unexpected output from rpm!")
|
|
LOG.error(msg)
|
|
|
|
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 DebianPackagerMixin(BasePackagerMixin):
|
|
|
|
def _fix(self, time_out):
|
|
"""Sometimes you have to run this command before a pkg will install."""
|
|
try:
|
|
utils.execute("dpkg", "--configure", "-a", run_as_root=True,
|
|
root_helper="sudo")
|
|
except ProcessExecutionError as e:
|
|
LOG.error(_("Error fixing dpkg"))
|
|
|
|
def _install(self, package_name, time_out):
|
|
"""Attempts to install a package.
|
|
|
|
Returns OK if the package installs fine or a result code if a
|
|
recoverable-error occurred.
|
|
Raises an exception if a non-recoverable error or time out occurs.
|
|
|
|
"""
|
|
cmd = "sudo -E DEBIAN_FRONTEND=noninteractive " \
|
|
"apt-get -y --allow-unauthenticated install %s" % package_name
|
|
output_expects = ['.*password*',
|
|
'E: Unable to locate package %s' % package_name,
|
|
"Couldn't find package % s" % package_name,
|
|
("dpkg was interrupted, you must manually run "
|
|
"'sudo dpkg --configure -a'"),
|
|
"Unable to lock the administration directory",
|
|
"Setting up %s*" % package_name,
|
|
"is already the newest version"]
|
|
i = self.pexpect_run(cmd, output_expects, time_out)
|
|
if i == 0:
|
|
raise PkgPermissionError("Invalid permissions.")
|
|
elif i == 1 or i == 2:
|
|
raise PkgNotFoundError("Could not find apt %s" % package_name)
|
|
elif i == 3:
|
|
return RUN_DPKG_FIRST
|
|
elif i == 4:
|
|
raise PkgAdminLockError()
|
|
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 time out 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]
|
|
i = self.pexpect_run(cmd, output_expects, time_out)
|
|
if i == 0:
|
|
raise PkgPermissionError("Invalid permissions.")
|
|
elif i == 1:
|
|
raise PkgNotFoundError("Could not find pkg %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, package_name, time_out):
|
|
"""Installs a package."""
|
|
try:
|
|
utils.execute("apt-get", "update", run_as_root=True,
|
|
root_helper="sudo")
|
|
except ProcessExecutionError as e:
|
|
LOG.error(_("Error updating the apt sources"))
|
|
|
|
result = self._install(package_name, time_out)
|
|
if result != OK:
|
|
if result == RUN_DPKG_FIRST:
|
|
self._fix(time_out)
|
|
result = self._install(package_name, time_out)
|
|
if result != OK:
|
|
raise PkgPackageStateError("Package %s is in a bad state."
|
|
% package_name)
|
|
|
|
def pkg_version(self, package_name):
|
|
cmd_list = ["dpkg", "-l", package_name]
|
|
p = commands.getstatusoutput(' '.join(cmd_list))
|
|
# check the command status code
|
|
if not p[0] == 0:
|
|
return None
|
|
# Need to capture the version string
|
|
# check the command output
|
|
std_out = p[1]
|
|
patterns = ['.*No packages found matching.*',
|
|
"\w\w\s+(\S+)\s+(\S+)\s+(.*)$"]
|
|
for line in std_out.split("\n"):
|
|
for p in patterns:
|
|
regex = re.compile(p)
|
|
matches = regex.match(line)
|
|
if matches:
|
|
line = matches.group()
|
|
parts = line.split()
|
|
if not parts:
|
|
msg = _("returned nothing")
|
|
LOG.error(msg)
|
|
raise exception.GuestError(msg)
|
|
if len(parts) <= 2:
|
|
msg = _("Unexpected output.")
|
|
LOG.error(msg)
|
|
raise exception.GuestError(msg)
|
|
if parts[1] != package_name:
|
|
msg = _("Unexpected output:[1] = %s" % str(parts[1]))
|
|
LOG.error(msg)
|
|
raise exception.GuestError(msg)
|
|
if parts[0] == 'un' or parts[2] == '<none>':
|
|
return None
|
|
return parts[2]
|
|
msg = _("version() saw unexpected output from dpkg!")
|
|
LOG.error(msg)
|
|
|
|
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)
|
|
|
|
|
|
class BasePackage(type):
|
|
|
|
def __new__(meta, name, bases, dct):
|
|
if system.OS == system.REDHAT:
|
|
bases += (RedhatPackagerMixin, )
|
|
else:
|
|
# The default is debian
|
|
bases += (DebianPackagerMixin,)
|
|
return super(BasePackage, meta).__new__(meta, name, bases, dct)
|
|
|
|
|
|
class Package(object):
|
|
|
|
__metaclass__ = BasePackage
|