From 608f7acfa2af9d728a369e02fb47a16b55b8f57a Mon Sep 17 00:00:00 2001 From: Twangboy Date: Tue, 23 May 2023 16:18:40 -0600 Subject: [PATCH] Add appx.absent state, remove duplicated code --- salt/modules/win_appx.py | 29 ++++--- salt/modules/win_servermanager.py | 42 +--------- salt/modules/win_wusa.py | 38 +-------- salt/states/win_appx.py | 123 ++++++++++++++++++++++++++++++ 4 files changed, 148 insertions(+), 84 deletions(-) create mode 100644 salt/states/win_appx.py diff --git a/salt/modules/win_appx.py b/salt/modules/win_appx.py index 9ba86520bd8..8812ee1d827 100644 --- a/salt/modules/win_appx.py +++ b/salt/modules/win_appx.py @@ -4,7 +4,7 @@ Manage provisioned apps Provisioned apps are part of the image and are installed for every user the first time the user logs on. Provisioned apps are also updated and sometimes - reinstalled when the system is updated. +reinstalled when the system is updated. Apps removed with this module will remove the app for all users and deprovision the app. Deprovisioned apps will neither be installed for new users nor will @@ -44,6 +44,7 @@ import fnmatch import logging import salt.utils.platform +import salt.utils.win_pwsh import salt.utils.win_reg log = logging.getLogger(__name__) @@ -60,7 +61,11 @@ def __virtual__(): Load only on Windows """ if not salt.utils.platform.is_windows(): - return False, "Module appx: module only works on Windows systems." + return False, "Appx module: Only available on Windows systems" + + pwsh_info = __salt__["cmd.shell_info"](shell="powershell", list_modules=False) + if not pwsh_info["installed"]: + return False, "Appx module: PowerShell not available" return __virtualname__ @@ -164,10 +169,10 @@ def list_(query=None, field="Name", include_store=False, frameworks=False, bundl if not field: cmd.append("Sort-Object Name") cmd.append("Select Name, Version, PackageFullName, PackageFamilyName, IsBundle, IsFramework") - return __utils__["win_pwsh.run_dict"](" | ".join(cmd)) + return salt.utils.win_pwsh.run_dict(" | ".join(cmd)) else: cmd.append(f"Sort-Object {field}") - return _pkg_list(__utils__["win_pwsh.run_dict"](" | ".join(cmd)), field) + return _pkg_list(salt.utils.win_pwsh.run_dict(" | ".join(cmd)), field) def remove(query=None, include_store=False, frameworks=False, deprovision_only=False): @@ -176,8 +181,8 @@ def remove(query=None, include_store=False, frameworks=False, deprovision_only=F a bundle, the entire bundle will be removed. This function removes the package for all users on the system. It also - deprovisions the packages so that it isn't re-installed by later system - updates. To only deprovision a package and not remove for all users, set + deprovisions the package so that it isn't re-installed by later system + updates. To only deprovision a package and not remove it for all users, set ``deprovision_only=True``. Args: @@ -195,13 +200,13 @@ def remove(query=None, include_store=False, frameworks=False, deprovision_only=F ``include_store=True`` .. note:: - Use the ``appx.get`` function to make sure your query is + Use the ``appx.list`` function to make sure your query is returning what you expect. Then use the same query to remove those packages include_store (bool): Include the Microsoft Store in the results of the query to be - removed. Use this with caution. It difficult to reinstall the + removed. Use this with caution. It is difficult to reinstall the Microsoft Store once it has been removed with this function. Default is ``False`` @@ -210,7 +215,7 @@ def remove(query=None, include_store=False, frameworks=False, deprovision_only=F Default is ``False`` deprovision_only (bool): - Deprovision the package. The package will be removed from the + Only deprovision the package. The package will be removed from the current user and added to the list of deprovisioned packages. The package will not be re-installed in future system updates. New users of the system will not have the package installed. However, the @@ -260,7 +265,7 @@ def remove(query=None, include_store=False, frameworks=False, deprovision_only=F else: log.debug("Removing package: %s", remove_name) remove_cmd = f"Remove-AppxPackage -AllUsers -Package {remove_name}" - __utils__["win_pwsh.run_dict"](remove_cmd) + salt.utils.win_pwsh.run_dict(remove_cmd) if isinstance(packages, list): log.debug("Removing %s packages", len(packages)) @@ -314,6 +319,10 @@ def install(package): If a package installed using this function has been deprovisioned previously, the registry entry marking it as deprovisioned will be removed. + .. NOTE:: + There is no ``appx.present`` state. Instead, use the + ``dism.provisioned_package_installed`` state. + Args: package (str): diff --git a/salt/modules/win_servermanager.py b/salt/modules/win_servermanager.py index f26ce42183d..81e8e8e5044 100644 --- a/salt/modules/win_servermanager.py +++ b/salt/modules/win_servermanager.py @@ -15,6 +15,7 @@ import salt.utils.json import salt.utils.platform import salt.utils.powershell import salt.utils.versions +import salt.utils.win_pwsh from salt.exceptions import CommandExecutionError log = logging.getLogger(__name__) @@ -51,41 +52,6 @@ def __virtual__(): return __virtualname__ -def _pshell_json(cmd, cwd=None): - """ - Execute the desired powershell command and ensure that it returns data - in JSON format and load that into python - """ - cmd = "Import-Module ServerManager; {}".format(cmd) - if "convertto-json" not in cmd.lower(): - cmd = "{} | ConvertTo-Json".format(cmd) - log.debug("PowerShell: %s", cmd) - ret = __salt__["cmd.run_all"](cmd, shell="powershell", cwd=cwd) - - if "pid" in ret: - del ret["pid"] - - if ret.get("stderr", ""): - error = ret["stderr"].splitlines()[0] - raise CommandExecutionError(error, info=ret) - - if "retcode" not in ret or ret["retcode"] != 0: - # run_all logs an error to log.error, fail hard back to the user - raise CommandExecutionError( - "Issue executing PowerShell {}".format(cmd), info=ret - ) - - # Sometimes Powershell returns an empty string, which isn't valid JSON - if ret["stdout"] == "": - ret["stdout"] = "{}" - - try: - ret = salt.utils.json.loads(ret["stdout"], strict=False) - except ValueError: - raise CommandExecutionError("No JSON results from PowerShell", info=ret) - return ret - - def list_available(): """ List available features to install @@ -129,7 +95,7 @@ def list_installed(): "-WarningAction SilentlyContinue " "| Select DisplayName,Name,Installed" ) - features = _pshell_json(cmd) + features = salt.utils.win_pwsh.run_dict(cmd) ret = {} for entry in features: @@ -230,7 +196,7 @@ def install(feature, recurse=False, restart=False, source=None, exclude=None): "-IncludeAllSubFeature" if recurse else "", "" if source is None else "-Source {}".format(source), ) - out = _pshell_json(cmd) + out = salt.utils.win_pwsh.run_dict(cmd) # Uninstall items in the exclude list # The Install-WindowsFeature command doesn't have the concept of an exclude @@ -375,7 +341,7 @@ def remove(feature, remove_payload=False, restart=False): "-Restart" if restart else "", ) try: - out = _pshell_json(cmd) + out = salt.utils.win_pwsh.run_dict(cmd) except CommandExecutionError as exc: if "ArgumentNotValid" in exc.message: raise CommandExecutionError("Invalid Feature Name", info=exc.info) diff --git a/salt/modules/win_wusa.py b/salt/modules/win_wusa.py index e6a8730f009..fb8894ed230 100644 --- a/salt/modules/win_wusa.py +++ b/salt/modules/win_wusa.py @@ -13,6 +13,7 @@ import logging import os import salt.utils.platform +import salt.utils.win_pwsh from salt.exceptions import CommandExecutionError log = logging.getLogger(__name__) @@ -35,41 +36,6 @@ def __virtual__(): return __virtualname__ -def _pshell_json(cmd, cwd=None): - """ - Execute the desired powershell command and ensure that it returns data - in JSON format and load that into python - """ - if "convertto-json" not in cmd.lower(): - cmd = "{} | ConvertTo-Json".format(cmd) - log.debug("PowerShell: %s", cmd) - ret = __salt__["cmd.run_all"](cmd, shell="powershell", cwd=cwd) - - if "pid" in ret: - del ret["pid"] - - if ret.get("stderr", ""): - error = ret["stderr"].splitlines()[0] - raise CommandExecutionError(error, info=ret) - - if "retcode" not in ret or ret["retcode"] != 0: - # run_all logs an error to log.error, fail hard back to the user - raise CommandExecutionError( - "Issue executing PowerShell {}".format(cmd), info=ret - ) - - # Sometimes Powershell returns an empty string, which isn't valid JSON - if ret["stdout"] == "": - ret["stdout"] = "{}" - - try: - ret = salt.utils.json.loads(ret["stdout"], strict=False) - except ValueError: - raise CommandExecutionError("No JSON results from PowerShell", info=ret) - - return ret - - def is_installed(name): """ Check if a specific KB is installed. @@ -229,7 +195,7 @@ def list(): salt '*' wusa.list """ kbs = [] - ret = _pshell_json("Get-HotFix | Select HotFixID") + ret = salt.utils.win_pwsh.run_dict("Get-HotFix | Select HotFixID") for item in ret: kbs.append(item["HotFixID"]) return kbs diff --git a/salt/states/win_appx.py b/salt/states/win_appx.py new file mode 100644 index 00000000000..206a06c2159 --- /dev/null +++ b/salt/states/win_appx.py @@ -0,0 +1,123 @@ +""" +Manage Microsoft Store apps on Windows. Removing an app with this modules will +deprovision the app from the online Windows image. +""" +import fnmatch +import logging + +import salt.utils.data +import salt.utils.platform + +log = logging.getLogger(__name__) +__virtualname__ = "appx" + + +def __virtual__(): + """ + Only work on Windows where the DISM module is available + """ + if not salt.utils.platform.is_windows(): + return False, "Appx state: Only available on Windows" + + pwsh_info = __salt__["cmd.shell_info"](shell="powershell", list_modules=False) + if not pwsh_info["installed"]: + return False, "Appx state: PowerShell not available" + + return __virtualname__ + + +def absent( + name, query, include_store=False, frameworks=False, deprovision_only=False +): + """ + Removes Microsoft Store packages from the system. If the package is part of + a bundle, the entire bundle will be removed. + + This function removes the package for all users on the system. It also + deprovisions the package so that it isn't re-installed by later system + updates. To only deprovision a package and not remove it for all users, set + ``deprovision_only=True``. + + Args: + + query (str): + The query string to use to select the packages to be removed. If the + string matches multiple packages, they will all be removed. Here are + some example strings: + + - ``*teams*`` - Remove Microsoft Teams + - ``*zune*`` - Remove Windows Media Player and ZuneVideo + - ``*zuneMusic*`` - Only remove Windows Media Player + - ``*xbox*`` - Remove all xbox packages, there are 5 by default + - ``*`` - Remove everything but the Microsoft Store, unless + ``include_store=True`` + + .. note:: + Use the ``appx.list`` function to make sure your query is + returning what you expect. Then use the same query to remove + those packages + + include_store (bool): + Include the Microsoft Store in the results of the query to be + removed. Use this with caution. It is difficult to reinstall the + Microsoft Store once it has been removed with this function. Default + is ``False`` + + frameworks (bool): + Include frameworks in the results of the query to be removed. + Default is ``False`` + + deprovision_only (bool): + Only deprovision the package. The package will be removed from the + current user and added to the list of deprovisioned packages. The + package will not be re-installed in future system updates. New users + of the system will not have the package installed. However, the + package will still be installed for existing users. Default is + ``False`` + + Returns: + bool: ``True`` if successful, ``None`` if no packages found + + Raises: + CommandExecutionError: On errors encountered removing the package + + CLI Example: + + .. code-block:: bash + + salt + """ + ret = {"name": name, "result": True, "comment": "", "changes": {}} + + old = __salt__["appx.list"](include_store=include_store, frameworks=frameworks) + matches = fnmatch.filter(old, query) + if not matches: + ret["comment"] = f"No apps found matching query: {query}" + return ret + + if __opts__["test"]: + ret["changes"]["removed"] = matches + ret["comment"] = "Matching apps will be removed" + ret["result"] = None + return ret + + # Install the capability + status = __salt__["appx.remove"]( + query, + include_store=include_store, + frameworks=frameworks, + deprovision_only=deprovision_only + ) + + if status is None: + ret["comment"] = f"No apps found matching query: {query}" + ret["result"] = False + + new = __salt__["appx.list"](include_store=include_store, frameworks=frameworks) + changes = salt.utils.data.compare_lists(old, new) + + if changes: + ret["comment"] = f"Removed apps matching query: {query}" + ret["changes"] = changes + + return ret