From bc1217c99ded3078c5eceff2d3518ebc353e0b90 Mon Sep 17 00:00:00 2001 From: Twangboy Date: Fri, 19 May 2023 15:32:05 -0600 Subject: [PATCH] Add some documentation, add functions and states in dism modules --- salt/modules/win_appx.py | 129 +++++++++++++++++++++++++++++++-------- salt/modules/win_dism.py | 85 +++++++++++++++++++++++++- salt/states/win_dism.py | 80 +++++++++++++++++++++++- 3 files changed, 266 insertions(+), 28 deletions(-) diff --git a/salt/modules/win_appx.py b/salt/modules/win_appx.py index def5ac490ca..bfe03edcb7a 100644 --- a/salt/modules/win_appx.py +++ b/salt/modules/win_appx.py @@ -1,3 +1,45 @@ +""" +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. + +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 +they be upgraded. + +An app removed with this module can only be re-provisioned on the machine, but +it can't be re-installed for all users. Also, once a package has been +deprovisioned, the only way to reinstall it is to download the package. This is +difficult. I've outlined the steps below: + +1. Obtain the Microsoft Store URL for the app: + - Open the page for the app in the Microsoft Store + - Click the share button and copy the URL + +2. Look up the packages on https://store.rg-adguard.net/: + - Ensure ``URL (link)`` is selected in the first dropdown + - Paste the URL in the search field + - Ensure Retail is selected in the 2nd dropdown + - Click the checkmark button + +This should give you a list of URLs for the package and all dependencies for all +architectures. Download the package and all dependencies for your system +architecture. These will usually have one of the following file extensions: + +- ``.appx`` +- ``.appxbundle`` +- ``.msix`` +- ``.msixbundle`` + +Dependencies will need to be installed first. + +Not all packages can be found this way, but it seems like most of them can. + +Use the ``appx.install`` function to provision the new app. +""" import fnmatch import logging @@ -6,9 +48,11 @@ import salt.utils.win_reg log = logging.getLogger(__name__) -CURRENTVERSION_KEY = r"SOFTWARE\Microsoft\Windows\CurrentVersion" -DEPROVISIONED_KEY = fr"{CURRENTVERSION_KEY}\Appx\AppxAllUserStore\Deprovisioned" +CURRENT_VERSION_KEY = r"SOFTWARE\Microsoft\Windows\CurrentVersion" +DEPROVISIONED_KEY = fr"{CURRENT_VERSION_KEY}\Appx\AppxAllUserStore\Deprovisioned" + __virtualname__ = "appx" +__func_alias__ = {"list_": "list"} def __virtual__(): @@ -21,7 +65,7 @@ def __virtual__(): return __virtualname__ -def _pkg_list(raw, field="PackageFullName"): +def _pkg_list(raw, field="Name"): result = [] if raw: if isinstance(raw, list): @@ -34,7 +78,7 @@ def _pkg_list(raw, field="PackageFullName"): return result -def get(query=None, field="Name", include_store=False, frameworks=False, bundles=True): +def list_(query=None, field="Name", include_store=False, frameworks=False, bundles=True): """ Get a list of Microsoft Store packages installed on the system. @@ -82,6 +126,24 @@ def get(query=None, field="Name", include_store=False, frameworks=False, bundles Raises: CommandExecutionError: If an error is encountered retrieving packages + CLI Example: + + .. code-block:: bash + + # List installed apps that contain the word "candy" + salt '*' appx.list *candy* + + # Return more information about the package + salt '*' appx.list *candy* field=None + + # List all installed apps, including the Microsoft Store + salt '*' appx.list include_store=True + + # List all installed apps, including frameworks + salt '*' appx.list frameworks=True + + # List all installed apps that are bundles + salt '*' appx.list bundles=True """ cmd = [] @@ -113,9 +175,9 @@ def remove(query=None, include_store=False, frameworks=False, deprovision_only=F Removes Microsoft Store packages from the system. If the package is part of a bundle, the entire bundle will be removed. - By default, this function will remove the package for all users on the - system. It will also deprovision the packages so that it isn't re-installed - by later system updates. To only deprovision a package, set + 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 ``deprovision_only=True``. Args: @@ -134,7 +196,8 @@ def remove(query=None, include_store=False, frameworks=False, deprovision_only=F .. note:: Use the ``appx.get`` function to make sure your query is - returning what you expect + 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 @@ -160,8 +223,13 @@ def remove(query=None, include_store=False, frameworks=False, deprovision_only=F Raises: CommandExecutionError: On errors encountered removing the package + CLI Example: + + .. code-block:: bash + + salt """ - packages = get( + packages = list_( query=query, field=None, include_store=include_store, @@ -175,7 +243,7 @@ def remove(query=None, include_store=False, frameworks=False, deprovision_only=F # fail. Let's make sure it's a bundle if not package["IsBundle"]: # If it's not a bundle, let's see if we can find the bundle - bundle = get( + bundle = list_( query=f'{package["Name"]}*', field=None, include_store=include_store, @@ -210,8 +278,9 @@ def remove(query=None, include_store=False, frameworks=False, deprovision_only=F def get_deprovisioned(query=None): """ - Get a list of applications that have been deprovisioned. A deprovisioned - package will not be reinstalled during a Major system update. + When an app is deprovisioned, a registry key is created that will keep it + from being reinstalled during a major system update. This function returns a + list of keys for apps that have been deprovisioned. Args: @@ -236,20 +305,30 @@ def get_deprovisioned(query=None): return fnmatch.filter(ret, query) +def install(package): + """ + This function uses ``dism`` to provision a package. This means that it will + be made a part of the online image and added to new users on the system. If + a package has dependencies, those must be installed first. -def reprovision(query=None): - pkgs = salt.utils.win_reg.list_keys(hive="HKLM", key=f"{DEPROVISIONED_KEY}") - failed = [] - for item in fnmatch.filter(pkgs, query): - key = f"{DEPROVISIONED_KEY}\\{item}" - log.debug(f"Deprovisioning app: {item}") - ret = salt.utils.win_reg.delete_key_recursive(hive="HKLM", key=key) - if ret["Failed"]: - log.debug(f"Failed to deprovision: {item}") - failed.append(item) - if failed: - return {"Failed to deprovision": failed} - return True + If a package installed using this function has been deprovisioned + previously, the registry entry marking it as deprovisioned will be removed. + Args: + package (str): + The full path to the package to install. Can be one of the + following: + - ``.appx`` or ``.appxbundle`` + - ``.msix`` or ``.msixbundle`` + - ``.ppkg`` + + Returns: + bool: ``True`` if successful, otherwise ``False`` + """ + # I don't see a way to make the app installed for existing users on + # the system. The best we can do is provision the package for new + # users + ret = __salt__["dism.add_provisioned_package"](package) + return ret["retcode"] == 0 diff --git a/salt/modules/win_dism.py b/salt/modules/win_dism.py index c79e1c910a8..c94e38e3dd3 100644 --- a/salt/modules/win_dism.py +++ b/salt/modules/win_dism.py @@ -56,7 +56,10 @@ def _get_components(type_regex, plural_type, install_value, image=None): "/Get-{}".format(plural_type), ] out = __salt__["cmd.run"](cmd) - pattern = r"{} : (.*)\r\n.*State : {}\r\n".format(type_regex, install_value) + if install_value: + pattern = r"{} : (.*)\r\n.*State : {}\r\n".format(type_regex, install_value) + else: + pattern = r"{} : (.*)\r\n.*".format(type_regex, install_value) capabilities = re.findall(pattern, out, re.MULTILINE) capabilities.sort() return capabilities @@ -511,6 +514,58 @@ def add_package( return __salt__["cmd.run_all"](cmd) +def add_provisioned_package(package, image=None, restart=False): + """ + Provision a package using DISM. A provisioned package will install for new + users on the system. It will also be reinstalled on each user if the system + if updated. + + Args: + + package (str): + The package to install. Can be one of the following: + + - ``.appx`` or ``.appxbundle`` + - ``.msix`` or ``.msixbundle`` + - ``.ppkg`` + + image (Optional[str]): + The path to the root directory of an offline Windows image. If + ``None`` is passed, the running operating system is targeted. + Default is ``None``. + + restart (Optional[bool]): + Reboot the machine if required by the installation. Default is + ``False`` + + Returns: + dict: A dictionary containing the results of the command + + CLI Example: + + .. code-block:: bash + + salt '*' dism.add_provisioned_package C:\\Packages\\package.appx + salt '*' dism.add_provisioned_package C:\\Packages\\package.appxbundle + salt '*' dism.add_provisioned_package C:\\Packages\\package.msix + salt '*' dism.add_provisioned_package C:\\Packages\\package.msixbundle + salt '*' dism.add_provisioned_package C:\\Packages\\package.ppkg + """ + cmd = [ + bin_dism, + "/Quiet", + "/Image:{}".format(image) if image else "/Online", + "/Add-ProvisionedAppxPackage", + "/PackagePath:{}".format(package), + "/SkipLicense", + ] + + if not restart: + cmd.append("/NoRestart") + + return __salt__["cmd.run_all"](cmd) + + def remove_package(package, image=None, restart=False): """ Uninstall a package @@ -654,6 +709,32 @@ def installed_packages(image=None): ) +def provisioned_packages(image=None): + """ + List the packages installed on the system + + Args: + image (Optional[str]): The path to the root directory of an offline + Windows image. If `None` is passed, the running operating system is + targeted. Default is None. + + Returns: + list: A list of installed packages + + CLI Example: + + .. code-block:: bash + + salt '*' dism.installed_packages + """ + return _get_components( + type_regex="PackageName", + plural_type="ProvisionedAppxPackages", + install_value="", + image=image, + ) + + def package_info(package, image=None): """ Display information about a package @@ -674,7 +755,7 @@ def package_info(package, image=None): .. code-block:: bash - salt '*' dism. package_info C:\\packages\\package.cab + salt '*' dism.package_info C:\\packages\\package.cab """ cmd = [ bin_dism, diff --git a/salt/states/win_dism.py b/salt/states/win_dism.py index 486ff9720c0..45bde06ad1d 100644 --- a/salt/states/win_dism.py +++ b/salt/states/win_dism.py @@ -2,7 +2,7 @@ Installing of Windows features using DISM ========================================= -Install windows features/capabilties with DISM +Install Windows features/capabilities/packages with DISM .. code-block:: yaml @@ -342,6 +342,84 @@ def package_installed( return ret +def provisioned_package_installed(name, image=None, restart=False): + """ + Install a package. + + Args: + + name (str): + The package to install. Can be one of the following: + + - ``.appx`` or ``.appxbundle`` + - ``.msix`` or ``.msixbundle`` + - ``.ppkg`` + + The name of the file before the file extension must match the name + of the package after it is installed. This name can be found by + running ``dism.provisioned_packages". + + image (Optional[str]): + The path to the root directory of an offline Windows image. If + ``None`` is passed, the running operating system is targeted. + Default is ``None``. + + restart (Optional[bool]): + Reboot the machine if required by the installation. Default is + ``False`` + + Example: + + .. code-block:: yaml + + install_windows_media_player: + dism.provisioned_package_installed: + - name: C:\\Packages\\Microsoft.ZuneVideo_2019.22091.10036.0_neutral_~_8wekyb3d8bbwe.Msixbundle + + """ + ret = {"name": name, "result": True, "comment": "", "changes": {}} + + # Fail if using a non-existent package path + if not os.path.exists(name): + if __opts__["test"]: + ret["result"] = None + else: + ret["result"] = False + ret["comment"] = f"Package path {name} does not exist" + return ret + + old = __salt__["dism.provisioned_packages"]() + + # Get package name so we can see if it's already installed + package_name = os.path.splitext(os.path.basename(name)) + + if package_name in old: + ret["comment"] = f"The package {name} is already installed: {package_name}" + return ret + + if __opts__["test"]: + ret["changes"]["package"] = f"{name} will be installed" + ret["result"] = None + return ret + + # Install the package + status = __salt__["dism.add_provisioned_package"](name, image, restart) + + if status["retcode"] not in [0, 1641, 3010]: + ret["comment"] = f'Failed to install {name}: {status["stdout"]}' + ret["result"] = False + + new = __salt__["dism.provisioned_packages"]() + changes = salt.utils.data.compare_lists(old, new) + + if changes: + ret["comment"] = f"Installed {name}" + ret["changes"] = status + ret["changes"]["package"] = changes + + return ret + + def package_removed(name, image=None, restart=False): """ Uninstall a package