1""" 2A module to manage software on Windows 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 10The following functions require the existence of a :ref:`windows repository 11<windows-package-manager>` metadata DB, typically created by running 12:py:func:`pkg.refresh_db <salt.modules.win_pkg.refresh_db>`: 13 14- :py:func:`pkg.get_repo_data <salt.modules.win_pkg.get_repo_data>` 15- :py:func:`pkg.install <salt.modules.win_pkg.install>` 16- :py:func:`pkg.latest_version <salt.modules.win_pkg.latest_version>` 17- :py:func:`pkg.list_available <salt.modules.win_pkg.list_available>` 18- :py:func:`pkg.list_pkgs <salt.modules.win_pkg.list_pkgs>` 19- :py:func:`pkg.list_upgrades <salt.modules.win_pkg.list_upgrades>` 20- :py:func:`pkg.remove <salt.modules.win_pkg.remove>` 21 22If a metadata DB does not already exist and one of these functions is run, then 23one will be created from the repo SLS files that are present. 24 25As the creation of this metadata can take some time, the 26:conf_minion:`winrepo_cache_expire_min` minion config option can be used to 27suppress refreshes when the metadata is less than a given number of seconds 28old. 29 30.. note:: 31 Version numbers can be ``version number string``, ``latest`` and ``Not 32 Found``, where ``Not Found`` means this module was not able to determine 33 the version of the software installed, it can also be used as the version 34 number in sls definitions file in these cases. Versions numbers are sorted 35 in order of 0, ``Not Found``, ``order version numbers``, ..., ``latest``. 36 37""" 38 39 40import collections 41import datetime 42import errno 43import logging 44import os 45import re 46import sys 47import time 48import urllib.parse 49from functools import cmp_to_key 50 51import salt.payload 52import salt.syspaths 53import salt.utils.args 54import salt.utils.data 55import salt.utils.files 56import salt.utils.hashutils 57import salt.utils.path 58import salt.utils.pkg 59import salt.utils.platform 60import salt.utils.versions 61import salt.utils.win_functions 62from salt.exceptions import ( 63 CommandExecutionError, 64 MinionError, 65 SaltInvocationError, 66 SaltRenderError, 67) 68from salt.utils.versions import LooseVersion 69 70log = logging.getLogger(__name__) 71 72# Define the module's virtual name 73__virtualname__ = "pkg" 74 75 76def __virtual__(): 77 """ 78 Set the virtual pkg module if the os is Windows 79 """ 80 if salt.utils.platform.is_windows(): 81 return __virtualname__ 82 return (False, "Module win_pkg: module only works on Windows systems") 83 84 85def latest_version(*names, **kwargs): 86 """ 87 Return the latest version of the named package available for upgrade or 88 installation. If more than one package name is specified, a dict of 89 name/version pairs is returned. 90 91 If the latest version of a given package is already installed, an empty 92 string will be returned for that package. 93 94 .. note:: 95 Since this is looking for the latest version available, a refresh_db 96 will be triggered by default. This can take some time. To avoid this set 97 ``refresh`` to ``False``. 98 99 Args: 100 names (str): A single or multiple names to lookup 101 102 Kwargs: 103 saltenv (str): Salt environment. Default ``base`` 104 refresh (bool): Refresh package metadata. Default ``True`` 105 106 Returns: 107 dict: A dictionary of packages with the latest version available 108 109 CLI Example: 110 111 .. code-block:: bash 112 113 salt '*' pkg.latest_version <package name> 114 salt '*' pkg.latest_version <package1> <package2> <package3> ... 115 """ 116 if not names: 117 return "" 118 119 # Initialize the return dict with empty strings 120 ret = {} 121 for name in names: 122 ret[name] = "" 123 124 saltenv = kwargs.get("saltenv", "base") 125 # Refresh before looking for the latest version available 126 refresh = salt.utils.data.is_true(kwargs.get("refresh", True)) 127 128 # no need to call _refresh_db_conditional as list_pkgs will do it 129 installed_pkgs = list_pkgs(versions_as_list=True, saltenv=saltenv, refresh=refresh) 130 log.trace("List of installed packages: %s", installed_pkgs) 131 132 # iterate over all requested package names 133 for name in names: 134 latest_installed = "0" 135 136 # get latest installed version of package 137 if name in installed_pkgs: 138 log.trace("Determining latest installed version of %s", name) 139 try: 140 # installed_pkgs[name] Can be version number or 'Not Found' 141 # 'Not Found' occurs when version number is not found in the registry 142 latest_installed = sorted( 143 installed_pkgs[name], key=cmp_to_key(_reverse_cmp_pkg_versions) 144 ).pop() 145 except IndexError: 146 log.warning( 147 "%s was empty in pkg.list_pkgs return data, this is " 148 "probably a bug in list_pkgs", 149 name, 150 ) 151 else: 152 log.debug( 153 "Latest installed version of %s is %s", name, latest_installed 154 ) 155 156 # get latest available (from winrepo_dir) version of package 157 pkg_info = _get_package_info(name, saltenv=saltenv) 158 log.trace("Raw winrepo pkg_info for %s is %s", name, pkg_info) 159 160 # latest_available can be version number or 'latest' or even 'Not Found' 161 latest_available = _get_latest_pkg_version(pkg_info) 162 if latest_available: 163 log.debug( 164 "Latest available version of package %s is %s", name, latest_available 165 ) 166 167 # check, whether latest available version 168 # is newer than latest installed version 169 if compare_versions( 170 ver1=str(latest_available), 171 oper=">", 172 ver2=str(latest_installed), 173 ): 174 log.debug( 175 "Upgrade of %s from %s to %s is available", 176 name, 177 latest_installed, 178 latest_available, 179 ) 180 ret[name] = latest_available 181 else: 182 log.debug( 183 "No newer version than %s of %s is available", 184 latest_installed, 185 name, 186 ) 187 if len(names) == 1: 188 return ret[names[0]] 189 return ret 190 191 192def upgrade_available(name, **kwargs): 193 """ 194 Check whether or not an upgrade is available for a given package 195 196 Args: 197 name (str): The name of a single package 198 199 Kwargs: 200 refresh (bool): Refresh package metadata. Default ``True`` 201 saltenv (str): The salt environment. Default ``base`` 202 203 Returns: 204 bool: True if new version available, otherwise False 205 206 CLI Example: 207 208 .. code-block:: bash 209 210 salt '*' pkg.upgrade_available <package name> 211 """ 212 saltenv = kwargs.get("saltenv", "base") 213 # Refresh before looking for the latest version available, 214 # same default as latest_version 215 refresh = salt.utils.data.is_true(kwargs.get("refresh", True)) 216 217 # if latest_version returns blank, the latest version is already installed or 218 # their is no package definition. This is a salt standard which could be improved. 219 return latest_version(name, saltenv=saltenv, refresh=refresh) != "" 220 221 222def list_upgrades(refresh=True, **kwargs): 223 """ 224 List all available package upgrades on this system 225 226 Args: 227 refresh (bool): Refresh package metadata. Default ``True`` 228 229 Kwargs: 230 saltenv (str): Salt environment. Default ``base`` 231 232 Returns: 233 dict: A dictionary of packages with available upgrades 234 235 CLI Example: 236 237 .. code-block:: bash 238 239 salt '*' pkg.list_upgrades 240 """ 241 saltenv = kwargs.get("saltenv", "base") 242 refresh = salt.utils.data.is_true(refresh) 243 _refresh_db_conditional(saltenv, force=refresh) 244 245 installed_pkgs = list_pkgs(refresh=False, saltenv=saltenv) 246 available_pkgs = get_repo_data(saltenv).get("repo") 247 pkgs = {} 248 for pkg in installed_pkgs: 249 if pkg in available_pkgs: 250 # latest_version() will be blank if the latest version is installed. 251 # or the package name is wrong. Given we check available_pkgs, this 252 # should not be the case of wrong package name. 253 # Note: latest_version() is an expensive way to do this as it 254 # calls list_pkgs each time. 255 latest_ver = latest_version(pkg, refresh=False, saltenv=saltenv) 256 if latest_ver: 257 pkgs[pkg] = latest_ver 258 259 return pkgs 260 261 262def list_available(*names, **kwargs): 263 """ 264 Return a list of available versions of the specified package. 265 266 Args: 267 names (str): One or more package names 268 269 Kwargs: 270 271 saltenv (str): The salt environment to use. Default ``base``. 272 273 refresh (bool): Refresh package metadata. Default ``False``. 274 275 return_dict_always (bool): 276 Default ``False`` dict when a single package name is queried. 277 278 Returns: 279 dict: The package name with its available versions 280 281 .. code-block:: cfg 282 283 {'<package name>': ['<version>', '<version>', ]} 284 285 CLI Example: 286 287 .. code-block:: bash 288 289 salt '*' pkg.list_available <package name> return_dict_always=True 290 salt '*' pkg.list_available <package name01> <package name02> 291 """ 292 if not names: 293 return "" 294 295 saltenv = kwargs.get("saltenv", "base") 296 refresh = salt.utils.data.is_true(kwargs.get("refresh", False)) 297 _refresh_db_conditional(saltenv, force=refresh) 298 return_dict_always = salt.utils.data.is_true( 299 kwargs.get("return_dict_always", False) 300 ) 301 if len(names) == 1 and not return_dict_always: 302 pkginfo = _get_package_info(names[0], saltenv=saltenv) 303 if not pkginfo: 304 return "" 305 versions = sorted( 306 list(pkginfo.keys()), key=cmp_to_key(_reverse_cmp_pkg_versions) 307 ) 308 else: 309 versions = {} 310 for name in names: 311 pkginfo = _get_package_info(name, saltenv=saltenv) 312 if not pkginfo: 313 continue 314 verlist = sorted( 315 list(pkginfo.keys()) if pkginfo else [], 316 key=cmp_to_key(_reverse_cmp_pkg_versions), 317 ) 318 versions[name] = verlist 319 return versions 320 321 322def version(*names, **kwargs): 323 """ 324 Returns a string representing the package version or an empty string if not 325 installed. If more than one package name is specified, a dict of 326 name/version pairs is returned. 327 328 Args: 329 name (str): One or more package names 330 331 Kwargs: 332 saltenv (str): The salt environment to use. Default ``base``. 333 refresh (bool): Refresh package metadata. Default ``False``. 334 335 Returns: 336 str: version string when a single package is specified. 337 dict: The package name(s) with the installed versions. 338 339 .. code-block:: cfg 340 341 {['<version>', '<version>', ]} OR 342 {'<package name>': ['<version>', '<version>', ]} 343 344 CLI Example: 345 346 .. code-block:: bash 347 348 salt '*' pkg.version <package name> 349 salt '*' pkg.version <package name01> <package name02> 350 351 """ 352 # Standard is return empty string even if not a valid name 353 # TODO: Look at returning an error across all platforms with 354 # CommandExecutionError(msg,info={'errors': errors }) 355 # available_pkgs = get_repo_data(saltenv).get('repo') 356 # for name in names: 357 # if name in available_pkgs: 358 # ret[name] = installed_pkgs.get(name, '') 359 360 saltenv = kwargs.get("saltenv", "base") 361 installed_pkgs = list_pkgs(saltenv=saltenv, refresh=kwargs.get("refresh", False)) 362 363 if len(names) == 1: 364 return installed_pkgs.get(names[0], "") 365 366 ret = {} 367 for name in names: 368 ret[name] = installed_pkgs.get(name, "") 369 return ret 370 371 372def list_pkgs( 373 versions_as_list=False, include_components=True, include_updates=True, **kwargs 374): 375 """ 376 List the packages currently installed. 377 378 .. note:: 379 To view installed software as displayed in the Add/Remove Programs, set 380 ``include_components`` and ``include_updates`` to False. 381 382 Args: 383 384 versions_as_list (bool): 385 Returns the versions as a list 386 387 include_components (bool): 388 Include sub components of installed software. Default is ``True`` 389 390 include_updates (bool): 391 Include software updates and Windows updates. Default is ``True`` 392 393 Kwargs: 394 395 saltenv (str): 396 The salt environment to use. Default ``base`` 397 398 refresh (bool): 399 Refresh package metadata. Default ``False`` 400 401 Returns: 402 dict: A dictionary of installed software with versions installed 403 404 .. code-block:: cfg 405 406 {'<package_name>': '<version>'} 407 408 CLI Example: 409 410 .. code-block:: bash 411 412 salt '*' pkg.list_pkgs 413 salt '*' pkg.list_pkgs versions_as_list=True 414 """ 415 versions_as_list = salt.utils.data.is_true(versions_as_list) 416 # not yet implemented or not applicable 417 if any( 418 [salt.utils.data.is_true(kwargs.get(x)) for x in ("removed", "purge_desired")] 419 ): 420 return {} 421 saltenv = kwargs.get("saltenv", "base") 422 refresh = salt.utils.data.is_true(kwargs.get("refresh", False)) 423 _refresh_db_conditional(saltenv, force=refresh) 424 425 ret = {} 426 name_map = _get_name_map(saltenv) 427 for pkg_name, val_list in _get_reg_software( 428 include_components=include_components, include_updates=include_updates 429 ).items(): 430 if pkg_name in name_map: 431 key = name_map[pkg_name] 432 for val in val_list: 433 if val == "Not Found": 434 # Look up version from winrepo 435 pkg_info = _get_package_info(key, saltenv=saltenv) 436 if not pkg_info: 437 continue 438 for pkg_ver in pkg_info.keys(): 439 if pkg_info[pkg_ver]["full_name"] == pkg_name: 440 val = pkg_ver 441 __salt__["pkg_resource.add_pkg"](ret, key, val) 442 else: 443 key = pkg_name 444 for val in val_list: 445 __salt__["pkg_resource.add_pkg"](ret, key, val) 446 447 __salt__["pkg_resource.sort_pkglist"](ret) 448 if not versions_as_list: 449 __salt__["pkg_resource.stringify"](ret) 450 return ret 451 452 453def _get_reg_software(include_components=True, include_updates=True): 454 """ 455 This searches the uninstall keys in the registry to find a match in the sub 456 keys, it will return a dict with the display name as the key and the 457 version as the value 458 459 Args: 460 461 include_components (bool): 462 Include sub components of installed software. Default is ``True`` 463 464 include_updates (bool): 465 Include software updates and Windows updates. Default is ``True`` 466 467 Returns: 468 dict: A dictionary of installed software with versions installed 469 470 .. code-block:: cfg 471 472 {'<package_name>': '<version>'} 473 """ 474 # Logic for this can be found in this question: 475 # https://social.technet.microsoft.com/Forums/windows/en-US/d913471a-d7fb-448d-869b-da9025dcc943/where-does-addremove-programs-get-its-information-from-in-the-registry 476 # and also in the collectPlatformDependentApplicationData function in 477 # https://github.com/aws/amazon-ssm-agent/blob/master/agent/plugins/inventory/gatherers/application/dataProvider_windows.go 478 reg_software = {} 479 480 def skip_component(hive, key, sub_key, use_32bit_registry): 481 """ 482 'SystemComponent' must be either absent or present with a value of 0, 483 because this value is usually set on programs that have been installed 484 via a Windows Installer Package (MSI). 485 486 Returns: 487 bool: True if the package needs to be skipped, otherwise False 488 """ 489 if include_components: 490 return False 491 if __utils__["reg.value_exists"]( 492 hive=hive, 493 key="{}\\{}".format(key, sub_key), 494 vname="SystemComponent", 495 use_32bit_registry=use_32bit_registry, 496 ): 497 if ( 498 __utils__["reg.read_value"]( 499 hive=hive, 500 key="{}\\{}".format(key, sub_key), 501 vname="SystemComponent", 502 use_32bit_registry=use_32bit_registry, 503 )["vdata"] 504 > 0 505 ): 506 return True 507 return False 508 509 def skip_win_installer(hive, key, sub_key, use_32bit_registry): 510 """ 511 'WindowsInstaller' must be either absent or present with a value of 0. 512 If the value is set to 1, then the application is included in the list 513 if and only if the corresponding compressed guid is also present in 514 HKLM:\\Software\\Classes\\Installer\\Products 515 516 Returns: 517 bool: True if the package needs to be skipped, otherwise False 518 """ 519 products_key = "Software\\Classes\\Installer\\Products\\{0}" 520 if __utils__["reg.value_exists"]( 521 hive=hive, 522 key="{}\\{}".format(key, sub_key), 523 vname="WindowsInstaller", 524 use_32bit_registry=use_32bit_registry, 525 ): 526 if ( 527 __utils__["reg.read_value"]( 528 hive=hive, 529 key="{}\\{}".format(key, sub_key), 530 vname="WindowsInstaller", 531 use_32bit_registry=use_32bit_registry, 532 )["vdata"] 533 > 0 534 ): 535 squid = salt.utils.win_functions.guid_to_squid(sub_key) 536 if not __utils__["reg.key_exists"]( 537 hive="HKLM", 538 key=products_key.format(squid), 539 use_32bit_registry=use_32bit_registry, 540 ): 541 return True 542 return False 543 544 def skip_uninstall_string(hive, key, sub_key, use_32bit_registry): 545 """ 546 'UninstallString' must be present, because it stores the command line 547 that gets executed by Add/Remove programs, when the user tries to 548 uninstall a program. 549 550 Returns: 551 bool: True if the package needs to be skipped, otherwise False 552 """ 553 if not __utils__["reg.value_exists"]( 554 hive=hive, 555 key="{}\\{}".format(key, sub_key), 556 vname="UninstallString", 557 use_32bit_registry=use_32bit_registry, 558 ): 559 return True 560 return False 561 562 def skip_release_type(hive, key, sub_key, use_32bit_registry): 563 """ 564 'ReleaseType' must either be absent or if present must not have a 565 value set to 'Security Update', 'Update Rollup', or 'Hotfix', because 566 that indicates it's an update to an existing program. 567 568 Returns: 569 bool: True if the package needs to be skipped, otherwise False 570 """ 571 if include_updates: 572 return False 573 skip_types = ["Hotfix", "Security Update", "Update Rollup"] 574 if __utils__["reg.value_exists"]( 575 hive=hive, 576 key="{}\\{}".format(key, sub_key), 577 vname="ReleaseType", 578 use_32bit_registry=use_32bit_registry, 579 ): 580 if ( 581 __utils__["reg.read_value"]( 582 hive=hive, 583 key="{}\\{}".format(key, sub_key), 584 vname="ReleaseType", 585 use_32bit_registry=use_32bit_registry, 586 )["vdata"] 587 in skip_types 588 ): 589 return True 590 return False 591 592 def skip_parent_key(hive, key, sub_key, use_32bit_registry): 593 """ 594 'ParentKeyName' must NOT be present, because that indicates it's an 595 update to the parent program. 596 597 Returns: 598 bool: True if the package needs to be skipped, otherwise False 599 """ 600 if __utils__["reg.value_exists"]( 601 hive=hive, 602 key="{}\\{}".format(key, sub_key), 603 vname="ParentKeyName", 604 use_32bit_registry=use_32bit_registry, 605 ): 606 return True 607 608 return False 609 610 def add_software(hive, key, sub_key, use_32bit_registry): 611 """ 612 'DisplayName' must be present with a valid value, as this is reflected 613 as the software name returned by pkg.list_pkgs. Also, its value must 614 not start with 'KB' followed by 6 numbers - as that indicates a 615 Windows update. 616 """ 617 d_name_regdata = __utils__["reg.read_value"]( 618 hive=hive, 619 key="{}\\{}".format(key, sub_key), 620 vname="DisplayName", 621 use_32bit_registry=use_32bit_registry, 622 ) 623 624 if ( 625 not d_name_regdata["success"] 626 or d_name_regdata["vtype"] not in ["REG_SZ", "REG_EXPAND_SZ"] 627 or d_name_regdata["vdata"] in ["(value not set)", None, False] 628 ): 629 return 630 d_name = d_name_regdata["vdata"] 631 632 if not include_updates: 633 if re.match(r"^KB[0-9]{6}", d_name): 634 return 635 636 d_vers_regdata = __utils__["reg.read_value"]( 637 hive=hive, 638 key="{}\\{}".format(key, sub_key), 639 vname="DisplayVersion", 640 use_32bit_registry=use_32bit_registry, 641 ) 642 643 d_vers = "Not Found" 644 if d_vers_regdata["success"] and d_vers_regdata["vtype"] in [ 645 "REG_SZ", 646 "REG_EXPAND_SZ", 647 "REG_DWORD", 648 ]: 649 if isinstance(d_vers_regdata["vdata"], int): 650 d_vers = str(d_vers_regdata["vdata"]) 651 elif ( 652 d_vers_regdata["vdata"] and d_vers_regdata["vdata"] != "(value not set)" 653 ): # Check for blank values 654 d_vers = d_vers_regdata["vdata"] 655 656 reg_software.setdefault(d_name, []).append(d_vers) 657 658 # Start gathering information from the registry 659 # HKLM Uninstall 64 bit 660 kwargs = { 661 "hive": "HKLM", 662 "key": "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall", 663 "use_32bit_registry": False, 664 } 665 for sub_key in __utils__["reg.list_keys"](**kwargs): 666 kwargs["sub_key"] = sub_key 667 if skip_component(**kwargs): 668 continue 669 if skip_win_installer(**kwargs): 670 continue 671 if skip_uninstall_string(**kwargs): 672 continue 673 if skip_release_type(**kwargs): 674 continue 675 if skip_parent_key(**kwargs): 676 continue 677 add_software(**kwargs) 678 679 # HKLM Uninstall 32 bit 680 kwargs["use_32bit_registry"] = True 681 kwargs.pop("sub_key", False) 682 for sub_key in __utils__["reg.list_keys"](**kwargs): 683 kwargs["sub_key"] = sub_key 684 if skip_component(**kwargs): 685 continue 686 if skip_win_installer(**kwargs): 687 continue 688 if skip_uninstall_string(**kwargs): 689 continue 690 if skip_release_type(**kwargs): 691 continue 692 if skip_parent_key(**kwargs): 693 continue 694 add_software(**kwargs) 695 696 # HKLM Uninstall 64 bit 697 kwargs = { 698 "hive": "HKLM", 699 "key": "Software\\Classes\\Installer\\Products", 700 "use_32bit_registry": False, 701 } 702 userdata_key = ( 703 "Software\\Microsoft\\Windows\\CurrentVersion\\Installer\\" 704 "UserData\\S-1-5-18\\Products" 705 ) 706 for sub_key in __utils__["reg.list_keys"](**kwargs): 707 # If the key does not exist in userdata, skip it 708 if not __utils__["reg.key_exists"]( 709 hive=kwargs["hive"], key="{}\\{}".format(userdata_key, sub_key) 710 ): 711 continue 712 kwargs["sub_key"] = sub_key 713 if skip_component(**kwargs): 714 continue 715 if skip_win_installer(**kwargs): 716 continue 717 add_software(**kwargs) 718 719 # Uninstall for each user on the system (HKU), 64 bit 720 # This has a propensity to take a while on a machine where many users have 721 # logged in. Untested in such a scenario 722 hive_hku = "HKU" 723 uninstall_key = "{0}\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall" 724 product_key = "{0}\\Software\\Microsoft\\Installer\\Products" 725 user_data_key = ( 726 "Software\\Microsoft\\Windows\\CurrentVersion\\Installer\\" 727 "UserData\\{0}\\Products\\{1}" 728 ) 729 for user_guid in __utils__["reg.list_keys"](hive=hive_hku): 730 kwargs = { 731 "hive": hive_hku, 732 "key": uninstall_key.format(user_guid), 733 "use_32bit_registry": False, 734 } 735 if __utils__["reg.key_exists"](**kwargs): 736 for sub_key in __utils__["reg.list_keys"](**kwargs): 737 kwargs["sub_key"] = sub_key 738 if skip_component(**kwargs): 739 continue 740 if skip_win_installer(**kwargs): 741 continue 742 if skip_uninstall_string(**kwargs): 743 continue 744 if skip_release_type(**kwargs): 745 continue 746 if skip_parent_key(**kwargs): 747 continue 748 add_software(**kwargs) 749 750 # While we have the user guid, we're gong to check userdata in HKLM 751 kwargs = { 752 "hive": hive_hku, 753 "key": product_key.format(user_guid), 754 "use_32bit_registry": False, 755 } 756 if __utils__["reg.key_exists"](**kwargs): 757 for sub_key in __utils__["reg.list_keys"](**kwargs): 758 kwargs = { 759 "hive": "HKLM", 760 "key": user_data_key.format(user_guid, sub_key), 761 "use_32bit_registry": False, 762 } 763 if __utils__["reg.key_exists"](**kwargs): 764 kwargs["sub_key"] = "InstallProperties" 765 if skip_component(**kwargs): 766 continue 767 add_software(**kwargs) 768 769 # Uninstall for each user on the system (HKU), 32 bit 770 for user_guid in __utils__["reg.list_keys"](hive=hive_hku, use_32bit_registry=True): 771 kwargs = { 772 "hive": hive_hku, 773 "key": uninstall_key.format(user_guid), 774 "use_32bit_registry": True, 775 } 776 if __utils__["reg.key_exists"](**kwargs): 777 for sub_key in __utils__["reg.list_keys"](**kwargs): 778 kwargs["sub_key"] = sub_key 779 if skip_component(**kwargs): 780 continue 781 if skip_win_installer(**kwargs): 782 continue 783 if skip_uninstall_string(**kwargs): 784 continue 785 if skip_release_type(**kwargs): 786 continue 787 if skip_parent_key(**kwargs): 788 continue 789 add_software(**kwargs) 790 791 kwargs = { 792 "hive": hive_hku, 793 "key": product_key.format(user_guid), 794 "use_32bit_registry": True, 795 } 796 if __utils__["reg.key_exists"](**kwargs): 797 # While we have the user guid, we're going to check userdata in HKLM 798 for sub_key_2 in __utils__["reg.list_keys"](**kwargs): 799 kwargs = { 800 "hive": "HKLM", 801 "key": user_data_key.format(user_guid, sub_key_2), 802 "use_32bit_registry": True, 803 } 804 if __utils__["reg.key_exists"](**kwargs): 805 kwargs["sub_key"] = "InstallProperties" 806 if skip_component(**kwargs): 807 continue 808 add_software(**kwargs) 809 810 return reg_software 811 812 813def _refresh_db_conditional(saltenv, **kwargs): 814 """ 815 Internal use only in this module, has a different set of defaults and 816 returns True or False. And supports checking the age of the existing 817 generated metadata db, as well as ensure metadata db exists to begin with 818 819 Args: 820 saltenv (str): Salt environment 821 822 Kwargs: 823 824 force (bool): 825 Force a refresh if the minimum age has been reached. Default is 826 False. 827 828 failhard (bool): 829 If ``True``, an error will be raised if any repo SLS files failed to 830 process. 831 832 Returns: 833 bool: True Fetched or Cache uptodate, False to indicate an issue 834 835 :codeauthor: Damon Atkins <https://github.com/damon-atkins> 836 """ 837 force = salt.utils.data.is_true(kwargs.pop("force", False)) 838 failhard = salt.utils.data.is_true(kwargs.pop("failhard", False)) 839 expired_max = __opts__["winrepo_cache_expire_max"] 840 expired_min = __opts__["winrepo_cache_expire_min"] 841 842 repo_details = _get_repo_details(saltenv) 843 844 # Skip force if age less than minimum age 845 if force and expired_min > 0 and repo_details.winrepo_age < expired_min: 846 log.info( 847 "Refresh skipped, age of winrepo metadata in seconds (%s) is less " 848 "than winrepo_cache_expire_min (%s)", 849 repo_details.winrepo_age, 850 expired_min, 851 ) 852 force = False 853 854 # winrepo_age is -1 if repo db does not exist 855 refresh = ( 856 True 857 if force 858 or repo_details.winrepo_age == -1 859 or repo_details.winrepo_age > expired_max 860 else False 861 ) 862 863 if not refresh: 864 log.debug( 865 "Using existing pkg metadata db for saltenv '%s' (age is %s)", 866 saltenv, 867 datetime.timedelta(seconds=repo_details.winrepo_age), 868 ) 869 return True 870 871 if repo_details.winrepo_age == -1: 872 # no repo meta db 873 log.debug("No winrepo.p cache file for saltenv '%s', creating one now", saltenv) 874 875 results = refresh_db(saltenv=saltenv, verbose=False, failhard=failhard) 876 try: 877 # Return True if there were no failed winrepo SLS files, and False if 878 # failures were reported. 879 return not bool(results.get("failed", 0)) 880 except AttributeError: 881 return False 882 883 884def refresh_db(**kwargs): 885 r""" 886 Generates the local software metadata database (`winrepo.p`) on the minion. 887 The database is stored in a serialized format located by default at the 888 following location: 889 890 ``C:\salt\var\cache\salt\minion\files\base\win\repo-ng\winrepo.p`` 891 892 This module performs the following steps to generate the software metadata 893 database: 894 895 - Fetch the package definition files (.sls) from `winrepo_source_dir` 896 (default `salt://win/repo-ng`) and cache them in 897 `<cachedir>\files\<saltenv>\<winrepo_source_dir>` 898 (default: ``C:\salt\var\cache\salt\minion\files\base\win\repo-ng``) 899 - Call :py:func:`pkg.genrepo <salt.modules.win_pkg.genrepo>` to parse the 900 package definition files and generate the repository metadata database 901 file (`winrepo.p`) 902 - Return the report received from 903 :py:func:`pkg.genrepo <salt.modules.win_pkg.genrepo>` 904 905 The default winrepo directory on the master is `/srv/salt/win/repo-ng`. All 906 files that end with `.sls` in this and all subdirectories will be used to 907 generate the repository metadata database (`winrepo.p`). 908 909 .. note:: 910 - Hidden directories (directories beginning with '`.`', such as 911 '`.git`') will be ignored. 912 913 .. note:: 914 There is no need to call `pkg.refresh_db` every time you work with the 915 pkg module. Automatic refresh will occur based on the following minion 916 configuration settings: 917 918 - `winrepo_cache_expire_min` 919 - `winrepo_cache_expire_max` 920 921 However, if the package definition files have changed, as would be the 922 case if you are developing a new package definition, this function 923 should be called to ensure the minion has the latest information about 924 packages available to it. 925 926 .. warning:: 927 Directories and files fetched from <winrepo_source_dir> 928 (`/srv/salt/win/repo-ng`) will be processed in alphabetical order. If 929 two or more software definition files contain the same name, the last 930 one processed replaces all data from the files processed before it. 931 932 For more information see 933 :ref:`Windows Software Repository <windows-package-manager>` 934 935 Arguments: 936 937 saltenv (str): Salt environment. Default: ``base`` 938 939 verbose (bool): 940 Return a verbose data structure which includes 'success_list', a 941 list of all sls files and the package names contained within. 942 Default is 'False' 943 944 failhard (bool): 945 If ``True``, an error will be raised if any repo SLS files fails to 946 process. If ``False``, no error will be raised, and a dictionary 947 containing the full results will be returned. 948 949 Returns: 950 dict: A dictionary containing the results of the database refresh. 951 952 .. note:: 953 A result with a `total: 0` generally means that the files are in the 954 wrong location on the master. Try running the following command on the 955 minion: `salt-call -l debug pkg.refresh saltenv=base` 956 957 .. warning:: 958 When calling this command from a state using `module.run` be sure to 959 pass `failhard: False`. Otherwise the state will report failure if it 960 encounters a bad software definition file. 961 962 CLI Example: 963 964 .. code-block:: bash 965 966 salt '*' pkg.refresh_db 967 salt '*' pkg.refresh_db saltenv=base 968 """ 969 # Remove rtag file to keep multiple refreshes from happening in pkg states 970 salt.utils.pkg.clear_rtag(__opts__) 971 saltenv = kwargs.pop("saltenv", "base") 972 verbose = salt.utils.data.is_true(kwargs.pop("verbose", False)) 973 failhard = salt.utils.data.is_true(kwargs.pop("failhard", True)) 974 __context__.pop("winrepo.data", None) 975 repo_details = _get_repo_details(saltenv) 976 977 log.debug( 978 "Refreshing pkg metadata db for saltenv '%s' (age of existing metadata is %s)", 979 saltenv, 980 datetime.timedelta(seconds=repo_details.winrepo_age), 981 ) 982 983 # Clear minion repo-ng cache see #35342 discussion 984 log.info("Removing all *.sls files under '%s'", repo_details.local_dest) 985 failed = [] 986 for root, _, files in salt.utils.path.os_walk( 987 repo_details.local_dest, followlinks=False 988 ): 989 for name in files: 990 if name.endswith(".sls"): 991 full_filename = os.path.join(root, name) 992 try: 993 os.remove(full_filename) 994 except OSError as exc: 995 if exc.errno != errno.ENOENT: 996 log.error("Failed to remove %s: %s", full_filename, exc) 997 failed.append(full_filename) 998 if failed: 999 raise CommandExecutionError( 1000 "Failed to clear one or more winrepo cache files", info={"failed": failed} 1001 ) 1002 1003 # Cache repo-ng locally 1004 log.info("Fetching *.sls files from %s", repo_details.winrepo_source_dir) 1005 try: 1006 __salt__["cp.cache_dir"]( 1007 path=repo_details.winrepo_source_dir, 1008 saltenv=saltenv, 1009 include_pat="*.sls", 1010 exclude_pat=r"E@\/\..*?\/", # Exclude all hidden directories (.git) 1011 ) 1012 except MinionError as exc: 1013 log.exception( 1014 "Failed to cache %s", repo_details.winrepo_source_dir, exc_info=exc 1015 ) 1016 return genrepo(saltenv=saltenv, verbose=verbose, failhard=failhard) 1017 1018 1019def _get_repo_details(saltenv): 1020 """ 1021 Return repo details for the specified saltenv as a namedtuple 1022 """ 1023 contextkey = "winrepo._get_repo_details.{}".format(saltenv) 1024 1025 if contextkey in __context__: 1026 (winrepo_source_dir, local_dest, winrepo_file) = __context__[contextkey] 1027 else: 1028 winrepo_source_dir = __opts__["winrepo_source_dir"] 1029 dirs = [__opts__["cachedir"], "files", saltenv] 1030 url_parts = urllib.parse.urlparse(winrepo_source_dir) 1031 dirs.append(url_parts.netloc) 1032 dirs.extend(url_parts.path.strip("/").split("/")) 1033 local_dest = os.sep.join(dirs) 1034 1035 winrepo_file = os.path.join(local_dest, "winrepo.p") # Default 1036 # Check for a valid windows file name 1037 if not re.search( 1038 r'[\/:*?"<>|]', __opts__["winrepo_cachefile"], flags=re.IGNORECASE 1039 ): 1040 winrepo_file = os.path.join(local_dest, __opts__["winrepo_cachefile"]) 1041 else: 1042 log.error( 1043 "minion configuration option 'winrepo_cachefile' has been " 1044 "ignored as its value (%s) is invalid. Please ensure this " 1045 "option is set to a valid filename.", 1046 __opts__["winrepo_cachefile"], 1047 ) 1048 1049 # Do some safety checks on the repo_path as its contents can be removed, 1050 # this includes check for bad coding 1051 system_root = os.environ.get("SystemRoot", r"C:\Windows") 1052 if not salt.utils.path.safe_path( 1053 path=local_dest, allow_path="\\".join([system_root, "TEMP"]) 1054 ): 1055 1056 raise CommandExecutionError( 1057 "Attempting to delete files from a possibly unsafe location: {}".format( 1058 local_dest 1059 ) 1060 ) 1061 1062 __context__[contextkey] = (winrepo_source_dir, local_dest, winrepo_file) 1063 1064 try: 1065 os.makedirs(local_dest) 1066 except OSError as exc: 1067 if exc.errno != errno.EEXIST: 1068 raise CommandExecutionError( 1069 "Failed to create {}: {}".format(local_dest, exc) 1070 ) 1071 1072 winrepo_age = -1 1073 try: 1074 stat_result = os.stat(winrepo_file) 1075 mtime = stat_result.st_mtime 1076 winrepo_age = time.time() - mtime 1077 except OSError as exc: 1078 if exc.errno != errno.ENOENT: 1079 raise CommandExecutionError( 1080 "Failed to get age of {}: {}".format(winrepo_file, exc) 1081 ) 1082 except AttributeError: 1083 # Shouldn't happen but log if it does 1084 log.warning("st_mtime missing from stat result %s", stat_result) 1085 except TypeError: 1086 # Shouldn't happen but log if it does 1087 log.warning("mtime of %s (%s) is an invalid type", winrepo_file, mtime) 1088 1089 repo_details = collections.namedtuple( 1090 "RepoDetails", 1091 ("winrepo_source_dir", "local_dest", "winrepo_file", "winrepo_age"), 1092 ) 1093 return repo_details(winrepo_source_dir, local_dest, winrepo_file, winrepo_age) 1094 1095 1096def genrepo(**kwargs): 1097 """ 1098 Generate package metadata db based on files within the winrepo_source_dir 1099 1100 Kwargs: 1101 1102 saltenv (str): Salt environment. Default: ``base`` 1103 1104 verbose (bool): 1105 Return verbose data structure which includes 'success_list', a list 1106 of all sls files and the package names contained within. 1107 Default ``False``. 1108 1109 failhard (bool): 1110 If ``True``, an error will be raised if any repo SLS files failed 1111 to process. If ``False``, no error will be raised, and a dictionary 1112 containing the full results will be returned. 1113 1114 .. note:: 1115 - Hidden directories (directories beginning with '`.`', such as 1116 '`.git`') will be ignored. 1117 1118 Returns: 1119 dict: A dictionary of the results of the command 1120 1121 CLI Example: 1122 1123 .. code-block:: bash 1124 1125 salt-run pkg.genrepo 1126 salt -G 'os:windows' pkg.genrepo verbose=true failhard=false 1127 salt -G 'os:windows' pkg.genrepo saltenv=base 1128 """ 1129 saltenv = kwargs.pop("saltenv", "base") 1130 verbose = salt.utils.data.is_true(kwargs.pop("verbose", False)) 1131 failhard = salt.utils.data.is_true(kwargs.pop("failhard", True)) 1132 1133 ret = {} 1134 successful_verbose = {} 1135 total_files_processed = 0 1136 ret["repo"] = {} 1137 ret["errors"] = {} 1138 repo_details = _get_repo_details(saltenv) 1139 1140 for root, _, files in salt.utils.path.os_walk( 1141 repo_details.local_dest, followlinks=False 1142 ): 1143 1144 # Skip hidden directories (.git) 1145 if re.search(r"[\\/]\..*", root): 1146 log.debug("Skipping files in directory: %s", root) 1147 continue 1148 1149 short_path = os.path.relpath(root, repo_details.local_dest) 1150 if short_path == ".": 1151 short_path = "" 1152 1153 for name in files: 1154 if name.endswith(".sls"): 1155 total_files_processed += 1 1156 _repo_process_pkg_sls( 1157 os.path.join(root, name), 1158 os.path.join(short_path, name), 1159 ret, 1160 successful_verbose, 1161 ) 1162 1163 with salt.utils.files.fopen(repo_details.winrepo_file, "wb") as repo_cache: 1164 repo_cache.write(salt.payload.dumps(ret)) 1165 # For some reason we can not save ret into __context__['winrepo.data'] as this breaks due to utf8 issues 1166 successful_count = len(successful_verbose) 1167 error_count = len(ret["errors"]) 1168 if verbose: 1169 results = { 1170 "total": total_files_processed, 1171 "success": successful_count, 1172 "failed": error_count, 1173 "success_list": successful_verbose, 1174 "failed_list": ret["errors"], 1175 } 1176 else: 1177 if error_count > 0: 1178 results = { 1179 "total": total_files_processed, 1180 "success": successful_count, 1181 "failed": error_count, 1182 "failed_list": ret["errors"], 1183 } 1184 else: 1185 results = { 1186 "total": total_files_processed, 1187 "success": successful_count, 1188 "failed": error_count, 1189 } 1190 1191 if error_count > 0 and failhard: 1192 raise CommandExecutionError( 1193 "Error occurred while generating repo db", info=results 1194 ) 1195 else: 1196 return results 1197 1198 1199def _repo_process_pkg_sls(filename, short_path_name, ret, successful_verbose): 1200 renderers = salt.loader.render(__opts__, __salt__) 1201 1202 def _failed_compile(prefix_msg, error_msg): 1203 log.error("%s '%s': %s", prefix_msg, short_path_name, error_msg) 1204 ret.setdefault("errors", {})[short_path_name] = [ 1205 "{}, {} ".format(prefix_msg, error_msg) 1206 ] 1207 return False 1208 1209 try: 1210 config = salt.template.compile_template( 1211 filename, 1212 renderers, 1213 __opts__["renderer"], 1214 __opts__.get("renderer_blacklist", ""), 1215 __opts__.get("renderer_whitelist", ""), 1216 ) 1217 except SaltRenderError as exc: 1218 return _failed_compile("Failed to compile", exc) 1219 except Exception as exc: # pylint: disable=broad-except 1220 return _failed_compile("Failed to read", exc) 1221 1222 if config and isinstance(config, dict): 1223 revmap = {} 1224 errors = [] 1225 for pkgname, version_list in config.items(): 1226 if pkgname in ret["repo"]: 1227 log.error( 1228 "package '%s' within '%s' already defined, skipping", 1229 pkgname, 1230 short_path_name, 1231 ) 1232 errors.append("package '{}' already defined".format(pkgname)) 1233 break 1234 for version_str, repodata in version_list.items(): 1235 # Ensure version is a string/unicode 1236 if not isinstance(version_str, str): 1237 log.error( 1238 "package '%s' within '%s', version number %s' is not a string", 1239 pkgname, 1240 short_path_name, 1241 version_str, 1242 ) 1243 errors.append( 1244 "package '{}', version number {} is not a string".format( 1245 pkgname, version_str 1246 ) 1247 ) 1248 continue 1249 # Ensure version contains a dict 1250 if not isinstance(repodata, dict): 1251 log.error( 1252 "package '%s' within '%s', repo data for " 1253 "version number %s is not defined as a dictionary", 1254 pkgname, 1255 short_path_name, 1256 version_str, 1257 ) 1258 errors.append( 1259 "package '{}', repo data for " 1260 "version number {} is not defined as a dictionary".format( 1261 pkgname, version_str 1262 ) 1263 ) 1264 continue 1265 revmap[repodata["full_name"]] = pkgname 1266 if errors: 1267 ret.setdefault("errors", {})[short_path_name] = errors 1268 else: 1269 ret.setdefault("repo", {}).update(config) 1270 ret.setdefault("name_map", {}).update(revmap) 1271 successful_verbose[short_path_name] = list(config.keys()) 1272 elif config: 1273 return _failed_compile("Compiled contents", "not a dictionary/hash") 1274 else: 1275 log.debug("No data within '%s' after processing", short_path_name) 1276 # no pkgname found after render 1277 successful_verbose[short_path_name] = [] 1278 1279 1280def _get_source_sum(source_hash, file_path, saltenv): 1281 """ 1282 Extract the hash sum, whether it is in a remote hash file, or just a string. 1283 """ 1284 ret = dict() 1285 schemes = ("salt", "http", "https", "ftp", "swift", "s3", "file") 1286 invalid_hash_msg = ( 1287 "Source hash '{}' format is invalid. It must be in " 1288 "the format <hash type>=<hash>".format(source_hash) 1289 ) 1290 source_hash = str(source_hash) 1291 source_hash_scheme = urllib.parse.urlparse(source_hash).scheme 1292 1293 if source_hash_scheme in schemes: 1294 # The source_hash is a file on a server 1295 try: 1296 cached_hash_file = __salt__["cp.cache_file"](source_hash, saltenv) 1297 except MinionError as exc: 1298 log.exception("Failed to cache %s", source_hash, exc_info=exc) 1299 raise 1300 1301 if not cached_hash_file: 1302 raise CommandExecutionError( 1303 "Source hash file {} not found".format(source_hash) 1304 ) 1305 1306 ret = __salt__["file.extract_hash"](cached_hash_file, "", file_path) 1307 if ret is None: 1308 raise SaltInvocationError(invalid_hash_msg) 1309 else: 1310 # The source_hash is a hash string 1311 items = source_hash.split("=", 1) 1312 1313 if len(items) != 2: 1314 invalid_hash_msg = "{}, or it must be a supported protocol: {}".format( 1315 invalid_hash_msg, ", ".join(schemes) 1316 ) 1317 raise SaltInvocationError(invalid_hash_msg) 1318 1319 ret["hash_type"], ret["hsum"] = [item.strip().lower() for item in items] 1320 1321 return ret 1322 1323 1324def _get_msiexec(use_msiexec): 1325 """ 1326 Return if msiexec.exe will be used and the command to invoke it. 1327 """ 1328 if use_msiexec is False: 1329 return False, "" 1330 if isinstance(use_msiexec, str): 1331 if os.path.isfile(use_msiexec): 1332 return True, use_msiexec 1333 else: 1334 log.warning( 1335 "msiexec path '%s' not found. Using system registered msiexec instead", 1336 use_msiexec, 1337 ) 1338 use_msiexec = True 1339 if use_msiexec is True: 1340 return True, "msiexec" 1341 1342 1343def install(name=None, refresh=False, pkgs=None, **kwargs): 1344 r""" 1345 Install the passed package(s) on the system using winrepo 1346 1347 Args: 1348 1349 name (str): 1350 The name of a single package, or a comma-separated list of packages 1351 to install. (no spaces after the commas) 1352 1353 refresh (bool): 1354 Boolean value representing whether or not to refresh the winrepo db. 1355 Default ``False``. 1356 1357 pkgs (list): 1358 A list of packages to install from a software repository. All 1359 packages listed under ``pkgs`` will be installed via a single 1360 command. 1361 1362 You can specify a version by passing the item as a dict: 1363 1364 CLI Example: 1365 1366 .. code-block:: bash 1367 1368 # will install the latest version of foo and bar 1369 salt '*' pkg.install pkgs='["foo", "bar"]' 1370 1371 # will install the latest version of foo and version 1.2.3 of bar 1372 salt '*' pkg.install pkgs='["foo", {"bar": "1.2.3"}]' 1373 1374 Kwargs: 1375 1376 version (str): 1377 The specific version to install. If omitted, the latest version will 1378 be installed. Recommend for use when installing a single package. 1379 1380 If passed with a list of packages in the ``pkgs`` parameter, the 1381 version will be ignored. 1382 1383 CLI Example: 1384 1385 .. code-block:: bash 1386 1387 # Version is ignored 1388 salt '*' pkg.install pkgs="['foo', 'bar']" version=1.2.3 1389 1390 If passed with a comma separated list in the ``name`` parameter, the 1391 version will apply to all packages in the list. 1392 1393 CLI Example: 1394 1395 .. code-block:: bash 1396 1397 # Version 1.2.3 will apply to packages foo and bar 1398 salt '*' pkg.install foo,bar version=1.2.3 1399 1400 extra_install_flags (str): 1401 Additional install flags that will be appended to the 1402 ``install_flags`` defined in the software definition file. Only 1403 applies when single package is passed. 1404 1405 saltenv (str): 1406 Salt environment. Default 'base' 1407 1408 report_reboot_exit_codes (bool): 1409 If the installer exits with a recognized exit code indicating that 1410 a reboot is required, the module function 1411 1412 *win_system.set_reboot_required_witnessed* 1413 1414 will be called, preserving the knowledge of this event for the 1415 remainder of the current boot session. For the time being, 3010 is 1416 the only recognized exit code. The value of this param defaults to 1417 True. 1418 1419 .. versionadded:: 2016.11.0 1420 1421 Returns: 1422 dict: Return a dict containing the new package names and versions. If 1423 the package is already installed, an empty dict is returned. 1424 1425 If the package is installed by ``pkg.install``: 1426 1427 .. code-block:: cfg 1428 1429 {'<package>': {'old': '<old-version>', 1430 'new': '<new-version>'}} 1431 1432 The following example will refresh the winrepo and install a single 1433 package, 7zip. 1434 1435 CLI Example: 1436 1437 .. code-block:: bash 1438 1439 salt '*' pkg.install 7zip refresh=True 1440 1441 CLI Example: 1442 1443 .. code-block:: bash 1444 1445 salt '*' pkg.install 7zip 1446 salt '*' pkg.install 7zip,filezilla 1447 salt '*' pkg.install pkgs='["7zip","filezilla"]' 1448 1449 WinRepo Definition File Examples: 1450 1451 The following example demonstrates the use of ``cache_file``. This would be 1452 used if you have multiple installers in the same directory that use the 1453 same ``install.ini`` file and you don't want to download the additional 1454 installers. 1455 1456 .. code-block:: bash 1457 1458 ntp: 1459 4.2.8: 1460 installer: 'salt://win/repo/ntp/ntp-4.2.8-win32-setup.exe' 1461 full_name: Meinberg NTP Windows Client 1462 locale: en_US 1463 reboot: False 1464 cache_file: 'salt://win/repo/ntp/install.ini' 1465 install_flags: '/USEFILE=C:\salt\var\cache\salt\minion\files\base\win\repo\ntp\install.ini' 1466 uninstaller: 'NTP/uninst.exe' 1467 1468 The following example demonstrates the use of ``cache_dir``. It assumes a 1469 file named ``install.ini`` resides in the same directory as the installer. 1470 1471 .. code-block:: bash 1472 1473 ntp: 1474 4.2.8: 1475 installer: 'salt://win/repo/ntp/ntp-4.2.8-win32-setup.exe' 1476 full_name: Meinberg NTP Windows Client 1477 locale: en_US 1478 reboot: False 1479 cache_dir: True 1480 install_flags: '/USEFILE=C:\salt\var\cache\salt\minion\files\base\win\repo\ntp\install.ini' 1481 uninstaller: 'NTP/uninst.exe' 1482 """ 1483 ret = {} 1484 saltenv = kwargs.pop("saltenv", "base") 1485 1486 refresh = salt.utils.data.is_true(refresh) 1487 # no need to call _refresh_db_conditional as list_pkgs will do it 1488 1489 # Make sure name or pkgs is passed 1490 if not name and not pkgs: 1491 return "Must pass a single package or a list of packages" 1492 1493 # Ignore pkg_type from parse_targets, Windows does not support the 1494 # "sources" argument 1495 pkg_params = __salt__["pkg_resource.parse_targets"](name, pkgs, **kwargs)[0] 1496 1497 if len(pkg_params) > 1: 1498 if kwargs.get("extra_install_flags") is not None: 1499 log.warning( 1500 "'extra_install_flags' argument will be ignored for " 1501 "multiple package targets" 1502 ) 1503 1504 # Windows expects an Options dictionary containing 'version' 1505 for pkg in pkg_params: 1506 pkg_params[pkg] = {"version": pkg_params[pkg]} 1507 1508 if not pkg_params: 1509 log.error("No package definition found") 1510 return {} 1511 1512 if not pkgs and len(pkg_params) == 1: 1513 # Only use the 'version' param if a single item was passed to the 'name' 1514 # parameter 1515 pkg_params = { 1516 name: { 1517 "version": kwargs.get("version"), 1518 "extra_install_flags": kwargs.get("extra_install_flags"), 1519 } 1520 } 1521 elif len(pkg_params) == 1: 1522 # A dict of packages was passed, but it contains only 1 key, so we need 1523 # to add the 'extra_install_flags' 1524 pkg = next(iter(pkg_params)) 1525 pkg_params[pkg]["extra_install_flags"] = kwargs.get("extra_install_flags") 1526 1527 # Get a list of currently installed software for comparison at the end 1528 old = list_pkgs(saltenv=saltenv, refresh=refresh, versions_as_list=True) 1529 1530 # Loop through each package 1531 changed = [] 1532 for pkg_name, options in pkg_params.items(): 1533 1534 # Load package information for the package 1535 pkginfo = _get_package_info(pkg_name, saltenv=saltenv) 1536 1537 # Make sure pkginfo was found 1538 if not pkginfo: 1539 log.error("Unable to locate package %s", pkg_name) 1540 ret[pkg_name] = "Unable to locate package {}".format(pkg_name) 1541 continue 1542 1543 version_num = options.get("version") 1544 # Using the salt cmdline with version=5.3 might be interpreted 1545 # as a float it must be converted to a string in order for 1546 # string matching to work. 1547 if not isinstance(version_num, str) and version_num is not None: 1548 version_num = str(version_num) 1549 1550 # If the version was not passed, version_num will be None 1551 if not version_num: 1552 if pkg_name in old: 1553 log.debug( 1554 "pkg.install: '%s' version '%s' is already installed", 1555 pkg_name, 1556 old[pkg_name][0], 1557 ) 1558 continue 1559 # Get the most recent version number available from winrepo.p 1560 # May also return `latest` or an empty string 1561 version_num = _get_latest_pkg_version(pkginfo) 1562 1563 if version_num == "latest" and "latest" not in pkginfo: 1564 # Get the most recent version number available from winrepo.p 1565 # May also return `latest` or an empty string 1566 version_num = _get_latest_pkg_version(pkginfo) 1567 1568 # Check if the version is already installed 1569 if version_num in old.get(pkg_name, []): 1570 # Desired version number already installed 1571 log.debug( 1572 "pkg.install: '%s' version '%s' is already installed", 1573 pkg_name, 1574 version_num, 1575 ) 1576 continue 1577 # If version number not installed, is the version available? 1578 elif version_num != "latest" and version_num not in pkginfo: 1579 log.error("Version %s not found for package %s", version_num, pkg_name) 1580 ret[pkg_name] = {"not found": version_num} 1581 continue 1582 1583 # Get the installer settings from winrepo.p 1584 installer = pkginfo[version_num].get("installer", "") 1585 cache_dir = pkginfo[version_num].get("cache_dir", False) 1586 cache_file = pkginfo[version_num].get("cache_file", "") 1587 1588 # Is there an installer configured? 1589 if not installer: 1590 log.error( 1591 "No installer configured for version %s of package %s", 1592 version_num, 1593 pkg_name, 1594 ) 1595 ret[pkg_name] = {"no installer": version_num} 1596 continue 1597 1598 # Is the installer in a location that requires caching 1599 if __salt__["config.valid_fileproto"](installer): 1600 1601 # Check for the 'cache_dir' parameter in the .sls file 1602 # If true, the entire directory will be cached instead of the 1603 # individual file. This is useful for installations that are not 1604 # single files 1605 if cache_dir and installer.startswith("salt:"): 1606 path, _ = os.path.split(installer) 1607 try: 1608 __salt__["cp.cache_dir"]( 1609 path=path, 1610 saltenv=saltenv, 1611 include_empty=False, 1612 include_pat=None, 1613 exclude_pat="E@init.sls$", 1614 ) 1615 except MinionError as exc: 1616 msg = "Failed to cache {}".format(path) 1617 log.exception(msg, exc_info=exc) 1618 return "{}\n{}".format(msg, exc) 1619 1620 # Check to see if the cache_file is cached... if passed 1621 if cache_file and cache_file.startswith("salt:"): 1622 1623 # Check to see if the file is cached 1624 cached_file = __salt__["cp.is_cached"](cache_file, saltenv) 1625 if not cached_file: 1626 try: 1627 cached_file = __salt__["cp.cache_file"](cache_file, saltenv) 1628 except MinionError as exc: 1629 msg = "Failed to cache {}".format(cache_file) 1630 log.exception(msg, exc_info=exc) 1631 return "{}\n{}".format(msg, exc) 1632 1633 # Make sure the cached file is the same as the source 1634 if __salt__["cp.hash_file"](cache_file, saltenv) != __salt__[ 1635 "cp.hash_file" 1636 ](cached_file): 1637 try: 1638 cached_file = __salt__["cp.cache_file"](cache_file, saltenv) 1639 except MinionError as exc: 1640 msg = "Failed to cache {}".format(cache_file) 1641 log.exception(msg, exc_info=exc) 1642 return "{}\n{}".format(msg, exc) 1643 1644 # Check if the cache_file was cached successfully 1645 if not cached_file: 1646 log.error("Unable to cache %s", cache_file) 1647 ret[pkg_name] = {"failed to cache cache_file": cache_file} 1648 continue 1649 1650 # Check to see if the installer is cached 1651 cached_pkg = __salt__["cp.is_cached"](installer, saltenv) 1652 if not cached_pkg: 1653 # It's not cached. Cache it, mate. 1654 try: 1655 cached_pkg = __salt__["cp.cache_file"](installer, saltenv) 1656 except MinionError as exc: 1657 msg = "Failed to cache {}".format(installer) 1658 log.exception(msg, exc_info=exc) 1659 return "{}\n{}".format(msg, exc) 1660 1661 # Check if the installer was cached successfully 1662 if not cached_pkg: 1663 log.error( 1664 "Unable to cache file %s from saltenv: %s", installer, saltenv 1665 ) 1666 ret[pkg_name] = {"unable to cache": installer} 1667 continue 1668 1669 # Compare the hash of the cached installer to the source only if the 1670 # file is hosted on salt: 1671 if installer.startswith("salt:"): 1672 if __salt__["cp.hash_file"](installer, saltenv) != __salt__[ 1673 "cp.hash_file" 1674 ](cached_pkg): 1675 try: 1676 cached_pkg = __salt__["cp.cache_file"](installer, saltenv) 1677 except MinionError as exc: 1678 msg = "Failed to cache {}".format(installer) 1679 log.exception(msg, exc_info=exc) 1680 return "{}\n{}".format(msg, exc) 1681 1682 # Check if the installer was cached successfully 1683 if not cached_pkg: 1684 log.error("Unable to cache %s", installer) 1685 ret[pkg_name] = {"unable to cache": installer} 1686 continue 1687 else: 1688 # Run the installer directly (not hosted on salt:, https:, etc.) 1689 cached_pkg = installer 1690 1691 # Fix non-windows slashes 1692 cached_pkg = cached_pkg.replace("/", "\\") 1693 cache_path = os.path.dirname(cached_pkg) 1694 1695 # Compare the hash sums 1696 source_hash = pkginfo[version_num].get("source_hash", False) 1697 if source_hash: 1698 source_sum = _get_source_sum(source_hash, cached_pkg, saltenv) 1699 log.debug( 1700 "pkg.install: Source %s hash: %s", 1701 source_sum["hash_type"], 1702 source_sum["hsum"], 1703 ) 1704 1705 cached_pkg_sum = salt.utils.hashutils.get_hash( 1706 cached_pkg, source_sum["hash_type"] 1707 ) 1708 log.debug( 1709 "pkg.install: Package %s hash: %s", 1710 source_sum["hash_type"], 1711 cached_pkg_sum, 1712 ) 1713 1714 if source_sum["hsum"] != cached_pkg_sum: 1715 raise SaltInvocationError( 1716 "Source hash '{}' does not match package hash '{}'".format( 1717 source_sum["hsum"], cached_pkg_sum 1718 ) 1719 ) 1720 log.debug("pkg.install: Source hash matches package hash.") 1721 1722 # Get install flags 1723 1724 install_flags = pkginfo[version_num].get("install_flags", "") 1725 if options and options.get("extra_install_flags"): 1726 install_flags = "{} {}".format( 1727 install_flags, options.get("extra_install_flags", "") 1728 ) 1729 1730 # Compute msiexec string 1731 use_msiexec, msiexec = _get_msiexec(pkginfo[version_num].get("msiexec", False)) 1732 1733 # Build cmd and arguments 1734 # cmd and arguments must be separated for use with the task scheduler 1735 cmd_shell = os.getenv( 1736 "ComSpec", "{}\\system32\\cmd.exe".format(os.getenv("WINDIR")) 1737 ) 1738 if use_msiexec: 1739 arguments = '"{}" /I "{}"'.format(msiexec, cached_pkg) 1740 if pkginfo[version_num].get("allusers", True): 1741 arguments = "{} ALLUSERS=1".format(arguments) 1742 else: 1743 arguments = '"{}"'.format(cached_pkg) 1744 1745 if install_flags: 1746 arguments = "{} {}".format(arguments, install_flags) 1747 1748 # Install the software 1749 # Check Use Scheduler Option 1750 if pkginfo[version_num].get("use_scheduler", False): 1751 # Create Scheduled Task 1752 __salt__["task.create_task"]( 1753 name="update-salt-software", 1754 user_name="System", 1755 force=True, 1756 action_type="Execute", 1757 cmd=cmd_shell, 1758 arguments='/s /c "{}"'.format(arguments), 1759 start_in=cache_path, 1760 trigger_type="Once", 1761 start_date="1975-01-01", 1762 start_time="01:00", 1763 ac_only=False, 1764 stop_if_on_batteries=False, 1765 ) 1766 1767 # Run Scheduled Task 1768 # Special handling for installing salt 1769 if ( 1770 re.search( 1771 r"salt[\s_.-]*minion", pkg_name, flags=re.IGNORECASE + re.UNICODE 1772 ) 1773 is not None 1774 ): 1775 ret[pkg_name] = {"install status": "task started"} 1776 if not __salt__["task.run"](name="update-salt-software"): 1777 log.error( 1778 "Scheduled Task failed to run. Failed to install %s", pkg_name 1779 ) 1780 ret[pkg_name] = {"install status": "failed"} 1781 else: 1782 1783 # Make sure the task is running, try for 5 secs 1784 t_end = time.time() + 5 1785 while time.time() < t_end: 1786 time.sleep(0.25) 1787 task_running = ( 1788 __salt__["task.status"]("update-salt-software") == "Running" 1789 ) 1790 if task_running: 1791 break 1792 1793 if not task_running: 1794 log.error( 1795 "Scheduled Task failed to run. Failed to install %s", 1796 pkg_name, 1797 ) 1798 ret[pkg_name] = {"install status": "failed"} 1799 1800 # All other packages run with task scheduler 1801 else: 1802 if not __salt__["task.run_wait"](name="update-salt-software"): 1803 log.error( 1804 "Scheduled Task failed to run. Failed to install %s", pkg_name 1805 ) 1806 ret[pkg_name] = {"install status": "failed"} 1807 else: 1808 # Launch the command 1809 result = __salt__["cmd.run_all"]( 1810 '"{}" /s /c "{}"'.format(cmd_shell, arguments), 1811 cache_path, 1812 output_loglevel="trace", 1813 python_shell=False, 1814 redirect_stderr=True, 1815 ) 1816 if not result["retcode"]: 1817 ret[pkg_name] = {"install status": "success"} 1818 changed.append(pkg_name) 1819 elif result["retcode"] == 3010: 1820 # 3010 is ERROR_SUCCESS_REBOOT_REQUIRED 1821 report_reboot_exit_codes = kwargs.pop("report_reboot_exit_codes", True) 1822 if report_reboot_exit_codes: 1823 __salt__["system.set_reboot_required_witnessed"]() 1824 ret[pkg_name] = {"install status": "success, reboot required"} 1825 changed.append(pkg_name) 1826 elif result["retcode"] == 1641: 1827 # 1641 is ERROR_SUCCESS_REBOOT_INITIATED 1828 ret[pkg_name] = {"install status": "success, reboot initiated"} 1829 changed.append(pkg_name) 1830 else: 1831 log.error( 1832 "Failed to install %s; retcode: %s; installer output: %s", 1833 pkg_name, 1834 result["retcode"], 1835 result["stdout"], 1836 ) 1837 ret[pkg_name] = {"install status": "failed"} 1838 1839 # Get a new list of installed software 1840 new = list_pkgs(saltenv=saltenv, refresh=False) 1841 1842 # Take the "old" package list and convert the values to strings in 1843 # preparation for the comparison below. 1844 __salt__["pkg_resource.stringify"](old) 1845 1846 # Check for changes in the registry 1847 difference = salt.utils.data.compare_dicts(old, new) 1848 1849 # Compare the software list before and after 1850 # Add the difference to ret 1851 ret.update(difference) 1852 1853 return ret 1854 1855 1856def upgrade(**kwargs): 1857 """ 1858 Upgrade all software. Currently not implemented 1859 1860 Kwargs: 1861 saltenv (str): The salt environment to use. Default ``base``. 1862 refresh (bool): Refresh package metadata. Default ``True``. 1863 1864 .. note:: 1865 This feature is not yet implemented for Windows. 1866 1867 Returns: 1868 dict: Empty dict, until implemented 1869 1870 CLI Example: 1871 1872 .. code-block:: bash 1873 1874 salt '*' pkg.upgrade 1875 """ 1876 log.warning("pkg.upgrade not implemented on Windows yet") 1877 refresh = salt.utils.data.is_true(kwargs.get("refresh", True)) 1878 saltenv = kwargs.get("saltenv", "base") 1879 log.warning( 1880 "pkg.upgrade not implemented on Windows yet refresh:%s saltenv:%s", 1881 refresh, 1882 saltenv, 1883 ) 1884 # Uncomment the below once pkg.upgrade has been implemented 1885 1886 # if salt.utils.data.is_true(refresh): 1887 # refresh_db() 1888 return {} 1889 1890 1891def remove(name=None, pkgs=None, **kwargs): 1892 """ 1893 Remove the passed package(s) from the system using winrepo 1894 1895 .. versionadded:: 0.16.0 1896 1897 Args: 1898 name (str): 1899 The name(s) of the package(s) to be uninstalled. Can be a 1900 single package or a comma delimited list of packages, no spaces. 1901 1902 pkgs (list): 1903 A list of packages to delete. Must be passed as a python list. The 1904 ``name`` parameter will be ignored if this option is passed. 1905 1906 Kwargs: 1907 1908 version (str): 1909 The version of the package to be uninstalled. If this option is 1910 used to to uninstall multiple packages, then this version will be 1911 applied to all targeted packages. Recommended using only when 1912 uninstalling a single package. If this parameter is omitted, the 1913 latest version will be uninstalled. 1914 1915 saltenv (str): Salt environment. Default ``base`` 1916 refresh (bool): Refresh package metadata. Default ``False`` 1917 1918 Returns: 1919 dict: Returns a dict containing the changes. 1920 1921 If the package is removed by ``pkg.remove``: 1922 1923 {'<package>': {'old': '<old-version>', 1924 'new': '<new-version>'}} 1925 1926 If the package is already uninstalled: 1927 1928 {'<package>': {'current': 'not installed'}} 1929 1930 CLI Example: 1931 1932 .. code-block:: bash 1933 1934 salt '*' pkg.remove <package name> 1935 salt '*' pkg.remove <package1>,<package2>,<package3> 1936 salt '*' pkg.remove pkgs='["foo", "bar"]' 1937 """ 1938 saltenv = kwargs.get("saltenv", "base") 1939 refresh = salt.utils.data.is_true(kwargs.get("refresh", False)) 1940 # no need to call _refresh_db_conditional as list_pkgs will do it 1941 ret = {} 1942 1943 # Make sure name or pkgs is passed 1944 if not name and not pkgs: 1945 return "Must pass a single package or a list of packages" 1946 1947 # Get package parameters 1948 pkg_params = __salt__["pkg_resource.parse_targets"](name, pkgs, **kwargs)[0] 1949 1950 # Get a list of currently installed software for comparison at the end 1951 old = list_pkgs(saltenv=saltenv, refresh=refresh, versions_as_list=True) 1952 1953 # Loop through each package 1954 changed = [] # list of changed package names 1955 for pkgname, version_num in pkg_params.items(): 1956 1957 # Load package information for the package 1958 pkginfo = _get_package_info(pkgname, saltenv=saltenv) 1959 1960 # Make sure pkginfo was found 1961 if not pkginfo: 1962 msg = "Unable to locate package {}".format(pkgname) 1963 log.error(msg) 1964 ret[pkgname] = msg 1965 continue 1966 1967 # Check to see if package is installed on the system 1968 if pkgname not in old: 1969 log.debug( 1970 "%s %s not installed", pkgname, version_num if version_num else "" 1971 ) 1972 ret[pkgname] = {"current": "not installed"} 1973 continue 1974 1975 removal_targets = [] 1976 # Only support a single version number 1977 if version_num is not None: 1978 # Using the salt cmdline with version=5.3 might be interpreted 1979 # as a float it must be converted to a string in order for 1980 # string matching to work. 1981 version_num = str(version_num) 1982 1983 # At least one version of the software is installed. 1984 if version_num is None: 1985 for ver_install in old[pkgname]: 1986 if ver_install not in pkginfo and "latest" in pkginfo: 1987 log.debug( 1988 "%s %s using package latest entry to to remove", 1989 pkgname, 1990 version_num, 1991 ) 1992 removal_targets.append("latest") 1993 else: 1994 removal_targets.append(ver_install) 1995 else: 1996 if version_num in pkginfo: 1997 # we known how to remove this version 1998 if version_num in old[pkgname]: 1999 removal_targets.append(version_num) 2000 else: 2001 log.debug("%s %s not installed", pkgname, version_num) 2002 ret[pkgname] = {"current": "{} not installed".format(version_num)} 2003 continue 2004 elif "latest" in pkginfo: 2005 # we do not have version entry, assume software can self upgrade and use latest 2006 log.debug( 2007 "%s %s using package latest entry to to remove", 2008 pkgname, 2009 version_num, 2010 ) 2011 removal_targets.append("latest") 2012 2013 if not removal_targets: 2014 log.error( 2015 "%s %s no definition to remove this version", pkgname, version_num 2016 ) 2017 ret[pkgname] = { 2018 "current": "{} no definition, cannot removed".format(version_num) 2019 } 2020 continue 2021 2022 for target in removal_targets: 2023 # Get the uninstaller 2024 uninstaller = pkginfo[target].get("uninstaller", "") 2025 cache_dir = pkginfo[target].get("cache_dir", False) 2026 uninstall_flags = pkginfo[target].get("uninstall_flags", "") 2027 2028 # If no uninstaller found, use the installer with uninstall flags 2029 if not uninstaller and uninstall_flags: 2030 uninstaller = pkginfo[target].get("installer", "") 2031 2032 # If still no uninstaller found, fail 2033 if not uninstaller: 2034 log.error( 2035 "No installer or uninstaller configured for package %s", 2036 pkgname, 2037 ) 2038 ret[pkgname] = {"no uninstaller defined": target} 2039 continue 2040 2041 # Where is the uninstaller 2042 if uninstaller.startswith(("salt:", "http:", "https:", "ftp:")): 2043 2044 # Check for the 'cache_dir' parameter in the .sls file 2045 # If true, the entire directory will be cached instead of the 2046 # individual file. This is useful for installations that are not 2047 # single files 2048 2049 if cache_dir and uninstaller.startswith("salt:"): 2050 path, _ = os.path.split(uninstaller) 2051 try: 2052 __salt__["cp.cache_dir"]( 2053 path, saltenv, False, None, "E@init.sls$" 2054 ) 2055 except MinionError as exc: 2056 msg = "Failed to cache {}".format(path) 2057 log.exception(msg, exc_info=exc) 2058 return "{}\n{}".format(msg, exc) 2059 2060 # Check to see if the uninstaller is cached 2061 cached_pkg = __salt__["cp.is_cached"](uninstaller, saltenv) 2062 if not cached_pkg: 2063 # It's not cached. Cache it, mate. 2064 try: 2065 cached_pkg = __salt__["cp.cache_file"](uninstaller, saltenv) 2066 except MinionError as exc: 2067 msg = "Failed to cache {}".format(uninstaller) 2068 log.exception(msg, exc_info=exc) 2069 return "{}\n{}".format(msg, exc) 2070 2071 # Check if the uninstaller was cached successfully 2072 if not cached_pkg: 2073 log.error("Unable to cache %s", uninstaller) 2074 ret[pkgname] = {"unable to cache": uninstaller} 2075 continue 2076 2077 # Compare the hash of the cached installer to the source only if 2078 # the file is hosted on salt: 2079 # TODO cp.cache_file does cache and hash checking? So why do it again? 2080 if uninstaller.startswith("salt:"): 2081 if __salt__["cp.hash_file"](uninstaller, saltenv) != __salt__[ 2082 "cp.hash_file" 2083 ](cached_pkg): 2084 try: 2085 cached_pkg = __salt__["cp.cache_file"](uninstaller, saltenv) 2086 except MinionError as exc: 2087 msg = "Failed to cache {}".format(uninstaller) 2088 log.exception(msg, exc_info=exc) 2089 return "{}\n{}".format(msg, exc) 2090 2091 # Check if the installer was cached successfully 2092 if not cached_pkg: 2093 log.error("Unable to cache %s", uninstaller) 2094 ret[pkgname] = {"unable to cache": uninstaller} 2095 continue 2096 else: 2097 # Run the uninstaller directly 2098 # (not hosted on salt:, https:, etc.) 2099 cached_pkg = os.path.expandvars(uninstaller) 2100 2101 # Fix non-windows slashes 2102 cached_pkg = cached_pkg.replace("/", "\\") 2103 cache_path, _ = os.path.split(cached_pkg) 2104 2105 # os.path.expandvars is not required as we run everything through cmd.exe /s /c 2106 2107 if kwargs.get("extra_uninstall_flags"): 2108 uninstall_flags = "{} {}".format( 2109 uninstall_flags, kwargs.get("extra_uninstall_flags", "") 2110 ) 2111 2112 # Compute msiexec string 2113 use_msiexec, msiexec = _get_msiexec(pkginfo[target].get("msiexec", False)) 2114 cmd_shell = os.getenv( 2115 "ComSpec", "{}\\system32\\cmd.exe".format(os.getenv("WINDIR")) 2116 ) 2117 2118 # Build cmd and arguments 2119 # cmd and arguments must be separated for use with the task scheduler 2120 if use_msiexec: 2121 # Check if uninstaller is set to {guid}, if not we assume its a remote msi file. 2122 # which has already been downloaded. 2123 arguments = '"{}" /X "{}"'.format(msiexec, cached_pkg) 2124 else: 2125 arguments = '"{}"'.format(cached_pkg) 2126 2127 if uninstall_flags: 2128 arguments = "{} {}".format(arguments, uninstall_flags) 2129 2130 # Uninstall the software 2131 changed.append(pkgname) 2132 # Check Use Scheduler Option 2133 if pkginfo[target].get("use_scheduler", False): 2134 # Create Scheduled Task 2135 __salt__["task.create_task"]( 2136 name="update-salt-software", 2137 user_name="System", 2138 force=True, 2139 action_type="Execute", 2140 cmd=cmd_shell, 2141 arguments='/s /c "{}"'.format(arguments), 2142 start_in=cache_path, 2143 trigger_type="Once", 2144 start_date="1975-01-01", 2145 start_time="01:00", 2146 ac_only=False, 2147 stop_if_on_batteries=False, 2148 ) 2149 # Run Scheduled Task 2150 if not __salt__["task.run_wait"](name="update-salt-software"): 2151 log.error( 2152 "Scheduled Task failed to run. Failed to remove %s", pkgname 2153 ) 2154 ret[pkgname] = {"uninstall status": "failed"} 2155 else: 2156 # Launch the command 2157 result = __salt__["cmd.run_all"]( 2158 '"{}" /s /c "{}"'.format(cmd_shell, arguments), 2159 output_loglevel="trace", 2160 python_shell=False, 2161 redirect_stderr=True, 2162 ) 2163 if not result["retcode"]: 2164 ret[pkgname] = {"uninstall status": "success"} 2165 changed.append(pkgname) 2166 elif result["retcode"] == 3010: 2167 # 3010 is ERROR_SUCCESS_REBOOT_REQUIRED 2168 report_reboot_exit_codes = kwargs.pop( 2169 "report_reboot_exit_codes", True 2170 ) 2171 if report_reboot_exit_codes: 2172 __salt__["system.set_reboot_required_witnessed"]() 2173 ret[pkgname] = {"uninstall status": "success, reboot required"} 2174 changed.append(pkgname) 2175 elif result["retcode"] == 1641: 2176 # 1641 is ERROR_SUCCESS_REBOOT_INITIATED 2177 ret[pkgname] = {"uninstall status": "success, reboot initiated"} 2178 changed.append(pkgname) 2179 else: 2180 log.error( 2181 "Failed to remove %s; retcode: %s; uninstaller output: %s", 2182 pkgname, 2183 result["retcode"], 2184 result["stdout"], 2185 ) 2186 ret[pkgname] = {"uninstall status": "failed"} 2187 2188 # Get a new list of installed software 2189 new = list_pkgs(saltenv=saltenv, refresh=False) 2190 2191 # Take the "old" package list and convert the values to strings in 2192 # preparation for the comparison below. 2193 __salt__["pkg_resource.stringify"](old) 2194 2195 # Check for changes in the registry 2196 difference = salt.utils.data.compare_dicts(old, new) 2197 found_chgs = all(name in difference for name in changed) 2198 end_t = time.time() + 3 # give it 3 seconds to catch up. 2199 while not found_chgs and time.time() < end_t: 2200 time.sleep(0.5) 2201 new = list_pkgs(saltenv=saltenv, refresh=False) 2202 difference = salt.utils.data.compare_dicts(old, new) 2203 found_chgs = all(name in difference for name in changed) 2204 2205 if not found_chgs: 2206 log.warning("Expected changes for package removal may not have occurred") 2207 2208 # Compare the software list before and after 2209 # Add the difference to ret 2210 ret.update(difference) 2211 return ret 2212 2213 2214def purge(name=None, pkgs=None, **kwargs): 2215 """ 2216 Package purges are not supported on Windows, this function is identical to 2217 ``remove()``. 2218 2219 .. note:: 2220 At some point in the future, ``pkg.purge`` may direct the installer to 2221 remove all configs and settings for software packages that support that 2222 option. 2223 2224 .. versionadded:: 0.16.0 2225 2226 Args: 2227 2228 name (str): The name of the package to be deleted. 2229 2230 version (str): 2231 The version of the package to be deleted. If this option is 2232 used in combination with the ``pkgs`` option below, then this 2233 version will be applied to all targeted packages. 2234 2235 pkgs (list): 2236 A list of packages to delete. Must be passed as a python 2237 list. The ``name`` parameter will be ignored if this option is 2238 passed. 2239 2240 Kwargs: 2241 saltenv (str): Salt environment. Default ``base`` 2242 refresh (bool): Refresh package metadata. Default ``False`` 2243 2244 Returns: 2245 dict: A dict containing the changes. 2246 2247 CLI Example: 2248 2249 .. code-block:: bash 2250 2251 salt '*' pkg.purge <package name> 2252 salt '*' pkg.purge <package1>,<package2>,<package3> 2253 salt '*' pkg.purge pkgs='["foo", "bar"]' 2254 """ 2255 return remove(name=name, pkgs=pkgs, **kwargs) 2256 2257 2258def get_repo_data(saltenv="base"): 2259 """ 2260 Returns the existing package metadata db. Will create it, if it does not 2261 exist, however will not refresh it. 2262 2263 Args: 2264 saltenv (str): Salt environment. Default ``base`` 2265 2266 Returns: 2267 dict: A dict containing contents of metadata db. 2268 2269 CLI Example: 2270 2271 .. code-block:: bash 2272 2273 salt '*' pkg.get_repo_data 2274 """ 2275 # we only call refresh_db if it does not exist, as we want to return 2276 # the existing data even if its old, other parts of the code call this, 2277 # but they will call refresh if they need too. 2278 repo_details = _get_repo_details(saltenv) 2279 2280 if repo_details.winrepo_age == -1: 2281 # no repo meta db 2282 log.debug("No winrepo.p cache file. Refresh pkg db now.") 2283 refresh_db(saltenv=saltenv) 2284 2285 if "winrepo.data" in __context__: 2286 log.trace("get_repo_data returning results from __context__") 2287 return __context__["winrepo.data"] 2288 else: 2289 log.trace("get_repo_data called reading from disk") 2290 2291 try: 2292 with salt.utils.files.fopen(repo_details.winrepo_file, "rb") as repofile: 2293 try: 2294 repodata = salt.utils.data.decode( 2295 salt.payload.loads(repofile.read()) or {} 2296 ) 2297 __context__["winrepo.data"] = repodata 2298 return repodata 2299 except Exception as exc: # pylint: disable=broad-except 2300 log.exception(exc) 2301 return {} 2302 except OSError as exc: 2303 log.exception("Not able to read repo file: %s", exc) 2304 return {} 2305 2306 2307def _get_name_map(saltenv="base"): 2308 """ 2309 Return a reverse map of full pkg names to the names recognized by winrepo. 2310 """ 2311 u_name_map = {} 2312 name_map = get_repo_data(saltenv).get("name_map", {}) 2313 return name_map 2314 2315 2316def get_package_info(name, saltenv="base"): 2317 """ 2318 Return package info. Returns empty map if package not available. 2319 """ 2320 return _get_package_info(name=name, saltenv=saltenv) 2321 2322 2323def _get_package_info(name, saltenv="base"): 2324 """ 2325 Return package info. Returns empty map if package not available 2326 TODO: Add option for version 2327 """ 2328 return get_repo_data(saltenv).get("repo", {}).get(name, {}) 2329 2330 2331def _reverse_cmp_pkg_versions(pkg1, pkg2): 2332 """ 2333 Compare software package versions 2334 """ 2335 return 1 if LooseVersion(pkg1) > LooseVersion(pkg2) else -1 2336 2337 2338def _get_latest_pkg_version(pkginfo): 2339 """ 2340 Returns the latest version of the package. 2341 Will return 'latest' or version number string, and 2342 'Not Found' if 'Not Found' is the only entry. 2343 """ 2344 if len(pkginfo) == 1: 2345 return next(iter(pkginfo.keys())) 2346 try: 2347 return sorted(pkginfo, key=cmp_to_key(_reverse_cmp_pkg_versions)).pop() 2348 except IndexError: 2349 return "" 2350 2351 2352def compare_versions(ver1="", oper="==", ver2=""): 2353 """ 2354 Compare software package versions. Made public for use with Jinja 2355 2356 Args: 2357 ver1 (str): A software version to compare 2358 oper (str): The operand to use to compare 2359 ver2 (str): A software version to compare 2360 2361 Returns: 2362 bool: True if the comparison is valid, otherwise False 2363 2364 CLI Example: 2365 2366 .. code-block:: bash 2367 2368 salt '*' pkg.compare_versions 1.2 >= 1.3 2369 """ 2370 if not ver1: 2371 raise SaltInvocationError("compare_version, ver1 is blank") 2372 if not ver2: 2373 raise SaltInvocationError("compare_version, ver2 is blank") 2374 2375 # Support version being the special meaning of 'latest' 2376 if ver1 == "latest": 2377 ver1 = str(sys.maxsize) 2378 if ver2 == "latest": 2379 ver2 = str(sys.maxsize) 2380 # Support version being the special meaning of 'Not Found' 2381 if ver1 == "Not Found": 2382 ver1 = "0.0.0.0.0" 2383 if ver2 == "Not Found": 2384 ver2 = "0.0.0.0.0" 2385 2386 return salt.utils.versions.compare(ver1, oper, ver2, ignore_epoch=True) 2387