1""" 2Remote package support using ``pkg_add(1)`` 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.. warning:: 11 12 This module has been completely rewritten. Up to and including version 13 0.17.0, it supported ``pkg_add(1)``, but checked for the existence of a 14 pkgng local database and, if found, would provide some of pkgng's 15 functionality. The rewrite of this module has removed all pkgng support, 16 and moved it to the :mod:`pkgng <salt.modules.pkgng>` execution module. For 17 versions <= 0.17.0, the documentation here should not be considered 18 accurate. If your Minion is running one of these versions, then the 19 documentation for this module can be viewed using the :mod:`sys.doc 20 <salt.modules.sys.doc>` function: 21 22 .. code-block:: bash 23 24 salt bsdminion sys.doc pkg 25 26 27This module acts as the default package provider for FreeBSD 9 and older. If 28you need to use pkgng on a FreeBSD 9 system, you will need to override the 29``pkg`` provider by setting the :conf_minion:`providers` parameter in your 30Minion config file, in order to use pkgng. 31 32.. code-block:: yaml 33 34 providers: 35 pkg: pkgng 36 37More information on pkgng support can be found in the documentation for the 38:mod:`pkgng <salt.modules.pkgng>` module. 39 40This module will respect the ``PACKAGEROOT`` and ``PACKAGESITE`` environment 41variables, if set, but these values can also be overridden in several ways: 42 431. :strong:`Salt configuration parameters.` The configuration parameters 44 ``freebsdpkg.PACKAGEROOT`` and ``freebsdpkg.PACKAGESITE`` are recognized. 45 These config parameters are looked up using :mod:`config.get 46 <salt.modules.config.get>` and can thus be specified in the Master config 47 file, Grains, Pillar, or in the Minion config file. Example: 48 49 .. code-block:: yaml 50 51 freebsdpkg.PACKAGEROOT: ftp://ftp.freebsd.org/ 52 freebsdpkg.PACKAGESITE: ftp://ftp.freebsd.org/pub/FreeBSD/ports/ia64/packages-9-stable/Latest/ 53 542. :strong:`CLI arguments.` Both the ``packageroot`` (used interchangeably with 55 ``fromrepo`` for API compatibility) and ``packagesite`` CLI arguments are 56 recognized, and override their config counterparts from section 1 above. 57 58 .. code-block:: bash 59 60 salt -G 'os:FreeBSD' pkg.install zsh fromrepo=ftp://ftp2.freebsd.org/ 61 salt -G 'os:FreeBSD' pkg.install zsh packageroot=ftp://ftp2.freebsd.org/ 62 salt -G 'os:FreeBSD' pkg.install zsh packagesite=ftp://ftp2.freebsd.org/pub/FreeBSD/ports/ia64/packages-9-stable/Latest/ 63 64 .. note:: 65 66 These arguments can also be passed through in states: 67 68 .. code-block:: yaml 69 70 zsh: 71 pkg.installed: 72 - fromrepo: ftp://ftp2.freebsd.org/ 73""" 74 75import copy 76import logging 77import re 78 79import salt.utils.data 80import salt.utils.functools 81import salt.utils.pkg 82from salt.exceptions import CommandExecutionError, MinionError 83 84log = logging.getLogger(__name__) 85 86# Define the module's virtual name 87__virtualname__ = "pkg" 88 89 90def __virtual__(): 91 """ 92 Load as 'pkg' on FreeBSD versions less than 10. 93 Don't load on FreeBSD 9 when the config option 94 ``providers:pkg`` is set to 'pkgng'. 95 """ 96 if __grains__["os"] == "FreeBSD" and float(__grains__["osrelease"]) < 10: 97 providers = {} 98 if "providers" in __opts__: 99 providers = __opts__["providers"] 100 if providers and "pkg" in providers and providers["pkg"] == "pkgng": 101 log.debug( 102 "Configuration option 'providers:pkg' is set to " 103 "'pkgng', won't load old provider 'freebsdpkg'." 104 ) 105 return ( 106 False, 107 "The freebsdpkg execution module cannot be loaded: the configuration" 108 " option 'providers:pkg' is set to 'pkgng'", 109 ) 110 return __virtualname__ 111 return ( 112 False, 113 "The freebsdpkg execution module cannot be loaded: either the os is not FreeBSD" 114 " or the version of FreeBSD is >= 10.", 115 ) 116 117 118def _get_repo_options(fromrepo=None, packagesite=None): 119 """ 120 Return a list of tuples to seed the "env" list, which is used to set 121 environment variables for any pkg_add commands that are spawned. 122 123 If ``fromrepo`` or ``packagesite`` are None, then their corresponding 124 config parameter will be looked up with config.get. 125 126 If both ``fromrepo`` and ``packagesite`` are None, and neither 127 freebsdpkg.PACKAGEROOT nor freebsdpkg.PACKAGESITE are specified, then an 128 empty list is returned, and it is assumed that the system defaults (or 129 environment variables) will be used. 130 """ 131 root = ( 132 fromrepo 133 if fromrepo is not None 134 else __salt__["config.get"]("freebsdpkg.PACKAGEROOT", None) 135 ) 136 site = ( 137 packagesite 138 if packagesite is not None 139 else __salt__["config.get"]("freebsdpkg.PACKAGESITE", None) 140 ) 141 ret = {} 142 if root is not None: 143 ret["PACKAGEROOT"] = root 144 if site is not None: 145 ret["PACKAGESITE"] = site 146 return ret 147 148 149def _match(names): 150 """ 151 Since pkg_delete requires the full "pkgname-version" string, this function 152 will attempt to match the package name with its version. Returns a list of 153 partial matches and package names that match the "pkgname-version" string 154 required by pkg_delete, and a list of errors encountered. 155 """ 156 pkgs = list_pkgs(versions_as_list=True) 157 errors = [] 158 159 # Look for full matches 160 full_pkg_strings = [] 161 out = __salt__["cmd.run_stdout"]( 162 ["pkg_info"], output_loglevel="trace", python_shell=False 163 ) 164 for line in out.splitlines(): 165 try: 166 full_pkg_strings.append(line.split()[0]) 167 except IndexError: 168 continue 169 full_matches = [x for x in names if x in full_pkg_strings] 170 171 # Look for pkgname-only matches 172 matches = [] 173 ambiguous = [] 174 for name in set(names) - set(full_matches): 175 cver = pkgs.get(name) 176 if cver is not None: 177 if len(cver) == 1: 178 matches.append("{}-{}".format(name, cver[0])) 179 else: 180 ambiguous.append(name) 181 errors.append( 182 "Ambiguous package '{}'. Full name/version required. " 183 "Possible matches: {}".format( 184 name, ", ".join(["{}-{}".format(name, x) for x in cver]) 185 ) 186 ) 187 188 # Find packages that did not match anything 189 not_matched = set(names) - set(matches) - set(full_matches) - set(ambiguous) 190 for name in not_matched: 191 errors.append("Package '{}' not found".format(name)) 192 193 return matches + full_matches, errors 194 195 196def latest_version(*names, **kwargs): 197 """ 198 ``pkg_add(1)`` is not capable of querying for remote packages, so this 199 function will always return results as if there is no package available for 200 install or upgrade. 201 202 CLI Example: 203 204 .. code-block:: bash 205 206 salt '*' pkg.latest_version <package name> 207 salt '*' pkg.latest_version <package1> <package2> <package3> ... 208 """ 209 return "" if len(names) == 1 else {x: "" for x in names} 210 211 212# available_version is being deprecated 213available_version = salt.utils.functools.alias_function( 214 latest_version, "available_version" 215) 216 217 218def version(*names, **kwargs): 219 """ 220 Returns a string representing the package version or an empty string if not 221 installed. If more than one package name is specified, a dict of 222 name/version pairs is returned. 223 224 with_origin : False 225 Return a nested dictionary containing both the origin name and version 226 for each specified package. 227 228 .. versionadded:: 2014.1.0 229 230 CLI Example: 231 232 .. code-block:: bash 233 234 salt '*' pkg.version <package name> 235 salt '*' pkg.version <package1> <package2> <package3> ... 236 """ 237 with_origin = kwargs.pop("with_origin", False) 238 ret = __salt__["pkg_resource.version"](*names, **kwargs) 239 if not salt.utils.data.is_true(with_origin): 240 return ret 241 # Put the return value back into a dict since we're adding a subdict 242 if len(names) == 1: 243 ret = {names[0]: ret} 244 origins = __context__.get("pkg.origin", {}) 245 return {x: {"origin": origins.get(x, ""), "version": y} for x, y in ret.items()} 246 247 248def refresh_db(**kwargs): 249 """ 250 ``pkg_add(1)`` does not use a local database of available packages, so this 251 function simply returns ``True``. it exists merely for API compatibility. 252 253 CLI Example: 254 255 .. code-block:: bash 256 257 salt '*' pkg.refresh_db 258 """ 259 # Remove rtag file to keep multiple refreshes from happening in pkg states 260 salt.utils.pkg.clear_rtag(__opts__) 261 return True 262 263 264def _list_pkgs_from_context(versions_as_list, with_origin): 265 """ 266 Use pkg list from __context__ 267 """ 268 ret = copy.deepcopy(__context__["pkg.list_pkgs"]) 269 if not versions_as_list: 270 __salt__["pkg_resource.stringify"](ret) 271 if salt.utils.data.is_true(with_origin): 272 origins = __context__.get("pkg.origin", {}) 273 return {x: {"origin": origins.get(x, ""), "version": y} for x, y in ret.items()} 274 return ret 275 276 277def list_pkgs(versions_as_list=False, with_origin=False, **kwargs): 278 """ 279 List the packages currently installed as a dict:: 280 281 {'<package_name>': '<version>'} 282 283 with_origin : False 284 Return a nested dictionary containing both the origin name and version 285 for each installed package. 286 287 .. versionadded:: 2014.1.0 288 289 CLI Example: 290 291 .. code-block:: bash 292 293 salt '*' pkg.list_pkgs 294 """ 295 versions_as_list = salt.utils.data.is_true(versions_as_list) 296 # not yet implemented or not applicable 297 if any( 298 [salt.utils.data.is_true(kwargs.get(x)) for x in ("removed", "purge_desired")] 299 ): 300 return {} 301 302 if "pkg.list_pkgs" in __context__ and kwargs.get("use_context", True): 303 return _list_pkgs_from_context(versions_as_list, with_origin) 304 305 ret = {} 306 origins = {} 307 out = __salt__["cmd.run_stdout"]( 308 ["pkg_info", "-ao"], output_loglevel="trace", python_shell=False 309 ) 310 pkgs_re = re.compile(r"Information for ([^:]+):\s*Origin:\n([^\n]+)") 311 for pkg, origin in pkgs_re.findall(out): 312 if not pkg: 313 continue 314 try: 315 pkgname, pkgver = pkg.rsplit("-", 1) 316 except ValueError: 317 continue 318 __salt__["pkg_resource.add_pkg"](ret, pkgname, pkgver) 319 origins[pkgname] = origin 320 321 __salt__["pkg_resource.sort_pkglist"](ret) 322 __context__["pkg.list_pkgs"] = copy.deepcopy(ret) 323 __context__["pkg.origin"] = origins 324 if not versions_as_list: 325 __salt__["pkg_resource.stringify"](ret) 326 if salt.utils.data.is_true(with_origin): 327 return {x: {"origin": origins.get(x, ""), "version": y} for x, y in ret.items()} 328 return ret 329 330 331def install(name=None, refresh=False, fromrepo=None, pkgs=None, sources=None, **kwargs): 332 """ 333 Install package(s) using ``pkg_add(1)`` 334 335 name 336 The name of the package to be installed. 337 338 refresh 339 Whether or not to refresh the package database before installing. 340 341 fromrepo or packageroot 342 Specify a package repository from which to install. Overrides the 343 system default, as well as the PACKAGEROOT environment variable. 344 345 packagesite 346 Specify the exact directory from which to install the remote package. 347 Overrides the PACKAGESITE environment variable, if present. 348 349 350 Multiple Package Installation Options: 351 352 pkgs 353 A list of packages to install from a software repository. Must be 354 passed as a python list. 355 356 CLI Example: 357 358 .. code-block:: bash 359 360 salt '*' pkg.install pkgs='["foo", "bar"]' 361 362 sources 363 A list of packages to install. Must be passed as a list of dicts, 364 with the keys being package names, and the values being the source URI 365 or local path to the package. 366 367 CLI Example: 368 369 .. code-block:: bash 370 371 salt '*' pkg.install sources='[{"foo": "salt://foo.deb"}, {"bar": "salt://bar.deb"}]' 372 373 Return a dict containing the new package names and versions:: 374 375 {'<package>': {'old': '<old-version>', 376 'new': '<new-version>'}} 377 378 CLI Example: 379 380 .. code-block:: bash 381 382 salt '*' pkg.install <package name> 383 """ 384 try: 385 pkg_params, pkg_type = __salt__["pkg_resource.parse_targets"]( 386 name, pkgs, sources, **kwargs 387 ) 388 except MinionError as exc: 389 raise CommandExecutionError(exc) 390 391 if not pkg_params: 392 return {} 393 394 packageroot = kwargs.get("packageroot") 395 if not fromrepo and packageroot: 396 fromrepo = packageroot 397 398 env = _get_repo_options(fromrepo, kwargs.get("packagesite")) 399 args = [] 400 401 if pkg_type == "repository": 402 args.append("-r") # use remote repo 403 404 args.extend(pkg_params) 405 406 old = list_pkgs() 407 out = __salt__["cmd.run_all"]( 408 ["pkg_add"] + args, env=env, output_loglevel="trace", python_shell=False 409 ) 410 if out["retcode"] != 0 and out["stderr"]: 411 errors = [out["stderr"]] 412 else: 413 errors = [] 414 415 __context__.pop("pkg.list_pkgs", None) 416 new = list_pkgs() 417 _rehash() 418 ret = salt.utils.data.compare_dicts(old, new) 419 420 if errors: 421 raise CommandExecutionError( 422 "Problem encountered installing package(s)", 423 info={"errors": errors, "changes": ret}, 424 ) 425 426 return ret 427 428 429def remove(name=None, pkgs=None, **kwargs): 430 """ 431 Remove packages using ``pkg_delete(1)`` 432 433 name 434 The name of the package to be deleted. 435 436 437 Multiple Package Options: 438 439 pkgs 440 A list of packages to delete. Must be passed as a python list. The 441 ``name`` parameter will be ignored if this option is passed. 442 443 .. versionadded:: 0.16.0 444 445 446 Returns a dict containing the changes. 447 448 CLI Example: 449 450 .. code-block:: bash 451 452 salt '*' pkg.remove <package name> 453 salt '*' pkg.remove <package1>,<package2>,<package3> 454 salt '*' pkg.remove pkgs='["foo", "bar"]' 455 """ 456 try: 457 pkg_params = __salt__["pkg_resource.parse_targets"](name, pkgs)[0] 458 except MinionError as exc: 459 raise CommandExecutionError(exc) 460 461 old = list_pkgs() 462 targets, errors = _match([x for x in pkg_params]) 463 for error in errors: 464 log.error(error) 465 if not targets: 466 return {} 467 468 out = __salt__["cmd.run_all"]( 469 ["pkg_delete"] + targets, output_loglevel="trace", python_shell=False 470 ) 471 if out["retcode"] != 0 and out["stderr"]: 472 errors = [out["stderr"]] 473 else: 474 errors = [] 475 476 __context__.pop("pkg.list_pkgs", None) 477 new = list_pkgs() 478 ret = salt.utils.data.compare_dicts(old, new) 479 480 if errors: 481 raise CommandExecutionError( 482 "Problem encountered removing package(s)", 483 info={"errors": errors, "changes": ret}, 484 ) 485 486 return ret 487 488 489# Support pkg.delete to remove packages to more closely match pkg_delete 490delete = salt.utils.functools.alias_function(remove, "delete") 491# No equivalent to purge packages, use remove instead 492purge = salt.utils.functools.alias_function(remove, "purge") 493 494 495def _rehash(): 496 """ 497 Recomputes internal hash table for the PATH variable. Use whenever a new 498 command is created during the current session. 499 """ 500 shell = __salt__["environ.get"]("SHELL") 501 if shell.split("/")[-1] in ("csh", "tcsh"): 502 __salt__["cmd.shell"]("rehash", output_loglevel="trace") 503 504 505def file_list(*packages, **kwargs): 506 """ 507 List the files that belong to a package. Not specifying any packages will 508 return a list of _every_ file on the system's package database (not 509 generally recommended). 510 511 CLI Examples: 512 513 .. code-block:: bash 514 515 salt '*' pkg.file_list httpd 516 salt '*' pkg.file_list httpd postfix 517 salt '*' pkg.file_list 518 """ 519 ret = file_dict(*packages) 520 files = [] 521 for pkg_files in ret["files"].values(): 522 files.extend(pkg_files) 523 ret["files"] = files 524 return ret 525 526 527def file_dict(*packages, **kwargs): 528 """ 529 List the files that belong to a package, grouped by package. Not 530 specifying any packages will return a list of _every_ file on the 531 system's package database (not generally recommended). 532 533 CLI Examples: 534 535 .. code-block:: bash 536 537 salt '*' pkg.file_list httpd 538 salt '*' pkg.file_list httpd postfix 539 salt '*' pkg.file_list 540 """ 541 errors = [] 542 files = {} 543 544 if packages: 545 match_pattern = "'{0}-[0-9]*'" 546 cmd = ["pkg_info", "-QL"] + [match_pattern.format(p) for p in packages] 547 else: 548 cmd = ["pkg_info", "-QLa"] 549 550 ret = __salt__["cmd.run_all"](cmd, output_loglevel="trace", python_shell=False) 551 552 for line in ret["stderr"].splitlines(): 553 errors.append(line) 554 555 pkg = None 556 for line in ret["stdout"].splitlines(): 557 if pkg is not None and line.startswith("/"): 558 files[pkg].append(line) 559 elif ":/" in line: 560 pkg, fn = line.split(":", 1) 561 pkg, ver = pkg.rsplit("-", 1) 562 files[pkg] = [fn] 563 else: 564 continue # unexpected string 565 566 return {"errors": errors, "files": files} 567