1""" 2Support for MacPorts under macOS. 3 4This module has some caveats. 5 61. Updating the database of available ports is quite resource-intensive. 7However, `refresh=True` is the default for all operations that need an 8up-to-date copy of available ports. Consider `refresh=False` when you are 9sure no db update is needed. 10 112. In some cases MacPorts doesn't always realize when another copy of itself 12is running and will gleefully tromp all over the available ports database. 13This makes MacPorts behave in undefined ways until a fresh complete 14copy is retrieved. 15 16Because of 1 and 2 it is possible to get the salt-minion into a state where 17`salt mac-machine pkg./something/` won't want to return. Use 18 19`salt-run jobs.active` 20 21on the master to check for potentially long-running calls to `port`. 22 23Finally, ports database updates are always handled with `port selfupdate` 24as opposed to `port sync`. This makes sense in the MacPorts user community 25but may confuse experienced Linux admins as Linux package managers 26don't upgrade the packaging software when doing a package database update. 27In other words `salt mac-machine pkg.refresh_db` is more like 28`apt-get update; apt-get upgrade dpkg apt-get` than simply `apt-get update`. 29 30""" 31 32import copy 33import logging 34import re 35 36import salt.utils.data 37import salt.utils.functools 38import salt.utils.mac_utils 39import salt.utils.path 40import salt.utils.pkg 41import salt.utils.platform 42import salt.utils.versions 43from salt.exceptions import CommandExecutionError 44 45log = logging.getLogger(__name__) 46 47LIST_ACTIVE_ONLY = True 48__virtualname__ = "pkg" 49 50 51def __virtual__(): 52 """ 53 Confine this module to Mac OS with MacPorts. 54 """ 55 if not salt.utils.platform.is_darwin(): 56 return False, "mac_ports only available on MacOS" 57 58 if not salt.utils.path.which("port"): 59 return False, 'mac_ports requires the "port" binary' 60 61 return __virtualname__ 62 63 64def _list(query=""): 65 cmd = "port list {}".format(query) 66 out = salt.utils.mac_utils.execute_return_result(cmd) 67 68 ret = {} 69 for line in out.splitlines(): 70 try: 71 name, version_num, category = re.split(r"\s+", line.lstrip())[0:3] 72 version_num = version_num[1:] 73 except ValueError: 74 continue 75 ret[name] = version_num 76 77 return ret 78 79 80def _list_pkgs_from_context(versions_as_list): 81 """ 82 Use pkg list from __context__ 83 """ 84 if versions_as_list: 85 return __context__["pkg.list_pkgs"] 86 else: 87 ret = copy.deepcopy(__context__["pkg.list_pkgs"]) 88 __salt__["pkg_resource.stringify"](ret) 89 return ret 90 91 92def list_pkgs(versions_as_list=False, **kwargs): 93 """ 94 List the packages currently installed in a dict:: 95 96 {'<package_name>': '<version>'} 97 98 CLI Example: 99 100 .. code-block:: bash 101 102 salt '*' pkg.list_pkgs 103 """ 104 versions_as_list = salt.utils.data.is_true(versions_as_list) 105 # 'removed', 'purge_desired' not yet implemented or not applicable 106 if any( 107 [salt.utils.data.is_true(kwargs.get(x)) for x in ("removed", "purge_desired")] 108 ): 109 return {} 110 111 if "pkg.list_pkgs" in __context__ and kwargs.get("use_context", True): 112 return _list_pkgs_from_context(versions_as_list) 113 114 ret = {} 115 cmd = ["port", "installed"] 116 out = salt.utils.mac_utils.execute_return_result(cmd) 117 for line in out.splitlines(): 118 try: 119 name, version_num, active = re.split(r"\s+", line.lstrip())[0:3] 120 version_num = version_num[1:] 121 except ValueError: 122 continue 123 if not LIST_ACTIVE_ONLY or active == "(active)": 124 __salt__["pkg_resource.add_pkg"](ret, name, version_num) 125 126 __salt__["pkg_resource.sort_pkglist"](ret) 127 __context__["pkg.list_pkgs"] = copy.deepcopy(ret) 128 if not versions_as_list: 129 __salt__["pkg_resource.stringify"](ret) 130 return ret 131 132 133def version(*names, **kwargs): 134 """ 135 Returns a string representing the package version or an empty string if not 136 installed. If more than one package name is specified, a dict of 137 name/version pairs is returned. 138 139 CLI Example: 140 141 .. code-block:: bash 142 143 salt '*' pkg.version <package name> 144 salt '*' pkg.version <package1> <package2> <package3> 145 """ 146 return __salt__["pkg_resource.version"](*names, **kwargs) 147 148 149def latest_version(*names, **kwargs): 150 """ 151 Return the latest version of the named package available for upgrade or 152 installation 153 154 Options: 155 156 refresh 157 Update ports with ``port selfupdate`` 158 159 CLI Example: 160 161 .. code-block:: bash 162 163 salt '*' pkg.latest_version <package name> 164 salt '*' pkg.latest_version <package1> <package2> <package3> 165 """ 166 167 if salt.utils.data.is_true(kwargs.get("refresh", True)): 168 refresh_db() 169 170 available = _list(" ".join(names)) or {} 171 installed = __salt__["pkg.list_pkgs"]() or {} 172 173 ret = {} 174 175 for key, val in available.items(): 176 if key not in installed or salt.utils.versions.compare( 177 ver1=installed[key], oper="<", ver2=val 178 ): 179 ret[key] = val 180 else: 181 ret[key] = "{} (installed)".format(version(key)) 182 183 return ret 184 185 186# available_version is being deprecated 187available_version = salt.utils.functools.alias_function( 188 latest_version, "available_version" 189) 190 191 192def remove(name=None, pkgs=None, **kwargs): 193 """ 194 Removes packages with ``port uninstall``. 195 196 name 197 The name of the package to be deleted. 198 199 200 Multiple Package Options: 201 202 pkgs 203 A list of packages to delete. Must be passed as a python list. The 204 ``name`` parameter will be ignored if this option is passed. 205 206 .. versionadded:: 0.16.0 207 208 209 Returns a dict containing the changes. 210 211 CLI Example: 212 213 .. code-block:: bash 214 215 salt '*' pkg.remove <package name> 216 salt '*' pkg.remove <package1>,<package2>,<package3> 217 salt '*' pkg.remove pkgs='["foo", "bar"]' 218 """ 219 pkg_params = __salt__["pkg_resource.parse_targets"](name, pkgs, **kwargs)[0] 220 old = list_pkgs() 221 targets = [x for x in pkg_params if x in old] 222 if not targets: 223 return {} 224 225 cmd = ["port", "uninstall"] 226 cmd.extend(targets) 227 228 err_message = "" 229 try: 230 salt.utils.mac_utils.execute_return_success(cmd) 231 except CommandExecutionError as exc: 232 err_message = exc.strerror 233 234 __context__.pop("pkg.list_pkgs", None) 235 new = list_pkgs() 236 ret = salt.utils.data.compare_dicts(old, new) 237 238 if err_message: 239 raise CommandExecutionError( 240 "Problem encountered removing package(s)", 241 info={"errors": err_message, "changes": ret}, 242 ) 243 244 return ret 245 246 247def install(name=None, refresh=False, pkgs=None, **kwargs): 248 """ 249 Install the passed package(s) with ``port install`` 250 251 name 252 The name of the formula to be installed. Note that this parameter is 253 ignored if "pkgs" is passed. 254 255 CLI Example: 256 257 .. code-block:: bash 258 259 salt '*' pkg.install <package name> 260 261 version 262 Specify a version to pkg to install. Ignored if pkgs is specified. 263 264 CLI Example: 265 266 .. code-block:: bash 267 268 salt '*' pkg.install <package name> 269 salt '*' pkg.install git-core version='1.8.5.5' 270 271 variant 272 Specify a variant to pkg to install. Ignored if pkgs is specified. 273 274 CLI Example: 275 276 .. code-block:: bash 277 278 salt '*' pkg.install <package name> 279 salt '*' pkg.install git-core version='1.8.5.5' variant='+credential_osxkeychain+doc+pcre' 280 281 Multiple Package Installation Options: 282 283 pkgs 284 A list of formulas to install. Must be passed as a python list. 285 286 CLI Example: 287 288 .. code-block:: bash 289 290 salt '*' pkg.install pkgs='["foo","bar"]' 291 salt '*' pkg.install pkgs='["foo@1.2","bar"]' 292 salt '*' pkg.install pkgs='["foo@1.2+ssl","bar@2.3"]' 293 294 295 Returns a dict containing the new package names and versions:: 296 297 {'<package>': {'old': '<old-version>', 298 'new': '<new-version>'}} 299 300 CLI Example: 301 302 .. code-block:: bash 303 304 salt '*' pkg.install 'package package package' 305 """ 306 pkg_params, pkg_type = __salt__["pkg_resource.parse_targets"](name, pkgs, {}) 307 308 if salt.utils.data.is_true(refresh): 309 refresh_db() 310 311 # Handle version kwarg for a single package target 312 if pkgs is None: 313 version_num = kwargs.get("version") 314 variant_spec = kwargs.get("variant") 315 spec = {} 316 317 if version_num: 318 spec["version"] = version_num 319 320 if variant_spec: 321 spec["variant"] = variant_spec 322 323 pkg_params = {name: spec} 324 325 if not pkg_params: 326 return {} 327 328 formulas_array = [] 329 for pname, pparams in pkg_params.items(): 330 formulas_array.append(pname) 331 332 if pparams: 333 if "version" in pparams: 334 formulas_array.append("@" + pparams["version"]) 335 336 if "variant" in pparams: 337 formulas_array.append(pparams["variant"]) 338 339 old = list_pkgs() 340 cmd = ["port", "install"] 341 cmd.extend(formulas_array) 342 343 err_message = "" 344 try: 345 salt.utils.mac_utils.execute_return_success(cmd) 346 except CommandExecutionError as exc: 347 err_message = exc.strerror 348 349 __context__.pop("pkg.list_pkgs", None) 350 new = list_pkgs() 351 ret = salt.utils.data.compare_dicts(old, new) 352 353 if err_message: 354 raise CommandExecutionError( 355 "Problem encountered installing package(s)", 356 info={"errors": err_message, "changes": ret}, 357 ) 358 359 return ret 360 361 362def list_upgrades(refresh=True, **kwargs): # pylint: disable=W0613 363 """ 364 Check whether or not an upgrade is available for all packages 365 366 Options: 367 368 refresh 369 Update ports with ``port selfupdate`` 370 371 CLI Example: 372 373 .. code-block:: bash 374 375 salt '*' pkg.list_upgrades 376 """ 377 378 if refresh: 379 refresh_db() 380 return _list("outdated") 381 382 383def upgrade_available(pkg, refresh=True, **kwargs): 384 """ 385 Check whether or not an upgrade is available for a given package 386 387 CLI Example: 388 389 .. code-block:: bash 390 391 salt '*' pkg.upgrade_available <package name> 392 """ 393 return pkg in list_upgrades(refresh=refresh) 394 395 396def refresh_db(**kwargs): 397 """ 398 Update ports with ``port selfupdate`` 399 400 CLI Example: 401 402 .. code-block:: bash 403 404 salt mac pkg.refresh_db 405 """ 406 # Remove rtag file to keep multiple refreshes from happening in pkg states 407 salt.utils.pkg.clear_rtag(__opts__) 408 cmd = ["port", "selfupdate"] 409 return salt.utils.mac_utils.execute_return_success(cmd) 410 411 412def upgrade(refresh=True, **kwargs): # pylint: disable=W0613 413 """ 414 Run a full upgrade using MacPorts 'port upgrade outdated' 415 416 Options: 417 418 refresh 419 Update ports with ``port selfupdate`` 420 421 Returns a dictionary containing the changes: 422 423 .. code-block:: python 424 425 {'<package>': {'old': '<old-version>', 426 'new': '<new-version>'}} 427 428 CLI Example: 429 430 .. code-block:: bash 431 432 salt '*' pkg.upgrade 433 """ 434 if refresh: 435 refresh_db() 436 437 old = list_pkgs() 438 cmd = ["port", "upgrade", "outdated"] 439 result = __salt__["cmd.run_all"](cmd, output_loglevel="trace", python_shell=False) 440 __context__.pop("pkg.list_pkgs", None) 441 new = list_pkgs() 442 ret = salt.utils.data.compare_dicts(old, new) 443 444 if result["retcode"] != 0: 445 raise CommandExecutionError( 446 "Problem encountered upgrading packages", 447 info={"changes": ret, "result": result}, 448 ) 449 450 return ret 451