#============================================================================== # Copyright 2011 Amazon.com, Inc. or its affiliates. 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. #============================================================================== from functools import cmp_to_key from cfnbootstrap.construction_errors import ToolError from cfnbootstrap.util import ProcessHelper, LoggingProcessHelper import logging import re import subprocess log = logging.getLogger("cfn.init") class YumTool(object): """ Installs packages via Yum """ def apply(self, action, auth_config=None): """ Install a set of packages via yum, returning the packages actually installed or updated. Arguments: action -- a dict of package name to version; version can be empty, a single string or a list of strings Exceptions: ToolError -- on expected failures (such as a non-zero exit code) """ pkgs_changed = [] if not action.keys(): log.debug("No packages specified for yum") return pkgs_changed cache_result = LoggingProcessHelper(['yum', '-y', 'makecache'], name=u'yum makecache').call() if cache_result.returncode: log.error("Yum makecache failed. Output: %s", cache_result.stdout) raise ToolError("Could not create yum cache", cache_result.returncode) pkg_specs_to_upgrade = [] pkg_specs_to_downgrade = [] for pkg_name in action: if action[pkg_name]: if isinstance(action[pkg_name], str): pkg_ver = action[pkg_name] else: # Yum only cares about one version anyway... so take the max specified version in the list pkg_ver = RpmTool.max_version(action[pkg_name]) else: pkg_ver = None pkg_spec = '%s-%s' % (pkg_name, pkg_ver) if pkg_ver else pkg_name if self._pkg_installed(pkg_spec): # If the EXACT requested spec is installed, don't do anything log.debug("%s will not be installed as it is already present", pkg_spec) elif not self._pkg_available(pkg_spec): # If the requested spec is not available, blow up log.error("%s is not available to be installed", pkg_spec) raise ToolError("Yum does not have %s available for installation" % pkg_spec) elif not pkg_ver: # If they didn't request a specific version, always upgrade pkg_specs_to_upgrade.append(pkg_spec) pkgs_changed.append(pkg_name) else: # They've requested a specific version that's available but not installed. # Figure out if it's an upgrade or a downgrade installed_version = RpmTool.get_package_version(pkg_name, False)[1] if self._should_upgrade(pkg_ver, installed_version): pkg_specs_to_upgrade.append(pkg_spec) pkgs_changed.append(pkg_name) else: log.debug("Downgrading to %s from installed version %s", pkg_spec, installed_version) pkg_specs_to_downgrade.append(pkg_spec) pkgs_changed.append(pkg_name) if not pkgs_changed: log.debug("All yum packages were already installed") return [] if pkg_specs_to_upgrade: log.debug("Installing/updating %s via yum", pkg_specs_to_upgrade) result = LoggingProcessHelper(['yum', '-y', 'install'] + pkg_specs_to_upgrade, name=u'yum install %s' % ' '.join(pkg_specs_to_upgrade)).call() if result.returncode: log.error("Yum failed. Output: %s", result.stdout) raise ToolError("Could not successfully install/update yum packages", result.returncode) if pkg_specs_to_downgrade: log.debug("Downgrading %s via yum", pkg_specs_to_downgrade) result = LoggingProcessHelper(['yum', '-y', 'downgrade'] + pkg_specs_to_downgrade, name=u'yum downgrade %s' % ' '.join(pkg_specs_to_downgrade)).call() if result.returncode: log.error("Yum failed. Output: %s", result.stdout) raise ToolError("Could not successfully downgrade yum packages", result.returncode) log.info("Yum installed %s", pkgs_changed) return pkgs_changed def _should_upgrade(self, requested_ver, installed_version): # If they haven't requested a version, always install if not requested_ver: return True #Now we need to detect whether or not we need to upgrade ver_cmp = RpmTool.compare_rpm_versions(requested_ver, installed_version) if ver_cmp > 0: log.debug("Requested version %s is greater than installed version %s, so we will upgrade", requested_ver, installed_version) return True else: log.debug("Requested version %s is NOT greater than installed version %s, so we will NOT upgrade", requested_ver, installed_version) return False def _pkg_installed(self, pkg): result = LoggingProcessHelper(['yum', '-y', 'list', 'installed', pkg], name="yum list installed %s" % pkg).call() return result.returncode == 0 def _pkg_available(self, pkg): # --showduplicates seems to be required to see downgradable versions when running yum non-interactively # but not when running interactively -- but we rarely run interactively result = LoggingProcessHelper(['yum', '-y', '--showduplicates', 'list', 'available', pkg], name="yum list available %s" % pkg).call() return result.returncode == 0 class RpmTool(object): def apply(self, action, auth_config=None, strict_mode=False): """ Install a set of packages via RPM, returning the packages actually installed or updated. Arguments: action -- a dict of package name to version; version can be empty, a single string or a list of strings Exceptions: ToolError -- on expected failures (such as a non-zero exit code) """ pkgs_changed = [] if not action.keys(): log.debug("No packages installed for RPM") return pkgs_changed pkgs = [] for pkg_name, loc in action.items(): pkgs_to_process = [loc] if isinstance(loc, str) else loc pkgs_filtered = [ pkg_key for pkg_key in pkgs_to_process if self._package_filter(pkg_key, strict_mode) ] if pkgs_filtered: pkgs.extend(pkgs_filtered) pkgs_changed.append(pkg_name) if not pkgs: log.info("All RPMs were already installed") return [] log.debug("Installing %s via RPM", pkgs) result = ProcessHelper(['rpm', '-U', '--quiet', '--nosignature', '--replacepkgs'] + pkgs).call() if result.returncode: log.error("RPM failed. Output: %s", result.stdout) raise ToolError("Could not successfully install rpm packages", result.returncode) else: log.debug("RPM output: %s", result.stdout) return pkgs_changed def _package_filter(self, pkg, strict_mode): if not pkg: log.warn("RPM specified with no location") return False if self._is_installed(pkg, strict_mode): log.debug("Skipping RPM at %s as it is already installed", pkg) return False return True @classmethod def get_package_version(cls, pkg, is_file=True): """ Given the name of an installed package or package location, return a tuple of (name, version-release) of either the installed package or the specified package location Parameters: - pkg: the package name/location - is_file : if True, pkg refers to a package location; if False, the name of an installed package """ query_mode = '-qp' if is_file else '-qa' log.debug("Querying for version of package %s", pkg) query_result = ProcessHelper(['rpm', query_mode, '--queryformat', '%{NAME}|%{VERSION}-%{RELEASE}', '--nosignature', pkg], stderr=subprocess.PIPE).call() log.debug("RPM stdout: %s", query_result.stdout) log.debug("RPM stderr: %s", query_result.stderr) if query_result.returncode: log.error("Could not determine package contained by rpm at %s", pkg) return (None, None) # The output from the command is just name|version-release name, sep, version = query_result.stdout.strip().partition('|') return (name, version) @classmethod def order_versions(cls, pkg_vers): return sorted(pkg_vers, key=cmp_to_key(cls.compare_rpm_versions)) @classmethod def max_version(cls, versions): max_ver = None for ver in versions: if cls.compare_rpm_versions(max_ver, ver) < 0: max_ver = ver return max_ver @classmethod def compare_rpm_versions(cls, first_pkg, second_pkg): """ Given two package versions in form VERSION-RELEASE, (-RELEASE optional), compare them based on "newness" (where "greater than" equals "newer") """ # Partition the RPM version strings into (VERSION, RELEASE) first_fields = first_pkg.split('-', 1) if first_pkg else () second_fields = second_pkg.split('-', 1) if second_pkg else () # Compare VERSION and then RELEASE for i in range(2): # Build a list of wholly-alpha and wholly-numeric fields; treat non-alphanumeric sequences as separators first_chars = re.findall('[a-zA-Z]+|[0-9]+', first_fields[i]) if i < len(first_fields) else [] second_chars = re.findall('[a-zA-Z]+|[0-9]+', second_fields[i]) if i < len(second_fields) else [] # Compare position by position for j in range(min(len(first_chars), len(second_chars))): c1 = first_chars[j] c2 = second_chars[j] if c1.isdigit(): if c2.isdigit(): # If both fields are numeric, compare based on int values int_cmp = (int(c1) > int(c2)) - (int(c1) < int(c2)) if int_cmp: return int_cmp else: # If one is alpha and one is numeric, then numeric is "greater" return 1 elif c2.isdigit(): # If one is alpha and one is numeric, then numeric is "greater" return -1 else: # If they're both strings, just compare lexicographically str_cmp = (c1 > c2) - (c1 < c2) if str_cmp: return str_cmp # If all of the intersecting fields match, the longer string is newer first_chars_len = len(first_chars) second_chars_len = len(second_chars) len_cmp = (first_chars_len > second_chars_len) - (first_chars_len < second_chars_len) if len_cmp: return len_cmp # If both VERSION and RELEASE match for both RPMs, ignoring non-alphanumeric chars, they are equal return 0 def _is_installed(self, pkg, strict_mode): pkg_with_version = RpmTool.get_package_version(pkg) if not pkg_with_version or not pkg_with_version[0]: # In strict mode False is returned so that a retry can happen, otherwise just return True return False if strict_mode else True pkg_spec = '-'.join(pkg_with_version) if pkg_with_version[1] else pkg_with_version[0] # rpm -q will try to find the specific RPM in the local system # --quiet will reduce this command to just an exit code test_result = ProcessHelper(['rpm', '-q', '--quiet', pkg_spec]).call() # if rpm -q returns 0, that means the package exists return test_result.returncode == 0