1"""
2Support for APT (Advanced Packaging Tool)
3
4.. important::
5    If you feel that Salt should be using this module to manage packages on a
6    minion, and it is using a different module (or gives an error similar to
7    *'pkg.install' is not available*), see :ref:`here
8    <module-provider-override>`.
9
10    For repository management, the ``python-apt`` package must be installed.
11"""
12
13import copy
14import datetime
15import fnmatch
16import logging
17import os
18import pathlib
19import re
20import shutil
21import tempfile
22import time
23from urllib.error import HTTPError
24from urllib.request import Request as _Request
25from urllib.request import urlopen as _urlopen
26
27import salt.config
28import salt.syspaths
29import salt.utils.args
30import salt.utils.data
31import salt.utils.environment
32import salt.utils.files
33import salt.utils.functools
34import salt.utils.itertools
35import salt.utils.json
36import salt.utils.path
37import salt.utils.pkg
38import salt.utils.pkg.deb
39import salt.utils.stringutils
40import salt.utils.systemd
41import salt.utils.versions
42import salt.utils.yaml
43from salt.exceptions import (
44    CommandExecutionError,
45    CommandNotFoundError,
46    MinionError,
47    SaltInvocationError,
48)
49from salt.modules.cmdmod import _parse_env
50
51log = logging.getLogger(__name__)
52
53# pylint: disable=import-error
54try:
55    import apt.cache
56    import apt.debfile
57    from aptsources.sourceslist import (
58        SourceEntry,
59        SourcesList,
60    )
61
62    HAS_APT = True
63except ImportError:
64    HAS_APT = False
65
66try:
67    import apt_pkg
68
69    HAS_APTPKG = True
70except ImportError:
71    HAS_APTPKG = False
72
73try:
74    import softwareproperties.ppa
75
76    HAS_SOFTWAREPROPERTIES = True
77except ImportError:
78    HAS_SOFTWAREPROPERTIES = False
79# pylint: enable=import-error
80
81APT_LISTS_PATH = "/var/lib/apt/lists"
82PKG_ARCH_SEPARATOR = ":"
83
84# Source format for urllib fallback on PPA handling
85LP_SRC_FORMAT = "deb http://ppa.launchpad.net/{0}/{1}/ubuntu {2} main"
86LP_PVT_SRC_FORMAT = "deb https://{0}private-ppa.launchpad.net/{1}/{2}/ubuntu {3} main"
87
88_MODIFY_OK = frozenset(["uri", "comps", "architectures", "disabled", "file", "dist"])
89DPKG_ENV_VARS = {
90    "APT_LISTBUGS_FRONTEND": "none",
91    "APT_LISTCHANGES_FRONTEND": "none",
92    "DEBIAN_FRONTEND": "noninteractive",
93    "UCF_FORCE_CONFFOLD": "1",
94}
95
96# Define the module's virtual name
97__virtualname__ = "pkg"
98
99
100def __virtual__():
101    """
102    Confirm this module is on a Debian-based system
103    """
104    # If your minion is running an OS which is Debian-based but does not have
105    # an "os_family" grain of Debian, then the proper fix is NOT to check for
106    # the minion's "os_family" grain here in the __virtual__. The correct fix
107    # is to add the value from the minion's "os" grain to the _OS_FAMILY_MAP
108    # dict in salt/grains/core.py, so that we assign the correct "os_family"
109    # grain to the minion.
110    if __grains__.get("os_family") == "Debian":
111        return __virtualname__
112    return False, "The pkg module could not be loaded: unsupported OS family"
113
114
115def __init__(opts):
116    """
117    For Debian and derivative systems, set up
118    a few env variables to keep apt happy and
119    non-interactive.
120    """
121    if __virtual__() == __virtualname__:
122        # Export these puppies so they persist
123        os.environ.update(DPKG_ENV_VARS)
124
125
126if not HAS_APT:
127
128    class SourceEntry:  # pylint: disable=function-redefined
129        def __init__(self, line, file=None):
130            self.invalid = False
131            self.comps = []
132            self.disabled = False
133            self.comment = ""
134            self.dist = ""
135            self.type = ""
136            self.uri = ""
137            self.line = line
138            self.architectures = []
139            self.file = file
140            if not self.file:
141                self.file = str(pathlib.Path(os.sep, "etc", "apt", "sources.list"))
142            self._parse_sources(line)
143
144        def repo_line(self):
145            """
146            Return the repo line for the sources file
147            """
148            repo_line = []
149            if self.invalid:
150                return self.line
151            if self.disabled:
152                repo_line.append("#")
153
154            repo_line.append(self.type)
155            if self.architectures:
156                repo_line.append("[arch={}]".format(" ".join(self.architectures)))
157
158            repo_line = repo_line + [self.uri, self.dist, " ".join(self.comps)]
159            if self.comment:
160                repo_line.append("#{}".format(self.comment))
161            return " ".join(repo_line) + "\n"
162
163        def _parse_sources(self, line):
164            """
165            Parse lines from sources files
166            """
167            self.disabled = False
168            repo_line = self.line.strip().split()
169            if not repo_line:
170                self.invalid = True
171                return False
172            if repo_line[0].startswith("#"):
173                repo_line.pop(0)
174                self.disabled = True
175            if repo_line[0] not in ["deb", "deb-src", "rpm", "rpm-src"]:
176                self.invalid = True
177                return False
178            if repo_line[1].startswith("["):
179                opts = re.search(r"\[.*\]", self.line).group(0).strip("[]")
180                repo_line = [x for x in (line.strip("[]") for line in repo_line) if x]
181                for opt in opts.split():
182                    if opt.startswith("arch"):
183                        self.architectures.extend(opt.split("=", 1)[1].split(","))
184                    try:
185                        repo_line.pop(repo_line.index(opt))
186                    except ValueError:
187                        repo_line.pop(repo_line.index("[" + opt + "]"))
188            self.type = repo_line[0]
189            self.uri = repo_line[1]
190            self.dist = repo_line[2]
191            self.comps = repo_line[3:]
192
193    class SourcesList:  # pylint: disable=function-redefined
194        def __init__(self):
195            self.list = []
196            self.files = [
197                pathlib.Path(os.sep, "etc", "apt", "sources.list"),
198                pathlib.Path(os.sep, "etc", "apt", "sources.list.d"),
199            ]
200            for file in self.files:
201                if file.is_dir():
202                    for fp in file.glob("**/*.list"):
203                        self.add_file(file=fp)
204                else:
205                    self.add_file(file)
206
207        def __iter__(self):
208            yield from self.list
209
210        def add_file(self, file):
211            """
212            Add the lines of a file to self.list
213            """
214            if file.is_file():
215                with salt.utils.files.fopen(file) as source:
216                    for line in source:
217                        self.list.append(SourceEntry(line, file=str(file)))
218            else:
219                log.debug("The apt sources file %s does not exist", file)
220
221        def add(self, type, uri, dist, orig_comps, architectures):
222            repo_line = [
223                type,
224                " [arch={}] ".format(" ".join(architectures)) if architectures else "",
225                uri,
226                dist,
227                " ".join(orig_comps),
228            ]
229            return SourceEntry(" ".join(repo_line))
230
231        def remove(self, source):
232            """
233            remove a source from the list of sources
234            """
235            self.list.remove(source)
236
237        def save(self):
238            """
239            write all of the sources from the list of sources
240            to the file.
241            """
242            filemap = {}
243            with tempfile.TemporaryDirectory() as tmpdir:
244                for source in self.list:
245                    fname = pathlib.Path(tmpdir, pathlib.Path(source.file).name)
246                    with salt.utils.files.fopen(fname, "a") as fp:
247                        fp.write(source.repo_line())
248                    if source.file not in filemap:
249                        filemap[source.file] = {"tmp": fname}
250
251                for fp in filemap:
252                    shutil.move(filemap[fp]["tmp"], fp)
253
254
255def _get_ppa_info_from_launchpad(owner_name, ppa_name):
256    """
257    Idea from softwareproperties.ppa.
258    Uses urllib2 which sacrifices server cert verification.
259
260    This is used as fall-back code or for secure PPAs
261
262    :param owner_name:
263    :param ppa_name:
264    :return:
265    """
266
267    lp_url = "https://launchpad.net/api/1.0/~{}/+archive/{}".format(
268        owner_name, ppa_name
269    )
270    request = _Request(lp_url, headers={"Accept": "application/json"})
271    lp_page = _urlopen(request)
272    return salt.utils.json.load(lp_page)
273
274
275def _reconstruct_ppa_name(owner_name, ppa_name):
276    """
277    Stringify PPA name from args.
278    """
279    return "ppa:{}/{}".format(owner_name, ppa_name)
280
281
282def _call_apt(args, scope=True, **kwargs):
283    """
284    Call apt* utilities.
285    """
286    cmd = []
287    if (
288        scope
289        and salt.utils.systemd.has_scope(__context__)
290        and __salt__["config.get"]("systemd.scope", True)
291    ):
292        cmd.extend(["systemd-run", "--scope", "--description", '"{}"'.format(__name__)])
293    cmd.extend(args)
294
295    params = {
296        "output_loglevel": "trace",
297        "python_shell": False,
298        "env": salt.utils.environment.get_module_environment(globals()),
299    }
300    params.update(kwargs)
301
302    cmd_ret = __salt__["cmd.run_all"](cmd, **params)
303    count = 0
304    while "Could not get lock" in cmd_ret.get("stderr", "") and count < 10:
305        count += 1
306        log.warning("Waiting for dpkg lock release: retrying... %s/100", count)
307        time.sleep(2 ** count)
308        cmd_ret = __salt__["cmd.run_all"](cmd, **params)
309    return cmd_ret
310
311
312def _warn_software_properties(repo):
313    """
314    Warn of missing python-software-properties package.
315    """
316    log.warning(
317        "The 'python-software-properties' package is not installed. "
318        "For more accurate support of PPA repositories, you should "
319        "install this package."
320    )
321    log.warning("Best guess at ppa format: %s", repo)
322
323
324def normalize_name(name):
325    """
326    Strips the architecture from the specified package name, if necessary.
327
328    CLI Example:
329
330    .. code-block:: bash
331
332        salt '*' pkg.normalize_name zsh:amd64
333    """
334    try:
335        pkgname, pkgarch = name.rsplit(PKG_ARCH_SEPARATOR, 1)
336    except ValueError:
337        pkgname = name
338        pkgarch = __grains__["osarch"]
339
340    return pkgname if pkgarch in (__grains__["osarch"], "all", "any") else name
341
342
343def parse_arch(name):
344    """
345    Parse name and architecture from the specified package name.
346
347    CLI Example:
348
349    .. code-block:: bash
350
351        salt '*' pkg.parse_arch zsh:amd64
352    """
353    try:
354        _name, _arch = name.rsplit(PKG_ARCH_SEPARATOR, 1)
355    except ValueError:
356        _name, _arch = name, None
357    return {"name": _name, "arch": _arch}
358
359
360def latest_version(*names, **kwargs):
361    """
362    Return the latest version of the named package available for upgrade or
363    installation. If more than one package name is specified, a dict of
364    name/version pairs is returned.
365
366    If the latest version of a given package is already installed, an empty
367    string will be returned for that package.
368
369    A specific repo can be requested using the ``fromrepo`` keyword argument.
370
371    cache_valid_time
372
373        .. versionadded:: 2016.11.0
374
375        Skip refreshing the package database if refresh has already occurred within
376        <value> seconds
377
378    CLI Example:
379
380    .. code-block:: bash
381
382        salt '*' pkg.latest_version <package name>
383        salt '*' pkg.latest_version <package name> fromrepo=unstable
384        salt '*' pkg.latest_version <package1> <package2> <package3> ...
385    """
386    refresh = salt.utils.data.is_true(kwargs.pop("refresh", True))
387    show_installed = salt.utils.data.is_true(kwargs.pop("show_installed", False))
388    if "repo" in kwargs:
389        raise SaltInvocationError(
390            "The 'repo' argument is invalid, use 'fromrepo' instead"
391        )
392    fromrepo = kwargs.pop("fromrepo", None)
393    cache_valid_time = kwargs.pop("cache_valid_time", 0)
394
395    if not names:
396        return ""
397    ret = {}
398    # Initialize the dict with empty strings
399    for name in names:
400        ret[name] = ""
401    pkgs = list_pkgs(versions_as_list=True)
402    repo = ["-o", "APT::Default-Release={}".format(fromrepo)] if fromrepo else None
403
404    # Refresh before looking for the latest version available
405    if refresh:
406        refresh_db(cache_valid_time)
407
408    for name in names:
409        cmd = ["apt-cache", "-q", "policy", name]
410        if repo is not None:
411            cmd.extend(repo)
412        out = _call_apt(cmd, scope=False)
413
414        candidate = ""
415        for line in salt.utils.itertools.split(out["stdout"], "\n"):
416            if "Candidate" in line:
417                comps = line.split()
418                if len(comps) >= 2:
419                    candidate = comps[-1]
420                    if candidate.lower() == "(none)":
421                        candidate = ""
422                break
423
424        installed = pkgs.get(name, [])
425        if not installed:
426            ret[name] = candidate
427        elif installed and show_installed:
428            ret[name] = candidate
429        elif candidate:
430            # If there are no installed versions that are greater than or equal
431            # to the install candidate, then the candidate is an upgrade, so
432            # add it to the return dict
433            if not any(
434                salt.utils.versions.compare(
435                    ver1=x, oper=">=", ver2=candidate, cmp_func=version_cmp
436                )
437                for x in installed
438            ):
439                ret[name] = candidate
440
441    # Return a string if only one package name passed
442    if len(names) == 1:
443        return ret[names[0]]
444    return ret
445
446
447# available_version is being deprecated
448available_version = salt.utils.functools.alias_function(
449    latest_version, "available_version"
450)
451
452
453def version(*names, **kwargs):
454    """
455    Returns a string representing the package version or an empty string if not
456    installed. If more than one package name is specified, a dict of
457    name/version pairs is returned.
458
459    CLI Example:
460
461    .. code-block:: bash
462
463        salt '*' pkg.version <package name>
464        salt '*' pkg.version <package1> <package2> <package3> ...
465    """
466    return __salt__["pkg_resource.version"](*names, **kwargs)
467
468
469def refresh_db(cache_valid_time=0, failhard=False, **kwargs):
470    """
471    Updates the APT database to latest packages based upon repositories
472
473    Returns a dict, with the keys being package databases and the values being
474    the result of the update attempt. Values can be one of the following:
475
476    - ``True``: Database updated successfully
477    - ``False``: Problem updating database
478    - ``None``: Database already up-to-date
479
480    cache_valid_time
481
482        .. versionadded:: 2016.11.0
483
484        Skip refreshing the package database if refresh has already occurred within
485        <value> seconds
486
487    failhard
488
489        If False, return results of Err lines as ``False`` for the package database that
490        encountered the error.
491        If True, raise an error with a list of the package databases that encountered
492        errors.
493
494    CLI Example:
495
496    .. code-block:: bash
497
498        salt '*' pkg.refresh_db
499    """
500    # Remove rtag file to keep multiple refreshes from happening in pkg states
501    salt.utils.pkg.clear_rtag(__opts__)
502    failhard = salt.utils.data.is_true(failhard)
503    ret = {}
504    error_repos = list()
505
506    if cache_valid_time:
507        try:
508            latest_update = os.stat(APT_LISTS_PATH).st_mtime
509            now = time.time()
510            log.debug(
511                "now: %s, last update time: %s, expire after: %s seconds",
512                now,
513                latest_update,
514                cache_valid_time,
515            )
516            if latest_update + cache_valid_time > now:
517                return ret
518        except TypeError as exp:
519            log.warning(
520                "expected integer for cache_valid_time parameter, failed with: %s", exp
521            )
522        except OSError as exp:
523            log.warning("could not stat cache directory due to: %s", exp)
524
525    call = _call_apt(["apt-get", "-q", "update"], scope=False)
526    if call["retcode"] != 0:
527        comment = ""
528        if "stderr" in call:
529            comment += call["stderr"]
530
531        raise CommandExecutionError(comment)
532    else:
533        out = call["stdout"]
534
535    for line in out.splitlines():
536        cols = line.split()
537        if not cols:
538            continue
539        ident = " ".join(cols[1:])
540        if "Get" in cols[0]:
541            # Strip filesize from end of line
542            ident = re.sub(r" \[.+B\]$", "", ident)
543            ret[ident] = True
544        elif "Ign" in cols[0]:
545            ret[ident] = False
546        elif "Hit" in cols[0]:
547            ret[ident] = None
548        elif "Err" in cols[0]:
549            ret[ident] = False
550            error_repos.append(ident)
551
552    if failhard and error_repos:
553        raise CommandExecutionError(
554            "Error getting repos: {}".format(", ".join(error_repos))
555        )
556
557    return ret
558
559
560def install(
561    name=None,
562    refresh=False,
563    fromrepo=None,
564    skip_verify=False,
565    debconf=None,
566    pkgs=None,
567    sources=None,
568    reinstall=False,
569    downloadonly=False,
570    ignore_epoch=False,
571    **kwargs
572):
573    """
574    .. versionchanged:: 2015.8.12,2016.3.3,2016.11.0
575        On minions running systemd>=205, `systemd-run(1)`_ is now used to
576        isolate commands which modify installed packages from the
577        ``salt-minion`` daemon's control group. This is done to keep systemd
578        from killing any apt-get/dpkg commands spawned by Salt when the
579        ``salt-minion`` service is restarted. (see ``KillMode`` in the
580        `systemd.kill(5)`_ manpage for more information). If desired, usage of
581        `systemd-run(1)`_ can be suppressed by setting a :mod:`config option
582        <salt.modules.config.get>` called ``systemd.scope``, with a value of
583        ``False`` (no quotes).
584
585    .. _`systemd-run(1)`: https://www.freedesktop.org/software/systemd/man/systemd-run.html
586    .. _`systemd.kill(5)`: https://www.freedesktop.org/software/systemd/man/systemd.kill.html
587
588    Install the passed package, add refresh=True to update the dpkg database.
589
590    name
591        The name of the package to be installed. Note that this parameter is
592        ignored if either "pkgs" or "sources" is passed. Additionally, please
593        note that this option can only be used to install packages from a
594        software repository. To install a package file manually, use the
595        "sources" option.
596
597        32-bit packages can be installed on 64-bit systems by appending the
598        architecture designation (``:i386``, etc.) to the end of the package
599        name.
600
601        CLI Example:
602
603        .. code-block:: bash
604
605            salt '*' pkg.install <package name>
606
607    refresh
608        Whether or not to refresh the package database before installing.
609
610    cache_valid_time
611
612        .. versionadded:: 2016.11.0
613
614        Skip refreshing the package database if refresh has already occurred within
615        <value> seconds
616
617    fromrepo
618        Specify a package repository to install from
619        (e.g., ``apt-get -t unstable install somepackage``)
620
621    skip_verify
622        Skip the GPG verification check (e.g., ``--allow-unauthenticated``, or
623        ``--force-bad-verify`` for install from package file).
624
625    debconf
626        Provide the path to a debconf answers file, processed before
627        installation.
628
629    version
630        Install a specific version of the package, e.g. 1.2.3~0ubuntu0. Ignored
631        if "pkgs" or "sources" is passed.
632
633        .. versionchanged:: 2018.3.0
634            version can now contain comparison operators (e.g. ``>1.2.3``,
635            ``<=2.0``, etc.)
636
637    reinstall : False
638        Specifying reinstall=True will use ``apt-get install --reinstall``
639        rather than simply ``apt-get install`` for requested packages that are
640        already installed.
641
642        If a version is specified with the requested package, then ``apt-get
643        install --reinstall`` will only be used if the installed version
644        matches the requested version.
645
646        .. versionadded:: 2015.8.0
647
648    ignore_epoch : False
649        Only used when the version of a package is specified using a comparison
650        operator (e.g. ``>4.1``). If set to ``True``, then the epoch will be
651        ignored when comparing the currently-installed version to the desired
652        version.
653
654        .. versionadded:: 2018.3.0
655
656    Multiple Package Installation Options:
657
658    pkgs
659        A list of packages to install from a software repository. Must be
660        passed as a python list.
661
662        CLI Example:
663
664        .. code-block:: bash
665
666            salt '*' pkg.install pkgs='["foo", "bar"]'
667            salt '*' pkg.install pkgs='["foo", {"bar": "1.2.3-0ubuntu0"}]'
668
669    sources
670        A list of DEB packages to install. Must be passed as a list of dicts,
671        with the keys being package names, and the values being the source URI
672        or local path to the package.  Dependencies are automatically resolved
673        and marked as auto-installed.
674
675        32-bit packages can be installed on 64-bit systems by appending the
676        architecture designation (``:i386``, etc.) to the end of the package
677        name.
678
679        .. versionchanged:: 2014.7.0
680
681        CLI Example:
682
683        .. code-block:: bash
684
685            salt '*' pkg.install sources='[{"foo": "salt://foo.deb"},{"bar": "salt://bar.deb"}]'
686
687    force_yes
688        Passes ``--force-yes`` to the apt-get command.  Don't use this unless
689        you know what you're doing.
690
691        .. versionadded:: 0.17.4
692
693    install_recommends
694        Whether to install the packages marked as recommended.  Default is True.
695
696        .. versionadded:: 2015.5.0
697
698    only_upgrade
699        Only upgrade the packages, if they are already installed. Default is False.
700
701        .. versionadded:: 2015.5.0
702
703    force_conf_new
704        Always install the new version of any configuration files.
705
706        .. versionadded:: 2015.8.0
707
708    Returns a dict containing the new package names and versions::
709
710        {'<package>': {'old': '<old-version>',
711                       'new': '<new-version>'}}
712    """
713    _refresh_db = False
714    if salt.utils.data.is_true(refresh):
715        _refresh_db = True
716        if "version" in kwargs and kwargs["version"]:
717            _refresh_db = False
718            _latest_version = latest_version(name, refresh=False, show_installed=True)
719            _version = kwargs.get("version")
720            # If the versions don't match, refresh is True, otherwise no need
721            # to refresh
722            if not _latest_version == _version:
723                _refresh_db = True
724
725        if pkgs:
726            _refresh_db = False
727            for pkg in pkgs:
728                if isinstance(pkg, dict):
729                    _name = next(iter(pkg.keys()))
730                    _latest_version = latest_version(
731                        _name, refresh=False, show_installed=True
732                    )
733                    _version = pkg[_name]
734                    # If the versions don't match, refresh is True, otherwise
735                    # no need to refresh
736                    if not _latest_version == _version:
737                        _refresh_db = True
738                else:
739                    # No version specified, so refresh should be True
740                    _refresh_db = True
741
742    if debconf:
743        __salt__["debconf.set_file"](debconf)
744
745    try:
746        pkg_params, pkg_type = __salt__["pkg_resource.parse_targets"](
747            name, pkgs, sources, **kwargs
748        )
749    except MinionError as exc:
750        raise CommandExecutionError(exc)
751
752    # Support old "repo" argument
753    repo = kwargs.get("repo", "")
754    if not fromrepo and repo:
755        fromrepo = repo
756
757    if not pkg_params:
758        return {}
759
760    cmd_prefix = []
761
762    old = list_pkgs()
763    targets = []
764    downgrade = []
765    to_reinstall = {}
766    errors = []
767    if pkg_type == "repository":
768        pkg_params_items = list(pkg_params.items())
769        has_comparison = [
770            x
771            for x, y in pkg_params_items
772            if y is not None and (y.startswith("<") or y.startswith(">"))
773        ]
774        _available = (
775            list_repo_pkgs(*has_comparison, byrepo=False, **kwargs)
776            if has_comparison
777            else {}
778        )
779        # Build command prefix
780        cmd_prefix.extend(["apt-get", "-q", "-y"])
781        if kwargs.get("force_yes", False):
782            cmd_prefix.append("--force-yes")
783        if "force_conf_new" in kwargs and kwargs["force_conf_new"]:
784            cmd_prefix.extend(["-o", "DPkg::Options::=--force-confnew"])
785        else:
786            cmd_prefix.extend(["-o", "DPkg::Options::=--force-confold"])
787            cmd_prefix += ["-o", "DPkg::Options::=--force-confdef"]
788        if "install_recommends" in kwargs:
789            if not kwargs["install_recommends"]:
790                cmd_prefix.append("--no-install-recommends")
791            else:
792                cmd_prefix.append("--install-recommends")
793        if "only_upgrade" in kwargs and kwargs["only_upgrade"]:
794            cmd_prefix.append("--only-upgrade")
795        if skip_verify:
796            cmd_prefix.append("--allow-unauthenticated")
797        if fromrepo:
798            cmd_prefix.extend(["-t", fromrepo])
799        cmd_prefix.append("install")
800    else:
801        pkg_params_items = []
802        for pkg_source in pkg_params:
803            if "lowpkg.bin_pkg_info" in __salt__:
804                deb_info = __salt__["lowpkg.bin_pkg_info"](pkg_source)
805            else:
806                deb_info = None
807            if deb_info is None:
808                log.error(
809                    "pkg.install: Unable to get deb information for %s. "
810                    "Version comparisons will be unavailable.",
811                    pkg_source,
812                )
813                pkg_params_items.append([pkg_source])
814            else:
815                pkg_params_items.append(
816                    [deb_info["name"], pkg_source, deb_info["version"]]
817                )
818        # Build command prefix
819        if "force_conf_new" in kwargs and kwargs["force_conf_new"]:
820            cmd_prefix.extend(["dpkg", "-i", "--force-confnew"])
821        else:
822            cmd_prefix.extend(["dpkg", "-i", "--force-confold"])
823        if skip_verify:
824            cmd_prefix.append("--force-bad-verify")
825        if HAS_APT:
826            _resolve_deps(name, pkg_params, **kwargs)
827
828    for pkg_item_list in pkg_params_items:
829        if pkg_type == "repository":
830            pkgname, version_num = pkg_item_list
831            if name and pkgs is None and kwargs.get("version") and len(pkg_params) == 1:
832                # Only use the 'version' param if 'name' was not specified as a
833                # comma-separated list
834                version_num = kwargs["version"]
835        else:
836            try:
837                pkgname, pkgpath, version_num = pkg_item_list
838            except ValueError:
839                pkgname = None
840                pkgpath = pkg_item_list[0]
841                version_num = None
842
843        if version_num is None:
844            if pkg_type == "repository":
845                if reinstall and pkgname in old:
846                    to_reinstall[pkgname] = pkgname
847                else:
848                    targets.append(pkgname)
849            else:
850                targets.append(pkgpath)
851        else:
852            # If we are installing a package file and not one from the repo,
853            # and version_num is not None, then we can assume that pkgname is
854            # not None, since the only way version_num is not None is if DEB
855            # metadata parsing was successful.
856            if pkg_type == "repository":
857                # Remove leading equals sign(s) to keep from building a pkgstr
858                # with multiple equals (which would be invalid)
859                version_num = version_num.lstrip("=")
860                if pkgname in has_comparison:
861                    candidates = _available.get(pkgname, [])
862                    target = salt.utils.pkg.match_version(
863                        version_num,
864                        candidates,
865                        cmp_func=version_cmp,
866                        ignore_epoch=ignore_epoch,
867                    )
868                    if target is None:
869                        errors.append(
870                            "No version matching '{}{}' could be found "
871                            "(available: {})".format(
872                                pkgname,
873                                version_num,
874                                ", ".join(candidates) if candidates else None,
875                            )
876                        )
877                        continue
878                    else:
879                        version_num = target
880                pkgstr = "{}={}".format(pkgname, version_num)
881            else:
882                pkgstr = pkgpath
883
884            cver = old.get(pkgname, "")
885            if (
886                reinstall
887                and cver
888                and salt.utils.versions.compare(
889                    ver1=version_num, oper="==", ver2=cver, cmp_func=version_cmp
890                )
891            ):
892                to_reinstall[pkgname] = pkgstr
893            elif not cver or salt.utils.versions.compare(
894                ver1=version_num, oper=">=", ver2=cver, cmp_func=version_cmp
895            ):
896                targets.append(pkgstr)
897            else:
898                downgrade.append(pkgstr)
899
900    if fromrepo and not sources:
901        log.info("Targeting repo '%s'", fromrepo)
902
903    cmds = []
904    all_pkgs = []
905    if targets:
906        all_pkgs.extend(targets)
907        cmd = copy.deepcopy(cmd_prefix)
908        cmd.extend(targets)
909        cmds.append(cmd)
910
911    if downgrade:
912        cmd = copy.deepcopy(cmd_prefix)
913        if pkg_type == "repository" and "--force-yes" not in cmd:
914            # Downgrading requires --force-yes. Insert this before 'install'
915            cmd.insert(-1, "--force-yes")
916        cmd.extend(downgrade)
917        cmds.append(cmd)
918
919    if downloadonly:
920        cmd.append("--download-only")
921
922    if to_reinstall:
923        all_pkgs.extend(to_reinstall)
924        cmd = copy.deepcopy(cmd_prefix)
925        if not sources:
926            cmd.append("--reinstall")
927        cmd.extend([x for x in to_reinstall.values()])
928        cmds.append(cmd)
929
930    if not cmds:
931        ret = {}
932    else:
933        cache_valid_time = kwargs.pop("cache_valid_time", 0)
934        if _refresh_db:
935            refresh_db(cache_valid_time)
936
937        env = _parse_env(kwargs.get("env"))
938        env.update(DPKG_ENV_VARS.copy())
939
940        hold_pkgs = get_selections(state="hold").get("hold", [])
941        # all_pkgs contains the argument to be passed to apt-get install, which
942        # when a specific version is requested will be in the format
943        # name=version.  Strip off the '=' if present so we can compare the
944        # held package names against the packages we are trying to install.
945        targeted_names = [x.split("=")[0] for x in all_pkgs]
946        to_unhold = [x for x in hold_pkgs if x in targeted_names]
947
948        if to_unhold:
949            unhold(pkgs=to_unhold)
950
951        for cmd in cmds:
952            out = _call_apt(cmd)
953            if out["retcode"] != 0 and out["stderr"]:
954                errors.append(out["stderr"])
955
956        __context__.pop("pkg.list_pkgs", None)
957        new = list_pkgs()
958        ret = salt.utils.data.compare_dicts(old, new)
959
960        for pkgname in to_reinstall:
961            if pkgname not in ret or pkgname in old:
962                ret.update(
963                    {
964                        pkgname: {
965                            "old": old.get(pkgname, ""),
966                            "new": new.get(pkgname, ""),
967                        }
968                    }
969                )
970
971        if to_unhold:
972            hold(pkgs=to_unhold)
973
974    if errors:
975        raise CommandExecutionError(
976            "Problem encountered installing package(s)",
977            info={"errors": errors, "changes": ret},
978        )
979
980    return ret
981
982
983def _uninstall(action="remove", name=None, pkgs=None, **kwargs):
984    """
985    remove and purge do identical things but with different apt-get commands,
986    this function performs the common logic.
987    """
988    try:
989        pkg_params = __salt__["pkg_resource.parse_targets"](name, pkgs)[0]
990    except MinionError as exc:
991        raise CommandExecutionError(exc)
992
993    old = list_pkgs()
994    old_removed = list_pkgs(removed=True)
995    targets = [x for x in pkg_params if x in old]
996    if action == "purge":
997        targets.extend([x for x in pkg_params if x in old_removed])
998    if not targets:
999        return {}
1000    cmd = ["apt-get", "-q", "-y", action]
1001    cmd.extend(targets)
1002    env = _parse_env(kwargs.get("env"))
1003    env.update(DPKG_ENV_VARS.copy())
1004    out = _call_apt(cmd, env=env)
1005    if out["retcode"] != 0 and out["stderr"]:
1006        errors = [out["stderr"]]
1007    else:
1008        errors = []
1009
1010    __context__.pop("pkg.list_pkgs", None)
1011    new = list_pkgs()
1012    new_removed = list_pkgs(removed=True)
1013
1014    changes = salt.utils.data.compare_dicts(old, new)
1015    if action == "purge":
1016        ret = {
1017            "removed": salt.utils.data.compare_dicts(old_removed, new_removed),
1018            "installed": changes,
1019        }
1020    else:
1021        ret = changes
1022
1023    if errors:
1024        raise CommandExecutionError(
1025            "Problem encountered removing package(s)",
1026            info={"errors": errors, "changes": ret},
1027        )
1028
1029    return ret
1030
1031
1032def autoremove(list_only=False, purge=False):
1033    """
1034    .. versionadded:: 2015.5.0
1035
1036    Remove packages not required by another package using ``apt-get
1037    autoremove``.
1038
1039    list_only : False
1040        Only retrieve the list of packages to be auto-removed, do not actually
1041        perform the auto-removal.
1042
1043    purge : False
1044        Also remove package config data when autoremoving packages.
1045
1046        .. versionadded:: 2015.8.0
1047
1048    CLI Example:
1049
1050    .. code-block:: bash
1051
1052        salt '*' pkg.autoremove
1053        salt '*' pkg.autoremove list_only=True
1054        salt '*' pkg.autoremove purge=True
1055    """
1056    cmd = []
1057    if list_only:
1058        ret = []
1059        cmd.extend(["apt-get", "--assume-no"])
1060        if purge:
1061            cmd.append("--purge")
1062        cmd.append("autoremove")
1063        out = _call_apt(cmd, ignore_retcode=True)["stdout"]
1064        found = False
1065        for line in out.splitlines():
1066            if found is True:
1067                if line.startswith(" "):
1068                    ret.extend(line.split())
1069                else:
1070                    found = False
1071            elif "The following packages will be REMOVED:" in line:
1072                found = True
1073        ret.sort()
1074        return ret
1075    else:
1076        old = list_pkgs()
1077        cmd.extend(["apt-get", "--assume-yes"])
1078        if purge:
1079            cmd.append("--purge")
1080        cmd.append("autoremove")
1081        _call_apt(cmd, ignore_retcode=True)
1082        __context__.pop("pkg.list_pkgs", None)
1083        new = list_pkgs()
1084        return salt.utils.data.compare_dicts(old, new)
1085
1086
1087def remove(name=None, pkgs=None, **kwargs):
1088    """
1089    .. versionchanged:: 2015.8.12,2016.3.3,2016.11.0
1090        On minions running systemd>=205, `systemd-run(1)`_ is now used to
1091        isolate commands which modify installed packages from the
1092        ``salt-minion`` daemon's control group. This is done to keep systemd
1093        from killing any apt-get/dpkg commands spawned by Salt when the
1094        ``salt-minion`` service is restarted. (see ``KillMode`` in the
1095        `systemd.kill(5)`_ manpage for more information). If desired, usage of
1096        `systemd-run(1)`_ can be suppressed by setting a :mod:`config option
1097        <salt.modules.config.get>` called ``systemd.scope``, with a value of
1098        ``False`` (no quotes).
1099
1100    .. _`systemd-run(1)`: https://www.freedesktop.org/software/systemd/man/systemd-run.html
1101    .. _`systemd.kill(5)`: https://www.freedesktop.org/software/systemd/man/systemd.kill.html
1102
1103    Remove packages using ``apt-get remove``.
1104
1105    name
1106        The name of the package to be deleted.
1107
1108
1109    Multiple Package Options:
1110
1111    pkgs
1112        A list of packages to delete. Must be passed as a python list. The
1113        ``name`` parameter will be ignored if this option is passed.
1114
1115    .. versionadded:: 0.16.0
1116
1117
1118    Returns a dict containing the changes.
1119
1120    CLI Example:
1121
1122    .. code-block:: bash
1123
1124        salt '*' pkg.remove <package name>
1125        salt '*' pkg.remove <package1>,<package2>,<package3>
1126        salt '*' pkg.remove pkgs='["foo", "bar"]'
1127    """
1128    return _uninstall(action="remove", name=name, pkgs=pkgs, **kwargs)
1129
1130
1131def purge(name=None, pkgs=None, **kwargs):
1132    """
1133    .. versionchanged:: 2015.8.12,2016.3.3,2016.11.0
1134        On minions running systemd>=205, `systemd-run(1)`_ is now used to
1135        isolate commands which modify installed packages from the
1136        ``salt-minion`` daemon's control group. This is done to keep systemd
1137        from killing any apt-get/dpkg commands spawned by Salt when the
1138        ``salt-minion`` service is restarted. (see ``KillMode`` in the
1139        `systemd.kill(5)`_ manpage for more information). If desired, usage of
1140        `systemd-run(1)`_ can be suppressed by setting a :mod:`config option
1141        <salt.modules.config.get>` called ``systemd.scope``, with a value of
1142        ``False`` (no quotes).
1143
1144    .. _`systemd-run(1)`: https://www.freedesktop.org/software/systemd/man/systemd-run.html
1145    .. _`systemd.kill(5)`: https://www.freedesktop.org/software/systemd/man/systemd.kill.html
1146
1147    Remove packages via ``apt-get purge`` along with all configuration files.
1148
1149    name
1150        The name of the package to be deleted.
1151
1152
1153    Multiple Package Options:
1154
1155    pkgs
1156        A list of packages to delete. Must be passed as a python list. The
1157        ``name`` parameter will be ignored if this option is passed.
1158
1159    .. versionadded:: 0.16.0
1160
1161
1162    Returns a dict containing the changes.
1163
1164    CLI Example:
1165
1166    .. code-block:: bash
1167
1168        salt '*' pkg.purge <package name>
1169        salt '*' pkg.purge <package1>,<package2>,<package3>
1170        salt '*' pkg.purge pkgs='["foo", "bar"]'
1171    """
1172    return _uninstall(action="purge", name=name, pkgs=pkgs, **kwargs)
1173
1174
1175def upgrade(refresh=True, dist_upgrade=False, **kwargs):
1176    """
1177    .. versionchanged:: 2015.8.12,2016.3.3,2016.11.0
1178        On minions running systemd>=205, `systemd-run(1)`_ is now used to
1179        isolate commands which modify installed packages from the
1180        ``salt-minion`` daemon's control group. This is done to keep systemd
1181        from killing any apt-get/dpkg commands spawned by Salt when the
1182        ``salt-minion`` service is restarted. (see ``KillMode`` in the
1183        `systemd.kill(5)`_ manpage for more information). If desired, usage of
1184        `systemd-run(1)`_ can be suppressed by setting a :mod:`config option
1185        <salt.modules.config.get>` called ``systemd.scope``, with a value of
1186        ``False`` (no quotes).
1187
1188    .. _`systemd-run(1)`: https://www.freedesktop.org/software/systemd/man/systemd-run.html
1189    .. _`systemd.kill(5)`: https://www.freedesktop.org/software/systemd/man/systemd.kill.html
1190
1191    Upgrades all packages via ``apt-get upgrade`` or ``apt-get dist-upgrade``
1192    if  ``dist_upgrade`` is ``True``.
1193
1194    Returns a dictionary containing the changes:
1195
1196    .. code-block:: python
1197
1198        {'<package>':  {'old': '<old-version>',
1199                        'new': '<new-version>'}}
1200
1201    dist_upgrade
1202        Whether to perform the upgrade using dist-upgrade vs upgrade.  Default
1203        is to use upgrade.
1204
1205        .. versionadded:: 2014.7.0
1206
1207    refresh : True
1208        If ``True``, the apt cache will be refreshed first. By default,
1209        this is ``True`` and a refresh is performed.
1210
1211    cache_valid_time
1212
1213        .. versionadded:: 2016.11.0
1214
1215        Skip refreshing the package database if refresh has already occurred within
1216        <value> seconds
1217
1218    download_only (or downloadonly)
1219        Only download the packages, don't unpack or install them. Use
1220        downloadonly to be in line with yum and zypper module.
1221
1222        .. versionadded:: 2018.3.0
1223
1224    force_conf_new
1225        Always install the new version of any configuration files.
1226
1227        .. versionadded:: 2015.8.0
1228
1229    CLI Example:
1230
1231    .. code-block:: bash
1232
1233        salt '*' pkg.upgrade
1234    """
1235    cache_valid_time = kwargs.pop("cache_valid_time", 0)
1236    if salt.utils.data.is_true(refresh):
1237        refresh_db(cache_valid_time)
1238
1239    old = list_pkgs()
1240    if "force_conf_new" in kwargs and kwargs["force_conf_new"]:
1241        dpkg_options = ["--force-confnew"]
1242    else:
1243        dpkg_options = ["--force-confold", "--force-confdef"]
1244    cmd = [
1245        "apt-get",
1246        "-q",
1247        "-y",
1248    ]
1249    for option in dpkg_options:
1250        cmd.append("-o")
1251        cmd.append("DPkg::Options::={}".format(option))
1252
1253    if kwargs.get("force_yes", False):
1254        cmd.append("--force-yes")
1255    if kwargs.get("skip_verify", False):
1256        cmd.append("--allow-unauthenticated")
1257    if kwargs.get("download_only", False) or kwargs.get("downloadonly", False):
1258        cmd.append("--download-only")
1259
1260    cmd.append("dist-upgrade" if dist_upgrade else "upgrade")
1261    result = _call_apt(cmd, env=DPKG_ENV_VARS.copy())
1262    __context__.pop("pkg.list_pkgs", None)
1263    new = list_pkgs()
1264    ret = salt.utils.data.compare_dicts(old, new)
1265
1266    if result["retcode"] != 0:
1267        raise CommandExecutionError(
1268            "Problem encountered upgrading packages",
1269            info={"changes": ret, "result": result},
1270        )
1271
1272    return ret
1273
1274
1275def hold(name=None, pkgs=None, sources=None, **kwargs):  # pylint: disable=W0613
1276    """
1277    .. versionadded:: 2014.7.0
1278
1279    Set package in 'hold' state, meaning it will not be upgraded.
1280
1281    name
1282        The name of the package, e.g., 'tmux'
1283
1284        CLI Example:
1285
1286        .. code-block:: bash
1287
1288            salt '*' pkg.hold <package name>
1289
1290    pkgs
1291        A list of packages to hold. Must be passed as a python list.
1292
1293        CLI Example:
1294
1295        .. code-block:: bash
1296
1297            salt '*' pkg.hold pkgs='["foo", "bar"]'
1298    """
1299    if not name and not pkgs and not sources:
1300        raise SaltInvocationError("One of name, pkgs, or sources must be specified.")
1301    if pkgs and sources:
1302        raise SaltInvocationError("Only one of pkgs or sources can be specified.")
1303
1304    targets = []
1305    if pkgs:
1306        targets.extend(pkgs)
1307    elif sources:
1308        for source in sources:
1309            targets.append(next(iter(source)))
1310    else:
1311        targets.append(name)
1312
1313    ret = {}
1314    for target in targets:
1315        if isinstance(target, dict):
1316            target = next(iter(target))
1317
1318        ret[target] = {"name": target, "changes": {}, "result": False, "comment": ""}
1319
1320        state = get_selections(pattern=target, state="hold")
1321        if not state:
1322            ret[target]["comment"] = "Package {} not currently held.".format(target)
1323        elif not salt.utils.data.is_true(state.get("hold", False)):
1324            if "test" in __opts__ and __opts__["test"]:
1325                ret[target].update(result=None)
1326                ret[target]["comment"] = "Package {} is set to be held.".format(target)
1327            else:
1328                result = set_selections(selection={"hold": [target]})
1329                ret[target].update(changes=result[target], result=True)
1330                ret[target]["comment"] = "Package {} is now being held.".format(target)
1331        else:
1332            ret[target].update(result=True)
1333            ret[target]["comment"] = "Package {} is already set to be held.".format(
1334                target
1335            )
1336    return ret
1337
1338
1339def unhold(name=None, pkgs=None, sources=None, **kwargs):  # pylint: disable=W0613
1340    """
1341    .. versionadded:: 2014.7.0
1342
1343    Set package current in 'hold' state to install state,
1344    meaning it will be upgraded.
1345
1346    name
1347        The name of the package, e.g., 'tmux'
1348
1349        CLI Example:
1350
1351        .. code-block:: bash
1352
1353            salt '*' pkg.unhold <package name>
1354
1355    pkgs
1356        A list of packages to unhold. Must be passed as a python list.
1357
1358        CLI Example:
1359
1360        .. code-block:: bash
1361
1362            salt '*' pkg.unhold pkgs='["foo", "bar"]'
1363    """
1364    if not name and not pkgs and not sources:
1365        raise SaltInvocationError("One of name, pkgs, or sources must be specified.")
1366    if pkgs and sources:
1367        raise SaltInvocationError("Only one of pkgs or sources can be specified.")
1368
1369    targets = []
1370    if pkgs:
1371        targets.extend(pkgs)
1372    elif sources:
1373        for source in sources:
1374            targets.append(next(iter(source)))
1375    else:
1376        targets.append(name)
1377
1378    ret = {}
1379    for target in targets:
1380        if isinstance(target, dict):
1381            target = next(iter(target))
1382
1383        ret[target] = {"name": target, "changes": {}, "result": False, "comment": ""}
1384
1385        state = get_selections(pattern=target)
1386        if not state:
1387            ret[target]["comment"] = "Package {} does not have a state.".format(target)
1388        elif salt.utils.data.is_true(state.get("hold", False)):
1389            if "test" in __opts__ and __opts__["test"]:
1390                ret[target].update(result=None)
1391                ret[target]["comment"] = "Package {} is set not to be held.".format(
1392                    target
1393                )
1394            else:
1395                result = set_selections(selection={"install": [target]})
1396                ret[target].update(changes=result[target], result=True)
1397                ret[target]["comment"] = "Package {} is no longer being held.".format(
1398                    target
1399                )
1400        else:
1401            ret[target].update(result=True)
1402            ret[target]["comment"] = "Package {} is already set not to be held.".format(
1403                target
1404            )
1405    return ret
1406
1407
1408def _list_pkgs_from_context(versions_as_list, removed, purge_desired):
1409    """
1410    Use pkg list from __context__
1411    """
1412    if removed:
1413        ret = copy.deepcopy(__context__["pkg.list_pkgs"]["removed"])
1414    else:
1415        ret = copy.deepcopy(__context__["pkg.list_pkgs"]["purge_desired"])
1416        if not purge_desired:
1417            ret.update(__context__["pkg.list_pkgs"]["installed"])
1418    if not versions_as_list:
1419        __salt__["pkg_resource.stringify"](ret)
1420    return ret
1421
1422
1423def list_pkgs(
1424    versions_as_list=False, removed=False, purge_desired=False, **kwargs
1425):  # pylint: disable=W0613
1426    """
1427    List the packages currently installed in a dict::
1428
1429        {'<package_name>': '<version>'}
1430
1431    removed
1432        If ``True``, then only packages which have been removed (but not
1433        purged) will be returned.
1434
1435    purge_desired
1436        If ``True``, then only packages which have been marked to be purged,
1437        but can't be purged due to their status as dependencies for other
1438        installed packages, will be returned. Note that these packages will
1439        appear in installed
1440
1441        .. versionchanged:: 2014.1.1
1442
1443            Packages in this state now correctly show up in the output of this
1444            function.
1445
1446    CLI Example:
1447
1448    .. code-block:: bash
1449
1450        salt '*' pkg.list_pkgs
1451        salt '*' pkg.list_pkgs versions_as_list=True
1452    """
1453    versions_as_list = salt.utils.data.is_true(versions_as_list)
1454    removed = salt.utils.data.is_true(removed)
1455    purge_desired = salt.utils.data.is_true(purge_desired)
1456
1457    if "pkg.list_pkgs" in __context__ and kwargs.get("use_context", True):
1458        return _list_pkgs_from_context(versions_as_list, removed, purge_desired)
1459
1460    ret = {"installed": {}, "removed": {}, "purge_desired": {}}
1461    cmd = [
1462        "dpkg-query",
1463        "--showformat",
1464        "${Status} ${Package} ${Version} ${Architecture}\n",
1465        "-W",
1466    ]
1467
1468    out = __salt__["cmd.run_stdout"](cmd, output_loglevel="trace", python_shell=False)
1469    # Typical lines of output:
1470    # install ok installed zsh 4.3.17-1ubuntu1 amd64
1471    # deinstall ok config-files mc 3:4.8.1-2ubuntu1 amd64
1472    for line in out.splitlines():
1473        cols = line.split()
1474        try:
1475            linetype, status, name, version_num, arch = [
1476                cols[x] for x in (0, 2, 3, 4, 5)
1477            ]
1478        except (ValueError, IndexError):
1479            continue
1480        if __grains__.get("cpuarch", "") == "x86_64":
1481            osarch = __grains__.get("osarch", "")
1482            if arch != "all" and osarch == "amd64" and osarch != arch:
1483                name += ":{}".format(arch)
1484        if cols:
1485            if ("install" in linetype or "hold" in linetype) and "installed" in status:
1486                __salt__["pkg_resource.add_pkg"](ret["installed"], name, version_num)
1487            elif "deinstall" in linetype:
1488                __salt__["pkg_resource.add_pkg"](ret["removed"], name, version_num)
1489            elif "purge" in linetype and status == "installed":
1490                __salt__["pkg_resource.add_pkg"](
1491                    ret["purge_desired"], name, version_num
1492                )
1493
1494    for pkglist_type in ("installed", "removed", "purge_desired"):
1495        __salt__["pkg_resource.sort_pkglist"](ret[pkglist_type])
1496
1497    __context__["pkg.list_pkgs"] = copy.deepcopy(ret)
1498
1499    if removed:
1500        ret = ret["removed"]
1501    else:
1502        ret = copy.deepcopy(__context__["pkg.list_pkgs"]["purge_desired"])
1503        if not purge_desired:
1504            ret.update(__context__["pkg.list_pkgs"]["installed"])
1505    if not versions_as_list:
1506        __salt__["pkg_resource.stringify"](ret)
1507    return ret
1508
1509
1510def _get_upgradable(dist_upgrade=True, **kwargs):
1511    """
1512    Utility function to get upgradable packages
1513
1514    Sample return data:
1515    { 'pkgname': '1.2.3-45', ... }
1516    """
1517
1518    cmd = ["apt-get", "--just-print"]
1519    if dist_upgrade:
1520        cmd.append("dist-upgrade")
1521    else:
1522        cmd.append("upgrade")
1523    try:
1524        cmd.extend(["-o", "APT::Default-Release={}".format(kwargs["fromrepo"])])
1525    except KeyError:
1526        pass
1527
1528    call = _call_apt(cmd)
1529    if call["retcode"] != 0:
1530        msg = "Failed to get upgrades"
1531        for key in ("stderr", "stdout"):
1532            if call[key]:
1533                msg += ": " + call[key]
1534                break
1535        raise CommandExecutionError(msg)
1536    else:
1537        out = call["stdout"]
1538
1539    # rexp parses lines that look like the following:
1540    # Conf libxfont1 (1:1.4.5-1 Debian:testing [i386])
1541    rexp = re.compile("(?m)^Conf " "([^ ]+) " r"\(([^ ]+)")  # Package name  # Version
1542    keys = ["name", "version"]
1543    _get = lambda l, k: l[keys.index(k)]
1544
1545    upgrades = rexp.findall(out)
1546
1547    ret = {}
1548    for line in upgrades:
1549        name = _get(line, "name")
1550        version_num = _get(line, "version")
1551        ret[name] = version_num
1552
1553    return ret
1554
1555
1556def list_upgrades(refresh=True, dist_upgrade=True, **kwargs):
1557    """
1558    List all available package upgrades.
1559
1560    refresh
1561        Whether to refresh the package database before listing upgrades.
1562        Default: True.
1563
1564    cache_valid_time
1565
1566        .. versionadded:: 2016.11.0
1567
1568        Skip refreshing the package database if refresh has already occurred within
1569        <value> seconds
1570
1571    dist_upgrade
1572        Whether to list the upgrades using dist-upgrade vs upgrade.  Default is
1573        to use dist-upgrade.
1574
1575    CLI Example:
1576
1577    .. code-block:: bash
1578
1579        salt '*' pkg.list_upgrades
1580    """
1581    cache_valid_time = kwargs.pop("cache_valid_time", 0)
1582    if salt.utils.data.is_true(refresh):
1583        refresh_db(cache_valid_time)
1584    return _get_upgradable(dist_upgrade, **kwargs)
1585
1586
1587def upgrade_available(name, **kwargs):
1588    """
1589    Check whether or not an upgrade is available for a given package
1590
1591    CLI Example:
1592
1593    .. code-block:: bash
1594
1595        salt '*' pkg.upgrade_available <package name>
1596    """
1597    return latest_version(name) != ""
1598
1599
1600def version_cmp(pkg1, pkg2, ignore_epoch=False, **kwargs):
1601    """
1602    Do a cmp-style comparison on two packages. Return -1 if pkg1 < pkg2, 0 if
1603    pkg1 == pkg2, and 1 if pkg1 > pkg2. Return None if there was a problem
1604    making the comparison.
1605
1606    ignore_epoch : False
1607        Set to ``True`` to ignore the epoch when comparing versions
1608
1609        .. versionadded:: 2015.8.10,2016.3.2
1610
1611    CLI Example:
1612
1613    .. code-block:: bash
1614
1615        salt '*' pkg.version_cmp '0.2.4-0ubuntu1' '0.2.4.1-0ubuntu1'
1616    """
1617    normalize = lambda x: str(x).split(":", 1)[-1] if ignore_epoch else str(x)
1618    # both apt_pkg.version_compare and _cmd_quote need string arguments.
1619    pkg1 = normalize(pkg1)
1620    pkg2 = normalize(pkg2)
1621
1622    # if we have apt_pkg, this will be quickier this way
1623    # and also do not rely on shell.
1624    if HAS_APTPKG:
1625        try:
1626            # the apt_pkg module needs to be manually initialized
1627            apt_pkg.init_system()
1628
1629            # if there is a difference in versions, apt_pkg.version_compare will
1630            # return an int representing the difference in minor versions, or
1631            # 1/-1 if the difference is smaller than minor versions. normalize
1632            # to -1, 0 or 1.
1633            try:
1634                ret = apt_pkg.version_compare(pkg1, pkg2)
1635            except TypeError:
1636                ret = apt_pkg.version_compare(str(pkg1), str(pkg2))
1637            return 1 if ret > 0 else -1 if ret < 0 else 0
1638        except Exception:  # pylint: disable=broad-except
1639            # Try to use shell version in case of errors w/python bindings
1640            pass
1641    try:
1642        for oper, ret in (("lt", -1), ("eq", 0), ("gt", 1)):
1643            cmd = ["dpkg", "--compare-versions", pkg1, oper, pkg2]
1644            retcode = __salt__["cmd.retcode"](
1645                cmd, output_loglevel="trace", python_shell=False, ignore_retcode=True
1646            )
1647            if retcode == 0:
1648                return ret
1649    except Exception as exc:  # pylint: disable=broad-except
1650        log.error(exc)
1651    return None
1652
1653
1654def _split_repo_str(repo):
1655    """
1656    Return APT source entry as a tuple.
1657    """
1658    split = SourceEntry(repo)
1659    return split.type, split.architectures, split.uri, split.dist, split.comps
1660
1661
1662def _consolidate_repo_sources(sources):
1663    """
1664    Consolidate APT sources.
1665    """
1666    if not isinstance(sources, SourcesList):
1667        raise TypeError("'{}' not a '{}'".format(type(sources), SourcesList))
1668
1669    consolidated = {}
1670    delete_files = set()
1671    base_file = SourceEntry("").file
1672
1673    repos = [s for s in sources.list if not s.invalid]
1674
1675    for repo in repos:
1676        key = str(
1677            (
1678                getattr(repo, "architectures", []),
1679                repo.disabled,
1680                repo.type,
1681                repo.uri,
1682                repo.dist,
1683            )
1684        )
1685        if key in consolidated:
1686            combined = consolidated[key]
1687            combined_comps = set(repo.comps).union(set(combined.comps))
1688            consolidated[key].comps = list(combined_comps)
1689        else:
1690            consolidated[key] = SourceEntry(repo.line)
1691
1692        if repo.file != base_file:
1693            delete_files.add(repo.file)
1694
1695    sources.list = list(consolidated.values())
1696    sources.save()
1697    for file_ in delete_files:
1698        try:
1699            os.remove(file_)
1700        except OSError:
1701            pass
1702    return sources
1703
1704
1705def list_repo_pkgs(*args, **kwargs):  # pylint: disable=unused-import
1706    """
1707    .. versionadded:: 2017.7.0
1708
1709    Returns all available packages. Optionally, package names (and name globs)
1710    can be passed and the results will be filtered to packages matching those
1711    names.
1712
1713    This function can be helpful in discovering the version or repo to specify
1714    in a :mod:`pkg.installed <salt.states.pkg.installed>` state.
1715
1716    The return data will be a dictionary mapping package names to a list of
1717    version numbers, ordered from newest to oldest. For example:
1718
1719    .. code-block:: python
1720
1721        {
1722            'bash': ['4.3-14ubuntu1.1',
1723                     '4.3-14ubuntu1'],
1724            'nginx': ['1.10.0-0ubuntu0.16.04.4',
1725                      '1.9.15-0ubuntu1']
1726        }
1727
1728    CLI Examples:
1729
1730    .. code-block:: bash
1731
1732        salt '*' pkg.list_repo_pkgs
1733        salt '*' pkg.list_repo_pkgs foo bar baz
1734    """
1735    if args:
1736        # Get only information about packages in args
1737        cmd = ["apt-cache", "show"] + [arg for arg in args]
1738    else:
1739        # Get information about all available packages
1740        cmd = ["apt-cache", "dump"]
1741
1742    out = _call_apt(cmd, scope=False, ignore_retcode=True)
1743
1744    ret = {}
1745    pkg_name = None
1746    skip_pkg = False
1747    new_pkg = re.compile("^Package: (.+)")
1748    for line in salt.utils.itertools.split(out["stdout"], "\n"):
1749        if not line.strip():
1750            continue
1751        try:
1752            cur_pkg = new_pkg.match(line).group(1)
1753        except AttributeError:
1754            pass
1755        else:
1756            if cur_pkg != pkg_name:
1757                pkg_name = cur_pkg
1758                continue
1759        comps = line.strip().split(None, 1)
1760        if comps[0] == "Version:":
1761            ret.setdefault(pkg_name, []).append(comps[1])
1762
1763    return ret
1764
1765
1766def _skip_source(source):
1767    """
1768    Decide to skip source or not.
1769
1770    :param source:
1771    :return:
1772    """
1773    if source.invalid:
1774        if (
1775            source.uri
1776            and source.type
1777            and source.type in ("deb", "deb-src", "rpm", "rpm-src")
1778        ):
1779            pieces = source.mysplit(source.line)
1780            if pieces[1].strip()[0] == "[":
1781                options = pieces.pop(1).strip("[]").split()
1782                if len(options) > 0:
1783                    log.debug(
1784                        "Source %s will be included although is marked invalid",
1785                        source.uri,
1786                    )
1787                    return False
1788            return True
1789        else:
1790            return True
1791    return False
1792
1793
1794def list_repos(**kwargs):
1795    """
1796    Lists all repos in the sources.list (and sources.lists.d) files
1797
1798    CLI Example:
1799
1800    .. code-block:: bash
1801
1802       salt '*' pkg.list_repos
1803       salt '*' pkg.list_repos disabled=True
1804    """
1805    repos = {}
1806    sources = SourcesList()
1807    for source in sources.list:
1808        if _skip_source(source):
1809            continue
1810        repo = {}
1811        repo["file"] = source.file
1812        repo["comps"] = getattr(source, "comps", [])
1813        repo["disabled"] = source.disabled
1814        repo["dist"] = source.dist
1815        repo["type"] = source.type
1816        repo["uri"] = source.uri
1817        repo["line"] = source.line.strip()
1818        repo["architectures"] = getattr(source, "architectures", [])
1819        repos.setdefault(source.uri, []).append(repo)
1820    return repos
1821
1822
1823def get_repo(repo, **kwargs):
1824    """
1825    Display a repo from the sources.list / sources.list.d
1826
1827    The repo passed in needs to be a complete repo entry.
1828
1829    CLI Examples:
1830
1831    .. code-block:: bash
1832
1833        salt '*' pkg.get_repo "myrepo definition"
1834    """
1835    ppa_auth = kwargs.get("ppa_auth", None)
1836    # we have to be clever about this since the repo definition formats
1837    # are a bit more "loose" than in some other distributions
1838    if repo.startswith("ppa:") and __grains__["os"] in ("Ubuntu", "Mint", "neon"):
1839        # This is a PPA definition meaning special handling is needed
1840        # to derive the name.
1841        dist = __grains__["lsb_distrib_codename"]
1842        owner_name, ppa_name = repo[4:].split("/")
1843        if ppa_auth:
1844            auth_info = "{}@".format(ppa_auth)
1845            repo = LP_PVT_SRC_FORMAT.format(auth_info, owner_name, ppa_name, dist)
1846        else:
1847            if HAS_SOFTWAREPROPERTIES:
1848                try:
1849                    if hasattr(softwareproperties.ppa, "PPAShortcutHandler"):
1850                        repo = softwareproperties.ppa.PPAShortcutHandler(repo).expand(
1851                            dist
1852                        )[0]
1853                    else:
1854                        repo = softwareproperties.ppa.expand_ppa_line(repo, dist)[0]
1855                except NameError as name_error:
1856                    raise CommandExecutionError(
1857                        "Could not find ppa {}: {}".format(repo, name_error)
1858                    )
1859            else:
1860                repo = LP_SRC_FORMAT.format(owner_name, ppa_name, dist)
1861
1862    repos = list_repos()
1863
1864    if repos:
1865        try:
1866            (
1867                repo_type,
1868                repo_architectures,
1869                repo_uri,
1870                repo_dist,
1871                repo_comps,
1872            ) = _split_repo_str(repo)
1873            if ppa_auth:
1874                uri_match = re.search("(http[s]?://)(.+)", repo_uri)
1875                if uri_match:
1876                    if not uri_match.group(2).startswith(ppa_auth):
1877                        repo_uri = "{}{}@{}".format(
1878                            uri_match.group(1), ppa_auth, uri_match.group(2)
1879                        )
1880        except SyntaxError:
1881            raise CommandExecutionError(
1882                "Error: repo '{}' is not a well formatted definition".format(repo)
1883            )
1884
1885        for source in repos.values():
1886            for sub in source:
1887                if (
1888                    sub["type"] == repo_type
1889                    and sub["uri"] == repo_uri
1890                    and sub["dist"] == repo_dist
1891                ):
1892                    if not repo_comps:
1893                        return sub
1894                    for comp in repo_comps:
1895                        if comp in sub.get("comps", []):
1896                            return sub
1897    return {}
1898
1899
1900def del_repo(repo, **kwargs):
1901    """
1902    Delete a repo from the sources.list / sources.list.d
1903
1904    If the .list file is in the sources.list.d directory
1905    and the file that the repo exists in does not contain any other
1906    repo configuration, the file itself will be deleted.
1907
1908    The repo passed in must be a fully formed repository definition
1909    string.
1910
1911    CLI Examples:
1912
1913    .. code-block:: bash
1914
1915        salt '*' pkg.del_repo "myrepo definition"
1916    """
1917    is_ppa = False
1918    if repo.startswith("ppa:") and __grains__["os"] in ("Ubuntu", "Mint", "neon"):
1919        # This is a PPA definition meaning special handling is needed
1920        # to derive the name.
1921        is_ppa = True
1922        dist = __grains__["lsb_distrib_codename"]
1923        if not HAS_SOFTWAREPROPERTIES:
1924            _warn_software_properties(repo)
1925            owner_name, ppa_name = repo[4:].split("/")
1926            if "ppa_auth" in kwargs:
1927                auth_info = "{}@".format(kwargs["ppa_auth"])
1928                repo = LP_PVT_SRC_FORMAT.format(auth_info, dist, owner_name, ppa_name)
1929            else:
1930                repo = LP_SRC_FORMAT.format(owner_name, ppa_name, dist)
1931        else:
1932            if hasattr(softwareproperties.ppa, "PPAShortcutHandler"):
1933                repo = softwareproperties.ppa.PPAShortcutHandler(repo).expand(dist)[0]
1934            else:
1935                repo = softwareproperties.ppa.expand_ppa_line(repo, dist)[0]
1936
1937    sources = SourcesList()
1938    repos = [s for s in sources.list if not s.invalid]
1939    if repos:
1940        deleted_from = dict()
1941        try:
1942            (
1943                repo_type,
1944                repo_architectures,
1945                repo_uri,
1946                repo_dist,
1947                repo_comps,
1948            ) = _split_repo_str(repo)
1949        except SyntaxError:
1950            raise SaltInvocationError(
1951                "Error: repo '{}' not a well formatted definition".format(repo)
1952            )
1953
1954        for source in repos:
1955            if (
1956                source.type == repo_type
1957                and source.architectures == repo_architectures
1958                and source.uri == repo_uri
1959                and source.dist == repo_dist
1960            ):
1961
1962                s_comps = set(source.comps)
1963                r_comps = set(repo_comps)
1964                if s_comps.intersection(r_comps):
1965                    deleted_from[source.file] = 0
1966                    source.comps = list(s_comps.difference(r_comps))
1967                    if not source.comps:
1968                        try:
1969                            sources.remove(source)
1970                        except ValueError:
1971                            pass
1972            # PPAs are special and can add deb-src where expand_ppa_line
1973            # doesn't always reflect this.  Lets just cleanup here for good
1974            # measure
1975            if (
1976                is_ppa
1977                and repo_type == "deb"
1978                and source.type == "deb-src"
1979                and source.uri == repo_uri
1980                and source.dist == repo_dist
1981            ):
1982
1983                s_comps = set(source.comps)
1984                r_comps = set(repo_comps)
1985                if s_comps.intersection(r_comps):
1986                    deleted_from[source.file] = 0
1987                    source.comps = list(s_comps.difference(r_comps))
1988                    if not source.comps:
1989                        try:
1990                            sources.remove(source)
1991                        except ValueError:
1992                            pass
1993            sources.save()
1994        if deleted_from:
1995            ret = ""
1996            for source in sources:
1997                if source.file in deleted_from:
1998                    deleted_from[source.file] += 1
1999            for repo_file, count in deleted_from.items():
2000                msg = "Repo '{0}' has been removed from {1}.\n"
2001                if count == 0 and "sources.list.d/" in repo_file:
2002                    if os.path.isfile(repo_file):
2003                        msg = "File {1} containing repo '{0}' has been removed."
2004                        try:
2005                            os.remove(repo_file)
2006                        except OSError:
2007                            pass
2008                ret += msg.format(repo, repo_file)
2009            # explicit refresh after a repo is deleted
2010            refresh_db()
2011            return ret
2012
2013    raise CommandExecutionError(
2014        "Repo {} doesn't exist in the sources.list(s)".format(repo)
2015    )
2016
2017
2018def _convert_if_int(value):
2019    """
2020    .. versionadded:: 2017.7.0
2021
2022    Convert to an int if necessary.
2023
2024    :param str value: The value to check/convert.
2025
2026    :return: The converted or passed value.
2027    :rtype: bool|int|str
2028    """
2029    try:
2030        value = int(str(value))
2031    except ValueError:
2032        pass
2033    return value
2034
2035
2036def get_repo_keys():
2037    """
2038    .. versionadded:: 2017.7.0
2039
2040    List known repo key details.
2041
2042    :return: A dictionary containing the repo keys.
2043    :rtype: dict
2044
2045    CLI Examples:
2046
2047    .. code-block:: bash
2048
2049        salt '*' pkg.get_repo_keys
2050    """
2051    ret = dict()
2052    repo_keys = list()
2053
2054    # The double usage of '--with-fingerprint' is necessary in order to
2055    # retrieve the fingerprint of the subkey.
2056    cmd = [
2057        "apt-key",
2058        "adv",
2059        "--batch",
2060        "--list-public-keys",
2061        "--with-fingerprint",
2062        "--with-fingerprint",
2063        "--with-colons",
2064        "--fixed-list-mode",
2065    ]
2066
2067    cmd_ret = _call_apt(cmd, scope=False)
2068
2069    if cmd_ret["retcode"] != 0:
2070        log.error(cmd_ret["stderr"])
2071        return ret
2072
2073    lines = [line for line in cmd_ret["stdout"].splitlines() if line.strip()]
2074
2075    # Reference for the meaning of each item in the colon-separated
2076    # record can be found here: https://goo.gl/KIZbvp
2077    for line in lines:
2078        items = [
2079            _convert_if_int(item.strip()) if item.strip() else None
2080            for item in line.split(":")
2081        ]
2082        key_props = dict()
2083
2084        if len(items) < 2:
2085            log.debug("Skipping line: %s", line)
2086            continue
2087
2088        if items[0] in ("pub", "sub"):
2089            key_props.update(
2090                {
2091                    "algorithm": items[3],
2092                    "bits": items[2],
2093                    "capability": items[11],
2094                    "date_creation": items[5],
2095                    "date_expiration": items[6],
2096                    "keyid": items[4],
2097                    "validity": items[1],
2098                }
2099            )
2100
2101            if items[0] == "pub":
2102                repo_keys.append(key_props)
2103            else:
2104                repo_keys[-1]["subkey"] = key_props
2105        elif items[0] == "fpr":
2106            if repo_keys[-1].get("subkey", False):
2107                repo_keys[-1]["subkey"].update({"fingerprint": items[9]})
2108            else:
2109                repo_keys[-1].update({"fingerprint": items[9]})
2110        elif items[0] == "uid":
2111            repo_keys[-1].update({"uid": items[9], "uid_hash": items[7]})
2112
2113    for repo_key in repo_keys:
2114        ret[repo_key["keyid"]] = repo_key
2115    return ret
2116
2117
2118def add_repo_key(path=None, text=None, keyserver=None, keyid=None, saltenv="base"):
2119    """
2120    .. versionadded:: 2017.7.0
2121
2122    Add a repo key using ``apt-key add``.
2123
2124    :param str path: The path of the key file to import.
2125    :param str text: The key data to import, in string form.
2126    :param str keyserver: The server to download the repo key specified by the keyid.
2127    :param str keyid: The key id of the repo key to add.
2128    :param str saltenv: The environment the key file resides in.
2129
2130    :return: A boolean representing whether the repo key was added.
2131    :rtype: bool
2132
2133    CLI Examples:
2134
2135    .. code-block:: bash
2136
2137        salt '*' pkg.add_repo_key 'salt://apt/sources/test.key'
2138
2139        salt '*' pkg.add_repo_key text="'$KEY1'"
2140
2141        salt '*' pkg.add_repo_key keyserver='keyserver.example' keyid='0000AAAA'
2142    """
2143    cmd = ["apt-key"]
2144    kwargs = {}
2145
2146    current_repo_keys = get_repo_keys()
2147
2148    if path:
2149        cached_source_path = __salt__["cp.cache_file"](path, saltenv)
2150
2151        if not cached_source_path:
2152            log.error("Unable to get cached copy of file: %s", path)
2153            return False
2154
2155        cmd.extend(["add", cached_source_path])
2156    elif text:
2157        log.debug("Received value: %s", text)
2158
2159        cmd.extend(["add", "-"])
2160        kwargs.update({"stdin": text})
2161    elif keyserver:
2162        if not keyid:
2163            error_msg = "No keyid or keyid too short for keyserver: {}".format(
2164                keyserver
2165            )
2166            raise SaltInvocationError(error_msg)
2167
2168        cmd.extend(["adv", "--batch", "--keyserver", keyserver, "--recv", keyid])
2169    elif keyid:
2170        error_msg = "No keyserver specified for keyid: {}".format(keyid)
2171        raise SaltInvocationError(error_msg)
2172    else:
2173        raise TypeError(
2174            "{}() takes at least 1 argument (0 given)".format(add_repo_key.__name__)
2175        )
2176
2177    # If the keyid is provided or determined, check it against the existing
2178    # repo key ids to determine whether it needs to be imported.
2179    if keyid:
2180        for current_keyid in current_repo_keys:
2181            if current_keyid[-(len(keyid)) :] == keyid:
2182                log.debug("The keyid '%s' already present: %s", keyid, current_keyid)
2183                return True
2184
2185    cmd_ret = _call_apt(cmd, **kwargs)
2186
2187    if cmd_ret["retcode"] == 0:
2188        return True
2189    log.error("Unable to add repo key: %s", cmd_ret["stderr"])
2190    return False
2191
2192
2193def del_repo_key(name=None, **kwargs):
2194    """
2195    .. versionadded:: 2015.8.0
2196
2197    Remove a repo key using ``apt-key del``
2198
2199    name
2200        Repo from which to remove the key. Unnecessary if ``keyid`` is passed.
2201
2202    keyid
2203        The KeyID of the GPG key to remove
2204
2205    keyid_ppa : False
2206        If set to ``True``, the repo's GPG key ID will be looked up from
2207        ppa.launchpad.net and removed.
2208
2209        .. note::
2210
2211            Setting this option to ``True`` requires that the ``name`` param
2212            also be passed.
2213
2214    CLI Examples:
2215
2216    .. code-block:: bash
2217
2218        salt '*' pkg.del_repo_key keyid=0123ABCD
2219        salt '*' pkg.del_repo_key name='ppa:foo/bar' keyid_ppa=True
2220    """
2221    if kwargs.get("keyid_ppa", False):
2222        if isinstance(name, str) and name.startswith("ppa:"):
2223            owner_name, ppa_name = name[4:].split("/")
2224            ppa_info = _get_ppa_info_from_launchpad(owner_name, ppa_name)
2225            keyid = ppa_info["signing_key_fingerprint"][-8:]
2226        else:
2227            raise SaltInvocationError("keyid_ppa requires that a PPA be passed")
2228    else:
2229        if "keyid" in kwargs:
2230            keyid = kwargs.get("keyid")
2231        else:
2232            raise SaltInvocationError("keyid or keyid_ppa and PPA name must be passed")
2233
2234    result = _call_apt(["apt-key", "del", keyid], scope=False)
2235    if result["retcode"] != 0:
2236        msg = "Failed to remove keyid {0}"
2237        if result["stderr"]:
2238            msg += ": {}".format(result["stderr"])
2239        raise CommandExecutionError(msg)
2240    return keyid
2241
2242
2243def mod_repo(repo, saltenv="base", **kwargs):
2244    """
2245    Modify one or more values for a repo.  If the repo does not exist, it will
2246    be created, so long as the definition is well formed.  For Ubuntu the
2247    ``ppa:<project>/repo`` format is acceptable. ``ppa:`` format can only be
2248    used to create a new repository.
2249
2250    The following options are available to modify a repo definition:
2251
2252    architectures
2253        A comma-separated list of supported architectures, e.g. ``amd64`` If
2254        this option is not set, all architectures (configured in the system)
2255        will be used.
2256
2257    comps
2258        A comma separated list of components for the repo, e.g. ``main``
2259
2260    file
2261        A file name to be used
2262
2263    keyserver
2264        Keyserver to get gpg key from
2265
2266    keyid
2267        Key ID or a list of key IDs to load with the ``keyserver`` argument
2268
2269    key_url
2270        URL to a GPG key to add to the APT GPG keyring
2271
2272    key_text
2273        GPG key in string form to add to the APT GPG keyring
2274
2275        .. versionadded:: 2018.3.0
2276
2277    consolidate : False
2278        If ``True``, will attempt to de-duplicate and consolidate sources
2279
2280    comments
2281        Sometimes you want to supply additional information, but not as
2282        enabled configuration. All comments provided here will be joined
2283        into a single string and appended to the repo configuration with a
2284        comment marker (#) before it.
2285
2286        .. versionadded:: 2015.8.9
2287
2288    refresh : True
2289        Enable or disable (True or False) refreshing of the apt package
2290        database. The previous ``refresh_db`` argument was deprecated in
2291        favor of ``refresh```. The ``refresh_db`` argument will still
2292        continue to work to ensure backwards compatibility, but please
2293        change to using the preferred ``refresh``.
2294
2295    .. note::
2296        Due to the way keys are stored for APT, there is a known issue where
2297        the key won't be updated unless another change is made at the same
2298        time. Keys should be properly added on initial configuration.
2299
2300    CLI Examples:
2301
2302    .. code-block:: bash
2303
2304        salt '*' pkg.mod_repo 'myrepo definition' uri=http://new/uri
2305        salt '*' pkg.mod_repo 'myrepo definition' comps=main,universe
2306    """
2307    if "refresh_db" in kwargs:
2308        refresh = kwargs["refresh_db"]
2309    else:
2310        refresh = kwargs.get("refresh", True)
2311
2312    # to ensure no one sets some key values that _shouldn't_ be changed on the
2313    # object itself, this is just a white-list of "ok" to set properties
2314    if repo.startswith("ppa:"):
2315        if __grains__["os"] in ("Ubuntu", "Mint", "neon"):
2316            # secure PPAs cannot be supported as of the time of this code
2317            # implementation via apt-add-repository.  The code path for
2318            # secure PPAs should be the same as urllib method
2319            if salt.utils.path.which("apt-add-repository") and "ppa_auth" not in kwargs:
2320                repo_info = get_repo(repo)
2321                if repo_info:
2322                    return {repo: repo_info}
2323                else:
2324                    env = None
2325                    http_proxy_url = _get_http_proxy_url()
2326                    if http_proxy_url:
2327                        env = {
2328                            "http_proxy": http_proxy_url,
2329                            "https_proxy": http_proxy_url,
2330                        }
2331                    if float(__grains__["osrelease"]) < 12.04:
2332                        cmd = ["apt-add-repository", repo]
2333                    else:
2334                        cmd = ["apt-add-repository", "-y", repo]
2335                    out = _call_apt(cmd, env=env, scope=False, **kwargs)
2336                    if out["retcode"]:
2337                        raise CommandExecutionError(
2338                            "Unable to add PPA '{}'. '{}' exited with "
2339                            "status {!s}: '{}' ".format(
2340                                repo[4:], cmd, out["retcode"], out["stderr"]
2341                            )
2342                        )
2343                    # explicit refresh when a repo is modified.
2344                    if refresh:
2345                        refresh_db()
2346                    return {repo: out}
2347            else:
2348                if not HAS_SOFTWAREPROPERTIES:
2349                    _warn_software_properties(repo)
2350                else:
2351                    log.info("Falling back to urllib method for private PPA")
2352
2353                # fall back to urllib style
2354                try:
2355                    owner_name, ppa_name = repo[4:].split("/", 1)
2356                except ValueError:
2357                    raise CommandExecutionError(
2358                        "Unable to get PPA info from argument. "
2359                        'Expected format "<PPA_OWNER>/<PPA_NAME>" '
2360                        "(e.g. saltstack/salt) not found.  Received "
2361                        "'{}' instead.".format(repo[4:])
2362                    )
2363                dist = __grains__["lsb_distrib_codename"]
2364                # ppa has a lot of implicit arguments. Make them explicit.
2365                # These will defer to any user-defined variants
2366                kwargs["dist"] = dist
2367                ppa_auth = ""
2368                if "file" not in kwargs:
2369                    filename = "/etc/apt/sources.list.d/{0}-{1}-{2}.list"
2370                    kwargs["file"] = filename.format(owner_name, ppa_name, dist)
2371                try:
2372                    launchpad_ppa_info = _get_ppa_info_from_launchpad(
2373                        owner_name, ppa_name
2374                    )
2375                    if "ppa_auth" not in kwargs:
2376                        kwargs["keyid"] = launchpad_ppa_info["signing_key_fingerprint"]
2377                    else:
2378                        if "keyid" not in kwargs:
2379                            error_str = (
2380                                "Private PPAs require a keyid to be specified: {0}/{1}"
2381                            )
2382                            raise CommandExecutionError(
2383                                error_str.format(owner_name, ppa_name)
2384                            )
2385                except HTTPError as exc:
2386                    raise CommandExecutionError(
2387                        "Launchpad does not know about {}/{}: {}".format(
2388                            owner_name, ppa_name, exc
2389                        )
2390                    )
2391                except IndexError as exc:
2392                    raise CommandExecutionError(
2393                        "Launchpad knows about {}/{} but did not "
2394                        "return a fingerprint. Please set keyid "
2395                        "manually: {}".format(owner_name, ppa_name, exc)
2396                    )
2397
2398                if "keyserver" not in kwargs:
2399                    kwargs["keyserver"] = "keyserver.ubuntu.com"
2400                if "ppa_auth" in kwargs:
2401                    if not launchpad_ppa_info["private"]:
2402                        raise CommandExecutionError(
2403                            "PPA is not private but auth credentials passed: {}".format(
2404                                repo
2405                            )
2406                        )
2407                # assign the new repo format to the "repo" variable
2408                # so we can fall through to the "normal" mechanism
2409                # here.
2410                if "ppa_auth" in kwargs:
2411                    ppa_auth = "{}@".format(kwargs["ppa_auth"])
2412                    repo = LP_PVT_SRC_FORMAT.format(
2413                        ppa_auth, owner_name, ppa_name, dist
2414                    )
2415                else:
2416                    repo = LP_SRC_FORMAT.format(owner_name, ppa_name, dist)
2417        else:
2418            raise CommandExecutionError(
2419                'cannot parse "ppa:" style repo definitions: {}'.format(repo)
2420            )
2421
2422    sources = SourcesList()
2423    if kwargs.get("consolidate", False):
2424        # attempt to de-dup and consolidate all sources
2425        # down to entries in sources.list
2426        # this option makes it easier to keep the sources
2427        # list in a "sane" state.
2428        #
2429        # this should remove duplicates, consolidate comps
2430        # for a given source down to one line
2431        # and eliminate "invalid" and comment lines
2432        #
2433        # the second side effect is removal of files
2434        # that are not the main sources.list file
2435        sources = _consolidate_repo_sources(sources)
2436
2437    repos = [s for s in sources if not s.invalid]
2438    mod_source = None
2439    try:
2440        (
2441            repo_type,
2442            repo_architectures,
2443            repo_uri,
2444            repo_dist,
2445            repo_comps,
2446        ) = _split_repo_str(repo)
2447    except SyntaxError:
2448        raise SyntaxError(
2449            "Error: repo '{}' not a well formatted definition".format(repo)
2450        )
2451
2452    full_comp_list = {comp.strip() for comp in repo_comps}
2453    no_proxy = __salt__["config.option"]("no_proxy")
2454
2455    if "keyid" in kwargs:
2456        keyid = kwargs.pop("keyid", None)
2457        keyserver = kwargs.pop("keyserver", None)
2458        if not keyid or not keyserver:
2459            error_str = "both keyserver and keyid options required."
2460            raise NameError(error_str)
2461        if not isinstance(keyid, list):
2462            keyid = [keyid]
2463
2464        for key in keyid:
2465            if isinstance(
2466                key, int
2467            ):  # yaml can make this an int, we need the hex version
2468                key = hex(key)
2469            cmd = ["apt-key", "export", key]
2470            output = __salt__["cmd.run_stdout"](cmd, python_shell=False, **kwargs)
2471            imported = output.startswith("-----BEGIN PGP")
2472            if keyserver:
2473                if not imported:
2474                    http_proxy_url = _get_http_proxy_url()
2475                    if http_proxy_url and keyserver not in no_proxy:
2476                        cmd = [
2477                            "apt-key",
2478                            "adv",
2479                            "--batch",
2480                            "--keyserver-options",
2481                            "http-proxy={}".format(http_proxy_url),
2482                            "--keyserver",
2483                            keyserver,
2484                            "--logger-fd",
2485                            "1",
2486                            "--recv-keys",
2487                            key,
2488                        ]
2489                    else:
2490                        cmd = [
2491                            "apt-key",
2492                            "adv",
2493                            "--batch",
2494                            "--keyserver",
2495                            keyserver,
2496                            "--logger-fd",
2497                            "1",
2498                            "--recv-keys",
2499                            key,
2500                        ]
2501                    ret = _call_apt(cmd, scope=False, **kwargs)
2502                    if ret["retcode"] != 0:
2503                        raise CommandExecutionError(
2504                            "Error: key retrieval failed: {}".format(ret["stdout"])
2505                        )
2506
2507    elif "key_url" in kwargs:
2508        key_url = kwargs["key_url"]
2509        fn_ = __salt__["cp.cache_file"](key_url, saltenv)
2510        if not fn_:
2511            raise CommandExecutionError("Error: file not found: {}".format(key_url))
2512        cmd = ["apt-key", "add", fn_]
2513        out = __salt__["cmd.run_stdout"](cmd, python_shell=False, **kwargs)
2514        if not out.upper().startswith("OK"):
2515            raise CommandExecutionError(
2516                "Error: failed to add key from {}".format(key_url)
2517            )
2518
2519    elif "key_text" in kwargs:
2520        key_text = kwargs["key_text"]
2521        cmd = ["apt-key", "add", "-"]
2522        out = __salt__["cmd.run_stdout"](
2523            cmd, stdin=key_text, python_shell=False, **kwargs
2524        )
2525        if not out.upper().startswith("OK"):
2526            raise CommandExecutionError(
2527                "Error: failed to add key:\n{}".format(key_text)
2528            )
2529
2530    if "comps" in kwargs:
2531        kwargs["comps"] = [comp.strip() for comp in kwargs["comps"].split(",")]
2532        full_comp_list |= set(kwargs["comps"])
2533    else:
2534        kwargs["comps"] = list(full_comp_list)
2535
2536    if "architectures" in kwargs:
2537        kwargs["architectures"] = kwargs["architectures"].split(",")
2538    else:
2539        kwargs["architectures"] = repo_architectures
2540
2541    if "disabled" in kwargs:
2542        kwargs["disabled"] = salt.utils.data.is_true(kwargs["disabled"])
2543    elif "enabled" in kwargs:
2544        kwargs["disabled"] = not salt.utils.data.is_true(kwargs["enabled"])
2545
2546    kw_type = kwargs.get("type")
2547    kw_dist = kwargs.get("dist")
2548
2549    for source in repos:
2550        # This series of checks will identify the starting source line
2551        # and the resulting source line.  The idea here is to ensure
2552        # we are not returning bogus data because the source line
2553        # has already been modified on a previous run.
2554        repo_matches = (
2555            source.type == repo_type
2556            and source.uri.rstrip("/") == repo_uri.rstrip("/")
2557            and source.dist == repo_dist
2558        )
2559        kw_matches = source.dist == kw_dist and source.type == kw_type
2560
2561        if repo_matches or kw_matches:
2562            for comp in full_comp_list:
2563                if comp in getattr(source, "comps", []):
2564                    mod_source = source
2565            if not source.comps:
2566                mod_source = source
2567            if kwargs["architectures"] != source.architectures:
2568                mod_source = source
2569            if mod_source:
2570                break
2571
2572    if "comments" in kwargs:
2573        kwargs["comments"] = salt.utils.pkg.deb.combine_comments(kwargs["comments"])
2574
2575    if not mod_source:
2576        mod_source = SourceEntry(repo)
2577        if "comments" in kwargs:
2578            mod_source.comment = kwargs["comments"]
2579        sources.list.append(mod_source)
2580    elif "comments" in kwargs:
2581        mod_source.comment = kwargs["comments"]
2582
2583    for key in kwargs:
2584        if key in _MODIFY_OK and hasattr(mod_source, key):
2585            setattr(mod_source, key, kwargs[key])
2586    sources.save()
2587    # on changes, explicitly refresh
2588    if refresh:
2589        refresh_db()
2590    return {
2591        repo: {
2592            "architectures": getattr(mod_source, "architectures", []),
2593            "comps": mod_source.comps,
2594            "disabled": mod_source.disabled,
2595            "file": mod_source.file,
2596            "type": mod_source.type,
2597            "uri": mod_source.uri,
2598            "line": mod_source.line,
2599        }
2600    }
2601
2602
2603def file_list(*packages, **kwargs):
2604    """
2605    List the files that belong to a package. Not specifying any packages will
2606    return a list of _every_ file on the system's package database (not
2607    generally recommended).
2608
2609    CLI Examples:
2610
2611    .. code-block:: bash
2612
2613        salt '*' pkg.file_list httpd
2614        salt '*' pkg.file_list httpd postfix
2615        salt '*' pkg.file_list
2616    """
2617    return __salt__["lowpkg.file_list"](*packages)
2618
2619
2620def file_dict(*packages, **kwargs):
2621    """
2622    List the files that belong to a package, grouped by package. Not
2623    specifying any packages will return a list of _every_ file on the system's
2624    package database (not generally recommended).
2625
2626    CLI Examples:
2627
2628    .. code-block:: bash
2629
2630        salt '*' pkg.file_dict httpd
2631        salt '*' pkg.file_dict httpd postfix
2632        salt '*' pkg.file_dict
2633    """
2634    return __salt__["lowpkg.file_dict"](*packages)
2635
2636
2637def expand_repo_def(**kwargs):
2638    """
2639    Take a repository definition and expand it to the full pkg repository dict
2640    that can be used for comparison.  This is a helper function to make
2641    the Debian/Ubuntu apt sources sane for comparison in the pkgrepo states.
2642
2643    This is designed to be called from pkgrepo states and will have little use
2644    being called on the CLI.
2645    """
2646    if "repo" not in kwargs:
2647        raise SaltInvocationError("missing 'repo' argument")
2648
2649    sanitized = {}
2650    repo = kwargs["repo"]
2651    if repo.startswith("ppa:") and __grains__["os"] in ("Ubuntu", "Mint", "neon"):
2652        dist = __grains__["lsb_distrib_codename"]
2653        owner_name, ppa_name = repo[4:].split("/", 1)
2654        if "ppa_auth" in kwargs:
2655            auth_info = "{}@".format(kwargs["ppa_auth"])
2656            repo = LP_PVT_SRC_FORMAT.format(auth_info, owner_name, ppa_name, dist)
2657        else:
2658            if HAS_SOFTWAREPROPERTIES:
2659                if hasattr(softwareproperties.ppa, "PPAShortcutHandler"):
2660                    repo = softwareproperties.ppa.PPAShortcutHandler(repo).expand(dist)[
2661                        0
2662                    ]
2663                else:
2664                    repo = softwareproperties.ppa.expand_ppa_line(repo, dist)[0]
2665            else:
2666                repo = LP_SRC_FORMAT.format(owner_name, ppa_name, dist)
2667
2668        if "file" not in kwargs:
2669            filename = "/etc/apt/sources.list.d/{0}-{1}-{2}.list"
2670            kwargs["file"] = filename.format(owner_name, ppa_name, dist)
2671
2672    source_entry = SourceEntry(repo)
2673    for list_args in ("architectures", "comps"):
2674        if list_args in kwargs:
2675            kwargs[list_args] = [
2676                kwarg.strip() for kwarg in kwargs[list_args].split(",")
2677            ]
2678    for kwarg in _MODIFY_OK:
2679        if kwarg in kwargs:
2680            setattr(source_entry, kwarg, kwargs[kwarg])
2681
2682    source_list = SourcesList()
2683    source_entry = source_list.add(
2684        type=source_entry.type,
2685        uri=source_entry.uri,
2686        dist=source_entry.dist,
2687        orig_comps=getattr(source_entry, "comps", []),
2688        architectures=getattr(source_entry, "architectures", []),
2689    )
2690
2691    sanitized["file"] = source_entry.file
2692    sanitized["comps"] = getattr(source_entry, "comps", [])
2693    sanitized["disabled"] = source_entry.disabled
2694    sanitized["dist"] = source_entry.dist
2695    sanitized["type"] = source_entry.type
2696    sanitized["uri"] = source_entry.uri
2697    sanitized["line"] = source_entry.line.strip()
2698    sanitized["architectures"] = getattr(source_entry, "architectures", [])
2699
2700    return sanitized
2701
2702
2703def _parse_selections(dpkgselection):
2704    """
2705    Parses the format from ``dpkg --get-selections`` and return a format that
2706    pkg.get_selections and pkg.set_selections work with.
2707    """
2708    ret = {}
2709    if isinstance(dpkgselection, str):
2710        dpkgselection = dpkgselection.split("\n")
2711    for line in dpkgselection:
2712        if line:
2713            _pkg, _state = line.split()
2714            if _state in ret:
2715                ret[_state].append(_pkg)
2716            else:
2717                ret[_state] = [_pkg]
2718    return ret
2719
2720
2721def get_selections(pattern=None, state=None):
2722    """
2723    View package state from the dpkg database.
2724
2725    Returns a dict of dicts containing the state, and package names:
2726
2727    .. code-block:: python
2728
2729        {'<host>':
2730            {'<state>': ['pkg1',
2731                         ...
2732                        ]
2733            },
2734            ...
2735        }
2736
2737    CLI Example:
2738
2739    .. code-block:: bash
2740
2741        salt '*' pkg.get_selections
2742        salt '*' pkg.get_selections 'python-*'
2743        salt '*' pkg.get_selections state=hold
2744        salt '*' pkg.get_selections 'openssh*' state=hold
2745    """
2746    ret = {}
2747    cmd = ["dpkg", "--get-selections"]
2748    cmd.append(pattern if pattern else "*")
2749    stdout = __salt__["cmd.run_stdout"](
2750        cmd, output_loglevel="trace", python_shell=False
2751    )
2752    ret = _parse_selections(stdout)
2753    if state:
2754        return {state: ret.get(state, [])}
2755    return ret
2756
2757
2758# TODO: allow state=None to be set, and that *args will be set to that state
2759# TODO: maybe use something similar to pkg_resources.pack_pkgs to allow a list
2760# passed to selection, with the default state set to whatever is passed by the
2761# above, but override that if explicitly specified
2762# TODO: handle path to selection file from local fs as well as from salt file
2763# server
2764def set_selections(path=None, selection=None, clear=False, saltenv="base"):
2765    """
2766    Change package state in the dpkg database.
2767
2768    The state can be any one of, documented in ``dpkg(1)``:
2769
2770    - install
2771    - hold
2772    - deinstall
2773    - purge
2774
2775    This command is commonly used to mark specific packages to be held from
2776    being upgraded, that is, to be kept at a certain version. When a state is
2777    changed to anything but being held, then it is typically followed by
2778    ``apt-get -u dselect-upgrade``.
2779
2780    Note: Be careful with the ``clear`` argument, since it will start
2781    with setting all packages to deinstall state.
2782
2783    Returns a dict of dicts containing the package names, and the new and old
2784    versions:
2785
2786    .. code-block:: python
2787
2788        {'<host>':
2789            {'<package>': {'new': '<new-state>',
2790                           'old': '<old-state>'}
2791            },
2792            ...
2793        }
2794
2795    CLI Example:
2796
2797    .. code-block:: bash
2798
2799        salt '*' pkg.set_selections selection='{"install": ["netcat"]}'
2800        salt '*' pkg.set_selections selection='{"hold": ["openssh-server", "openssh-client"]}'
2801        salt '*' pkg.set_selections salt://path/to/file
2802        salt '*' pkg.set_selections salt://path/to/file clear=True
2803    """
2804    ret = {}
2805    if not path and not selection:
2806        return ret
2807    if path and selection:
2808        err = (
2809            "The 'selection' and 'path' arguments to "
2810            "pkg.set_selections are mutually exclusive, and cannot be "
2811            "specified together"
2812        )
2813        raise SaltInvocationError(err)
2814
2815    if isinstance(selection, str):
2816        try:
2817            selection = salt.utils.yaml.safe_load(selection)
2818        except (
2819            salt.utils.yaml.parser.ParserError,
2820            salt.utils.yaml.scanner.ScannerError,
2821        ) as exc:
2822            raise SaltInvocationError("Improperly-formatted selection: {}".format(exc))
2823
2824    if path:
2825        path = __salt__["cp.cache_file"](path, saltenv)
2826        with salt.utils.files.fopen(path, "r") as ifile:
2827            content = [salt.utils.stringutils.to_unicode(x) for x in ifile.readlines()]
2828        selection = _parse_selections(content)
2829
2830    if selection:
2831        valid_states = ("install", "hold", "deinstall", "purge")
2832        bad_states = [x for x in selection if x not in valid_states]
2833        if bad_states:
2834            raise SaltInvocationError(
2835                "Invalid state(s): {}".format(", ".join(bad_states))
2836            )
2837
2838        if clear:
2839            cmd = ["dpkg", "--clear-selections"]
2840            if not __opts__["test"]:
2841                result = _call_apt(cmd, scope=False)
2842                if result["retcode"] != 0:
2843                    err = "Running dpkg --clear-selections failed: {}".format(
2844                        result["stderr"]
2845                    )
2846                    log.error(err)
2847                    raise CommandExecutionError(err)
2848
2849        sel_revmap = {}
2850        for _state, _pkgs in get_selections().items():
2851            sel_revmap.update({_pkg: _state for _pkg in _pkgs})
2852
2853        for _state, _pkgs in selection.items():
2854            for _pkg in _pkgs:
2855                if _state == sel_revmap.get(_pkg):
2856                    continue
2857                cmd = ["dpkg", "--set-selections"]
2858                cmd_in = "{} {}".format(_pkg, _state)
2859                if not __opts__["test"]:
2860                    result = _call_apt(cmd, scope=False, stdin=cmd_in)
2861                    if result["retcode"] != 0:
2862                        log.error("failed to set state %s for package %s", _state, _pkg)
2863                    else:
2864                        ret[_pkg] = {"old": sel_revmap.get(_pkg), "new": _state}
2865    return ret
2866
2867
2868def _resolve_deps(name, pkgs, **kwargs):
2869    """
2870    Installs missing dependencies and marks them as auto installed so they
2871    are removed when no more manually installed packages depend on them.
2872
2873    .. versionadded:: 2014.7.0
2874
2875    :depends:   - python-apt module
2876    """
2877    missing_deps = []
2878    for pkg_file in pkgs:
2879        deb = apt.debfile.DebPackage(filename=pkg_file, cache=apt.Cache())
2880        if deb.check():
2881            missing_deps.extend(deb.missing_deps)
2882
2883    if missing_deps:
2884        cmd = ["apt-get", "-q", "-y"]
2885        cmd = cmd + ["-o", "DPkg::Options::=--force-confold"]
2886        cmd = cmd + ["-o", "DPkg::Options::=--force-confdef"]
2887        cmd.append("install")
2888        cmd.extend(missing_deps)
2889
2890        ret = __salt__["cmd.retcode"](cmd, env=kwargs.get("env"), python_shell=False)
2891
2892        if ret != 0:
2893            raise CommandExecutionError(
2894                "Error: unable to resolve dependencies for: {}".format(name)
2895            )
2896        else:
2897            try:
2898                cmd = ["apt-mark", "auto"] + missing_deps
2899                __salt__["cmd.run"](cmd, env=kwargs.get("env"), python_shell=False)
2900            except MinionError as exc:
2901                raise CommandExecutionError(exc)
2902    return
2903
2904
2905def owner(*paths, **kwargs):
2906    """
2907    .. versionadded:: 2014.7.0
2908
2909    Return the name of the package that owns the file. Multiple file paths can
2910    be passed. Like :mod:`pkg.version <salt.modules.aptpkg.version>`, if a
2911    single path is passed, a string will be returned, and if multiple paths are
2912    passed, a dictionary of file/package name pairs will be returned.
2913
2914    If the file is not owned by a package, or is not present on the minion,
2915    then an empty string will be returned for that path.
2916
2917    CLI Example:
2918
2919    .. code-block:: bash
2920
2921        salt '*' pkg.owner /usr/bin/apachectl
2922        salt '*' pkg.owner /usr/bin/apachectl /usr/bin/basename
2923    """
2924    if not paths:
2925        return ""
2926    ret = {}
2927    for path in paths:
2928        cmd = ["dpkg", "-S", path]
2929        output = __salt__["cmd.run_stdout"](
2930            cmd, output_loglevel="trace", python_shell=False
2931        )
2932        ret[path] = output.split(":")[0]
2933        if "no path found" in ret[path].lower():
2934            ret[path] = ""
2935    if len(ret) == 1:
2936        return next(iter(ret.values()))
2937    return ret
2938
2939
2940def show(*names, **kwargs):
2941    """
2942    .. versionadded:: 2019.2.0
2943
2944    Runs an ``apt-cache show`` on the passed package names, and returns the
2945    results in a nested dictionary. The top level of the return data will be
2946    the package name, with each package name mapping to a dictionary of version
2947    numbers to any additional information returned by ``apt-cache show``.
2948
2949    filter
2950        An optional comma-separated list (or quoted Python list) of
2951        case-insensitive keys on which to filter. This allows one to restrict
2952        the information returned for each package to a smaller selection of
2953        pertinent items.
2954
2955    refresh : False
2956        If ``True``, the apt cache will be refreshed first. By default, no
2957        refresh is performed.
2958
2959    CLI Examples:
2960
2961    .. code-block:: bash
2962
2963        salt myminion pkg.show gawk
2964        salt myminion pkg.show 'nginx-*'
2965        salt myminion pkg.show 'nginx-*' filter=description,provides
2966    """
2967    kwargs = salt.utils.args.clean_kwargs(**kwargs)
2968    refresh = kwargs.pop("refresh", False)
2969    filter_ = salt.utils.args.split_input(
2970        kwargs.pop("filter", []),
2971        lambda x: str(x) if not isinstance(x, str) else x.lower(),
2972    )
2973    if kwargs:
2974        salt.utils.args.invalid_kwargs(kwargs)
2975
2976    if refresh:
2977        refresh_db()
2978
2979    if not names:
2980        return {}
2981
2982    result = _call_apt(["apt-cache", "show"] + list(names), scope=False)
2983
2984    def _add(ret, pkginfo):
2985        name = pkginfo.pop("Package", None)
2986        version = pkginfo.pop("Version", None)
2987        if name is not None and version is not None:
2988            ret.setdefault(name, {}).setdefault(version, {}).update(pkginfo)
2989
2990    def _check_filter(key):
2991        key = key.lower()
2992        return True if key in ("package", "version") or not filter_ else key in filter_
2993
2994    ret = {}
2995    pkginfo = {}
2996    for line in salt.utils.itertools.split(result["stdout"], "\n"):
2997        line = line.strip()
2998        if line:
2999            try:
3000                key, val = [x.strip() for x in line.split(":", 1)]
3001            except ValueError:
3002                pass
3003            else:
3004                if _check_filter(key):
3005                    pkginfo[key] = val
3006        else:
3007            # We've reached a blank line, which separates packages
3008            _add(ret, pkginfo)
3009            # Clear out the pkginfo dict for the next package
3010            pkginfo = {}
3011            continue
3012
3013    # Make sure to add whatever was in the pkginfo dict when we reached the end
3014    # of the output.
3015    _add(ret, pkginfo)
3016
3017    return ret
3018
3019
3020def info_installed(*names, **kwargs):
3021    """
3022    Return the information of the named package(s) installed on the system.
3023
3024    .. versionadded:: 2015.8.1
3025
3026    names
3027        The names of the packages for which to return information.
3028
3029    failhard
3030        Whether to throw an exception if none of the packages are installed.
3031        Defaults to True.
3032
3033        .. versionadded:: 2016.11.3
3034
3035    CLI Example:
3036
3037    .. code-block:: bash
3038
3039        salt '*' pkg.info_installed <package1>
3040        salt '*' pkg.info_installed <package1> <package2> <package3> ...
3041        salt '*' pkg.info_installed <package1> failhard=false
3042    """
3043    kwargs = salt.utils.args.clean_kwargs(**kwargs)
3044    failhard = kwargs.pop("failhard", True)
3045    if kwargs:
3046        salt.utils.args.invalid_kwargs(kwargs)
3047
3048    ret = dict()
3049    for pkg_name, pkg_nfo in __salt__["lowpkg.info"](*names, failhard=failhard).items():
3050        t_nfo = dict()
3051        if pkg_nfo.get("status", "ii")[1] != "i":
3052            continue  # return only packages that are really installed
3053        # Translate dpkg-specific keys to a common structure
3054        for key, value in pkg_nfo.items():
3055            if key == "package":
3056                t_nfo["name"] = value
3057            elif key == "origin":
3058                t_nfo["vendor"] = value
3059            elif key == "section":
3060                t_nfo["group"] = value
3061            elif key == "maintainer":
3062                t_nfo["packager"] = value
3063            elif key == "homepage":
3064                t_nfo["url"] = value
3065            elif key == "status":
3066                continue  # only installed pkgs are returned, no need for status
3067            else:
3068                t_nfo[key] = value
3069
3070        ret[pkg_name] = t_nfo
3071
3072    return ret
3073
3074
3075def _get_http_proxy_url():
3076    """
3077    Returns the http_proxy_url if proxy_username, proxy_password, proxy_host, and proxy_port
3078    config values are set.
3079
3080    Returns a string.
3081    """
3082    http_proxy_url = ""
3083    host = __salt__["config.option"]("proxy_host")
3084    port = __salt__["config.option"]("proxy_port")
3085    username = __salt__["config.option"]("proxy_username")
3086    password = __salt__["config.option"]("proxy_password")
3087
3088    # Set http_proxy_url for use in various internet facing actions...eg apt-key adv
3089    if host and port:
3090        if username and password:
3091            http_proxy_url = "http://{}:{}@{}:{}".format(username, password, host, port)
3092        else:
3093            http_proxy_url = "http://{}:{}".format(host, port)
3094
3095    return http_proxy_url
3096
3097
3098def list_downloaded(root=None, **kwargs):
3099    """
3100    .. versionadded:: 3000?
3101
3102    List prefetched packages downloaded by apt in the local disk.
3103
3104    root
3105        operate on a different root directory.
3106
3107    CLI Example:
3108
3109    .. code-block:: bash
3110
3111        salt '*' pkg.list_downloaded
3112    """
3113    CACHE_DIR = "/var/cache/apt"
3114    if root:
3115        CACHE_DIR = os.path.join(root, os.path.relpath(CACHE_DIR, os.path.sep))
3116
3117    ret = {}
3118    for root, dirnames, filenames in salt.utils.path.os_walk(CACHE_DIR):
3119        for filename in fnmatch.filter(filenames, "*.deb"):
3120            package_path = os.path.join(root, filename)
3121            pkg_info = __salt__["lowpkg.bin_pkg_info"](package_path)
3122            pkg_timestamp = int(os.path.getctime(package_path))
3123            ret.setdefault(pkg_info["name"], {})[pkg_info["version"]] = {
3124                "path": package_path,
3125                "size": os.path.getsize(package_path),
3126                "creation_date_time_t": pkg_timestamp,
3127                "creation_date_time": datetime.datetime.utcfromtimestamp(
3128                    pkg_timestamp
3129                ).isoformat(),
3130            }
3131    return ret
3132
3133
3134def services_need_restart(**kwargs):
3135    """
3136    .. versionadded:: 3003
3137
3138    List services that use files which have been changed by the
3139    package manager. It might be needed to restart them.
3140
3141    Requires checkrestart from the debian-goodies package.
3142
3143    CLI Examples:
3144
3145    .. code-block:: bash
3146
3147        salt '*' pkg.services_need_restart
3148    """
3149    if not salt.utils.path.which_bin(["checkrestart"]):
3150        raise CommandNotFoundError(
3151            "'checkrestart' is needed. It is part of the 'debian-goodies' "
3152            "package which can be installed from official repositories."
3153        )
3154
3155    cmd = ["checkrestart", "--machine", "--package"]
3156    services = set()
3157
3158    cr_output = __salt__["cmd.run_stdout"](cmd, python_shell=False)
3159    for line in cr_output.split("\n"):
3160        if not line.startswith("SERVICE:"):
3161            continue
3162        end_of_name = line.find(",")
3163        service = line[8:end_of_name]  # skip "SERVICE:"
3164        services.add(service)
3165
3166    return list(services)
3167