summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRyan S. Brown <sb@ryansb.com>2014-12-17 11:25:53 -0500
committerRyan S. Brown <sb@ryansb.com>2015-01-15 16:42:20 -0500
commita7ffb71ffd15d55f3945fe11c0049cc389851c22 (patch)
tree5e3443741c7eba9f41586145e3cd7b87957dd67b
parent9862bd7477274cd6ce2d45be9111eb9684ebe6d7 (diff)
downloadheat-cfntools-a7ffb71ffd15d55f3945fe11c0049cc389851c22.tar.gz
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
-rw-r--r--heat_cfntools/cfntools/cfn_helper.py101
-rw-r--r--heat_cfntools/tests/test_cfn_helper.py68
2 files changed, 164 insertions, 5 deletions
diff --git a/heat_cfntools/cfntools/cfn_helper.py b/heat_cfntools/cfntools/cfn_helper.py
index 42fab8d..d3de01c 100644
--- a/heat_cfntools/cfntools/cfn_helper.py
+++ b/heat_cfntools/cfntools/cfn_helper.py
@@ -291,6 +291,20 @@ class RpmHelper(object):
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
def zypper_package_available(cls, pkg):
"""Indicates whether pkg is available via zypper.
@@ -305,8 +319,8 @@ class RpmHelper(object):
return command.status == 0
@classmethod
- def install(cls, packages, rpms=True, zypper=False):
- """Installs (or upgrades) a set of packages via RPM or via Yum.
+ def install(cls, packages, rpms=True, zypper=False, dnf=False):
+ """Installs (or upgrades) packages via RPM, yum, dnf, or zypper.
Arguments:
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-release spec
(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:
cmd = "rpm -U --force --nosignature "
@@ -329,6 +348,11 @@ class RpmHelper(object):
cmd = "zypper -n install "
cmd += " ".join(packages)
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:
cmd = "yum -y install "
cmd += " ".join(packages)
@@ -338,8 +362,8 @@ class RpmHelper(object):
LOG.warn("Failed to install packages: %s" % cmd)
@classmethod
- def downgrade(cls, packages, rpms=True, zypper=False):
- """Downgrades a set of packages via RPM or via Yum.
+ def downgrade(cls, packages, rpms=True, zypper=False, dnf=False):
+ """Downgrades a set of packages via RPM, yum, dnf, or zypper.
Arguments:
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-release spec
(httpd-2.2.22-1.fc16)
+ dnf -- if True:
+ * Use dnf instead of RPM/yum
"""
if rpms:
cls.install(packages)
@@ -362,6 +388,13 @@ class RpmHelper(object):
command = CommandRunner(cmd).run()
if command.status:
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:
cmd = "yum -y downgrade "
cmd += " ".join(packages)
@@ -374,7 +407,7 @@ class RpmHelper(object):
class PackagesHandler(object):
_packages = {}
- _package_order = ["dpkg", "rpm", "apt", "yum"]
+ _package_order = ["dpkg", "rpm", "apt", "yum", "dnf"]
@staticmethod
def _pkgsort(pkg1, pkg2):
@@ -460,6 +493,51 @@ class PackagesHandler(object):
if downgrades:
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):
"""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
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
installs = []
downgrades = []
@@ -531,6 +620,7 @@ class PackagesHandler(object):
# map of function pointers to handle different package managers
_package_handlers = {"yum": _handle_yum_packages,
+ "dnf": _handle_dnf_packages,
"zypper": _handle_zypper_packages,
"rpm": _handle_rpm_packages,
"apt": _handle_apt_packages,
@@ -552,6 +642,7 @@ class PackagesHandler(object):
* rpm
* apt
* yum
+ * dnf
"""
if not self._packages:
return
diff --git a/heat_cfntools/tests/test_cfn_helper.py b/heat_cfntools/tests/test_cfn_helper.py
index 42fe7e2..85b41da 100644
--- a/heat_cfntools/tests/test_cfn_helper.py
+++ b/heat_cfntools/tests/test_cfn_helper.py
@@ -82,6 +82,9 @@ class TestPackages(MockPopenTestCase):
def test_yum_install(self):
install_list = []
+ self.mock_unorder_cmd_run(
+ ['su', 'root', '-c', 'which yum']) \
+ .AndReturn(FakePOpen(returncode=0))
for pack in ('httpd', 'wordpress', 'mysql-server'):
self.mock_unorder_cmd_run(
['su', 'root', '-c', 'rpm -q %s' % pack]) \
@@ -110,6 +113,71 @@ class TestPackages(MockPopenTestCase):
cfn_helper.PackagesHandler(packages).apply_packages()
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):
install_list = []
for pack in ('httpd', 'wordpress', 'mysql-server'):