Support dnf when specified or yum is missing

* handle install/upgrade, version checks, and downgrades

* Allow users to specify packages to be installed with dnf

* Use dnf if yum isn't available, letting older cloud-configs work on
  future Fedoras

Change-Id: Ib3ff49cfdd3e545aa199c944c110852700625496
This commit is contained in:
Ryan S. Brown 2014-12-17 11:25:53 -05:00
parent 9862bd7477
commit a7ffb71ffd
2 changed files with 164 additions and 5 deletions

View File

@ -290,6 +290,20 @@ class RpmHelper(object):
command = CommandRunner(cmd_str).run() command = CommandRunner(cmd_str).run()
return command.status == 0 return command.status == 0
@classmethod
def dnf_package_available(cls, pkg):
"""Indicates whether pkg is available via dnf.
Arguments:
pkg -- A package name (with optional version and release spec).
e.g., httpd
e.g., httpd-2.2.22
e.g., httpd-2.2.22-1.fc21
"""
cmd_str = "dnf -y --showduplicates list available %s" % pkg
command = CommandRunner(cmd_str).run()
return command.status == 0
@classmethod @classmethod
def zypper_package_available(cls, pkg): def zypper_package_available(cls, pkg):
"""Indicates whether pkg is available via zypper. """Indicates whether pkg is available via zypper.
@ -305,8 +319,8 @@ class RpmHelper(object):
return command.status == 0 return command.status == 0
@classmethod @classmethod
def install(cls, packages, rpms=True, zypper=False): def install(cls, packages, rpms=True, zypper=False, dnf=False):
"""Installs (or upgrades) a set of packages via RPM or via Yum. """Installs (or upgrades) packages via RPM, yum, dnf, or zypper.
Arguments: Arguments:
packages -- a list of packages to install packages -- a list of packages to install
@ -320,6 +334,11 @@ class RpmHelper(object):
- pkg name with version spec (httpd-2.2.22), or - pkg name with version spec (httpd-2.2.22), or
- pkg name with version-release spec - pkg name with version-release spec
(httpd-2.2.22-1.fc16) (httpd-2.2.22-1.fc16)
zypper -- if True:
* overrides use of yum, use zypper instead
dnf -- if True:
* overrides use of yum, use dnf instead
* packages must be in same format as yum pkg list
""" """
if rpms: if rpms:
cmd = "rpm -U --force --nosignature " cmd = "rpm -U --force --nosignature "
@ -329,6 +348,11 @@ class RpmHelper(object):
cmd = "zypper -n install " cmd = "zypper -n install "
cmd += " ".join(packages) cmd += " ".join(packages)
LOG.info("Installing packages: %s" % cmd) LOG.info("Installing packages: %s" % cmd)
elif dnf:
# use dnf --best to upgrade outdated-but-installed packages
cmd = "dnf -y --best install "
cmd += " ".join(packages)
LOG.info("Installing packages: %s" % cmd)
else: else:
cmd = "yum -y install " cmd = "yum -y install "
cmd += " ".join(packages) cmd += " ".join(packages)
@ -338,8 +362,8 @@ class RpmHelper(object):
LOG.warn("Failed to install packages: %s" % cmd) LOG.warn("Failed to install packages: %s" % cmd)
@classmethod @classmethod
def downgrade(cls, packages, rpms=True, zypper=False): def downgrade(cls, packages, rpms=True, zypper=False, dnf=False):
"""Downgrades a set of packages via RPM or via Yum. """Downgrades a set of packages via RPM, yum, dnf, or zypper.
Arguments: Arguments:
packages -- a list of packages to downgrade packages -- a list of packages to downgrade
@ -352,6 +376,8 @@ class RpmHelper(object):
- pkg name with version spec (httpd-2.2.22), or - pkg name with version spec (httpd-2.2.22), or
- pkg name with version-release spec - pkg name with version-release spec
(httpd-2.2.22-1.fc16) (httpd-2.2.22-1.fc16)
dnf -- if True:
* Use dnf instead of RPM/yum
""" """
if rpms: if rpms:
cls.install(packages) cls.install(packages)
@ -362,6 +388,13 @@ class RpmHelper(object):
command = CommandRunner(cmd).run() command = CommandRunner(cmd).run()
if command.status: if command.status:
LOG.warn("Failed to downgrade packages: %s" % cmd) LOG.warn("Failed to downgrade packages: %s" % cmd)
elif dnf:
cmd = "dnf -y downgrade "
cmd += " ".join(packages)
LOG.info("Downgrading packages: %s", cmd)
command = CommandRunner(cmd).run()
if command.status:
LOG.warn("Failed to downgrade packages: %s" % cmd)
else: else:
cmd = "yum -y downgrade " cmd = "yum -y downgrade "
cmd += " ".join(packages) cmd += " ".join(packages)
@ -374,7 +407,7 @@ class RpmHelper(object):
class PackagesHandler(object): class PackagesHandler(object):
_packages = {} _packages = {}
_package_order = ["dpkg", "rpm", "apt", "yum"] _package_order = ["dpkg", "rpm", "apt", "yum", "dnf"]
@staticmethod @staticmethod
def _pkgsort(pkg1, pkg2): def _pkgsort(pkg1, pkg2):
@ -460,6 +493,51 @@ class PackagesHandler(object):
if downgrades: if downgrades:
RpmHelper.downgrade(downgrades, zypper=True) RpmHelper.downgrade(downgrades, zypper=True)
def _handle_dnf_packages(self, packages):
"""Handle installation, upgrade, or downgrade of packages via dnf.
Arguments:
packages -- a package entries map of the form:
"pkg_name" : "version",
"pkg_name" : ["v1", "v2"],
"pkg_name" : []
For each package entry:
* if no version is supplied and the package is already installed, do
nothing
* if no version is supplied and the package is _not_ already
installed, install it
* if a version string is supplied, and the package is already
installed, determine whether to downgrade or upgrade (or do nothing
if version matches installed package)
* if a version array is supplied, choose the highest version from the
array and follow same logic for version string above
"""
# collect pkgs for batch processing at end
installs = []
downgrades = []
for pkg_name, versions in packages.iteritems():
ver = RpmHelper.newest_rpm_version(versions)
pkg = "%s-%s" % (pkg_name, ver) if ver else pkg_name
if RpmHelper.rpm_package_installed(pkg):
# FIXME:print non-error, but skipping pkg
pass
elif not RpmHelper.dnf_package_available(pkg):
LOG.warn("Skipping package '%s'. Not available via yum" % pkg)
elif not ver:
installs.append(pkg)
else:
current_ver = RpmHelper.rpm_package_version(pkg)
rc = RpmHelper.compare_rpm_versions(current_ver, ver)
if rc < 0:
installs.append(pkg)
elif rc > 0:
downgrades.append(pkg)
if installs:
RpmHelper.install(installs, rpms=False, dnf=True)
if downgrades:
RpmHelper.downgrade(downgrades, rpms=False, dnf=True)
def _handle_yum_packages(self, packages): def _handle_yum_packages(self, packages):
"""Handle installation, upgrade, or downgrade of packages via yum. """Handle installation, upgrade, or downgrade of packages via yum.
@ -480,6 +558,17 @@ class PackagesHandler(object):
* if a version array is supplied, choose the highest version from the * if a version array is supplied, choose the highest version from the
array and follow same logic for version string above array and follow same logic for version string above
""" """
cmd = CommandRunner("which yum").run()
if cmd.status == 1:
# yum not available, use DNF if available
self._handle_dnf_packages(packages)
return
elif cmd.status == 127:
# `which` command not found
LOG.info("`which` not found. Using yum without checking if dnf "
"is available")
# collect pkgs for batch processing at end # collect pkgs for batch processing at end
installs = [] installs = []
downgrades = [] downgrades = []
@ -531,6 +620,7 @@ class PackagesHandler(object):
# map of function pointers to handle different package managers # map of function pointers to handle different package managers
_package_handlers = {"yum": _handle_yum_packages, _package_handlers = {"yum": _handle_yum_packages,
"dnf": _handle_dnf_packages,
"zypper": _handle_zypper_packages, "zypper": _handle_zypper_packages,
"rpm": _handle_rpm_packages, "rpm": _handle_rpm_packages,
"apt": _handle_apt_packages, "apt": _handle_apt_packages,
@ -552,6 +642,7 @@ class PackagesHandler(object):
* rpm * rpm
* apt * apt
* yum * yum
* dnf
""" """
if not self._packages: if not self._packages:
return return

View File

@ -82,6 +82,9 @@ class TestPackages(MockPopenTestCase):
def test_yum_install(self): def test_yum_install(self):
install_list = [] install_list = []
self.mock_unorder_cmd_run(
['su', 'root', '-c', 'which yum']) \
.AndReturn(FakePOpen(returncode=0))
for pack in ('httpd', 'wordpress', 'mysql-server'): for pack in ('httpd', 'wordpress', 'mysql-server'):
self.mock_unorder_cmd_run( self.mock_unorder_cmd_run(
['su', 'root', '-c', 'rpm -q %s' % pack]) \ ['su', 'root', '-c', 'rpm -q %s' % pack]) \
@ -110,6 +113,71 @@ class TestPackages(MockPopenTestCase):
cfn_helper.PackagesHandler(packages).apply_packages() cfn_helper.PackagesHandler(packages).apply_packages()
self.m.VerifyAll() self.m.VerifyAll()
def test_dnf_install_yum_unavailable(self):
install_list = []
self.mock_unorder_cmd_run(
['su', 'root', '-c', 'which yum']) \
.AndReturn(FakePOpen(returncode=1))
pkgs = ('httpd', 'mysql-server', 'wordpress')
for pack in pkgs:
self.mock_unorder_cmd_run(
['su', 'root', '-c', 'rpm -q %s' % pack]) \
.AndReturn(FakePOpen(returncode=1))
self.mock_unorder_cmd_run(
['su', 'root', '-c',
'dnf -y --showduplicates list available %s' % pack]) \
.AndReturn(FakePOpen(returncode=0))
install_list.append(pack)
# This mock call corresponding to 'su root -c dnf -y list upgrades .*'
# and 'su root -c dnf -y install .*'
# But there is no way to ignore the order of the parameters, so only
# check the return value.
self.mock_cmd_run(mox.IgnoreArg()).AndReturn(FakePOpen(
returncode=0))
self.m.ReplayAll()
packages = {
"yum": {
"mysql-server": [],
"httpd": [],
"wordpress": []
}
}
cfn_helper.PackagesHandler(packages).apply_packages()
self.m.VerifyAll()
def test_dnf_install(self):
install_list = []
for pack in ('httpd', 'wordpress', 'mysql-server'):
self.mock_unorder_cmd_run(
['su', 'root', '-c', 'rpm -q %s' % pack]) \
.AndReturn(FakePOpen(returncode=1))
self.mock_unorder_cmd_run(
['su', 'root', '-c',
'dnf -y --showduplicates list available %s' % pack]) \
.AndReturn(FakePOpen(returncode=0))
install_list.append(pack)
# This mock call corresponding to 'su root -c dnf -y --best install .*'
# But there is no way to ignore the order of the parameters, so only
# check the return value.
self.mock_cmd_run(mox.IgnoreArg()).AndReturn(FakePOpen(
returncode=0))
self.m.ReplayAll()
packages = {
"dnf": {
"mysql-server": [],
"httpd": [],
"wordpress": []
}
}
cfn_helper.PackagesHandler(packages).apply_packages()
self.m.VerifyAll()
def test_zypper_install(self): def test_zypper_install(self):
install_list = [] install_list = []
for pack in ('httpd', 'wordpress', 'mysql-server'): for pack in ('httpd', 'wordpress', 'mysql-server'):