%PDF- %PDF-
Direktori : /proc/self/root/lib/python2.7/site-packages/salt/modules/ |
Current File : //proc/self/root/lib/python2.7/site-packages/salt/modules/rpm_lowpkg.py |
# -*- coding: utf-8 -*- ''' Support for rpm ''' # Import python libs from __future__ import absolute_import, print_function, unicode_literals import logging import os import re import datetime from salt.utils.versions import LooseVersion # Import Salt libs import salt.utils.decorators.path import salt.utils.itertools import salt.utils.path import salt.utils.pkg.rpm import salt.utils.versions # pylint: disable=import-error,redefined-builtin from salt.ext.six.moves import zip from salt.ext import six try: import rpm HAS_RPM = True except ImportError: HAS_RPM = False try: import rpmUtils.miscutils HAS_RPMUTILS = True except ImportError: HAS_RPMUTILS = False # pylint: enable=import-error,redefined-builtin from salt.exceptions import CommandExecutionError, SaltInvocationError log = logging.getLogger(__name__) # Define the module's virtual name __virtualname__ = 'lowpkg' def __virtual__(): ''' Confine this module to rpm based systems ''' if not salt.utils.path.which('rpm'): return (False, 'The rpm execution module failed to load: rpm binary is not in the path.') try: os_grain = __grains__['os'].lower() os_family = __grains__['os_family'].lower() except Exception: return (False, 'The rpm execution module failed to load: failed to detect os or os_family grains.') enabled = ('amazon', 'xcp', 'xenserver', 'VirtuozzoLinux') if os_family in ['redhat', 'suse'] or os_grain in enabled: return __virtualname__ return (False, 'The rpm execution module failed to load: only available on redhat/suse type systems ' 'or amazon, xcp or xenserver.') def bin_pkg_info(path, saltenv='base'): ''' .. versionadded:: 2015.8.0 Parses RPM metadata and returns a dictionary of information about the package (name, version, etc.). path Path to the file. Can either be an absolute path to a file on the minion, or a salt fileserver URL (e.g. ``salt://path/to/file.rpm``). If a salt fileserver URL is passed, the file will be cached to the minion so that it can be examined. saltenv : base Salt fileserver envrionment from which to retrieve the package. Ignored if ``path`` is a local file path on the minion. CLI Example: .. code-block:: bash salt '*' lowpkg.bin_pkg_info /root/salt-2015.5.1-2.el7.noarch.rpm salt '*' lowpkg.bin_pkg_info salt://salt-2015.5.1-2.el7.noarch.rpm ''' # If the path is a valid protocol, pull it down using cp.cache_file if __salt__['config.valid_fileproto'](path): newpath = __salt__['cp.cache_file'](path, saltenv) if not newpath: raise CommandExecutionError( 'Unable to retrieve {0} from saltenv \'{1}\'' .format(path, saltenv) ) path = newpath else: if not os.path.exists(path): raise CommandExecutionError( '{0} does not exist on minion'.format(path) ) elif not os.path.isabs(path): raise SaltInvocationError( '{0} does not exist on minion'.format(path) ) # REPOID is not a valid tag for the rpm command. Remove it and replace it # with 'none' queryformat = salt.utils.pkg.rpm.QUERYFORMAT.replace('%{REPOID}', 'none') output = __salt__['cmd.run_stdout']( ['rpm', '-qp', '--queryformat', queryformat, path], output_loglevel='trace', ignore_retcode=True, python_shell=False ) ret = {} pkginfo = salt.utils.pkg.rpm.parse_pkginfo( output, osarch=__grains__['osarch'] ) try: for field in pkginfo._fields: ret[field] = getattr(pkginfo, field) except AttributeError: # pkginfo is None return None return ret def list_pkgs(*packages): ''' List the packages currently installed in a dict:: {'<package_name>': '<version>'} CLI Example: .. code-block:: bash salt '*' lowpkg.list_pkgs ''' pkgs = {} cmd = ['rpm', '-q' if packages else '-qa', '--queryformat', r'%{NAME} %{VERSION}\n'] if packages: cmd.extend(packages) out = __salt__['cmd.run'](cmd, output_loglevel='trace', python_shell=False) for line in salt.utils.itertools.split(out, '\n'): if 'is not installed' in line: continue comps = line.split() pkgs[comps[0]] = comps[1] return pkgs def verify(*packages, **kwargs): ''' Runs an rpm -Va on a system, and returns the results in a dict Files with an attribute of config, doc, ghost, license or readme in the package header can be ignored using the ``ignore_types`` keyword argument CLI Example: .. code-block:: bash salt '*' lowpkg.verify salt '*' lowpkg.verify httpd salt '*' lowpkg.verify httpd postfix salt '*' lowpkg.verify httpd postfix ignore_types=['config','doc'] ''' ftypes = {'c': 'config', 'd': 'doc', 'g': 'ghost', 'l': 'license', 'r': 'readme'} ret = {} ignore_types = kwargs.get('ignore_types', []) if not isinstance(ignore_types, (list, six.string_types)): raise SaltInvocationError( 'ignore_types must be a list or a comma-separated string' ) if isinstance(ignore_types, six.string_types): try: ignore_types = [x.strip() for x in ignore_types.split(',')] except AttributeError: ignore_types = [x.strip() for x in six.text_type(ignore_types).split(',')] verify_options = kwargs.get('verify_options', []) if not isinstance(verify_options, (list, six.string_types)): raise SaltInvocationError( 'verify_options must be a list or a comma-separated string' ) if isinstance(verify_options, six.string_types): try: verify_options = [x.strip() for x in verify_options.split(',')] except AttributeError: verify_options = [x.strip() for x in six.text_type(verify_options).split(',')] cmd = ['rpm'] cmd.extend(['--' + x for x in verify_options]) if packages: cmd.append('-V') # Can't concatenate a tuple, must do a list.extend() cmd.extend(packages) else: cmd.append('-Va') out = __salt__['cmd.run_all'](cmd, output_loglevel='trace', ignore_retcode=True, python_shell=False) if not out['stdout'].strip() and out['retcode'] != 0: # If there is no stdout and the retcode is 0, then verification # succeeded, but if the retcode is nonzero, then the command failed. msg = 'Failed to verify package(s)' if out['stderr']: msg += ': {0}'.format(out['stderr']) raise CommandExecutionError(msg) for line in salt.utils.itertools.split(out['stdout'], '\n'): fdict = {'mismatch': []} if 'missing' in line: line = ' ' + line fdict['missing'] = True del fdict['mismatch'] fname = line[13:] if line[11:12] in ftypes: fdict['type'] = ftypes[line[11:12]] if 'type' not in fdict or fdict['type'] not in ignore_types: if line[0:1] == 'S': fdict['mismatch'].append('size') if line[1:2] == 'M': fdict['mismatch'].append('mode') if line[2:3] == '5': fdict['mismatch'].append('md5sum') if line[3:4] == 'D': fdict['mismatch'].append('device major/minor number') if line[4:5] == 'L': fdict['mismatch'].append('readlink path') if line[5:6] == 'U': fdict['mismatch'].append('user') if line[6:7] == 'G': fdict['mismatch'].append('group') if line[7:8] == 'T': fdict['mismatch'].append('mtime') if line[8:9] == 'P': fdict['mismatch'].append('capabilities') ret[fname] = fdict return ret def modified(*packages, **flags): ''' List the modified files that belong to a package. Not specifying any packages will return a list of _all_ modified files on the system's RPM database. .. versionadded:: 2015.5.0 CLI examples: .. code-block:: bash salt '*' lowpkg.modified httpd salt '*' lowpkg.modified httpd postfix salt '*' lowpkg.modified ''' ret = __salt__['cmd.run_all']( ['rpm', '-Va'] + list(packages), output_loglevel='trace', python_shell=False) data = {} # If verification has an output, then it means it failed # and the return code will be 1. We are interested in any bigger # than 1 code. if ret['retcode'] > 1: del ret['stdout'] return ret elif not ret['retcode']: return data ptrn = re.compile(r"\s+") changes = cfg = f_name = None for f_info in salt.utils.itertools.split(ret['stdout'], '\n'): f_info = ptrn.split(f_info) if len(f_info) == 3: # Config file changes, cfg, f_name = f_info else: changes, f_name = f_info cfg = None keys = ['size', 'mode', 'checksum', 'device', 'symlink', 'owner', 'group', 'time', 'capabilities'] changes = list(changes) if len(changes) == 8: # Older RPMs do not support capabilities changes.append('.') stats = [] for k, v in zip(keys, changes): if v != '.': stats.append(k) if cfg is not None: stats.append('config') data[f_name] = stats if not flags: return data # Filtering filtered_data = {} for f_name, stats in data.items(): include = True for param, pval in flags.items(): if param.startswith("_"): continue if (not pval and param in stats) or \ (pval and param not in stats): include = False break if include: filtered_data[f_name] = stats return filtered_data def file_list(*packages): ''' List the files that belong to a package. Not specifying any packages will return a list of _every_ file on the system's rpm database (not generally recommended). CLI Examples: .. code-block:: bash salt '*' lowpkg.file_list httpd salt '*' lowpkg.file_list httpd postfix salt '*' lowpkg.file_list ''' if not packages: cmd = ['rpm', '-qla'] else: cmd = ['rpm', '-ql'] # Can't concatenate a tuple, must do a list.extend() cmd.extend(packages) ret = __salt__['cmd.run']( cmd, output_loglevel='trace', python_shell=False).splitlines() return {'errors': [], 'files': ret} def file_dict(*packages): ''' List the files that belong to a package, sorted by group. Not specifying any packages will return a list of _every_ file on the system's rpm database (not generally recommended). CLI Examples: .. code-block:: bash salt '*' lowpkg.file_dict httpd salt '*' lowpkg.file_dict httpd postfix salt '*' lowpkg.file_dict ''' errors = [] ret = {} pkgs = {} cmd = ['rpm', '-q' if packages else '-qa', '--queryformat', r'%{NAME} %{VERSION}\n'] if packages: cmd.extend(packages) out = __salt__['cmd.run'](cmd, output_loglevel='trace', python_shell=False) for line in salt.utils.itertools.split(out, '\n'): if 'is not installed' in line: errors.append(line) continue comps = line.split() pkgs[comps[0]] = {'version': comps[1]} for pkg in pkgs: files = [] cmd = ['rpm', '-ql', pkg] out = __salt__['cmd.run']( ['rpm', '-ql', pkg], output_loglevel='trace', python_shell=False) ret[pkg] = out.splitlines() return {'errors': errors, 'packages': ret} def owner(*paths): ''' Return the name of the package that owns the file. Multiple file paths can be passed. If a single path is passed, a string will be returned, and if multiple paths are passed, a dictionary of file/package name pairs will be returned. If the file is not owned by a package, or is not present on the minion, then an empty string will be returned for that path. CLI Examples: .. code-block:: bash salt '*' lowpkg.owner /usr/bin/apachectl salt '*' lowpkg.owner /usr/bin/apachectl /etc/httpd/conf/httpd.conf ''' if not paths: return '' ret = {} for path in paths: cmd = ['rpm', '-qf', '--queryformat', '%{name}', path] ret[path] = __salt__['cmd.run_stdout'](cmd, output_loglevel='trace', python_shell=False) if 'not owned' in ret[path].lower(): ret[path] = '' if len(ret) == 1: return list(ret.values())[0] return ret @salt.utils.decorators.path.which('rpm2cpio') @salt.utils.decorators.path.which('cpio') @salt.utils.decorators.path.which('diff') def diff(package_path, path): ''' Return a formatted diff between current file and original in a package. NOTE: this function includes all files (configuration and not), but does not work on binary content. :param package: Full pack of the RPM file :param path: Full path to the installed file :return: Difference or empty string. For binary files only a notification. CLI example: .. code-block:: bash salt '*' lowpkg.diff /path/to/apache2.rpm /etc/apache2/httpd.conf ''' cmd = "rpm2cpio {0} " \ "| cpio -i --quiet --to-stdout .{1} " \ "| diff -u --label 'A {1}' --from-file=- --label 'B {1}' {1}" res = __salt__['cmd.shell'](cmd.format(package_path, path), output_loglevel='trace') if res and res.startswith('Binary file'): return 'File \'{0}\' is binary and its content has been ' \ 'modified.'.format(path) return res def info(*packages, **kwargs): ''' Return a detailed package(s) summary information. If no packages specified, all packages will be returned. :param packages: :param attr: Comma-separated package attributes. If no 'attr' is specified, all available attributes returned. Valid attributes are: version, vendor, release, build_date, build_date_time_t, install_date, install_date_time_t, build_host, group, source_rpm, arch, epoch, size, license, signature, packager, url, summary, description. :param all_versions: Return information for all installed versions of the packages :return: CLI example: .. code-block:: bash salt '*' lowpkg.info apache2 bash salt '*' lowpkg.info apache2 bash attr=version salt '*' lowpkg.info apache2 bash attr=version,build_date_iso,size salt '*' lowpkg.info apache2 bash attr=version,build_date_iso,size all_versions=True ''' all_versions = kwargs.get('all_versions', False) # LONGSIZE is not a valid tag for all versions of rpm. If LONGSIZE isn't # available, then we can just use SIZE for older versions. See Issue #31366. rpm_tags = __salt__['cmd.run_stdout']( ['rpm', '--querytags'], python_shell=False).splitlines() if 'LONGSIZE' in rpm_tags: size_tag = '%{LONGSIZE}' else: size_tag = '%{SIZE}' cmd = packages and "rpm -q {0}".format(' '.join(packages)) or "rpm -qa" # Construct query format attr_map = { "name": "name: %{NAME}\\n", "relocations": "relocations: %|PREFIXES?{[%{PREFIXES} ]}:{(not relocatable)}|\\n", "version": "version: %{VERSION}\\n", "vendor": "vendor: %{VENDOR}\\n", "release": "release: %{RELEASE}\\n", "epoch": "%|EPOCH?{epoch: %{EPOCH}\\n}|", "build_date_time_t": "build_date_time_t: %{BUILDTIME}\\n", "build_date": "build_date: %{BUILDTIME}\\n", "install_date_time_t": "install_date_time_t: %|INSTALLTIME?{%{INSTALLTIME}}:{(not installed)}|\\n", "install_date": "install_date: %|INSTALLTIME?{%{INSTALLTIME}}:{(not installed)}|\\n", "build_host": "build_host: %{BUILDHOST}\\n", "group": "group: %{GROUP}\\n", "source_rpm": "source_rpm: %{SOURCERPM}\\n", "size": "size: " + size_tag + "\\n", "arch": "arch: %{ARCH}\\n", "license": "%|LICENSE?{license: %{LICENSE}\\n}|", "signature": "signature: %|DSAHEADER?{%{DSAHEADER:pgpsig}}:{%|RSAHEADER?{%{RSAHEADER:pgpsig}}:" "{%|SIGGPG?{%{SIGGPG:pgpsig}}:{%|SIGPGP?{%{SIGPGP:pgpsig}}:{(none)}|}|}|}|\\n", "packager": "%|PACKAGER?{packager: %{PACKAGER}\\n}|", "url": "%|URL?{url: %{URL}\\n}|", "summary": "summary: %{SUMMARY}\\n", "description": "description:\\n%{DESCRIPTION}\\n", "edition": "edition: %|EPOCH?{%{EPOCH}:}|%{VERSION}-%{RELEASE}\\n", } attr = kwargs.get('attr', None) and kwargs['attr'].split(",") or None query = list() if attr: for attr_k in attr: if attr_k in attr_map and attr_k != 'description': query.append(attr_map[attr_k]) if not query: raise CommandExecutionError('No valid attributes found.') if 'name' not in attr: attr.append('name') query.append(attr_map['name']) if 'edition' not in attr: attr.append('edition') query.append(attr_map['edition']) else: for attr_k, attr_v in six.iteritems(attr_map): if attr_k != 'description': query.append(attr_v) if attr and 'description' in attr or not attr: query.append(attr_map['description']) query.append("-----\\n") call = __salt__['cmd.run_all'](cmd + (" --queryformat '{0}'".format(''.join(query))), output_loglevel='trace', env={'TZ': 'UTC'}, clean_env=True) if call['retcode'] != 0: comment = '' if 'stderr' in call: comment += (call['stderr'] or call['stdout']) raise CommandExecutionError(comment) elif 'error' in call['stderr']: raise CommandExecutionError(call['stderr']) else: out = call['stdout'] _ret = list() for pkg_info in re.split(r"----*", out): pkg_info = pkg_info.strip() if not pkg_info: continue pkg_info = pkg_info.split(os.linesep) if pkg_info[-1].lower().startswith('distribution'): pkg_info = pkg_info[:-1] pkg_data = dict() pkg_name = None descr_marker = False descr = list() for line in pkg_info: if descr_marker: descr.append(line) continue line = [item.strip() for item in line.split(':', 1)] if len(line) != 2: continue key, value = line if key == 'description': descr_marker = True continue if key == 'name': pkg_name = value # Convert Unix ticks into ISO time format if key in ['build_date', 'install_date']: try: pkg_data[key] = datetime.datetime.utcfromtimestamp(int(value)).isoformat() + "Z" except ValueError: log.warning('Could not convert "%s" into Unix time', value) continue # Convert Unix ticks into an Integer if key in ['build_date_time_t', 'install_date_time_t']: try: pkg_data[key] = int(value) except ValueError: log.warning('Could not convert "%s" into Unix time', value) continue if key not in ['description', 'name'] and value: pkg_data[key] = value if attr and 'description' in attr or not attr: pkg_data['description'] = os.linesep.join(descr) if pkg_name: pkg_data['name'] = pkg_name _ret.append(pkg_data) # Force-sort package data by version, # pick only latest versions # (in case multiple packages installed, e.g. kernel) ret = dict() for pkg_data in reversed(sorted(_ret, key=lambda x: LooseVersion(x['edition']))): pkg_name = pkg_data.pop('name') # Filter out GPG public keys packages if pkg_name.startswith('gpg-pubkey'): continue if pkg_name not in ret: if all_versions: ret[pkg_name] = [pkg_data.copy()] else: ret[pkg_name] = pkg_data.copy() del ret[pkg_name]['edition'] elif all_versions: ret[pkg_name].append(pkg_data.copy()) return ret def version_cmp(ver1, ver2, ignore_epoch=False): ''' .. versionadded:: 2015.8.9 Do a cmp-style comparison on two packages. Return -1 if ver1 < ver2, 0 if ver1 == ver2, and 1 if ver1 > ver2. Return None if there was a problem making the comparison. ignore_epoch : False Set to ``True`` to ignore the epoch when comparing versions .. versionadded:: 2015.8.10,2016.3.2 CLI Example: .. code-block:: bash salt '*' pkg.version_cmp '0.2-001' '0.2.0.1-002' ''' normalize = lambda x: six.text_type(x).split(':', 1)[-1] \ if ignore_epoch \ else six.text_type(x) ver1 = normalize(ver1) ver2 = normalize(ver2) try: cmp_func = None if HAS_RPM: try: cmp_func = rpm.labelCompare except AttributeError: # Catches corner case where someone has a module named "rpm" in # their pythonpath. log.debug( 'rpm module imported, but it does not have the ' 'labelCompare function. Not using rpm.labelCompare for ' 'version comparison.' ) if cmp_func is None and HAS_RPMUTILS: try: cmp_func = rpmUtils.miscutils.compareEVR except AttributeError: log.debug('rpmUtils.miscutils.compareEVR is not available') if cmp_func is None: if salt.utils.path.which('rpmdev-vercmp'): # rpmdev-vercmp always uses epochs, even when zero def _ensure_epoch(ver): def _prepend(ver): return '0:{0}'.format(ver) try: if ':' not in ver: return _prepend(ver) except TypeError: return _prepend(ver) return ver ver1 = _ensure_epoch(ver1) ver2 = _ensure_epoch(ver2) result = __salt__['cmd.run_all']( ['rpmdev-vercmp', ver1, ver2], python_shell=False, redirect_stderr=True, ignore_retcode=True) # rpmdev-vercmp returns 0 on equal, 11 on greater-than, and # 12 on less-than. if result['retcode'] == 0: return 0 elif result['retcode'] == 11: return 1 elif result['retcode'] == 12: return -1 else: # We'll need to fall back to salt.utils.versions.version_cmp() log.warning( 'Failed to interpret results of rpmdev-vercmp output. ' 'This is probably a bug, and should be reported. ' 'Return code was %s. Output: %s', result['retcode'], result['stdout'] ) else: # We'll need to fall back to salt.utils.versions.version_cmp() log.warning( 'rpmdevtools is not installed, please install it for ' 'more accurate version comparisons' ) else: # If one EVR is missing a release but not the other and they # otherwise would be equal, ignore the release. This can happen if # e.g. you are checking if a package version 3.2 is satisfied by # 3.2-1. (ver1_e, ver1_v, ver1_r) = salt.utils.pkg.rpm.version_to_evr(ver1) (ver2_e, ver2_v, ver2_r) = salt.utils.pkg.rpm.version_to_evr(ver2) if not ver1_r or not ver2_r: ver1_r = ver2_r = '' cmp_result = cmp_func((ver1_e, ver1_v, ver1_r), (ver2_e, ver2_v, ver2_r)) if cmp_result not in (-1, 0, 1): raise CommandExecutionError( 'Comparison result \'{0}\' is invalid'.format(cmp_result) ) return cmp_result except Exception as exc: log.warning( 'Failed to compare version \'%s\' to \'%s\' using RPM: %s', ver1, ver2, exc ) # We would already have normalized the versions at the beginning of this # function if ignore_epoch=True, so avoid unnecessary work and just pass # False for this value. return salt.utils.versions.version_cmp(ver1, ver2, ignore_epoch=False) def checksum(*paths): ''' Return if the signature of a RPM file is valid. CLI Example: .. code-block:: bash salt '*' lowpkg.checksum /path/to/package1.rpm salt '*' lowpkg.checksum /path/to/package1.rpm /path/to/package2.rpm ''' ret = dict() if not paths: raise CommandExecutionError("No package files has been specified.") for package_file in paths: ret[package_file] = (bool(__salt__['file.file_exists'](package_file)) and not __salt__['cmd.retcode'](["rpm", "-K", "--quiet", package_file], ignore_retcode=True, output_loglevel='trace', python_shell=False)) return ret