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