# -*- coding: utf-8 -*- ''' Installation of packages using OS package managers such as yum or apt-get ========================================================================= .. note:: On minions running systemd>=205, as of version 2015.8.12, 2016.3.3, and 2016.11.0, `systemd-run(1)`_ is now used to isolate commands which modify installed packages from the ``salt-minion`` daemon's control group. This is done to keep systemd from killing the package manager commands spawned by Salt, when Salt updates itself (see ``KillMode`` in the `systemd.kill(5)`_ manpage for more information). If desired, usage of `systemd-run(1)`_ can be suppressed by setting a :mod:`config option ` called ``systemd.use_scope``, with a value of ``False`` (no quotes). .. _`systemd-run(1)`: https://www.freedesktop.org/software/systemd/man/systemd-run.html .. _`systemd.kill(5)`: https://www.freedesktop.org/software/systemd/man/systemd.kill.html Salt can manage software packages via the pkg state module, packages can be set up to be installed, latest, removed and purged. Package management declarations are typically rather simple: .. code-block:: yaml vim: pkg.installed A more involved example involves pulling from a custom repository. .. code-block:: yaml base: pkgrepo.managed: - humanname: Logstash PPA - name: ppa:wolfnet/logstash - dist: precise - file: /etc/apt/sources.list.d/logstash.list - keyid: 28B04E4A - keyserver: keyserver.ubuntu.com logstash: pkg.installed: - fromrepo: ppa:wolfnet/logstash Multiple packages can also be installed with the use of the pkgs state module .. code-block:: yaml dotdeb.repo: pkgrepo.managed: - humanname: Dotdeb - name: deb http://packages.dotdeb.org wheezy-php55 all - dist: wheezy-php55 - file: /etc/apt/sources.list.d/dotbeb.list - keyid: 89DF5277 - keyserver: keys.gnupg.net - refresh_db: true php.packages: pkg.installed: - fromrepo: wheezy-php55 - pkgs: - php5-fpm - php5-cli - php5-curl .. warning:: Package names are currently case-sensitive. If the minion is using a package manager which is not case-sensitive (such as :mod:`pkgng `), then this state will fail if the proper case is not used. This will be addressed in a future release of Salt. ''' # Import python libs from __future__ import absolute_import, print_function, unicode_literals import fnmatch import logging import os import re # Import Salt libs import salt.utils.pkg import salt.utils.platform import salt.utils.versions from salt.output import nested from salt.utils.functools import namespaced_function as _namespaced_function from salt.utils.odict import OrderedDict as _OrderedDict from salt.exceptions import ( CommandExecutionError, MinionError, SaltInvocationError ) from salt.modules.pkg_resource import _repack_pkgs # Import 3rd-party libs from salt.ext import six # pylint: disable=invalid-name _repack_pkgs = _namespaced_function(_repack_pkgs, globals()) if salt.utils.platform.is_windows(): # pylint: disable=import-error,no-name-in-module,unused-import from salt.ext.six.moves.urllib.parse import urlparse as _urlparse from salt.exceptions import SaltRenderError import collections import datetime import errno import time from functools import cmp_to_key # pylint: disable=import-error # pylint: enable=unused-import from salt.modules.win_pkg import _get_package_info from salt.modules.win_pkg import get_repo_data from salt.modules.win_pkg import _get_repo_details from salt.modules.win_pkg import _refresh_db_conditional from salt.modules.win_pkg import refresh_db from salt.modules.win_pkg import genrepo from salt.modules.win_pkg import _repo_process_pkg_sls from salt.modules.win_pkg import _get_latest_pkg_version from salt.modules.win_pkg import _reverse_cmp_pkg_versions _get_package_info = _namespaced_function(_get_package_info, globals()) get_repo_data = _namespaced_function(get_repo_data, globals()) _get_repo_details = \ _namespaced_function(_get_repo_details, globals()) _refresh_db_conditional = \ _namespaced_function(_refresh_db_conditional, globals()) refresh_db = _namespaced_function(refresh_db, globals()) genrepo = _namespaced_function(genrepo, globals()) _repo_process_pkg_sls = \ _namespaced_function(_repo_process_pkg_sls, globals()) _get_latest_pkg_version = \ _namespaced_function(_get_latest_pkg_version, globals()) _reverse_cmp_pkg_versions = \ _namespaced_function(_reverse_cmp_pkg_versions, globals()) # The following imports are used by the namespaced win_pkg funcs # and need to be included in their globals. # pylint: disable=import-error,unused-import from salt.utils.versions import LooseVersion # pylint: enable=import-error,unused-import # pylint: enable=invalid-name log = logging.getLogger(__name__) def __virtual__(): ''' Only make these states available if a pkg provider has been detected or assigned for this minion ''' return 'pkg.install' in __salt__ def _get_comparison_spec(pkgver): ''' Return a tuple containing the comparison operator and the version. If no comparison operator was passed, the comparison is assumed to be an "equals" comparison, and "==" will be the operator returned. ''' oper, verstr = salt.utils.pkg.split_comparison(pkgver.strip()) if oper in ('=', ''): oper = '==' return oper, verstr def _parse_version_string(version_conditions_string): ''' Returns a list of two-tuples containing (operator, version). ''' result = [] version_conditions_string = version_conditions_string.strip() if not version_conditions_string: return result for version_condition in version_conditions_string.split(','): operator_and_version = _get_comparison_spec(version_condition) result.append(operator_and_version) return result def _fulfills_version_string(installed_versions, version_conditions_string, ignore_epoch=False, allow_updates=False): ''' Returns True if any of the installed versions match the specified version conditions, otherwise returns False. installed_versions The installed versions version_conditions_string The string containing all version conditions. E.G. 1.2.3-4 >=1.2.3-4 >=1.2.3-4, <2.3.4-5 >=1.2.3-4, <2.3.4-5, !=1.2.4-1 ignore_epoch : False When a package version contains an non-zero epoch (e.g. ``1:3.14.159-2.el7``, and a specific version of a package is desired, set this option to ``True`` to ignore the epoch when comparing versions. allow_updates : False Allow the package to be updated outside Salt's control (e.g. auto updates on Windows). This means a package on the Minion can have a newer version than the latest available in the repository without enforcing a re-installation of the package. (Only applicable if only one strict version condition is specified E.G. version: 2.0.6~ubuntu3) ''' version_conditions = _parse_version_string(version_conditions_string) for installed_version in installed_versions: fullfills_all = True for operator, version_string in version_conditions: if allow_updates and len(version_conditions) == 1 and operator == '==': operator = '>=' fullfills_all = fullfills_all and _fulfills_version_spec([installed_version], operator, version_string, ignore_epoch=ignore_epoch) if fullfills_all: return True return False def _fulfills_version_spec(versions, oper, desired_version, ignore_epoch=False): ''' Returns True if any of the installed versions match the specified version, otherwise returns False ''' cmp_func = __salt__.get('pkg.version_cmp') # stripping "with_origin" dict wrapper if salt.utils.platform.is_freebsd(): if isinstance(versions, dict) and 'version' in versions: versions = versions['version'] for ver in versions: if (oper == '==' and fnmatch.fnmatch(ver, desired_version)) \ or salt.utils.versions.compare(ver1=ver, oper=oper, ver2=desired_version, cmp_func=cmp_func, ignore_epoch=ignore_epoch): return True return False def _find_unpurge_targets(desired, **kwargs): ''' Find packages which are marked to be purged but can't yet be removed because they are dependencies for other installed packages. These are the packages which will need to be 'unpurged' because they are part of pkg.installed states. This really just applies to Debian-based Linuxes. ''' return [ x for x in desired if x in __salt__['pkg.list_pkgs'](purge_desired=True, **kwargs) ] def _find_download_targets(name=None, version=None, pkgs=None, normalize=True, skip_suggestions=False, ignore_epoch=False, **kwargs): ''' Inspect the arguments to pkg.downloaded and discover what packages need to be downloaded. Return a dict of packages to download. ''' cur_pkgs = __salt__['pkg.list_downloaded'](**kwargs) if pkgs: to_download = _repack_pkgs(pkgs, normalize=normalize) if not to_download: # Badly-formatted SLS return {'name': name, 'changes': {}, 'result': False, 'comment': 'Invalidly formatted pkgs parameter. See ' 'minion log.'} else: if normalize: _normalize_name = \ __salt__.get('pkg.normalize_name', lambda pkgname: pkgname) to_download = {_normalize_name(name): version} else: to_download = {name: version} cver = cur_pkgs.get(name, {}) if name in to_download: # Package already downloaded, no need to download again if cver and version in cver: return {'name': name, 'changes': {}, 'result': True, 'comment': 'Version {0} of package \'{1}\' is already ' 'downloaded'.format(version, name)} # if cver is not an empty string, the package is already downloaded elif cver and version is None: # The package is downloaded return {'name': name, 'changes': {}, 'result': True, 'comment': 'Package {0} is already ' 'downloaded'.format(name)} version_spec = False if not skip_suggestions: try: problems = _preflight_check(to_download, **kwargs) except CommandExecutionError: pass else: comments = [] if problems.get('no_suggest'): comments.append( 'The following package(s) were not found, and no ' 'possible matches were found in the package db: ' '{0}'.format( ', '.join(sorted(problems['no_suggest'])) ) ) if problems.get('suggest'): for pkgname, suggestions in \ six.iteritems(problems['suggest']): comments.append( 'Package \'{0}\' not found (possible matches: ' '{1})'.format(pkgname, ', '.join(suggestions)) ) if comments: if len(comments) > 1: comments.append('') return {'name': name, 'changes': {}, 'result': False, 'comment': '. '.join(comments).rstrip()} # Find out which packages will be targeted in the call to pkg.download # Check current downloaded versions against specified versions targets = {} problems = [] for pkgname, pkgver in six.iteritems(to_download): cver = cur_pkgs.get(pkgname, {}) # Package not yet downloaded, so add to targets if not cver: targets[pkgname] = pkgver continue # No version specified but package is already downloaded elif cver and not pkgver: continue version_spec = True try: if not _fulfills_version_string(cver.keys(), pkgver, ignore_epoch=ignore_epoch): targets[pkgname] = pkgver except CommandExecutionError as exc: problems.append(exc.strerror) continue if problems: return {'name': name, 'changes': {}, 'result': False, 'comment': ' '.join(problems)} if not targets: # All specified packages are already downloaded msg = ( 'All specified packages{0} are already downloaded' .format(' (matching specified versions)' if version_spec else '') ) return {'name': name, 'changes': {}, 'result': True, 'comment': msg} return targets def _find_advisory_targets(name=None, advisory_ids=None, **kwargs): ''' Inspect the arguments to pkg.patch_installed and discover what advisory patches need to be installed. Return a dict of advisory patches to install. ''' cur_patches = __salt__['pkg.list_installed_patches'](**kwargs) if advisory_ids: to_download = advisory_ids else: to_download = [name] if cur_patches.get(name, {}): # Advisory patch already installed, no need to install it again return {'name': name, 'changes': {}, 'result': True, 'comment': 'Advisory patch {0} is already ' 'installed'.format(name)} # Find out which advisory patches will be targeted in the call to pkg.install targets = [] for patch_name in to_download: cver = cur_patches.get(patch_name, {}) # Advisory patch not yet installed, so add to targets if not cver: targets.append(patch_name) continue if not targets: # All specified packages are already downloaded msg = ('All specified advisory patches are already installed') return {'name': name, 'changes': {}, 'result': True, 'comment': msg} return targets def _find_remove_targets(name=None, version=None, pkgs=None, normalize=True, ignore_epoch=False, **kwargs): ''' Inspect the arguments to pkg.removed and discover what packages need to be removed. Return a dict of packages to remove. ''' if __grains__['os'] == 'FreeBSD': kwargs['with_origin'] = True cur_pkgs = __salt__['pkg.list_pkgs'](versions_as_list=True, **kwargs) if pkgs: to_remove = _repack_pkgs(pkgs, normalize=normalize) if not to_remove: # Badly-formatted SLS return {'name': name, 'changes': {}, 'result': False, 'comment': 'Invalidly formatted pkgs parameter. See ' 'minion log.'} else: _normalize_name = \ __salt__.get('pkg.normalize_name', lambda pkgname: pkgname) to_remove = {_normalize_name(name): version} version_spec = False # Find out which packages will be targeted in the call to pkg.remove # Check current versions against specified versions targets = [] problems = [] for pkgname, pkgver in six.iteritems(to_remove): # FreeBSD pkg supports `openjdk` and `java/openjdk7` package names origin = bool(re.search('/', pkgname)) if __grains__['os'] == 'FreeBSD' and origin: cver = [k for k, v in six.iteritems(cur_pkgs) if v['origin'] == pkgname] else: cver = cur_pkgs.get(pkgname, []) # Package not installed, no need to remove if not cver: continue # No version specified and pkg is installed elif __salt__['pkg_resource.version_clean'](pkgver) is None: targets.append(pkgname) continue version_spec = True try: if _fulfills_version_string(cver, pkgver, ignore_epoch=ignore_epoch): targets.append(pkgname) else: log.debug( 'Current version (%s) did not match desired version ' 'specification (%s), will not remove', cver, pkgver ) except CommandExecutionError as exc: problems.append(exc.strerror) continue if problems: return {'name': name, 'changes': {}, 'result': False, 'comment': ' '.join(problems)} if not targets: # All specified packages are already absent msg = 'All specified packages{0} are already absent'.format( ' (matching specified versions)' if version_spec else '' ) return {'name': name, 'changes': {}, 'result': True, 'comment': msg} return targets def _find_install_targets(name=None, version=None, pkgs=None, sources=None, skip_suggestions=False, pkg_verify=False, normalize=True, ignore_epoch=False, reinstall=False, refresh=False, **kwargs): ''' Inspect the arguments to pkg.installed and discover what packages need to be installed. Return a dict of desired packages ''' was_refreshed = False if all((pkgs, sources)): return {'name': name, 'changes': {}, 'result': False, 'comment': 'Only one of "pkgs" and "sources" is permitted.'} # dict for packages that fail pkg.verify and their altered files altered_files = {} # Get the ignore_types list if any from the pkg_verify argument if isinstance(pkg_verify, list) \ and any(x.get('ignore_types') is not None for x in pkg_verify if isinstance(x, _OrderedDict) and 'ignore_types' in x): ignore_types = next(x.get('ignore_types') for x in pkg_verify if 'ignore_types' in x) else: ignore_types = [] # Get the verify_options list if any from the pkg_verify argument if isinstance(pkg_verify, list) \ and any(x.get('verify_options') is not None for x in pkg_verify if isinstance(x, _OrderedDict) and 'verify_options' in x): verify_options = next(x.get('verify_options') for x in pkg_verify if 'verify_options' in x) else: verify_options = [] if __grains__['os'] == 'FreeBSD': kwargs['with_origin'] = True if salt.utils.platform.is_windows(): # Windows requires a refresh to establish a pkg db if refresh=True, so # add it to the kwargs. kwargs['refresh'] = refresh resolve_capabilities = kwargs.get('resolve_capabilities', False) and 'pkg.list_provides' in __salt__ try: cur_pkgs = __salt__['pkg.list_pkgs'](versions_as_list=True, **kwargs) cur_prov = resolve_capabilities and __salt__['pkg.list_provides'](**kwargs) or dict() except CommandExecutionError as exc: return {'name': name, 'changes': {}, 'result': False, 'comment': exc.strerror} if salt.utils.platform.is_windows() and kwargs.pop('refresh', False): # We already refreshed when we called pkg.list_pkgs was_refreshed = True refresh = False if any((pkgs, sources)): if pkgs: desired = _repack_pkgs(pkgs, normalize=normalize) elif sources: desired = __salt__['pkg_resource.pack_sources']( sources, normalize=normalize, ) if not desired: # Badly-formatted SLS return {'name': name, 'changes': {}, 'result': False, 'comment': 'Invalidly formatted \'{0}\' parameter. See ' 'minion log.'.format('pkgs' if pkgs else 'sources')} to_unpurge = _find_unpurge_targets(desired, **kwargs) else: if salt.utils.platform.is_windows(): pkginfo = _get_package_info(name, saltenv=kwargs['saltenv']) if not pkginfo: return {'name': name, 'changes': {}, 'result': False, 'comment': 'Package {0} not found in the ' 'repository.'.format(name)} if version is None: version = _get_latest_pkg_version(pkginfo) if normalize: _normalize_name = \ __salt__.get('pkg.normalize_name', lambda pkgname: pkgname) desired = {_normalize_name(name): version} else: desired = {name: version} to_unpurge = _find_unpurge_targets(desired, **kwargs) # FreeBSD pkg supports `openjdk` and `java/openjdk7` package names origin = bool(re.search('/', name)) if __grains__['os'] == 'FreeBSD' and origin: cver = [k for k, v in six.iteritems(cur_pkgs) if v['origin'] == name] else: cver = cur_pkgs.get(name, []) if name not in to_unpurge: if version and version in cver \ and not reinstall \ and not pkg_verify: # The package is installed and is the correct version return {'name': name, 'changes': {}, 'result': True, 'comment': 'Version {0} of package \'{1}\' is already ' 'installed'.format(version, name)} # if cver is not an empty string, the package is already installed elif cver and version is None \ and not reinstall \ and not pkg_verify: # The package is installed return {'name': name, 'changes': {}, 'result': True, 'comment': 'Package {0} is already ' 'installed'.format(name)} version_spec = False if not sources: # Check for alternate package names if strict processing is not # enforced. Takes extra time. Disable for improved performance if not skip_suggestions: # Perform platform-specific pre-flight checks not_installed = dict([ (name, version) for name, version in desired.items() if not (name in cur_pkgs and (version is None or _fulfills_version_string(cur_pkgs[name], version))) ]) if not_installed: try: problems = _preflight_check(not_installed, **kwargs) except CommandExecutionError: pass else: comments = [] if problems.get('no_suggest'): comments.append( 'The following package(s) were not found, and no ' 'possible matches were found in the package db: ' '{0}'.format( ', '.join(sorted(problems['no_suggest'])) ) ) if problems.get('suggest'): for pkgname, suggestions in \ six.iteritems(problems['suggest']): comments.append( 'Package \'{0}\' not found (possible matches: ' '{1})'.format(pkgname, ', '.join(suggestions)) ) if comments: if len(comments) > 1: comments.append('') return {'name': name, 'changes': {}, 'result': False, 'comment': '. '.join(comments).rstrip()} # Resolve the latest package version for any packages with "latest" in the # package version wants_latest = [] \ if sources \ else [x for x, y in six.iteritems(desired) if y == 'latest'] if wants_latest: resolved_latest = __salt__['pkg.latest_version'](*wants_latest, refresh=refresh, **kwargs) if len(wants_latest) == 1: resolved_latest = {wants_latest[0]: resolved_latest} if refresh: was_refreshed = True refresh = False # pkg.latest_version returns an empty string when the package is # up-to-date. So check the currently-installed packages. If found, the # resolved latest version will be the currently installed one from # cur_pkgs. If not found, then the package doesn't exist and the # resolved latest version will be None. for key in resolved_latest: if not resolved_latest[key]: if key in cur_pkgs: resolved_latest[key] = cur_pkgs[key][-1] else: resolved_latest[key] = None # Update the desired versions with the ones we resolved desired.update(resolved_latest) # Find out which packages will be targeted in the call to pkg.install targets = {} to_reinstall = {} problems = [] warnings = [] failed_verify = False for package_name, version_string in six.iteritems(desired): cver = cur_pkgs.get(package_name, []) if resolve_capabilities and not cver and package_name in cur_prov: cver = cur_pkgs.get(cur_prov.get(package_name)[0], []) # Package not yet installed, so add to targets if not cver: targets[package_name] = version_string continue if sources: if reinstall: to_reinstall[package_name] = version_string continue elif 'lowpkg.bin_pkg_info' not in __salt__: continue # Metadata parser is available, cache the file and derive the # package's name and version err = 'Unable to cache {0}: {1}' try: cached_path = __salt__['cp.cache_file'](version_string, saltenv=kwargs['saltenv']) except CommandExecutionError as exc: problems.append(err.format(version_string, exc)) continue if not cached_path: problems.append(err.format(version_string, 'file not found')) continue elif not os.path.exists(cached_path): problems.append('{0} does not exist on minion'.format(version_string)) continue source_info = __salt__['lowpkg.bin_pkg_info'](cached_path) if source_info is None: warnings.append('Failed to parse metadata for {0}'.format(version_string)) continue else: verstr = source_info['version'] else: verstr = version_string if reinstall: to_reinstall[package_name] = version_string continue if not __salt__['pkg_resource.check_extra_requirements'](package_name, version_string): targets[package_name] = version_string continue # No version specified and pkg is installed elif __salt__['pkg_resource.version_clean'](version_string) is None: if (not reinstall) and pkg_verify: try: verify_result = __salt__['pkg.verify']( package_name, ignore_types=ignore_types, verify_options=verify_options, **kwargs ) except (CommandExecutionError, SaltInvocationError) as exc: failed_verify = exc.strerror continue if verify_result: to_reinstall[package_name] = version_string altered_files[package_name] = verify_result continue version_fulfilled = False allow_updates = bool(not sources and kwargs.get('allow_updates')) try: version_fulfilled = _fulfills_version_string(cver, verstr, ignore_epoch=ignore_epoch, allow_updates=allow_updates) except CommandExecutionError as exc: problems.append(exc.strerror) continue # Compare desired version against installed version. version_spec = True if not version_fulfilled: if reinstall: to_reinstall[package_name] = version_string else: version_conditions = _parse_version_string(version_string) if pkg_verify and any(oper == '==' for oper, version in version_conditions): try: verify_result = __salt__['pkg.verify']( package_name, ignore_types=ignore_types, verify_options=verify_options, **kwargs ) except (CommandExecutionError, SaltInvocationError) as exc: failed_verify = exc.strerror continue if verify_result: to_reinstall[package_name] = version_string altered_files[package_name] = verify_result else: log.debug( 'Current version (%s) did not match desired version ' 'specification (%s), adding to installation targets', cver, version_string ) targets[package_name] = version_string if failed_verify: problems.append(failed_verify) if problems: return {'name': name, 'changes': {}, 'result': False, 'comment': ' '.join(problems)} if not any((targets, to_unpurge, to_reinstall)): # All specified packages are installed msg = 'All specified packages are already installed{0}' msg = msg.format( ' and are at the desired version' if version_spec and not sources else '' ) ret = {'name': name, 'changes': {}, 'result': True, 'comment': msg} if warnings: ret.setdefault('warnings', []).extend(warnings) return ret return (desired, targets, to_unpurge, to_reinstall, altered_files, warnings, was_refreshed) def _verify_install(desired, new_pkgs, ignore_epoch=False, new_caps=None): ''' Determine whether or not the installed packages match what was requested in the SLS file. ''' ok = [] failed = [] if not new_caps: new_caps = dict() for pkgname, pkgver in desired.items(): # FreeBSD pkg supports `openjdk` and `java/openjdk7` package names. # Homebrew for Mac OSX does something similar with tap names # prefixing package names, separated with a slash. has_origin = '/' in pkgname if __grains__['os'] == 'FreeBSD' and has_origin: cver = [k for k, v in six.iteritems(new_pkgs) if v['origin'] == pkgname] elif __grains__['os'] == 'MacOS' and has_origin: cver = new_pkgs.get(pkgname, new_pkgs.get(pkgname.split('/')[-1])) elif __grains__['os'] == 'OpenBSD': cver = new_pkgs.get(pkgname.split('%')[0]) elif __grains__['os_family'] == 'Debian': cver = new_pkgs.get(pkgname.split('=')[0]) else: cver = new_pkgs.get(pkgname) if not cver and pkgname in new_caps: cver = new_pkgs.get(new_caps.get(pkgname)[0]) if not cver: failed.append(pkgname) continue elif pkgver == 'latest': ok.append(pkgname) continue elif not __salt__['pkg_resource.version_clean'](pkgver): ok.append(pkgname) continue elif pkgver.endswith("*") and cver[0].startswith(pkgver[:-1]): ok.append(pkgname) continue if _fulfills_version_string(cver, pkgver, ignore_epoch=ignore_epoch): ok.append(pkgname) else: failed.append(pkgname) return ok, failed def _get_desired_pkg(name, desired): ''' Helper function that retrieves and nicely formats the desired pkg (and version if specified) so that helpful information can be printed in the comment for the state. ''' if not desired[name] or desired[name].startswith(('<', '>', '=')): oper = '' else: oper = '=' return '{0}{1}{2}'.format(name, oper, '' if not desired[name] else desired[name]) def _preflight_check(desired, fromrepo, **kwargs): ''' Perform platform-specific checks on desired packages ''' if 'pkg.check_db' not in __salt__: return {} ret = {'suggest': {}, 'no_suggest': []} pkginfo = __salt__['pkg.check_db']( *list(desired.keys()), fromrepo=fromrepo, **kwargs ) for pkgname in pkginfo: if pkginfo[pkgname]['found'] is False: if pkginfo[pkgname]['suggestions']: ret['suggest'][pkgname] = pkginfo[pkgname]['suggestions'] else: ret['no_suggest'].append(pkgname) return ret def _nested_output(obj): ''' Serialize obj and format for output ''' nested.__opts__ = __opts__ ret = nested.output(obj).rstrip() return ret def _resolve_capabilities(pkgs, refresh=False, **kwargs): ''' Resolve capabilities in ``pkgs`` and exchange them with real package names, when the result is distinct. This feature can be turned on while setting the paramter ``resolve_capabilities`` to True. Return the input dictionary with replaced capability names and as second return value a bool which say if a refresh need to be run. In case of ``resolve_capabilities`` is False (disabled) or not supported by the implementation the input is returned unchanged. ''' if not pkgs or 'pkg.resolve_capabilities' not in __salt__: return pkgs, refresh ret = __salt__['pkg.resolve_capabilities'](pkgs, refresh=refresh, **kwargs) return ret, False def installed( name, version=None, refresh=None, fromrepo=None, skip_verify=False, skip_suggestions=False, pkgs=None, sources=None, allow_updates=False, pkg_verify=False, normalize=True, ignore_epoch=False, reinstall=False, update_holds=False, bypass_file=None, bypass_file_contains=None, **kwargs): ''' Ensure that the package is installed, and that it is the correct version (if specified). :param str name: The name of the package to be installed. This parameter is ignored if either "pkgs" or "sources" is used. Additionally, please note that this option can only be used to install packages from a software repository. To install a package file manually, use the "sources" option detailed below. :param str version: Install a specific version of a package. This option is ignored if "sources" is used. Currently, this option is supported for the following pkg providers: :mod:`apt `, :mod:`ebuild `, :mod:`pacman `, :mod:`pkgin `, :mod:`win_pkg `, :mod:`yumpkg `, and :mod:`zypper `. The version number includes the release designation where applicable, to allow Salt to target a specific release of a given version. When in doubt, using the ``pkg.latest_version`` function for an uninstalled package will tell you the version available. .. code-block:: bash # salt myminion pkg.latest_version vim-enhanced myminion: 2:7.4.160-1.el7 .. important:: As of version 2015.8.7, for distros which use yum/dnf, packages which have a version with a nonzero epoch (that is, versions which start with a number followed by a colon like in the ``pkg.latest_version`` output above) must have the epoch included when specifying the version number. For example: .. code-block:: yaml vim-enhanced: pkg.installed: - version: 2:7.4.160-1.el7 In version 2015.8.9, an **ignore_epoch** argument has been added to :py:mod:`pkg.installed `, :py:mod:`pkg.removed `, and :py:mod:`pkg.purged ` states, which causes the epoch to be disregarded when the state checks to see if the desired version was installed. Also, while this function is not yet implemented for all pkg frontends, :mod:`pkg.list_repo_pkgs ` will show all versions available in the various repositories for a given package, irrespective of whether or not it is installed. .. code-block:: bash # salt myminion pkg.list_repo_pkgs bash myminion: ---------- bash: - 4.2.46-21.el7_3 - 4.2.46-20.el7_2 This function was first added for :mod:`pkg.list_repo_pkgs ` in 2014.1.0, and was expanded to :py:func:`Debian/Ubuntu ` and :py:func:`Arch Linux `-based distros in the 2017.7.0 release. The version strings returned by either of these functions can be used as version specifiers in pkg states. You can install a specific version when using the ``pkgs`` argument by including the version after the package: .. code-block:: yaml common_packages: pkg.installed: - pkgs: - unzip - dos2unix - salt-minion: 2015.8.5-1.el6 If the version given is the string ``latest``, the latest available package version will be installed à la ``pkg.latest``. **WILDCARD VERSIONS** As of the 2017.7.0 release, this state now supports wildcards in package versions for SUSE SLES/Leap/Tumbleweed, Debian/Ubuntu, RHEL/CentOS, Arch Linux, and their derivatives. Using wildcards can be useful for packages where the release name is built into the version in some way, such as for RHEL/CentOS which typically has version numbers like ``1.2.34-5.el7``. An example of the usage for this would be: .. code-block:: yaml mypkg: pkg.installed: - version: '1.2.34*' Keep in mind that using wildcard versions will result in a slower state run since Salt must gather the available versions of the specified packages and figure out which of them match the specified wildcard expression. :param bool refresh: This parameter controls whether or not the package repo database is updated prior to installing the requested package(s). If ``True``, the package database will be refreshed (``apt-get update`` or equivalent, depending on platform) before installing. If ``False``, the package database will *not* be refreshed before installing. If unset, then Salt treats package database refreshes differently depending on whether or not a ``pkg`` state has been executed already during the current Salt run. Once a refresh has been performed in a ``pkg`` state, for the remainder of that Salt run no other refreshes will be performed for ``pkg`` states which do not explicitly set ``refresh`` to ``True``. This prevents needless additional refreshes from slowing down the Salt run. :param str cache_valid_time: .. versionadded:: 2016.11.0 This parameter sets the value in seconds after which the cache is marked as invalid, and a cache update is necessary. This overwrites the ``refresh`` parameter's default behavior. Example: .. code-block:: yaml httpd: pkg.installed: - fromrepo: mycustomrepo - skip_verify: True - skip_suggestions: True - version: 2.0.6~ubuntu3 - refresh: True - cache_valid_time: 300 - allow_updates: True - hold: False In this case, a refresh will not take place for 5 minutes since the last ``apt-get update`` was executed on the system. .. note:: This parameter is available only on Debian based distributions and has no effect on the rest. :param str fromrepo: Specify a repository from which to install .. note:: Distros which use APT (Debian, Ubuntu, etc.) do not have a concept of repositories, in the same way as YUM-based distros do. When a source is added, it is assigned to a given release. Consider the following source configuration: .. code-block:: text deb http://ppa.launchpad.net/saltstack/salt/ubuntu precise main The packages provided by this source would be made available via the ``precise`` release, therefore ``fromrepo`` would need to be set to ``precise`` for Salt to install the package from this source. Having multiple sources in the same release may result in the default install candidate being newer than what is desired. If this is the case, the desired version must be specified using the ``version`` parameter. If the ``pkgs`` parameter is being used to install multiple packages in the same state, then instead of using ``version``, use the method of version specification described in the **Multiple Package Installation Options** section below. Running the shell command ``apt-cache policy pkgname`` on a minion can help elucidate the APT configuration and aid in properly configuring states: .. code-block:: bash root@saltmaster:~# salt ubuntu01 cmd.run 'apt-cache policy ffmpeg' ubuntu01: ffmpeg: Installed: (none) Candidate: 7:0.10.11-1~precise1 Version table: 7:0.10.11-1~precise1 0 500 http://ppa.launchpad.net/jon-severinsson/ffmpeg/ubuntu/ precise/main amd64 Packages 4:0.8.10-0ubuntu0.12.04.1 0 500 http://us.archive.ubuntu.com/ubuntu/ precise-updates/main amd64 Packages 500 http://security.ubuntu.com/ubuntu/ precise-security/main amd64 Packages 4:0.8.1-0ubuntu1 0 500 http://us.archive.ubuntu.com/ubuntu/ precise/main amd64 Packages The release is located directly after the source's URL. The actual release name is the part before the slash, so to install version **4:0.8.10-0ubuntu0.12.04.1** either ``precise-updates`` or ``precise-security`` could be used for the ``fromrepo`` value. :param bool skip_verify: Skip the GPG verification check for the package to be installed :param bool skip_suggestions: Force strict package naming. Disables lookup of package alternatives. .. versionadded:: 2014.1.1 :param bool resolve_capabilities: Turn on resolving capabilities. This allow one to name "provides" or alias names for packages. .. versionadded:: 2018.3.0 :param bool allow_updates: Allow the package to be updated outside Salt's control (e.g. auto updates on Windows). This means a package on the Minion can have a newer version than the latest available in the repository without enforcing a re-installation of the package. .. versionadded:: 2014.7.0 Example: .. code-block:: yaml httpd: pkg.installed: - fromrepo: mycustomrepo - skip_verify: True - skip_suggestions: True - version: 2.0.6~ubuntu3 - refresh: True - allow_updates: True - hold: False :param bool pkg_verify: .. versionadded:: 2014.7.0 For requested packages that are already installed and would not be targeted for upgrade or downgrade, use pkg.verify to determine if any of the files installed by the package have been altered. If files have been altered, the reinstall option of pkg.install is used to force a reinstall. Types to ignore can be passed to pkg.verify. Additionally, ``verify_options`` can be used to modify further the behavior of pkg.verify. See examples below. Currently, this option is supported for the following pkg providers: :mod:`yumpkg `. Examples: .. code-block:: yaml httpd: pkg.installed: - version: 2.2.15-30.el6.centos - pkg_verify: True .. code-block:: yaml mypkgs: pkg.installed: - pkgs: - foo - bar: 1.2.3-4 - baz - pkg_verify: - ignore_types: - config - doc .. code-block:: yaml mypkgs: pkg.installed: - pkgs: - foo - bar: 1.2.3-4 - baz - pkg_verify: - ignore_types: - config - doc - verify_options: - nodeps - nofiledigest :param list ignore_types: List of types to ignore when verifying the package .. versionadded:: 2014.7.0 :param list verify_options: List of additional options to pass when verifying the package. These options will be added to the ``rpm -V`` command, prepended with ``--`` (for example, when ``nodeps`` is passed in this option, ``rpm -V`` will be run with ``--nodeps``). .. versionadded:: 2016.11.0 :param bool normalize: Normalize the package name by removing the architecture, if the architecture of the package is different from the architecture of the operating system. The ability to disable this behavior is useful for poorly-created packages which include the architecture as an actual part of the name, such as kernel modules which match a specific kernel version. .. versionadded:: 2014.7.0 Example: .. code-block:: yaml gpfs.gplbin-2.6.32-279.31.1.el6.x86_64: pkg.installed: - normalize: False :param bool ignore_epoch: When a package version contains an non-zero epoch (e.g. ``1:3.14.159-2.el7``, and a specific version of a package is desired, set this option to ``True`` to ignore the epoch when comparing versions. This allows for the following SLS to be used: .. code-block:: yaml # Actual vim-enhanced version: 2:7.4.160-1.el7 vim-enhanced: pkg.installed: - version: 7.4.160-1.el7 - ignore_epoch: True Without this option set to ``True`` in the above example, the package would be installed, but the state would report as failed because the actual installed version would be ``2:7.4.160-1.el7``. Alternatively, this option can be left as ``False`` and the full version string (with epoch) can be specified in the SLS file: .. code-block:: yaml vim-enhanced: pkg.installed: - version: 2:7.4.160-1.el7 .. versionadded:: 2015.8.9 | **MULTIPLE PACKAGE INSTALLATION OPTIONS: (not supported in pkgng)** :param list pkgs: A list of packages to install from a software repository. All packages listed under ``pkgs`` will be installed via a single command. .. code-block:: yaml mypkgs: pkg.installed: - pkgs: - foo - bar - baz - hold: True ``NOTE:`` For :mod:`apt `, :mod:`ebuild `, :mod:`pacman `, :mod:`winrepo `, :mod:`yumpkg `, and :mod:`zypper `, version numbers can be specified in the ``pkgs`` argument. For example: .. code-block:: yaml mypkgs: pkg.installed: - pkgs: - foo - bar: 1.2.3-4 - baz Additionally, :mod:`ebuild `, :mod:`pacman `, :mod:`zypper `, :mod:`yum/dnf `, and :mod:`apt ` support the ``<``, ``<=``, ``>=``, and ``>`` operators for more control over what versions will be installed. For example: .. code-block:: yaml mypkgs: pkg.installed: - pkgs: - foo - bar: '>=1.2.3-4' - baz ``NOTE:`` When using comparison operators, the expression must be enclosed in quotes to avoid a YAML render error. With :mod:`ebuild ` is also possible to specify a use flag list and/or if the given packages should be in package.accept_keywords file and/or the overlay from which you want the package to be installed. For example: .. code-block:: yaml mypkgs: pkg.installed: - pkgs: - foo: '~' - bar: '~>=1.2:slot::overlay[use,-otheruse]' - baz :param list sources: A list of packages to install, along with the source URI or local path from which to install each package. In the example below, ``foo``, ``bar``, ``baz``, etc. refer to the name of the package, as it would appear in the output of the ``pkg.version`` or ``pkg.list_pkgs`` salt CLI commands. .. code-block:: yaml mypkgs: pkg.installed: - sources: - foo: salt://rpms/foo.rpm - bar: http://somesite.org/bar.rpm - baz: ftp://someothersite.org/baz.rpm - qux: /minion/path/to/qux.rpm **PLATFORM-SPECIFIC ARGUMENTS** These are specific to each OS. If it does not apply to the execution module for your OS, it is ignored. :param bool hold: Force the package to be held at the current installed version. Currently works with YUM/DNF & APT based systems. .. versionadded:: 2014.7.0 :param bool update_holds: If ``True``, and this function would update the package version, any packages which are being held will be temporarily unheld so that they can be updated. Otherwise, if this function attempts to update a held package, the held package(s) will be skipped and the state will fail. By default, this parameter is set to ``False``. Currently works with YUM/DNF & APT based systems. .. versionadded:: 2016.11.0 :param list names: A list of packages to install from a software repository. Each package will be installed individually by the package manager. .. warning:: Unlike ``pkgs``, the ``names`` parameter cannot specify a version. In addition, it makes a separate call to the package management frontend to install each package, whereas ``pkgs`` makes just a single call. It is therefore recommended to use ``pkgs`` instead of ``names`` to install multiple packages, both for the additional features and the performance improvement that it brings. :param bool install_recommends: Whether to install the packages marked as recommended. Default is ``True``. Currently only works with APT-based systems. .. versionadded:: 2015.5.0 .. code-block:: yaml httpd: pkg.installed: - install_recommends: False :param bool only_upgrade: Only upgrade the packages, if they are already installed. Default is ``False``. Currently only works with APT-based systems. .. versionadded:: 2015.5.0 .. code-block:: yaml httpd: pkg.installed: - only_upgrade: True .. note:: If this parameter is set to True and the package is not already installed, the state will fail. :param bool report_reboot_exit_codes: If the installer exits with a recognized exit code indicating that a reboot is required, the module function *win_system.set_reboot_required_witnessed* will be called, preserving the knowledge of this event for the remainder of the current boot session. For the time being, ``3010`` is the only recognized exit code, but this is subject to future refinement. The value of this param defaults to ``True``. This parameter has no effect on non-Windows systems. .. versionadded:: 2016.11.0 .. code-block:: yaml ms vcpp installed: pkg.installed: - name: ms-vcpp - version: 10.0.40219 - report_reboot_exit_codes: False :param str bypass_file: If you wish to bypass the full package validation process, you can specify a file related to the installed pacakge as a way to validate the pacakge has already been installed. A good example would be a config file that is deployed with the package. Another bypass_file could be ``/run/salt-minon.pid``. .. code-block:: yaml install_ntp: pkg.installed: - name: ntp - bypass_file: /etc/ntp.conf The use case for this feature is when running salt at significant scale. Each state that has a requisite for a ``pkg.installed`` will have salt querying the package manager of the system. Compared to simple diff checks, querying the pacakge manager is a lengthy process. This feature is an attempt to reduce the run time of states. If only a config change is being made but you wish to keep all of the self resolving requisites this bypasses the lenghty cost of the package manager. The assumption is that if this file is present, the package should already be installed. :param str bypass_file_contains: This option can only be used in conjunction with the ``bypass_file`` option. It is to provide a second layer of validation before bypassing the ``pkg.installed`` process. .. code-block:: yaml install_ntp: pkg.installed: - name: ntp - bypass_file: /etc/ntp.conf - bypass_file_contains: version-20181218 The will have salt check to see if the file contains the specified string. If the value is found, the ``pkg.installed`` process will be bypassed under the assumption that two pieces of validation have passed and the package is already installed. .. warning:: Do not try and use ``{{ salt['pkg.version']('ntp') }}`` in a jinja template as part of your bypass_file_contains match. This will trigger a ``pkg.version`` lookup with the pacakge manager and negate any time saved by trying to use the bypass feature. :return: A dictionary containing the state of the software installation :rtype dict: .. note:: The ``pkg.installed`` state supports the usage of ``reload_modules``. This functionality allows you to force Salt to reload all modules. In many cases, Salt is clever enough to transparently reload the modules. For example, if you install a package, Salt reloads modules because some other module or state might require the package which was installed. However, there are some edge cases where this may not be the case, which is what ``reload_modules`` is meant to resolve. You should only use ``reload_modules`` if your ``pkg.installed`` does some sort of installation where if you do not reload the modules future items in your state which rely on the software being installed will fail. Please see the :ref:`Reloading Modules ` documentation for more information. ''' if not pkgs and isinstance(pkgs, list): return {'name': name, 'changes': {}, 'result': True, 'comment': 'No packages to install provided'} # If just a name (and optionally a version) is passed, just pack them into # the pkgs argument. if name and not any((pkgs, sources)): if version: pkgs = [{name: version}] version = None else: pkgs = [name] kwargs['saltenv'] = __env__ refresh = salt.utils.pkg.check_refresh(__opts__, refresh) if bypass_file is not None and bypass_file_contains is not None: if os.path.isfile(bypass_file): with salt.utils.fopen(bypass_file) as bypass_file_open: if bypass_file_contains in bypass_file_open.read(): return {'name': name, 'changes': {}, 'result': True, 'comment': 'pkg.installed was bypassed as {} was present in {}'.format(bypass_file_contains, bypass_file)} if bypass_file is not None and bypass_file_contains is None: if os.path.isfile(bypass_file): return {'name': name, 'changes': {}, 'result': True, 'comment': 'pkg.installed was bypassed as bypass_file {} was present'.format(bypass_file)} # check if capabilities should be checked and modify the requested packages # accordingly. if pkgs: pkgs, refresh = _resolve_capabilities(pkgs, refresh=refresh, **kwargs) if not isinstance(pkg_verify, list): pkg_verify = pkg_verify is True if (pkg_verify or isinstance(pkg_verify, list)) \ and 'pkg.verify' not in __salt__: return {'name': name, 'changes': {}, 'result': False, 'comment': 'pkg.verify not implemented'} if not isinstance(version, six.string_types) and version is not None: version = six.text_type(version) kwargs['allow_updates'] = allow_updates result = _find_install_targets(name, version, pkgs, sources, fromrepo=fromrepo, skip_suggestions=skip_suggestions, pkg_verify=pkg_verify, normalize=normalize, ignore_epoch=ignore_epoch, reinstall=reinstall, refresh=refresh, **kwargs) try: (desired, targets, to_unpurge, to_reinstall, altered_files, warnings, was_refreshed) = result if was_refreshed: refresh = False except ValueError: # _find_install_targets() found no targets or encountered an error # check that the hold function is available if 'pkg.hold' in __salt__ and 'hold' in kwargs: try: action = 'pkg.hold' if kwargs['hold'] else 'pkg.unhold' hold_ret = __salt__[action]( name=name, pkgs=pkgs, sources=sources ) except (CommandExecutionError, SaltInvocationError) as exc: return {'name': name, 'changes': {}, 'result': False, 'comment': six.text_type(exc)} if 'result' in hold_ret and not hold_ret['result']: return {'name': name, 'changes': {}, 'result': False, 'comment': 'An error was encountered while ' 'holding/unholding package(s): {0}' .format(hold_ret['comment'])} else: modified_hold = [hold_ret[x] for x in hold_ret if hold_ret[x]['changes']] not_modified_hold = [hold_ret[x] for x in hold_ret if not hold_ret[x]['changes'] and hold_ret[x]['result']] failed_hold = [hold_ret[x] for x in hold_ret if not hold_ret[x]['result']] for i in modified_hold: result['comment'] += '.\n{0}'.format(i['comment']) result['result'] = i['result'] result['changes'][i['name']] = i['changes'] for i in not_modified_hold: result['comment'] += '.\n{0}'.format(i['comment']) result['result'] = i['result'] for i in failed_hold: result['comment'] += '.\n{0}'.format(i['comment']) result['result'] = i['result'] return result if to_unpurge and 'lowpkg.unpurge' not in __salt__: ret = {'name': name, 'changes': {}, 'result': False, 'comment': 'lowpkg.unpurge not implemented'} if warnings: ret.setdefault('warnings', []).extend(warnings) return ret # Remove any targets not returned by _find_install_targets if pkgs: pkgs = [dict([(x, y)]) for x, y in six.iteritems(targets)] pkgs.extend([dict([(x, y)]) for x, y in six.iteritems(to_reinstall)]) elif sources: oldsources = sources sources = [x for x in oldsources if next(iter(list(x.keys()))) in targets] sources.extend([x for x in oldsources if next(iter(list(x.keys()))) in to_reinstall]) comment = [] if __opts__['test']: if targets: if sources: summary = ', '.join(targets) else: summary = ', '.join([_get_desired_pkg(x, targets) for x in targets]) comment.append('The following packages would be ' 'installed/updated: {0}'.format(summary)) if to_unpurge: comment.append( 'The following packages would have their selection status ' 'changed from \'purge\' to \'install\': {0}' .format(', '.join(to_unpurge)) ) if to_reinstall: # Add a comment for each package in to_reinstall with its # pkg.verify output if reinstall: reinstall_targets = [] for reinstall_pkg in to_reinstall: if sources: reinstall_targets.append(reinstall_pkg) else: reinstall_targets.append( _get_desired_pkg(reinstall_pkg, to_reinstall) ) msg = 'The following packages would be reinstalled: ' msg += ', '.join(reinstall_targets) comment.append(msg) else: for reinstall_pkg in to_reinstall: if sources: pkgstr = reinstall_pkg else: pkgstr = _get_desired_pkg(reinstall_pkg, to_reinstall) comment.append( 'Package \'{0}\' would be reinstalled because the ' 'following files have been altered:'.format(pkgstr) ) comment.append( _nested_output(altered_files[reinstall_pkg]) ) ret = {'name': name, 'changes': {}, 'result': None, 'comment': '\n'.join(comment)} if warnings: ret.setdefault('warnings', []).extend(warnings) return ret changes = {'installed': {}} modified_hold = None not_modified_hold = None failed_hold = None if targets or to_reinstall: try: pkg_ret = __salt__['pkg.install'](name=None, refresh=refresh, version=version, fromrepo=fromrepo, skip_verify=skip_verify, pkgs=pkgs, sources=sources, reinstall=bool(to_reinstall), normalize=normalize, update_holds=update_holds, ignore_epoch=ignore_epoch, **kwargs) except CommandExecutionError as exc: ret = {'name': name, 'result': False} if exc.info: # Get information for state return from the exception. ret['changes'] = exc.info.get('changes', {}) ret['comment'] = exc.strerror_without_changes else: ret['changes'] = {} ret['comment'] = ('An error was encountered while installing ' 'package(s): {0}'.format(exc)) if warnings: ret.setdefault('warnings', []).extend(warnings) return ret if refresh: refresh = False if isinstance(pkg_ret, dict): changes['installed'].update(pkg_ret) elif isinstance(pkg_ret, six.string_types): comment.append(pkg_ret) # Code below will be looking for a dictionary. If this is a string # it means that there was an exception raised and that no packages # changed, so now that we have added this error to the comments we # set this to an empty dictionary so that the code below which # checks reinstall targets works. pkg_ret = {} if 'pkg.hold' in __salt__ and 'hold' in kwargs: try: action = 'pkg.hold' if kwargs['hold'] else 'pkg.unhold' hold_ret = __salt__[action]( name=name, pkgs=desired ) except (CommandExecutionError, SaltInvocationError) as exc: comment.append(six.text_type(exc)) ret = {'name': name, 'changes': changes, 'result': False, 'comment': '\n'.join(comment)} if warnings: ret.setdefault('warnings', []).extend(warnings) return ret else: if 'result' in hold_ret and not hold_ret['result']: ret = {'name': name, 'changes': {}, 'result': False, 'comment': 'An error was encountered while ' 'holding/unholding package(s): {0}' .format(hold_ret['comment'])} if warnings: ret.setdefault('warnings', []).extend(warnings) return ret else: modified_hold = [hold_ret[x] for x in hold_ret if hold_ret[x]['changes']] not_modified_hold = [hold_ret[x] for x in hold_ret if not hold_ret[x]['changes'] and hold_ret[x]['result']] failed_hold = [hold_ret[x] for x in hold_ret if not hold_ret[x]['result']] if to_unpurge: changes['purge_desired'] = __salt__['lowpkg.unpurge'](*to_unpurge) # Analyze pkg.install results for packages in targets if sources: modified = [x for x in changes['installed'] if x in targets] not_modified = [x for x in desired if x not in targets and x not in to_reinstall] failed = [x for x in targets if x not in modified] else: if __grains__['os'] == 'FreeBSD': kwargs['with_origin'] = True new_pkgs = __salt__['pkg.list_pkgs'](versions_as_list=True, **kwargs) if kwargs.get('resolve_capabilities', False) and 'pkg.list_provides' in __salt__: new_caps = __salt__['pkg.list_provides'](**kwargs) else: new_caps = {} ok, failed = _verify_install(desired, new_pkgs, ignore_epoch=ignore_epoch, new_caps=new_caps) modified = [x for x in ok if x in targets] not_modified = [x for x in ok if x not in targets and x not in to_reinstall] failed = [x for x in failed if x in targets] # If there was nothing unpurged, just set the changes dict to the contents # of changes['installed']. if not changes.get('purge_desired'): changes = changes['installed'] if modified: if sources: summary = ', '.join(modified) else: summary = ', '.join([_get_desired_pkg(x, desired) for x in modified]) if len(summary) < 20: comment.append('The following packages were installed/updated: ' '{0}'.format(summary)) else: comment.append( '{0} targeted package{1} {2} installed/updated.'.format( len(modified), 's' if len(modified) > 1 else '', 'were' if len(modified) > 1 else 'was' ) ) if modified_hold: for i in modified_hold: change_name = i['name'] if change_name in changes: comment.append(i['comment']) if changes[change_name]['new']: changes[change_name]['new'] += '\n' changes[change_name]['new'] += '{0}'.format(i['changes']['new']) if changes[change_name]['old']: changes[change_name]['old'] += '\n' changes[change_name]['old'] += '{0}'.format(i['changes']['old']) else: comment.append(i['comment']) changes[change_name] = {} changes[change_name]['new'] = '{0}'.format(i['changes']['new']) # Any requested packages that were not targeted for install or reinstall if not_modified: if sources: summary = ', '.join(not_modified) else: summary = ', '.join([_get_desired_pkg(x, desired) for x in not_modified]) if len(not_modified) <= 20: comment.append('The following packages were already installed: ' '{0}'.format(summary)) else: comment.append( '{0} targeted package{1} {2} already installed'.format( len(not_modified), 's' if len(not_modified) > 1 else '', 'were' if len(not_modified) > 1 else 'was' ) ) if not_modified_hold: for i in not_modified_hold: comment.append(i['comment']) result = True if failed: if sources: summary = ', '.join(failed) else: summary = ', '.join([_get_desired_pkg(x, desired) for x in failed]) comment.insert(0, 'The following packages failed to ' 'install/update: {0}'.format(summary)) result = False if failed_hold: for i in failed_hold: comment.append(i['comment']) result = False # Get the ignore_types list if any from the pkg_verify argument if isinstance(pkg_verify, list) \ and any(x.get('ignore_types') is not None for x in pkg_verify if isinstance(x, _OrderedDict) and 'ignore_types' in x): ignore_types = next(x.get('ignore_types') for x in pkg_verify if 'ignore_types' in x) else: ignore_types = [] # Get the verify_options list if any from the pkg_verify argument if isinstance(pkg_verify, list) \ and any(x.get('verify_options') is not None for x in pkg_verify if isinstance(x, _OrderedDict) and 'verify_options' in x): verify_options = next(x.get('verify_options') for x in pkg_verify if 'verify_options' in x) else: verify_options = [] # Rerun pkg.verify for packages in to_reinstall to determine failed modified = [] failed = [] for reinstall_pkg in to_reinstall: if reinstall: if reinstall_pkg in pkg_ret: modified.append(reinstall_pkg) else: failed.append(reinstall_pkg) elif pkg_verify: # No need to wrap this in a try/except because we would already # have caught invalid arguments earlier. verify_result = __salt__['pkg.verify'](reinstall_pkg, ignore_types=ignore_types, verify_options=verify_options, **kwargs) if verify_result: failed.append(reinstall_pkg) altered_files[reinstall_pkg] = verify_result else: modified.append(reinstall_pkg) if modified: # Add a comment for each package in modified with its pkg.verify output for modified_pkg in modified: if sources: pkgstr = modified_pkg else: pkgstr = _get_desired_pkg(modified_pkg, desired) msg = 'Package {0} was reinstalled.'.format(pkgstr) if modified_pkg in altered_files: msg += ' The following files were remediated:' comment.append(msg) comment.append(_nested_output(altered_files[modified_pkg])) else: comment.append(msg) if failed: # Add a comment for each package in failed with its pkg.verify output for failed_pkg in failed: if sources: pkgstr = failed_pkg else: pkgstr = _get_desired_pkg(failed_pkg, desired) msg = ('Reinstall was not successful for package {0}.' .format(pkgstr)) if failed_pkg in altered_files: msg += ' The following files could not be remediated:' comment.append(msg) comment.append(_nested_output(altered_files[failed_pkg])) else: comment.append(msg) result = False ret = {'name': name, 'changes': changes, 'result': result, 'comment': '\n'.join(comment)} if warnings: ret.setdefault('warnings', []).extend(warnings) return ret def downloaded(name, version=None, pkgs=None, fromrepo=None, ignore_epoch=None, **kwargs): ''' .. versionadded:: 2017.7.0 Ensure that the package is downloaded, and that it is the correct version (if specified). Currently supported for the following pkg providers: :mod:`yumpkg ` and :mod:`zypper ` :param str name: The name of the package to be downloaded. This parameter is ignored if either "pkgs" is used. Additionally, please note that this option can only be used to download packages from a software repository. :param str version: Download a specific version of a package. .. important:: As of version 2015.8.7, for distros which use yum/dnf, packages which have a version with a nonzero epoch (that is, versions which start with a number followed by a colon must have the epoch included when specifying the version number. For example: .. code-block:: yaml vim-enhanced: pkg.downloaded: - version: 2:7.4.160-1.el7 An **ignore_epoch** argument has been added to which causes the epoch to be disregarded when the state checks to see if the desired version was installed. You can install a specific version when using the ``pkgs`` argument by including the version after the package: .. code-block:: yaml common_packages: pkg.downloaded: - pkgs: - unzip - dos2unix - salt-minion: 2015.8.5-1.el6 :param bool resolve_capabilities: Turn on resolving capabilities. This allow one to name "provides" or alias names for packages. .. versionadded:: 2018.3.0 CLI Example: .. code-block:: yaml zsh: pkg.downloaded: - version: 5.0.5-4.63 - fromrepo: "myrepository" ''' ret = {'name': name, 'changes': {}, 'result': None, 'comment': ''} if 'pkg.list_downloaded' not in __salt__: ret['result'] = False ret['comment'] = 'The pkg.downloaded state is not available on ' \ 'this platform' return ret if not pkgs and isinstance(pkgs, list): ret['result'] = True ret['comment'] = 'No packages to download provided' return ret # If just a name (and optionally a version) is passed, just pack them into # the pkgs argument. if name and not pkgs: if version: pkgs = [{name: version}] version = None else: pkgs = [name] # It doesn't make sense here to received 'downloadonly' as kwargs # as we're explicitly passing 'downloadonly=True' to execution module. if 'downloadonly' in kwargs: del kwargs['downloadonly'] pkgs, _refresh = _resolve_capabilities(pkgs, **kwargs) # Only downloading not yet downloaded packages targets = _find_download_targets(name, version, pkgs, fromrepo=fromrepo, ignore_epoch=ignore_epoch, **kwargs) if isinstance(targets, dict) and 'result' in targets: return targets elif not isinstance(targets, dict): ret['result'] = False ret['comment'] = 'An error was encountered while checking targets: ' \ '{0}'.format(targets) return ret if __opts__['test']: summary = ', '.join(targets) ret['comment'] = 'The following packages would be ' \ 'downloaded: {0}'.format(summary) return ret try: pkg_ret = __salt__['pkg.install'](name=name, pkgs=pkgs, version=version, downloadonly=True, fromrepo=fromrepo, ignore_epoch=ignore_epoch, **kwargs) ret['result'] = True ret['changes'].update(pkg_ret) except CommandExecutionError as exc: ret = {'name': name, 'result': False} if exc.info: # Get information for state return from the exception. ret['changes'] = exc.info.get('changes', {}) ret['comment'] = exc.strerror_without_changes else: ret['changes'] = {} ret['comment'] = 'An error was encountered while downloading ' \ 'package(s): {0}'.format(exc) return ret new_pkgs = __salt__['pkg.list_downloaded'](**kwargs) ok, failed = _verify_install(targets, new_pkgs, ignore_epoch=ignore_epoch) if failed: summary = ', '.join([_get_desired_pkg(x, targets) for x in failed]) ret['result'] = False ret['comment'] = 'The following packages failed to ' \ 'download: {0}'.format(summary) if not ret['changes'] and not ret['comment']: ret['result'] = True ret['comment'] = 'Packages are already downloaded: ' \ '{0}'.format(', '.join(targets)) return ret def patch_installed(name, advisory_ids=None, downloadonly=None, **kwargs): ''' .. versionadded:: 2017.7.0 Ensure that packages related to certain advisory ids are installed. Currently supported for the following pkg providers: :mod:`yumpkg ` and :mod:`zypper ` CLI Example: .. code-block:: yaml issue-foo-fixed: pkg.patch_installed: - advisory_ids: - SUSE-SLE-SERVER-12-SP2-2017-185 - SUSE-SLE-SERVER-12-SP2-2017-150 - SUSE-SLE-SERVER-12-SP2-2017-120 ''' ret = {'name': name, 'changes': {}, 'result': None, 'comment': ''} if 'pkg.list_patches' not in __salt__: ret['result'] = False ret['comment'] = 'The pkg.patch_installed state is not available on ' \ 'this platform' return ret if not advisory_ids and isinstance(advisory_ids, list): ret['result'] = True ret['comment'] = 'No advisory ids provided' return ret # Only downloading not yet downloaded packages targets = _find_advisory_targets(name, advisory_ids, **kwargs) if isinstance(targets, dict) and 'result' in targets: return targets elif not isinstance(targets, list): ret['result'] = False ret['comment'] = 'An error was encountered while checking targets: ' \ '{0}'.format(targets) return ret if __opts__['test']: summary = ', '.join(targets) ret['comment'] = 'The following advisory patches would be ' \ 'downloaded: {0}'.format(summary) return ret try: pkg_ret = __salt__['pkg.install'](name=name, advisory_ids=advisory_ids, downloadonly=downloadonly, **kwargs) ret['result'] = True ret['changes'].update(pkg_ret) except CommandExecutionError as exc: ret = {'name': name, 'result': False} if exc.info: # Get information for state return from the exception. ret['changes'] = exc.info.get('changes', {}) ret['comment'] = exc.strerror_without_changes else: ret['changes'] = {} ret['comment'] = ('An error was encountered while downloading ' 'package(s): {0}'.format(exc)) return ret if not ret['changes'] and not ret['comment']: status = 'downloaded' if downloadonly else 'installed' ret['result'] = True ret['comment'] = ('Advisory patch is not needed or related packages ' 'are already {0}'.format(status)) return ret def patch_downloaded(name, advisory_ids=None, **kwargs): ''' .. versionadded:: 2017.7.0 Ensure that packages related to certain advisory ids are downloaded. Currently supported for the following pkg providers: :mod:`yumpkg ` and :mod:`zypper ` CLI Example: .. code-block:: yaml preparing-to-fix-issues: pkg.patch_downloaded: - advisory_ids: - SUSE-SLE-SERVER-12-SP2-2017-185 - SUSE-SLE-SERVER-12-SP2-2017-150 - SUSE-SLE-SERVER-12-SP2-2017-120 ''' if 'pkg.list_patches' not in __salt__: return {'name': name, 'result': False, 'changes': {}, 'comment': 'The pkg.patch_downloaded state is not available on ' 'this platform'} # It doesn't make sense here to received 'downloadonly' as kwargs # as we're explicitly passing 'downloadonly=True' to execution module. if 'downloadonly' in kwargs: del kwargs['downloadonly'] return patch_installed(name=name, advisory_ids=advisory_ids, downloadonly=True, **kwargs) def latest( name, refresh=None, fromrepo=None, skip_verify=False, pkgs=None, watch_flags=True, **kwargs): ''' Ensure that the named package is installed and the latest available package. If the package can be updated, this state function will update the package. Generally it is better for the :mod:`installed ` function to be used, as :mod:`latest ` will update the package whenever a new package is available. name The name of the package to maintain at the latest available version. This parameter is ignored if "pkgs" is used. fromrepo Specify a repository from which to install skip_verify Skip the GPG verification check for the package to be installed refresh This parameter controls whether or not the package repo database is updated prior to checking for the latest available version of the requested packages. If ``True``, the package database will be refreshed (``apt-get update`` or equivalent, depending on platform) before checking for the latest available version of the requested packages. If ``False``, the package database will *not* be refreshed before checking. If unset, then Salt treats package database refreshes differently depending on whether or not a ``pkg`` state has been executed already during the current Salt run. Once a refresh has been performed in a ``pkg`` state, for the remainder of that Salt run no other refreshes will be performed for ``pkg`` states which do not explicitly set ``refresh`` to ``True``. This prevents needless additional refreshes from slowing down the Salt run. :param str cache_valid_time: .. versionadded:: 2016.11.0 This parameter sets the value in seconds after which the cache is marked as invalid, and a cache update is necessary. This overwrites the ``refresh`` parameter's default behavior. Example: .. code-block:: yaml httpd: pkg.latest: - refresh: True - cache_valid_time: 300 In this case, a refresh will not take place for 5 minutes since the last ``apt-get update`` was executed on the system. .. note:: This parameter is available only on Debian based distributions and has no effect on the rest. :param bool resolve_capabilities: Turn on resolving capabilities. This allow one to name "provides" or alias names for packages. .. versionadded:: 2018.3.0 Multiple Package Installation Options: (Not yet supported for: FreeBSD, OpenBSD, MacOS, and Solaris pkgutil) pkgs A list of packages to maintain at the latest available version. .. code-block:: yaml mypkgs: pkg.latest: - pkgs: - foo - bar - baz install_recommends Whether to install the packages marked as recommended. Default is ``True``. Currently only works with APT-based systems. .. versionadded:: 2015.5.0 .. code-block:: yaml httpd: pkg.latest: - install_recommends: False only_upgrade Only upgrade the packages, if they are already installed. Default is ``False``. Currently only works with APT-based systems. .. versionadded:: 2015.5.0 .. code-block:: yaml httpd: pkg.latest: - only_upgrade: True .. note:: If this parameter is set to True and the package is not already installed, the state will fail. report_reboot_exit_codes If the installer exits with a recognized exit code indicating that a reboot is required, the module function *win_system.set_reboot_required_witnessed* will be called, preserving the knowledge of this event for the remainder of the current boot session. For the time being, ``3010`` is the only recognized exit code, but this is subject to future refinement. The value of this param defaults to ``True``. This parameter has no effect on non-Windows systems. .. versionadded:: 2016.11.0 .. code-block:: yaml ms vcpp installed: pkg.latest: - name: ms-vcpp - report_reboot_exit_codes: False ''' refresh = salt.utils.pkg.check_refresh(__opts__, refresh) if kwargs.get('sources'): return {'name': name, 'changes': {}, 'result': False, 'comment': 'The "sources" parameter is not supported.'} elif pkgs: desired_pkgs = list(_repack_pkgs(pkgs).keys()) if not desired_pkgs: # Badly-formatted SLS return {'name': name, 'changes': {}, 'result': False, 'comment': 'Invalidly formatted "pkgs" parameter. See ' 'minion log.'} else: if not pkgs and isinstance(pkgs, list): return { 'name': name, 'changes': {}, 'result': True, 'comment': 'No packages to install provided' } else: desired_pkgs = [name] kwargs['saltenv'] = __env__ # check if capabilities should be checked and modify the requested packages # accordingly. desired_pkgs, refresh = _resolve_capabilities(desired_pkgs, refresh=refresh, **kwargs) try: avail = __salt__['pkg.latest_version'](*desired_pkgs, fromrepo=fromrepo, refresh=refresh, **kwargs) except CommandExecutionError as exc: return {'name': name, 'changes': {}, 'result': False, 'comment': 'An error was encountered while checking the ' 'newest available version of package(s): {0}' .format(exc)} try: cur = __salt__['pkg.version'](*desired_pkgs, **kwargs) except CommandExecutionError as exc: return {'name': name, 'changes': {}, 'result': False, 'comment': exc.strerror} # Repack the cur/avail data if only a single package is being checked if isinstance(cur, six.string_types): cur = {desired_pkgs[0]: cur} if isinstance(avail, six.string_types): avail = {desired_pkgs[0]: avail} targets = {} problems = [] for pkg in desired_pkgs: if not avail.get(pkg): # Package either a) is up-to-date, or b) does not exist if not cur.get(pkg): # Package does not exist msg = 'No information found for \'{0}\'.'.format(pkg) log.error(msg) problems.append(msg) elif watch_flags \ and __grains__.get('os_family') == 'Gentoo' \ and __salt__['portage_config.is_changed_uses'](pkg): # Package is up-to-date, but Gentoo USE flags are changing so # we need to add it to the targets targets[pkg] = cur[pkg] else: # Package either a) is not installed, or b) is installed and has an # upgrade available targets[pkg] = avail[pkg] if problems: return { 'name': name, 'changes': {}, 'result': False, 'comment': ' '.join(problems) } if targets: # Find up-to-date packages if not pkgs: # There couldn't have been any up-to-date packages if this state # only targeted a single package and is being allowed to proceed to # the install step. up_to_date = [] else: up_to_date = [x for x in pkgs if x not in targets] if __opts__['test']: comments = [] comments.append( 'The following packages would be installed/upgraded: ' + ', '.join(sorted(targets)) ) if up_to_date: up_to_date_count = len(up_to_date) if up_to_date_count <= 10: comments.append( 'The following packages are already up-to-date: ' + ', '.join( ['{0} ({1})'.format(x, cur[x]) for x in sorted(up_to_date)] ) ) else: comments.append( '{0} packages are already up-to-date' .format(up_to_date_count) ) return {'name': name, 'changes': {}, 'result': None, 'comment': '\n'.join(comments)} if salt.utils.platform.is_windows(): # pkg.install execution module on windows ensures the software # package is installed when no version is specified, it does not # upgrade the software to the latest. This is per the design. # Build updated list of pkgs *with verion number*, exclude # non-targeted ones targeted_pkgs = [{x: targets[x]} for x in targets] else: # Build updated list of pkgs to exclude non-targeted ones targeted_pkgs = list(targets) # No need to refresh, if a refresh was necessary it would have been # performed above when pkg.latest_version was run. try: changes = __salt__['pkg.install'](name=None, refresh=False, fromrepo=fromrepo, skip_verify=skip_verify, pkgs=targeted_pkgs, **kwargs) except CommandExecutionError as exc: return {'name': name, 'changes': {}, 'result': False, 'comment': 'An error was encountered while installing ' 'package(s): {0}'.format(exc)} if changes: # Find failed and successful updates failed = [x for x in targets if not changes.get(x) or changes[x].get('new') != targets[x] and targets[x] != 'latest'] successful = [x for x in targets if x not in failed] comments = [] if failed: msg = 'The following packages failed to update: ' \ '{0}'.format(', '.join(sorted(failed))) comments.append(msg) if successful: msg = 'The following packages were successfully ' \ 'installed/upgraded: ' \ '{0}'.format(', '.join(sorted(successful))) comments.append(msg) if up_to_date: if len(up_to_date) <= 10: msg = 'The following packages were already up-to-date: ' \ '{0}'.format(', '.join(sorted(up_to_date))) else: msg = '{0} packages were already up-to-date '.format( len(up_to_date)) comments.append(msg) return {'name': name, 'changes': changes, 'result': False if failed else True, 'comment': ' '.join(comments)} else: if len(targets) > 10: comment = ('{0} targeted packages failed to update. ' 'See debug log for details.'.format(len(targets))) elif len(targets) > 1: comment = ('The following targeted packages failed to update. ' 'See debug log for details: ({0}).' .format(', '.join(sorted(targets)))) else: comment = 'Package {0} failed to ' \ 'update.'.format(next(iter(list(targets.keys())))) if up_to_date: if len(up_to_date) <= 10: comment += ' The following packages were already ' \ 'up-to-date: ' \ '{0}'.format(', '.join(sorted(up_to_date))) else: comment += '{0} packages were already ' \ 'up-to-date'.format(len(up_to_date)) return {'name': name, 'changes': changes, 'result': False, 'comment': comment} else: if len(desired_pkgs) > 10: comment = 'All {0} packages are up-to-date.'.format( len(desired_pkgs)) elif len(desired_pkgs) > 1: comment = 'All packages are up-to-date ' \ '({0}).'.format(', '.join(sorted(desired_pkgs))) else: comment = 'Package {0} is already ' \ 'up-to-date'.format(desired_pkgs[0]) return {'name': name, 'changes': {}, 'result': True, 'comment': comment} def _uninstall( action='remove', name=None, version=None, pkgs=None, normalize=True, ignore_epoch=False, **kwargs): ''' Common function for package removal ''' if action not in ('remove', 'purge'): return {'name': name, 'changes': {}, 'result': False, 'comment': 'Invalid action \'{0}\'. ' 'This is probably a bug.'.format(action)} try: pkg_params = __salt__['pkg_resource.parse_targets']( name, pkgs, normalize=normalize)[0] except MinionError as exc: return {'name': name, 'changes': {}, 'result': False, 'comment': 'An error was encountered while parsing targets: ' '{0}'.format(exc)} targets = _find_remove_targets(name, version, pkgs, normalize, ignore_epoch=ignore_epoch, **kwargs) if isinstance(targets, dict) and 'result' in targets: return targets elif not isinstance(targets, list): return {'name': name, 'changes': {}, 'result': False, 'comment': 'An error was encountered while checking targets: ' '{0}'.format(targets)} if action == 'purge': old_removed = __salt__['pkg.list_pkgs'](versions_as_list=True, removed=True, **kwargs) targets.extend([x for x in pkg_params if x in old_removed]) targets.sort() if not targets: return {'name': name, 'changes': {}, 'result': True, 'comment': 'None of the targeted packages are installed' '{0}'.format(' or partially installed' if action == 'purge' else '')} if __opts__['test']: return {'name': name, 'changes': {}, 'result': None, 'comment': 'The following packages will be {0}d: ' '{1}.'.format(action, ', '.join(targets))} changes = __salt__['pkg.{0}'.format(action)](name, pkgs=pkgs, version=version, **kwargs) new = __salt__['pkg.list_pkgs'](versions_as_list=True, **kwargs) failed = [] for x in pkg_params: if __grains__['os_family'] in ['Suse', 'RedHat']: # Check if the package version set to be removed is actually removed: if x in new and not pkg_params[x]: failed.append(x) elif x in new and pkg_params[x] in new[x]: failed.append(x + "-" + pkg_params[x]) elif x in new: failed.append(x) if action == 'purge': new_removed = __salt__['pkg.list_pkgs'](versions_as_list=True, removed=True, **kwargs) failed.extend([x for x in pkg_params if x in new_removed]) failed.sort() if failed: return {'name': name, 'changes': changes, 'result': False, 'comment': 'The following packages failed to {0}: ' '{1}.'.format(action, ', '.join(failed))} comments = [] not_installed = sorted([x for x in pkg_params if x not in targets]) if not_installed: comments.append('The following packages were not installed: ' '{0}'.format(', '.join(not_installed))) comments.append('The following packages were {0}d: ' '{1}.'.format(action, ', '.join(targets))) else: comments.append('All targeted packages were {0}d.'.format(action)) return {'name': name, 'changes': changes, 'result': True, 'comment': ' '.join(comments)} def removed(name, version=None, pkgs=None, normalize=True, ignore_epoch=False, **kwargs): ''' Verify that a package is not installed, calling ``pkg.remove`` if necessary to remove the package. name The name of the package to be removed. version The version of the package that should be removed. Don't do anything if the package is installed with an unmatching version. .. important:: As of version 2015.8.7, for distros which use yum/dnf, packages which have a version with a nonzero epoch (that is, versions which start with a number followed by a colon like in the example above) must have the epoch included when specifying the version number. For example: .. code-block:: yaml vim-enhanced: pkg.removed: - version: 2:7.4.160-1.el7 In version 2015.8.9, an **ignore_epoch** argument has been added to :py:mod:`pkg.installed `, :py:mod:`pkg.removed `, and :py:mod:`pkg.purged ` states, which causes the epoch to be disregarded when the state checks to see if the desired version was installed. If **ignore_epoch** was not set to ``True``, and instead of ``2:7.4.160-1.el7`` a version of ``7.4.160-1.el7`` were used, this state would report success since the actual installed version includes the epoch, and the specified version would not match. normalize : True Normalize the package name by removing the architecture, if the architecture of the package is different from the architecture of the operating system. The ability to disable this behavior is useful for poorly-created packages which include the architecture as an actual part of the name, such as kernel modules which match a specific kernel version. .. versionadded:: 2015.8.0 ignore_epoch : False When a package version contains an non-zero epoch (e.g. ``1:3.14.159-2.el7``, and a specific version of a package is desired, set this option to ``True`` to ignore the epoch when comparing versions. This allows for the following SLS to be used: .. code-block:: yaml # Actual vim-enhanced version: 2:7.4.160-1.el7 vim-enhanced: pkg.removed: - version: 7.4.160-1.el7 - ignore_epoch: True Without this option set to ``True`` in the above example, the state would falsely report success since the actual installed version is ``2:7.4.160-1.el7``. Alternatively, this option can be left as ``False`` and the full version string (with epoch) can be specified in the SLS file: .. code-block:: yaml vim-enhanced: pkg.removed: - version: 2:7.4.160-1.el7 .. versionadded:: 2015.8.9 Multiple Package Options: pkgs A list of packages to remove. Must be passed as a python list. The ``name`` parameter will be ignored if this option is passed. It accepts version numbers as well. .. versionadded:: 0.16.0 ''' kwargs['saltenv'] = __env__ try: return _uninstall(action='remove', name=name, version=version, pkgs=pkgs, normalize=normalize, ignore_epoch=ignore_epoch, **kwargs) except CommandExecutionError as exc: ret = {'name': name, 'result': False} if exc.info: # Get information for state return from the exception. ret['changes'] = exc.info.get('changes', {}) ret['comment'] = exc.strerror_without_changes else: ret['changes'] = {} ret['comment'] = ('An error was encountered while removing ' 'package(s): {0}'.format(exc)) return ret def purged(name, version=None, pkgs=None, normalize=True, ignore_epoch=False, **kwargs): ''' Verify that a package is not installed, calling ``pkg.purge`` if necessary to purge the package. All configuration files are also removed. name The name of the package to be purged. version The version of the package that should be removed. Don't do anything if the package is installed with an unmatching version. .. important:: As of version 2015.8.7, for distros which use yum/dnf, packages which have a version with a nonzero epoch (that is, versions which start with a number followed by a colon like in the example above) must have the epoch included when specifying the version number. For example: .. code-block:: yaml vim-enhanced: pkg.purged: - version: 2:7.4.160-1.el7 In version 2015.8.9, an **ignore_epoch** argument has been added to :py:mod:`pkg.installed `, :py:mod:`pkg.removed `, and :py:mod:`pkg.purged ` states, which causes the epoch to be disregarded when the state checks to see if the desired version was installed. If **ignore_epoch** was not set to ``True``, and instead of ``2:7.4.160-1.el7`` a version of ``7.4.160-1.el7`` were used, this state would report success since the actual installed version includes the epoch, and the specified version would not match. normalize : True Normalize the package name by removing the architecture, if the architecture of the package is different from the architecture of the operating system. The ability to disable this behavior is useful for poorly-created packages which include the architecture as an actual part of the name, such as kernel modules which match a specific kernel version. .. versionadded:: 2015.8.0 ignore_epoch : False When a package version contains an non-zero epoch (e.g. ``1:3.14.159-2.el7``, and a specific version of a package is desired, set this option to ``True`` to ignore the epoch when comparing versions. This allows for the following SLS to be used: .. code-block:: yaml # Actual vim-enhanced version: 2:7.4.160-1.el7 vim-enhanced: pkg.purged: - version: 7.4.160-1.el7 - ignore_epoch: True Without this option set to ``True`` in the above example, the state would falsely report success since the actual installed version is ``2:7.4.160-1.el7``. Alternatively, this option can be left as ``False`` and the full version string (with epoch) can be specified in the SLS file: .. code-block:: yaml vim-enhanced: pkg.purged: - version: 2:7.4.160-1.el7 .. versionadded:: 2015.8.9 Multiple Package Options: pkgs A list of packages to purge. Must be passed as a python list. The ``name`` parameter will be ignored if this option is passed. It accepts version numbers as well. .. versionadded:: 0.16.0 ''' kwargs['saltenv'] = __env__ try: return _uninstall(action='purge', name=name, version=version, pkgs=pkgs, normalize=normalize, ignore_epoch=ignore_epoch, **kwargs) except CommandExecutionError as exc: ret = {'name': name, 'result': False} if exc.info: # Get information for state return from the exception. ret['changes'] = exc.info.get('changes', {}) ret['comment'] = exc.strerror_without_changes else: ret['changes'] = {} ret['comment'] = ('An error was encountered while purging ' 'package(s): {0}'.format(exc)) return ret def uptodate(name, refresh=False, pkgs=None, **kwargs): ''' .. versionadded:: 2014.7.0 .. versionchanged:: 2018.3.0 Added support for the ``pkgin`` provider. Verify that the system is completely up to date. name The name has no functional value and is only used as a tracking reference refresh refresh the package database before checking for new upgrades pkgs list of packages to upgrade :param str cache_valid_time: This parameter sets the value in seconds after which cache marked as invalid, and cache update is necessary. This overwrite ``refresh`` parameter default behavior. In this case cache_valid_time is set, refresh will not take place for amount in seconds since last ``apt-get update`` executed on the system. .. note:: This parameter available only on Debian based distributions, and have no effect on the rest. :param bool resolve_capabilities: Turn on resolving capabilities. This allow one to name "provides" or alias names for packages. .. versionadded:: 2018.3.0 kwargs Any keyword arguments to pass through to ``pkg.upgrade``. .. versionadded:: 2015.5.0 ''' ret = {'name': name, 'changes': {}, 'result': False, 'comment': 'Failed to update'} if 'pkg.list_upgrades' not in __salt__: ret['comment'] = 'State pkg.uptodate is not available' return ret # emerge --update doesn't appear to support repo notation if 'fromrepo' in kwargs and __grains__['os_family'] == 'Gentoo': ret['comment'] = '\'fromrepo\' argument not supported on this platform' return ret if isinstance(refresh, bool): pkgs, refresh = _resolve_capabilities(pkgs, refresh=refresh, **kwargs) try: packages = __salt__['pkg.list_upgrades'](refresh=refresh, **kwargs) expected = {pkgname: {'new': pkgver, 'old': __salt__['pkg.version'](pkgname, **kwargs)} for pkgname, pkgver in six.iteritems(packages)} if isinstance(pkgs, list): packages = [pkg for pkg in packages if pkg in pkgs] expected = {pkgname: pkgver for pkgname, pkgver in six.iteritems(expected) if pkgname in pkgs} except Exception as exc: ret['comment'] = six.text_type(exc) return ret else: ret['comment'] = 'refresh must be either True or False' return ret if not packages: ret['comment'] = 'System is already up-to-date' ret['result'] = True return ret elif __opts__['test']: ret['comment'] = 'System update will be performed' ret['changes'] = expected ret['result'] = None return ret try: ret['changes'] = __salt__['pkg.upgrade'](refresh=refresh, pkgs=pkgs, **kwargs) except CommandExecutionError as exc: if exc.info: # Get information for state return from the exception. ret['changes'] = exc.info.get('changes', {}) ret['comment'] = exc.strerror_without_changes else: ret['changes'] = {} ret['comment'] = ('An error was encountered while updating ' 'packages: {0}'.format(exc)) return ret # If a package list was provided, ensure those packages were updated missing = [] if isinstance(pkgs, list): missing = [pkg for pkg in six.iterkeys(expected) if pkg not in ret['changes']] if missing: ret['comment'] = 'The following package(s) failed to update: {0}'.format(', '.join(missing)) ret['result'] = False else: ret['comment'] = 'Upgrade ran successfully' ret['result'] = True return ret def group_installed(name, skip=None, include=None, **kwargs): ''' .. versionadded:: 2015.8.0 .. versionchanged:: 2016.11.0 Added support in :mod:`pacman ` Ensure that an entire package group is installed. This state is currently only supported for the :mod:`yum ` and :mod:`pacman ` package managers. skip Packages that would normally be installed by the package group ("default" packages), which should not be installed. .. code-block:: yaml Load Balancer: pkg.group_installed: - skip: - piranha include Packages which are included in a group, which would not normally be installed by a ``yum groupinstall`` ("optional" packages). Note that this will not enforce group membership; if you include packages which are not members of the specified groups, they will still be installed. .. code-block:: yaml Load Balancer: pkg.group_installed: - include: - haproxy .. versionchanged:: 2016.3.0 This option can no longer be passed as a comma-separated list, it must now be passed as a list (as shown in the above example). .. note:: Because this is essentially a wrapper around :py:func:`pkg.install `, any argument which can be passed to pkg.install may also be included here, and it will be passed on to the call to :py:func:`pkg.install `. ''' ret = {'name': name, 'changes': {}, 'result': False, 'comment': ''} if 'pkg.group_diff' not in __salt__: ret['comment'] = 'pkg.group_install not available for this platform' return ret if skip is None: skip = [] else: if not isinstance(skip, list): ret['comment'] = 'skip must be formatted as a list' return ret for idx, item in enumerate(skip): if not isinstance(item, six.string_types): skip[idx] = six.text_type(item) if include is None: include = [] else: if not isinstance(include, list): ret['comment'] = 'include must be formatted as a list' return ret for idx, item in enumerate(include): if not isinstance(item, six.string_types): include[idx] = six.text_type(item) try: diff = __salt__['pkg.group_diff'](name) except CommandExecutionError as err: ret['comment'] = ('An error was encountered while installing/updating ' 'group \'{0}\': {1}.'.format(name, err)) return ret mandatory = diff['mandatory']['installed'] + \ diff['mandatory']['not installed'] invalid_skip = [x for x in mandatory if x in skip] if invalid_skip: ret['comment'] = ( 'The following mandatory packages cannot be skipped: {0}' .format(', '.join(invalid_skip)) ) return ret targets = diff['mandatory']['not installed'] targets.extend([x for x in diff['default']['not installed'] if x not in skip]) targets.extend(include) if not targets: ret['result'] = True ret['comment'] = 'Group \'{0}\' is already installed'.format(name) return ret partially_installed = diff['mandatory']['installed'] \ or diff['default']['installed'] \ or diff['optional']['installed'] if __opts__['test']: ret['result'] = None if partially_installed: ret['comment'] = ( 'Group \'{0}\' is partially installed and will be updated' .format(name) ) else: ret['comment'] = 'Group \'{0}\' will be installed'.format(name) return ret try: ret['changes'] = __salt__['pkg.install'](pkgs=targets, **kwargs) except CommandExecutionError as exc: ret = {'name': name, 'result': False} if exc.info: # Get information for state return from the exception. ret['changes'] = exc.info.get('changes', {}) ret['comment'] = exc.strerror_without_changes else: ret['changes'] = {} ret['comment'] = ('An error was encountered while ' 'installing/updating group \'{0}\': {1}' .format(name, exc)) return ret failed = [x for x in targets if x not in __salt__['pkg.list_pkgs'](**kwargs)] if failed: ret['comment'] = ( 'Failed to install the following packages: {0}' .format(', '.join(failed)) ) return ret ret['result'] = True ret['comment'] = 'Group \'{0}\' was {1}'.format( name, 'updated' if partially_installed else 'installed' ) return ret def mod_init(low): ''' Set a flag to tell the install functions to refresh the package database. This ensures that the package database is refreshed only once during a state run significantly improving the speed of package management during a state run. It sets a flag for a number of reasons, primarily due to timeline logic. When originally setting up the mod_init for pkg a number of corner cases arose with different package managers and how they refresh package data. It also runs the "ex_mod_init" from the package manager module that is currently loaded. The "ex_mod_init" is expected to work as a normal "mod_init" function. .. seealso:: :py:func:`salt.modules.ebuild.ex_mod_init` ''' ret = True if 'pkg.ex_mod_init' in __salt__: ret = __salt__['pkg.ex_mod_init'](low) if low['fun'] == 'installed' or low['fun'] == 'latest': salt.utils.pkg.write_rtag(__opts__) return ret return False def mod_aggregate(low, chunks, running): ''' The mod_aggregate function which looks up all packages in the available low chunks and merges them into a single pkgs ref in the present low data ''' pkgs = [] pkg_type = None agg_enabled = [ 'installed', 'latest', 'removed', 'purged', ] if low.get('fun') not in agg_enabled: return low for chunk in chunks: tag = __utils__['state.gen_tag'](chunk) if tag in running: # Already ran the pkg state, skip aggregation continue if chunk.get('state') == 'pkg': if '__agg__' in chunk: continue # Check for the same function if chunk.get('fun') != low.get('fun'): continue # Check for the same repo if chunk.get('fromrepo') != low.get('fromrepo'): continue # Check first if 'sources' was passed so we don't aggregate pkgs # and sources together. if 'sources' in chunk: if pkg_type is None: pkg_type = 'sources' if pkg_type == 'sources': pkgs.extend(chunk['sources']) chunk['__agg__'] = True else: if pkg_type is None: pkg_type = 'pkgs' if pkg_type == 'pkgs': # Pull out the pkg names! if 'pkgs' in chunk: pkgs.extend(chunk['pkgs']) chunk['__agg__'] = True elif 'name' in chunk: version = chunk.pop('version', None) if version is not None: pkgs.append({chunk['name']: version}) else: pkgs.append(chunk['name']) chunk['__agg__'] = True if pkg_type is not None and pkgs: if pkg_type in low: low[pkg_type].extend(pkgs) else: low[pkg_type] = pkgs return low def mod_watch(name, **kwargs): ''' Install/reinstall a package based on a watch requisite .. note:: This state exists to support special handling of the ``watch`` :ref:`requisite `. It should not be called directly. Parameters for this function should be set by the state being triggered. ''' sfun = kwargs.pop('sfun', None) mapfun = {'purged': purged, 'latest': latest, 'removed': removed, 'installed': installed} if sfun in mapfun: return mapfun[sfun](name, **kwargs) return {'name': name, 'changes': {}, 'comment': 'pkg.{0} does not work with the watch requisite'.format(sfun), 'result': False}