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