.. versionchanged:: 2015.8.12,2016.3.3,2016.11.0
        On minions running systemd>=205, `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 any yum/dnf commands spawned by Salt when the
        ``salt-minion`` service is restarted. (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
        <salt.modules.config.get>` called ``systemd.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

    Install the passed package(s), add refresh=True to clean the yum database
    before package is installed.

    name
        The name of the package to be installed. Note that this parameter is
        ignored if either "pkgs" or "sources" is passed. 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.

        32-bit packages can be installed on 64-bit systems by appending the
        architecture designation (``.i686``, ``.i586``, etc.) to the end of the
        package name.

        CLI Example:

        .. code-block:: bash

            salt '*' pkg.install <package name>

    refresh
        Whether or not to update the yum database before executing.

    reinstall
        Specifying reinstall=True will use ``yum reinstall`` rather than
        ``yum install`` for requested packages that are already installed.

        If a version is specified with the requested package, then
        ``yum reinstall`` will only be used if the installed version
        matches the requested version.

        Works with ``sources`` when the package header of the source can be
        matched to the name and version of an installed package.

        .. versionadded:: 2014.7.0

    skip_verify
        Skip the GPG verification check (e.g., ``--nogpgcheck``)

    downloadonly
        Only download the packages, do not install.

    version
        Install a specific version of the package, e.g. 1.2.3-4.el5. Ignored
        if "pkgs" or "sources" is passed.

        .. versionchanged:: 2018.3.0
            version can now contain comparison operators (e.g. ``>1.2.3``,
            ``<=2.0``, etc.)

    update_holds : False
        If ``True``, and this function would update the package version, any
        packages held using the yum/dnf "versionlock" plugin will be 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 an
        error will be raised.

        .. versionadded:: 2016.11.0

    setopt
        A comma-separated or Python list of key=value options. This list will
        be expanded and ``--setopt`` prepended to each in the yum/dnf command
        that is run.

        CLI Example:

        .. code-block:: bash

            salt '*' pkg.install foo setopt='obsoletes=0,plugins=0'

        .. versionadded:: 2019.2.0

    Repository Options:

    fromrepo
        Specify a package repository (or repositories) from which to install.
        (e.g., ``yum --disablerepo='*' --enablerepo='somerepo'``)

    enablerepo (ignored if ``fromrepo`` is specified)
        Specify a disabled package repository (or repositories) to enable.
        (e.g., ``yum --enablerepo='somerepo'``)

    disablerepo (ignored if ``fromrepo`` is specified)
        Specify an enabled package repository (or repositories) to disable.
        (e.g., ``yum --disablerepo='somerepo'``)

    disableexcludes
        Disable exclude from main, for a repo or for everything.
        (e.g., ``yum --disableexcludes='main'``)

        .. versionadded:: 2014.7.0

    ignore_epoch : False
        Only used when the version of a package is specified using a comparison
        operator (e.g. ``>4.1``). If set to ``True``, then the epoch will be
        ignored when comparing the currently-installed version to the desired
        version.

        .. versionadded:: 2018.3.0


    Multiple Package Installation Options:

    pkgs
        A list of packages to install from a software repository. Must be
        passed as a python list. A specific version number can be specified
        by using a single-element dict representing the package and its
        version.

        CLI Examples:

        .. code-block:: bash

            salt '*' pkg.install pkgs='["foo", "bar"]'
            salt '*' pkg.install pkgs='["foo", {"bar": "1.2.3-4.el5"}]'

    sources
        A list of RPM packages to install. Must be passed as a list of dicts,
        with the keys being package names, and the values being the source URI
        or local path to the package.

        CLI Example:

        .. code-block:: bash

            salt '*' pkg.install sources='[{"foo": "salt://foo.rpm"}, {"bar": "salt://bar.rpm"}]'

    normalize : True
        Normalize the package name by removing the architecture. This is useful
        for poorly created packages which might include the architecture as an
        actual part of the name such as kernel modules which match a specific
        kernel version.

        .. code-block:: bash

            salt -G role:nsd pkg.install gpfs.gplbin-2.6.32-279.31.1.el6.x86_64 normalize=False

        .. versionadded:: 2014.7.0

    diff_attr:
        If a list of package attributes is specified, returned value will
        contain them, eg.::

            {'<package>': {
                'old': {
                    'version': '<old-version>',
                    'arch': '<old-arch>'},

                'new': {
                    'version': '<new-version>',
                    'arch': '<new-arch>'}}}

        Valid attributes are: ``epoch``, ``version``, ``release``, ``arch``,
        ``install_date``, ``install_date_time_t``.

        If ``all`` is specified, all valid attributes will be returned.

        .. versionadded:: 2018.3.0

    Returns a dict containing the new package names and versions::

        {'<package>': {'old': '<old-version>',
                       'new': '<new-version>'}}

    If an attribute list in diff_attr is specified, the dict will also contain
    any specified attribute, eg.::

        {'<package>': {
            'old': {
                'version': '<old-version>',
                'arch': '<old-arch>'},

            'new': {
                'version': '<new-version>',
                'arch': '<new-arch>'}}}

def install(name=None,
            refresh=False,
            skip_verify=False,
            pkgs=None,
            sources=None,
            downloadonly=False,
            reinstall=False,
            normalize=True,
            update_holds=False,
            saltenv='base',
            ignore_epoch=False,
            **kwargs):
    '''
    .. versionchanged:: 2015.8.12,2016.3.3,2016.11.0
        On minions running systemd>=205, `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 any yum/dnf commands spawned by Salt when the
        ``salt-minion`` service is restarted. (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
        <salt.modules.config.get>` called ``systemd.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

    Install the passed package(s), add refresh=True to clean the yum database
    before package is installed.

    name
        The name of the package to be installed. Note that this parameter is
        ignored if either "pkgs" or "sources" is passed. 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.

        32-bit packages can be installed on 64-bit systems by appending the
        architecture designation (``.i686``, ``.i586``, etc.) to the end of the
        package name.

        CLI Example:

        .. code-block:: bash

            salt '*' pkg.install <package name>

    refresh
        Whether or not to update the yum database before executing.

    reinstall
        Specifying reinstall=True will use ``yum reinstall`` rather than
        ``yum install`` for requested packages that are already installed.

        If a version is specified with the requested package, then
        ``yum reinstall`` will only be used if the installed version
        matches the requested version.

        Works with ``sources`` when the package header of the source can be
        matched to the name and version of an installed package.

        .. versionadded:: 2014.7.0

    skip_verify
        Skip the GPG verification check (e.g., ``--nogpgcheck``)

    downloadonly
        Only download the packages, do not install.

    version
        Install a specific version of the package, e.g. 1.2.3-4.el5. Ignored
        if "pkgs" or "sources" is passed.

        .. versionchanged:: 2018.3.0
            version can now contain comparison operators (e.g. ``>1.2.3``,
            ``<=2.0``, etc.)

    update_holds : False
        If ``True``, and this function would update the package version, any
        packages held using the yum/dnf "versionlock" plugin will be 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 an
        error will be raised.

        .. versionadded:: 2016.11.0

    setopt
        A comma-separated or Python list of key=value options. This list will
        be expanded and ``--setopt`` prepended to each in the yum/dnf command
        that is run.

        CLI Example:

        .. code-block:: bash

            salt '*' pkg.install foo setopt='obsoletes=0,plugins=0'

        .. versionadded:: 2019.2.0

    Repository Options:

    fromrepo
        Specify a package repository (or repositories) from which to install.
        (e.g., ``yum --disablerepo='*' --enablerepo='somerepo'``)

    enablerepo (ignored if ``fromrepo`` is specified)
        Specify a disabled package repository (or repositories) to enable.
        (e.g., ``yum --enablerepo='somerepo'``)

    disablerepo (ignored if ``fromrepo`` is specified)
        Specify an enabled package repository (or repositories) to disable.
        (e.g., ``yum --disablerepo='somerepo'``)

    disableexcludes
        Disable exclude from main, for a repo or for everything.
        (e.g., ``yum --disableexcludes='main'``)

        .. versionadded:: 2014.7.0

    ignore_epoch : False
        Only used when the version of a package is specified using a comparison
        operator (e.g. ``>4.1``). If set to ``True``, then the epoch will be
        ignored when comparing the currently-installed version to the desired
        version.

        .. versionadded:: 2018.3.0


    Multiple Package Installation Options:

    pkgs
        A list of packages to install from a software repository. Must be
        passed as a python list. A specific version number can be specified
        by using a single-element dict representing the package and its
        version.

        CLI Examples:

        .. code-block:: bash

            salt '*' pkg.install pkgs='["foo", "bar"]'
            salt '*' pkg.install pkgs='["foo", {"bar": "1.2.3-4.el5"}]'

    sources
        A list of RPM packages to install. Must be passed as a list of dicts,
        with the keys being package names, and the values being the source URI
        or local path to the package.

        CLI Example:

        .. code-block:: bash

            salt '*' pkg.install sources='[{"foo": "salt://foo.rpm"}, {"bar": "salt://bar.rpm"}]'

    normalize : True
        Normalize the package name by removing the architecture. This is useful
        for poorly created packages which might include the architecture as an
        actual part of the name such as kernel modules which match a specific
        kernel version.

        .. code-block:: bash

            salt -G role:nsd pkg.install gpfs.gplbin-2.6.32-279.31.1.el6.x86_64 normalize=False

        .. versionadded:: 2014.7.0

    diff_attr:
        If a list of package attributes is specified, returned value will
        contain them, eg.::

            {'<package>': {
                'old': {
                    'version': '<old-version>',
                    'arch': '<old-arch>'},

                'new': {
                    'version': '<new-version>',
                    'arch': '<new-arch>'}}}

        Valid attributes are: ``epoch``, ``version``, ``release``, ``arch``,
        ``install_date``, ``install_date_time_t``.

        If ``all`` is specified, all valid attributes will be returned.

        .. versionadded:: 2018.3.0

    Returns a dict containing the new package names and versions::

        {'<package>': {'old': '<old-version>',
                       'new': '<new-version>'}}

    If an attribute list in diff_attr is specified, the dict will also contain
    any specified attribute, eg.::

        {'<package>': {
            'old': {
                'version': '<old-version>',
                'arch': '<old-arch>'},

            'new': {
                'version': '<new-version>',
                'arch': '<new-arch>'}}}
    '''
    options = _get_options(**kwargs)

    if salt.utils.data.is_true(refresh):
        refresh_db(**kwargs)
    reinstall = salt.utils.data.is_true(reinstall)

    try:
        pkg_params, pkg_type = __salt__['pkg_resource.parse_targets'](
            name, pkgs, sources, saltenv=saltenv, normalize=normalize, **kwargs
        )
    except MinionError as exc:
        raise CommandExecutionError(exc)

    if not pkg_params:
        return {}

    version_num = kwargs.get('version')

    diff_attr = kwargs.get('diff_attr')
    old = list_pkgs(versions_as_list=False, attr=diff_attr) if not downloadonly else list_downloaded()
    # Use of __context__ means no duplicate work here, just accessing
    # information already in __context__ from the previous call to list_pkgs()
    old_as_list = list_pkgs(versions_as_list=True) if not downloadonly else list_downloaded()

    to_install = []
    to_downgrade = []
    to_reinstall = []
    _available = {}
    # The above three lists will be populated with tuples containing the
    # package name and the string being used for this particular package
    # modification. The reason for this method is that the string we use for
    # installation, downgrading, or reinstallation will be different than the
    # package name in a couple cases:
    #
    #   1) A specific version is being targeted. In this case the string being
    #      passed to install/downgrade/reinstall will contain the version
    #      information after the package name.
    #   2) A binary package is being installed via the "sources" param. In this
    #      case the string being passed will be the path to the local copy of
    #      the package in the minion cachedir.
    #
    # The reason that we need both items is to be able to modify the installed
    # version of held packages.
    if pkg_type == 'repository':
        has_wildcards = []
        has_comparison = []
        for pkgname, pkgver in six.iteritems(pkg_params):
            try:
                if '*' in pkgver:
                    has_wildcards.append(pkgname)
                elif pkgver.startswith('<') or pkgver.startswith('>'):
                    has_comparison.append(pkgname)
            except (TypeError, ValueError):
                continue
        _available = AvailablePackages(
            *has_wildcards + has_comparison,
            byrepo=False,
            **kwargs)
        pkg_params_items = six.iteritems(pkg_params)
    elif pkg_type == 'advisory':
        pkg_params_items = []
        cur_patches = list_patches()
        for advisory_id in pkg_params:
            if advisory_id not in cur_patches:
                raise CommandExecutionError(
                    'Advisory id "{0}" not found'.format(advisory_id)
                )
            else:
                pkg_params_items.append(advisory_id)
    else:
        pkg_params_items = []
        for pkg_source in pkg_params:
            if 'lowpkg.bin_pkg_info' in __salt__:
                rpm_info = __salt__['lowpkg.bin_pkg_info'](pkg_source)
            else:
                rpm_info = None
            if rpm_info is None:
                log.error(
                    'pkg.install: Unable to get rpm information for %s. '
                    'Version comparisons will be unavailable, and return '
                    'data may be inaccurate if reinstall=True.', pkg_source
                )
                pkg_params_items.append([pkg_source])
            else:
                pkg_params_items.append(
                    [rpm_info['name'], pkg_source, rpm_info['version']]
                )

    errors = []
    for pkg_item_list in pkg_params_items:
        if pkg_type == 'repository':
            pkgname, version_num = pkg_item_list
        elif pkg_type == 'advisory':
            pkgname = pkg_item_list
            version_num = None
        else:
            try:
                pkgname, pkgpath, version_num = pkg_item_list
            except ValueError:
                pkgname = None
                pkgpath = pkg_item_list[0]
                version_num = None

        if version_num is None:
            if pkg_type == 'repository':
                if reinstall and pkgname in old:
                    to_reinstall.append((pkgname, pkgname))
                else:
                    to_install.append((pkgname, pkgname))
            elif pkg_type == 'advisory':
                to_install.append((pkgname, pkgname))
            else:
                to_install.append((pkgname, pkgpath))
        else:
            # If we are installing a package file and not one from the repo,
            # and version_num is not None, then we can assume that pkgname is
            # not None, since the only way version_num is not None is if RPM
            # metadata parsing was successful.
            if pkg_type == 'repository':
                # yum/dnf does not support comparison operators. If the version
                # starts with an equals sign, ignore it.
                version_num = version_num.lstrip('=')
                if pkgname in has_comparison:
                    candidates = _available.get(pkgname, [])
                    target = salt.utils.pkg.match_version(
                        version_num,
                        candidates,
                        cmp_func=version_cmp,
                        ignore_epoch=ignore_epoch,
                    )
                    if target is None:
                        errors.append(
                            'No version matching \'{0}{1}\' could be found '
                            '(available: {2})'.format(
                                pkgname,
                                version_num,
                                ', '.join(candidates) if candidates else None
                            )
                        )
                        continue
                    else:
                        version_num = target
                if _yum() == 'yum':
                    # yum install does not support epoch without the arch, and
                    # we won't know what the arch will be when it's not
                    # provided. It could either be the OS architecture, or
                    # 'noarch', and we don't make that distinction in the
                    # pkg.list_pkgs return data.
                    if ignore_epoch is True:
                        version_num = version_num.split(':', 1)[-1]
                arch = ''
                try:
                    namepart, archpart = pkgname.rsplit('.', 1)
                except ValueError:
                    pass
                else:
                    if archpart in salt.utils.pkg.rpm.ARCHES:
                        arch = '.' + archpart
                        pkgname = namepart

                if '*' in version_num:
                    # Resolve wildcard matches
                    candidates = _available.get(pkgname, [])
                    match = salt.utils.itertools.fnmatch_multiple(candidates, version_num)
                    if match is not None:
                        version_num = match
                    else:
                        errors.append(
                            'No version matching \'{0}\' found for package '
                            '\'{1}\' (available: {2})'.format(
                                version_num,
                                pkgname,
                                ', '.join(candidates) if candidates else 'none'
                            )
                        )
                        continue

                if ignore_epoch is True:
                    pkgstr = '{0}-{1}{2}'.format(pkgname, version_num, arch)
                else:
                    pkgstr = '{0}-{1}{2}'.format(pkgname, version_num.split(':', 1)[-1], arch)

            else:
                pkgstr = pkgpath

            # Lambda to trim the epoch from the currently-installed version if
            # no epoch is specified in the specified version
            cver = old_as_list.get(pkgname, [])
            if reinstall and cver:
                for ver in cver:
                    if salt.utils.versions.compare(ver1=version_num,
                                                   oper='==',
                                                   ver2=ver,
                                                   cmp_func=version_cmp,
                                                   ignore_epoch=ignore_epoch):
                        # This version is already installed, so we need to
                        # reinstall.
                        to_reinstall.append((pkgname, pkgstr))
                        break
            else:
                if not cver:
                    to_install.append((pkgname, pkgstr))
                else:
                    for ver in cver:
                        if salt.utils.versions.compare(ver1=version_num,
                                                       oper='>=',
                                                       ver2=ver,
                                                   cmp_func=version_cmp,
                                                   ignore_epoch=ignore_epoch):
                            to_install.append((pkgname, pkgstr))
                            break
                    else:
                        if pkgname is not None:
                            if re.match('^kernel(|-devel)$', pkgname):
                                # kernel and kernel-devel support multiple
                                # installs as their paths do not conflict.
                                # Performing a yum/dnf downgrade will be a
                                # no-op so just do an install instead. It will
                                # fail if there are other interdependencies
                                # that have conflicts, and that's OK. We don't
                                # want to force anything, we just want to
                                # properly handle it if someone tries to
                                # install a kernel/kernel-devel of a lower
                                # version than the currently-installed one.
                                # TODO: find a better way to determine if a
                                # package supports multiple installs.
                                to_install.append((pkgname, pkgstr))
                            else:
                                # None of the currently-installed versions are
                                # greater than the specified version, so this
                                # is a downgrade.
                                to_downgrade.append((pkgname, pkgstr))

    def _add_common_args(cmd):
        '''
        DRY function to add args common to all yum/dnf commands
        '''
        cmd.extend(options)
        if skip_verify:
            cmd.append('--nogpgcheck')
        if downloadonly:
            cmd.append('--downloadonly')

    try:
        holds = list_holds(full=False)
    except SaltInvocationError:
        holds = []
        log.debug(
            'Failed to get holds, versionlock plugin is probably not '
            'installed'
        )
    unhold_prevented = []

    @contextlib.contextmanager
    def _temporarily_unhold(pkgs, targets):
        '''
        Temporarily unhold packages that need to be updated. Add any
        successfully-removed ones (and any packages not in the list of current
        holds) to the list of targets.
        '''
        to_unhold = {}
        for pkgname, pkgstr in pkgs:
            if pkgname in holds:
                if update_holds:
                    to_unhold[pkgname] = pkgstr
                else:
                    unhold_prevented.append(pkgname)
            else:
                targets.append(pkgstr)

        if not to_unhold:
            yield
        else:
            log.debug('Unholding packages: %s', ', '.join(to_unhold))
            try:
                # Using list() here for python3 compatibility, dict.keys() no
                # longer returns a list in python3.
                unhold_names = list(to_unhold.keys())
                for unheld_pkg, outcome in \
                        six.iteritems(unhold(pkgs=unhold_names)):
                    if outcome['result']:
                        # Package was successfully unheld, add to targets
                        targets.append(to_unhold[unheld_pkg])
                    else:
                        # Failed to unhold package
                        errors.append(unheld_pkg)
                yield
            except Exception as exc:
                errors.append(
                    'Error encountered unholding packages {0}: {1}'
                    .format(', '.join(to_unhold), exc)
                )
            finally:
                hold(pkgs=unhold_names)

    targets = []
    with _temporarily_unhold(to_install, targets):
        if targets:
            if pkg_type == 'advisory':
                targets = ["--advisory={0}".format(t) for t in targets]
            cmd = ['-y']
            if _yum() == 'dnf':
                cmd.extend(['--best', '--allowerasing'])
            _add_common_args(cmd)
            cmd.append('install' if pkg_type != 'advisory' else 'update')
            cmd.extend(targets)
            out = _call_yum(cmd, ignore_retcode=False, redirect_stderr=True)
            if out['retcode'] != 0:
                errors.append(out['stdout'])

    targets = []
    with _temporarily_unhold(to_downgrade, targets):
        if targets:
            cmd = ['-y']
            _add_common_args(cmd)
            cmd.append('downgrade')
            cmd.extend(targets)
            out = _call_yum(cmd)
            if out['retcode'] != 0:
                errors.append(out['stdout'])

    targets = []
    with _temporarily_unhold(to_reinstall, targets):
        if targets:
            cmd = ['-y']
            _add_common_args(cmd)
            cmd.append('reinstall')
            cmd.extend(targets)
            out = _call_yum(cmd)
            if out['retcode'] != 0:
                errors.append(out['stdout'])

    __context__.pop('pkg.list_pkgs', None)
    new = list_pkgs(versions_as_list=False, attr=diff_attr) if not downloadonly else list_downloaded()

    ret = salt.utils.data.compare_dicts(old, new)

    for pkgname, _ in to_reinstall:
        if pkgname not in ret or pkgname in old:
            ret.update({pkgname: {'old': old.get(pkgname, ''),
                                  'new': new.get(pkgname, '')}})

    if unhold_prevented:
        errors.append(
            'The following package(s) could not be updated because they are '
            'being held: {0}. Set \'update_holds\' to True to temporarily '
            'unhold these packages so that they can be updated.'.format(
                ', '.join(unhold_prevented)
            )
        )

    if errors:
        raise CommandExecutionError(
            'Error occurred installing{0} package(s)'.format(
                '/reinstalling' if to_reinstall else ''
            ),
            info={'errors': errors, 'changes': ret}
        )

    return ret