mirror of
https://github.com/saltstack/salt.git
synced 2025-04-17 10:10:20 +00:00
Merge branch 'master' into add-keyvalue-create_if_missing
This commit is contained in:
commit
a5bd72653f
25 changed files with 1652 additions and 479 deletions
1
changelog/64417.removed.md
Normal file
1
changelog/64417.removed.md
Normal file
|
@ -0,0 +1 @@
|
|||
Dropped Python 3.7 support since it's EOL in 27 Jun 2023
|
1
changelog/64418.added.md
Normal file
1
changelog/64418.added.md
Normal file
|
@ -0,0 +1 @@
|
|||
Add ability to return False result in test mode of configurable_test_state
|
1
changelog/64978.added.md
Normal file
1
changelog/64978.added.md
Normal file
|
@ -0,0 +1 @@
|
|||
Added win_appx state and execution modules for managing Microsoft Store apps and deprovisioning them from systems
|
|
@ -498,6 +498,7 @@ execution modules
|
|||
vmctl
|
||||
vsphere
|
||||
webutil
|
||||
win_appx
|
||||
win_auditpol
|
||||
win_autoruns
|
||||
win_certutil
|
||||
|
|
5
doc/ref/modules/all/salt.modules.win_appx.rst
Normal file
5
doc/ref/modules/all/salt.modules.win_appx.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
salt.modules.win_appx
|
||||
=====================
|
||||
|
||||
.. automodule:: salt.modules.win_appx
|
||||
:members:
|
|
@ -320,6 +320,7 @@ state modules
|
|||
virt
|
||||
virtualenv_mod
|
||||
webutil
|
||||
win_appx
|
||||
win_certutil
|
||||
win_dacl
|
||||
win_dism
|
||||
|
|
5
doc/ref/states/all/salt.states.win_appx.rst
Normal file
5
doc/ref/states/all/salt.states.win_appx.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
salt.states.win_appx
|
||||
====================
|
||||
|
||||
.. automodule:: salt.states.win_appx
|
||||
:members:
|
|
@ -6,6 +6,9 @@
|
|||
Add release specific details below
|
||||
-->
|
||||
|
||||
## Python 3.7 Support Dropped
|
||||
Support for python 3.7 has been dropped since it reached end-of-line in 27 Jun 2023.
|
||||
|
||||
## Azure Salt Extension
|
||||
|
||||
Starting from Salt version 3007.0, the Azure functionality previously available in the Salt code base is fully removed. To continue using Salt's features for interacting with Azure resources, users are required to utilize the Azure Salt extension. For more information, refer to the [Azure Salt Extension GitHub repository](https://github.com/salt-extensions/saltext-azurerm).
|
||||
|
|
|
@ -82,7 +82,7 @@ if IS_WINDOWS:
|
|||
else:
|
||||
ONEDIR_PYTHON_PATH = ONEDIR_ARTIFACT_PATH / "bin" / "python3"
|
||||
# Python versions to run against
|
||||
_PYTHON_VERSIONS = ("3", "3.5", "3.6", "3.7", "3.8", "3.9", "3.10")
|
||||
_PYTHON_VERSIONS = ("3", "3.8", "3.9", "3.10", "3.11")
|
||||
|
||||
# Nox options
|
||||
# Reuse existing virtualenvs
|
||||
|
|
363
salt/modules/win_appx.py
Normal file
363
salt/modules/win_appx.py
Normal file
|
@ -0,0 +1,363 @@
|
|||
"""
|
||||
Manage provisioned apps
|
||||
=======================
|
||||
|
||||
.. versionadded:: 3007.0
|
||||
|
||||
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. The steps are outlined 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 return 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
|
||||
|
||||
import salt.utils.platform
|
||||
import salt.utils.win_pwsh
|
||||
import salt.utils.win_reg
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
CURRENT_VERSION_KEY = r"SOFTWARE\Microsoft\Windows\CurrentVersion"
|
||||
DEPROVISIONED_KEY = rf"{CURRENT_VERSION_KEY}\Appx\AppxAllUserStore\Deprovisioned"
|
||||
|
||||
__virtualname__ = "appx"
|
||||
__func_alias__ = {"list_": "list"}
|
||||
|
||||
|
||||
def __virtual__():
|
||||
"""
|
||||
Load only on Windows
|
||||
"""
|
||||
if not salt.utils.platform.is_windows():
|
||||
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__
|
||||
|
||||
|
||||
def _pkg_list(raw, field="Name"):
|
||||
result = []
|
||||
if raw:
|
||||
if isinstance(raw, list):
|
||||
for pkg in raw:
|
||||
result.append(pkg[field])
|
||||
else:
|
||||
result.append(raw[field])
|
||||
else:
|
||||
result = None
|
||||
return result
|
||||
|
||||
|
||||
def list_(
|
||||
query=None, field="Name", include_store=False, frameworks=False, bundles=True
|
||||
):
|
||||
"""
|
||||
Get a list of Microsoft Store packages installed on the system.
|
||||
|
||||
Args:
|
||||
|
||||
query (str):
|
||||
The query string to use to filter packages to be listed. The string
|
||||
can match multiple packages. ``None`` will return all packages. Here
|
||||
are some example strings:
|
||||
|
||||
- ``*teams*`` - Returns Microsoft Teams
|
||||
- ``*zune*`` - Returns Windows Media Player and ZuneVideo
|
||||
- ``*zuneMusic*`` - Only returns Windows Media Player
|
||||
- ``*xbox*`` - Returns all xbox packages, there are 5 by default
|
||||
- ``*`` - Returns everything but the Microsoft Store, unless
|
||||
``include_store=True``
|
||||
|
||||
field (str):
|
||||
This function returns a list of packages on the system. It can
|
||||
display a short name or a full name. If ``None`` is passed, a
|
||||
dictionary will be returned with some common fields. The default is
|
||||
``Name``. Valid options are any fields returned by the powershell
|
||||
command ``Get-AppxPackage``. Here are some useful fields:
|
||||
|
||||
- Name
|
||||
- Version
|
||||
- PackageFullName
|
||||
- PackageFamilyName
|
||||
|
||||
include_store (bool):
|
||||
Include the Microsoft Store in the results. Default is ``False``
|
||||
|
||||
frameworks (bool):
|
||||
Include frameworks in the results. Default is ``False``
|
||||
|
||||
bundles (bool):
|
||||
If ``True``, this will return application bundles only. If
|
||||
``False``, this will return individual packages only, even if they
|
||||
are part of a bundle.
|
||||
|
||||
Returns:
|
||||
list: A list of packages ordered by the string passed in field
|
||||
list: A list of dictionaries of package information if field is ``None``
|
||||
|
||||
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 = []
|
||||
|
||||
if bundles:
|
||||
cmd_str = "Get-AppxPackage -AllUsers -PackageTypeFilter Bundle"
|
||||
else:
|
||||
cmd_str = "Get-AppxPackage -AllUsers"
|
||||
|
||||
if query:
|
||||
cmd.append(f"{cmd_str} -Name {query}")
|
||||
else:
|
||||
cmd.append(f"{cmd_str}")
|
||||
if not include_store:
|
||||
cmd.append('Where-Object {$_.name -notlike "Microsoft.WindowsStore*"}')
|
||||
if not frameworks:
|
||||
cmd.append("Where-Object -Property IsFramework -eq $false")
|
||||
cmd.append("Where-Object -Property NonRemovable -eq $false")
|
||||
if not field:
|
||||
cmd.append("Sort-Object Name")
|
||||
cmd.append(
|
||||
"Select Name, Version, PackageFullName, PackageFamilyName, IsBundle, IsFramework"
|
||||
)
|
||||
return salt.utils.win_pwsh.run_dict(" | ".join(cmd))
|
||||
else:
|
||||
cmd.append(f"Sort-Object {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):
|
||||
"""
|
||||
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 "*" appx.remove *candy*
|
||||
"""
|
||||
packages = list_(
|
||||
query=query,
|
||||
field=None,
|
||||
include_store=include_store,
|
||||
frameworks=frameworks,
|
||||
bundles=False,
|
||||
)
|
||||
|
||||
def remove_package(package):
|
||||
remove_name = package["PackageFullName"]
|
||||
# If the package is part of a bundle with the same name, removal will
|
||||
# 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 = list_(
|
||||
query=f'{package["Name"]}*',
|
||||
field=None,
|
||||
include_store=include_store,
|
||||
frameworks=frameworks,
|
||||
bundles=True,
|
||||
)
|
||||
if bundle and bundle["IsBundle"]:
|
||||
log.debug(f'Found bundle: {bundle["PackageFullName"]}')
|
||||
remove_name = bundle["PackageFullName"]
|
||||
|
||||
if deprovision_only:
|
||||
log.debug("Deprovisioning package: %s", remove_name)
|
||||
remove_cmd = (
|
||||
f"Remove-AppxProvisionedPackage -Online -PackageName {remove_name}"
|
||||
)
|
||||
else:
|
||||
log.debug("Removing package: %s", remove_name)
|
||||
remove_cmd = f"Remove-AppxPackage -AllUsers -Package {remove_name}"
|
||||
salt.utils.win_pwsh.run_dict(remove_cmd)
|
||||
|
||||
if isinstance(packages, list):
|
||||
log.debug("Removing %s packages", len(packages))
|
||||
for pkg in packages:
|
||||
remove_package(package=pkg)
|
||||
elif packages:
|
||||
log.debug("Removing a single package")
|
||||
remove_package(package=packages)
|
||||
else:
|
||||
log.debug("Package not found: %s", query)
|
||||
return None
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def list_deprovisioned(query=None):
|
||||
"""
|
||||
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:
|
||||
|
||||
query (str):
|
||||
The query string to use to filter packages to be listed. The string
|
||||
can match multiple packages. ``None`` will return all packages. Here
|
||||
are some example strings:
|
||||
|
||||
- ``*teams*`` - Returns Microsoft Teams
|
||||
- ``*zune*`` - Returns Windows Media Player and ZuneVideo
|
||||
- ``*zuneMusic*`` - Only returns Windows Media Player
|
||||
- ``*xbox*`` - Returns all xbox packages, there are 5 by default
|
||||
- ``*`` - Returns everything but the Microsoft Store, unless
|
||||
``include_store=True``
|
||||
|
||||
Returns:
|
||||
list: A list of packages matching the query criteria
|
||||
|
||||
CLI Example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
salt "*" appx.list_deprovisioned *zune*
|
||||
"""
|
||||
ret = salt.utils.win_reg.list_keys(hive="HKLM", key=f"{DEPROVISIONED_KEY}")
|
||||
if query is None:
|
||||
return ret
|
||||
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.
|
||||
|
||||
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):
|
||||
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``
|
||||
|
||||
CLI Example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
salt "*" appx.install "C:\\Temp\\Microsoft.ZuneMusic.msixbundle"
|
||||
"""
|
||||
# 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
|
|
@ -20,7 +20,7 @@ __virtualname__ = "dism"
|
|||
# host machine. On 32bit boxes that will always be System32. On 64bit boxes that
|
||||
# are running 64bit salt that will always be System32. On 64bit boxes that are
|
||||
# running 32bit salt the 64bit dism will be found in SysNative
|
||||
# Sysnative is a virtual folder, a special alias, that can be used to access the
|
||||
# SysNative is a virtual folder, a special alias, that can be used to access the
|
||||
# 64-bit System32 folder from a 32-bit application
|
||||
try:
|
||||
# This does not apply to Non-Windows platforms
|
||||
|
@ -52,11 +52,14 @@ def _get_components(type_regex, plural_type, install_value, image=None):
|
|||
cmd = [
|
||||
bin_dism,
|
||||
"/English",
|
||||
"/Image:{}".format(image) if image else "/Online",
|
||||
"/Get-{}".format(plural_type),
|
||||
f"/Image:{image}" if image else "/Online",
|
||||
f"/Get-{plural_type}",
|
||||
]
|
||||
out = __salt__["cmd.run"](cmd)
|
||||
pattern = r"{} : (.*)\r\n.*State : {}\r\n".format(type_regex, install_value)
|
||||
if install_value:
|
||||
pattern = rf"{type_regex} : (.*)\r\n.*State : {install_value}\r\n"
|
||||
else:
|
||||
pattern = rf"{type_regex} : (.*)\r\n.*"
|
||||
capabilities = re.findall(pattern, out, re.MULTILINE)
|
||||
capabilities.sort()
|
||||
return capabilities
|
||||
|
@ -95,19 +98,19 @@ def add_capability(
|
|||
if salt.utils.versions.version_cmp(__grains__["osversion"], "10") == -1:
|
||||
raise NotImplementedError(
|
||||
"`install_capability` is not available on this version of Windows: "
|
||||
"{}".format(__grains__["osversion"])
|
||||
f'{__grains__["osversion"]}'
|
||||
)
|
||||
|
||||
cmd = [
|
||||
bin_dism,
|
||||
"/Quiet",
|
||||
"/Image:{}".format(image) if image else "/Online",
|
||||
f"/Image:{image}" if image else "/Online",
|
||||
"/Add-Capability",
|
||||
"/CapabilityName:{}".format(capability),
|
||||
f"/CapabilityName:{capability}",
|
||||
]
|
||||
|
||||
if source:
|
||||
cmd.append("/Source:{}".format(source))
|
||||
cmd.append(f"/Source:{source}")
|
||||
if limit_access:
|
||||
cmd.append("/LimitAccess")
|
||||
if not restart:
|
||||
|
@ -143,15 +146,15 @@ def remove_capability(capability, image=None, restart=False):
|
|||
if salt.utils.versions.version_cmp(__grains__["osversion"], "10") == -1:
|
||||
raise NotImplementedError(
|
||||
"`uninstall_capability` is not available on this version of "
|
||||
"Windows: {}".format(__grains__["osversion"])
|
||||
f'Windows: {__grains__["osversion"]}'
|
||||
)
|
||||
|
||||
cmd = [
|
||||
bin_dism,
|
||||
"/Quiet",
|
||||
"/Image:{}".format(image) if image else "/Online",
|
||||
f"/Image:{image}" if image else "/Online",
|
||||
"/Remove-Capability",
|
||||
"/CapabilityName:{}".format(capability),
|
||||
f"/CapabilityName:{capability}",
|
||||
]
|
||||
|
||||
if not restart:
|
||||
|
@ -185,13 +188,13 @@ def get_capabilities(image=None):
|
|||
if salt.utils.versions.version_cmp(__grains__["osversion"], "10") == -1:
|
||||
raise NotImplementedError(
|
||||
"`installed_capabilities` is not available on this version of "
|
||||
"Windows: {}".format(__grains__["osversion"])
|
||||
f'Windows: {__grains__["osversion"]}'
|
||||
)
|
||||
|
||||
cmd = [
|
||||
bin_dism,
|
||||
"/English",
|
||||
"/Image:{}".format(image) if image else "/Online",
|
||||
f"/Image:{image}" if image else "/Online",
|
||||
"/Get-Capabilities",
|
||||
]
|
||||
out = __salt__["cmd.run"](cmd)
|
||||
|
@ -228,7 +231,7 @@ def installed_capabilities(image=None):
|
|||
if salt.utils.versions.version_cmp(__grains__["osversion"], "10") == -1:
|
||||
raise NotImplementedError(
|
||||
"`installed_capabilities` is not available on this version of "
|
||||
"Windows: {}".format(__grains__["osversion"])
|
||||
f'Windows: {__grains__["osversion"]}'
|
||||
)
|
||||
return _get_components("Capability Identity", "Capabilities", "Installed")
|
||||
|
||||
|
@ -258,7 +261,7 @@ def available_capabilities(image=None):
|
|||
if salt.utils.versions.version_cmp(__grains__["osversion"], "10") == -1:
|
||||
raise NotImplementedError(
|
||||
"`installed_capabilities` is not available on this version of "
|
||||
"Windows: {}".format(__grains__["osversion"])
|
||||
f'Windows: {__grains__["osversion"]}'
|
||||
)
|
||||
return _get_components("Capability Identity", "Capabilities", "Not Present")
|
||||
|
||||
|
@ -303,14 +306,14 @@ def add_feature(
|
|||
cmd = [
|
||||
bin_dism,
|
||||
"/Quiet",
|
||||
"/Image:{}".format(image) if image else "/Online",
|
||||
f"/Image:{image}" if image else "/Online",
|
||||
"/Enable-Feature",
|
||||
"/FeatureName:{}".format(feature),
|
||||
f"/FeatureName:{feature}",
|
||||
]
|
||||
if package:
|
||||
cmd.append("/PackageName:{}".format(package))
|
||||
cmd.append(f"/PackageName:{package}")
|
||||
if source:
|
||||
cmd.append("/Source:{}".format(source))
|
||||
cmd.append(f"/Source:{source}")
|
||||
if limit_access:
|
||||
cmd.append("/LimitAccess")
|
||||
if enable_parent:
|
||||
|
@ -346,9 +349,9 @@ def remove_feature(feature, remove_payload=False, image=None, restart=False):
|
|||
cmd = [
|
||||
bin_dism,
|
||||
"/Quiet",
|
||||
"/Image:{}".format(image) if image else "/Online",
|
||||
f"/Image:{image}" if image else "/Online",
|
||||
"/Disable-Feature",
|
||||
"/FeatureName:{}".format(feature),
|
||||
f"/FeatureName:{feature}",
|
||||
]
|
||||
|
||||
if remove_payload:
|
||||
|
@ -394,15 +397,15 @@ def get_features(package=None, image=None):
|
|||
cmd = [
|
||||
bin_dism,
|
||||
"/English",
|
||||
"/Image:{}".format(image) if image else "/Online",
|
||||
f"/Image:{image}" if image else "/Online",
|
||||
"/Get-Features",
|
||||
]
|
||||
|
||||
if package:
|
||||
if "~" in package:
|
||||
cmd.append("/PackageName:{}".format(package))
|
||||
cmd.append(f"/PackageName:{package}")
|
||||
else:
|
||||
cmd.append("/PackagePath:{}".format(package))
|
||||
cmd.append(f"/PackagePath:{package}")
|
||||
|
||||
out = __salt__["cmd.run"](cmd)
|
||||
|
||||
|
@ -496,9 +499,9 @@ def add_package(
|
|||
cmd = [
|
||||
bin_dism,
|
||||
"/Quiet",
|
||||
"/Image:{}".format(image) if image else "/Online",
|
||||
f"/Image:{image}" if image else "/Online",
|
||||
"/Add-Package",
|
||||
"/PackagePath:{}".format(package),
|
||||
f"/PackagePath:{package}",
|
||||
]
|
||||
|
||||
if ignore_check:
|
||||
|
@ -511,6 +514,60 @@ 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
|
||||
is updated.
|
||||
|
||||
.. versionadded:: 3007.0
|
||||
|
||||
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",
|
||||
f"/Image:{image}" if image else "/Online",
|
||||
"/Add-ProvisionedAppxPackage",
|
||||
f"/PackagePath:{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
|
||||
|
@ -542,7 +599,7 @@ def remove_package(package, image=None, restart=False):
|
|||
cmd = [
|
||||
bin_dism,
|
||||
"/Quiet",
|
||||
"/Image:{}".format(image) if image else "/Online",
|
||||
f"/Image:{image}" if image else "/Online",
|
||||
"/Remove-Package",
|
||||
]
|
||||
|
||||
|
@ -550,9 +607,9 @@ def remove_package(package, image=None, restart=False):
|
|||
cmd.append("/NoRestart")
|
||||
|
||||
if "~" in package:
|
||||
cmd.append("/PackageName:{}".format(package))
|
||||
cmd.append(f"/PackageName:{package}")
|
||||
else:
|
||||
cmd.append("/PackagePath:{}".format(package))
|
||||
cmd.append(f"/PackagePath:{package}")
|
||||
|
||||
return __salt__["cmd.run_all"](cmd)
|
||||
|
||||
|
@ -584,9 +641,9 @@ def get_kb_package_name(kb, image=None):
|
|||
salt '*' dism.get_kb_package_name 1231231
|
||||
"""
|
||||
packages = installed_packages(image=image)
|
||||
search = kb.upper() if kb.lower().startswith("kb") else "KB{}".format(kb)
|
||||
search = kb.upper() if kb.lower().startswith("kb") else f"KB{kb}"
|
||||
for package in packages:
|
||||
if "_{}~".format(search) in package:
|
||||
if f"_{search}~" in package:
|
||||
return package
|
||||
return None
|
||||
|
||||
|
@ -622,7 +679,7 @@ def remove_kb(kb, image=None, restart=False):
|
|||
"""
|
||||
pkg_name = get_kb_package_name(kb=kb, image=image)
|
||||
if pkg_name is None:
|
||||
msg = "{} not installed".format(kb)
|
||||
msg = f"{kb} not installed"
|
||||
raise CommandExecutionError(msg)
|
||||
log.debug("Found: %s", pkg_name)
|
||||
return remove_package(package=pkg_name, image=image, restart=restart)
|
||||
|
@ -654,6 +711,34 @@ def installed_packages(image=None):
|
|||
)
|
||||
|
||||
|
||||
def provisioned_packages(image=None):
|
||||
"""
|
||||
List the packages installed on the system
|
||||
|
||||
.. versionadded:: 3007.0
|
||||
|
||||
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,19 +759,19 @@ 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,
|
||||
"/English",
|
||||
"/Image:{}".format(image) if image else "/Online",
|
||||
f"/Image:{image}" if image else "/Online",
|
||||
"/Get-PackageInfo",
|
||||
]
|
||||
|
||||
if "~" in package:
|
||||
cmd.append("/PackageName:{}".format(package))
|
||||
cmd.append(f"/PackageName:{package}")
|
||||
else:
|
||||
cmd.append("/PackagePath:{}".format(package))
|
||||
cmd.append(f"/PackagePath:{package}")
|
||||
|
||||
out = __salt__["cmd.run_all"](cmd)
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
@ -228,9 +194,9 @@ def install(feature, recurse=False, restart=False, source=None, exclude=None):
|
|||
shlex.quote(feature),
|
||||
management_tools,
|
||||
"-IncludeAllSubFeature" if recurse else "",
|
||||
"" if source is None else "-Source {}".format(source),
|
||||
"" if source is None else f"-Source {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)
|
||||
|
|
|
@ -13,12 +13,14 @@ import logging
|
|||
import os
|
||||
|
||||
import salt.utils.platform
|
||||
import salt.utils.win_pwsh
|
||||
from salt.exceptions import CommandExecutionError
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# Define the module's virtual name
|
||||
__virtualname__ = "wusa"
|
||||
__func_alias__ = {"list_": "list"}
|
||||
|
||||
|
||||
def __virtual__():
|
||||
|
@ -35,41 +37,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.
|
||||
|
@ -90,7 +57,7 @@ def is_installed(name):
|
|||
"""
|
||||
return (
|
||||
__salt__["cmd.retcode"](
|
||||
cmd="Get-HotFix -Id {}".format(name),
|
||||
cmd=f"Get-HotFix -Id {name}",
|
||||
shell="powershell",
|
||||
ignore_retcode=True,
|
||||
)
|
||||
|
@ -138,17 +105,17 @@ def install(path, restart=False):
|
|||
# Check the ret_code
|
||||
file_name = os.path.basename(path)
|
||||
errors = {
|
||||
2359302: "{} is already installed".format(file_name),
|
||||
2359302: f"{file_name} is already installed",
|
||||
3010: (
|
||||
"{} correctly installed but server reboot is needed to complete"
|
||||
" installation".format(file_name)
|
||||
f"{file_name} correctly installed but server reboot is needed to "
|
||||
f"complete installation"
|
||||
),
|
||||
87: "Unknown error",
|
||||
}
|
||||
if ret_code in errors:
|
||||
raise CommandExecutionError(errors[ret_code], ret_code)
|
||||
elif ret_code:
|
||||
raise CommandExecutionError("Unknown error: {}".format(ret_code))
|
||||
raise CommandExecutionError(f"Unknown error: {ret_code}")
|
||||
|
||||
return True
|
||||
|
||||
|
@ -203,19 +170,19 @@ def uninstall(path, restart=False):
|
|||
# If you pass /quiet and specify /kb, you'll always get retcode 87 if there
|
||||
# is an error. Use the actual file to get a more descriptive error
|
||||
errors = {
|
||||
-2145116156: "{} does not support uninstall".format(kb),
|
||||
2359303: "{} not installed".format(kb),
|
||||
-2145116156: f"{kb} does not support uninstall",
|
||||
2359303: f"{kb} not installed",
|
||||
87: "Unknown error. Try specifying an .msu file",
|
||||
}
|
||||
if ret_code in errors:
|
||||
raise CommandExecutionError(errors[ret_code], ret_code)
|
||||
elif ret_code:
|
||||
raise CommandExecutionError("Unknown error: {}".format(ret_code))
|
||||
raise CommandExecutionError(f"Unknown error: {ret_code}")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def list():
|
||||
def list_():
|
||||
"""
|
||||
Get a list of updates installed on the machine
|
||||
|
||||
|
@ -229,7 +196,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
|
||||
|
|
|
@ -173,7 +173,14 @@ def fail_with_changes(name, **kwargs): # pylint: disable=unused-argument
|
|||
return ret
|
||||
|
||||
|
||||
def configurable_test_state(name, changes=True, result=True, comment="", warnings=None):
|
||||
def configurable_test_state(
|
||||
name,
|
||||
changes=True,
|
||||
result=True,
|
||||
comment="",
|
||||
warnings=None,
|
||||
allow_test_mode_failure=False,
|
||||
):
|
||||
"""
|
||||
.. versionadded:: 2014.7.0
|
||||
|
||||
|
@ -221,6 +228,13 @@ def configurable_test_state(name, changes=True, result=True, comment="", warning
|
|||
Default is None
|
||||
|
||||
.. versionadded:: 3000
|
||||
|
||||
allow_test_mode_failure
|
||||
When False, running this state in test mode can only return a True
|
||||
or None result. When set to True and result is set to False, the
|
||||
test mode result will be False. Default is False
|
||||
|
||||
.. versionadded:: 3007.0
|
||||
"""
|
||||
ret = {"name": name, "changes": {}, "result": False, "comment": comment}
|
||||
change_data = {
|
||||
|
@ -276,7 +290,11 @@ def configurable_test_state(name, changes=True, result=True, comment="", warning
|
|||
)
|
||||
|
||||
if __opts__["test"]:
|
||||
ret["result"] = True if changes is False else None
|
||||
if allow_test_mode_failure and result is False:
|
||||
test_result = result
|
||||
else:
|
||||
test_result = True if changes is False else None
|
||||
ret["result"] = test_result
|
||||
ret["comment"] = "This is a test" if not comment else comment
|
||||
|
||||
return ret
|
||||
|
|
127
salt/states/win_appx.py
Normal file
127
salt/states/win_appx.py
Normal file
|
@ -0,0 +1,127 @@
|
|||
"""
|
||||
Manage Microsoft Store apps on Windows. Removing an app with this modules will
|
||||
deprovision the app from the online Windows image.
|
||||
|
||||
.. versionadded:: 3007.0
|
||||
"""
|
||||
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:
|
||||
|
||||
| string | description |
|
||||
| --------------- | ----------- |
|
||||
| ``*teams*`` | Remove Microsoft Teams |
|
||||
| ``*zune*`` | Remove Windows Media Player and Zune Video |
|
||||
| ``*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:: yaml
|
||||
|
||||
remove_candy_crush:
|
||||
appx.absent:
|
||||
- query: "*candy*"
|
||||
"""
|
||||
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"]:
|
||||
comment = ["The following apps will be removed:"]
|
||||
comment.extend(matches)
|
||||
ret["comment"] = "\n- ".join(comment)
|
||||
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
|
|
@ -2,9 +2,9 @@
|
|||
Installing of Windows features using DISM
|
||||
=========================================
|
||||
|
||||
Install windows features/capabilties with DISM
|
||||
Install Windows features, capabilities, and packages with DISM
|
||||
|
||||
.. code-block:: yaml
|
||||
.. code-block:: bash
|
||||
|
||||
Language.Basic~~~en-US~0.0.1.0:
|
||||
dism.capability_installed
|
||||
|
@ -40,18 +40,28 @@ def capability_installed(
|
|||
Install a DISM capability
|
||||
|
||||
Args:
|
||||
name (str): The capability to install
|
||||
source (str): The optional source of the capability
|
||||
limit_access (bool): Prevent DISM from contacting Windows Update for
|
||||
online images
|
||||
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 install
|
||||
|
||||
name (str):
|
||||
The capability to install
|
||||
|
||||
source (str):
|
||||
The optional source of the capability
|
||||
|
||||
limit_access (bool):
|
||||
Prevent DISM from contacting Windows Update for online images
|
||||
|
||||
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 install
|
||||
|
||||
Example:
|
||||
|
||||
Run ``dism.available_capabilities`` to get a list of available
|
||||
capabilities. This will help you get the proper name to use.
|
||||
capabilities. This will help you get the proper name to use
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
|
@ -64,11 +74,11 @@ def capability_installed(
|
|||
old = __salt__["dism.installed_capabilities"]()
|
||||
|
||||
if name in old:
|
||||
ret["comment"] = "The capability {} is already installed".format(name)
|
||||
ret["comment"] = f"The capability {name} is already installed"
|
||||
return ret
|
||||
|
||||
if __opts__["test"]:
|
||||
ret["changes"]["capability"] = "{} will be installed".format(name)
|
||||
ret["changes"]["capability"] = f"{name} will be installed"
|
||||
ret["result"] = None
|
||||
return ret
|
||||
|
||||
|
@ -76,14 +86,14 @@ def capability_installed(
|
|||
status = __salt__["dism.add_capability"](name, source, limit_access, image, restart)
|
||||
|
||||
if status["retcode"] not in [0, 1641, 3010]:
|
||||
ret["comment"] = "Failed to install {}: {}".format(name, status["stdout"])
|
||||
ret["comment"] = f'Failed to install {name}: {status["stdout"]}'
|
||||
ret["result"] = False
|
||||
|
||||
new = __salt__["dism.installed_capabilities"]()
|
||||
changes = salt.utils.data.compare_lists(old, new)
|
||||
|
||||
if changes:
|
||||
ret["comment"] = "Installed {}".format(name)
|
||||
ret["comment"] = f"Installed {name}"
|
||||
ret["changes"] = status
|
||||
ret["changes"]["capability"] = changes
|
||||
|
||||
|
@ -95,16 +105,22 @@ def capability_removed(name, image=None, restart=False):
|
|||
Uninstall a DISM capability
|
||||
|
||||
Args:
|
||||
name (str): The capability to uninstall
|
||||
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
|
||||
uninstall
|
||||
|
||||
name (str):
|
||||
The capability to uninstall
|
||||
|
||||
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 uninstall
|
||||
|
||||
Example:
|
||||
|
||||
Run ``dism.installed_capabilities`` to get a list of installed
|
||||
capabilities. This will help you get the proper name to use.
|
||||
capabilities. This will help you get the proper name to use
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
|
@ -117,11 +133,11 @@ def capability_removed(name, image=None, restart=False):
|
|||
old = __salt__["dism.installed_capabilities"]()
|
||||
|
||||
if name not in old:
|
||||
ret["comment"] = "The capability {} is already removed".format(name)
|
||||
ret["comment"] = f"The capability {name} is already removed"
|
||||
return ret
|
||||
|
||||
if __opts__["test"]:
|
||||
ret["changes"]["capability"] = "{} will be removed".format(name)
|
||||
ret["changes"]["capability"] = f"{name} will be removed"
|
||||
ret["result"] = None
|
||||
return ret
|
||||
|
||||
|
@ -129,14 +145,14 @@ def capability_removed(name, image=None, restart=False):
|
|||
status = __salt__["dism.remove_capability"](name, image, restart)
|
||||
|
||||
if status["retcode"] not in [0, 1641, 3010]:
|
||||
ret["comment"] = "Failed to remove {}: {}".format(name, status["stdout"])
|
||||
ret["comment"] = f'Failed to remove {name}: {status["stdout"]}'
|
||||
ret["result"] = False
|
||||
|
||||
new = __salt__["dism.installed_capabilities"]()
|
||||
changes = salt.utils.data.compare_lists(old, new)
|
||||
|
||||
if changes:
|
||||
ret["comment"] = "Removed {}".format(name)
|
||||
ret["comment"] = f"Removed {name}"
|
||||
ret["changes"] = status
|
||||
ret["changes"]["capability"] = changes
|
||||
|
||||
|
@ -156,23 +172,36 @@ def feature_installed(
|
|||
Install a DISM feature
|
||||
|
||||
Args:
|
||||
name (str): The feature in which to install
|
||||
package (Optional[str]): The parent package for the feature. You do not
|
||||
have to specify the package if it is the Windows Foundation Package.
|
||||
Otherwise, use package to specify the parent package of the feature
|
||||
source (str): The optional source of the feature
|
||||
limit_access (bool): Prevent DISM from contacting Windows Update for
|
||||
online images
|
||||
enable_parent (Optional[bool]): True will enable all parent features of
|
||||
the specified feature
|
||||
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 install
|
||||
|
||||
name (str):
|
||||
The feature in which to install
|
||||
|
||||
package (Optional[str]):
|
||||
The parent package for the feature. You do not have to specify the
|
||||
package if it is the Windows Foundation Package. Otherwise, use
|
||||
package to specify the parent package of the feature
|
||||
|
||||
source (str):
|
||||
The optional source of the feature
|
||||
|
||||
limit_access (bool):
|
||||
Prevent DISM from contacting Windows Update for online images
|
||||
|
||||
enable_parent (Optional[bool]):
|
||||
``True`` will enable all parent features of the specified feature
|
||||
|
||||
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 install
|
||||
|
||||
Example:
|
||||
|
||||
Run ``dism.available_features`` to get a list of available features.
|
||||
This will help you get the proper name to use.
|
||||
This will help you get the proper name to use
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
|
@ -185,11 +214,11 @@ def feature_installed(
|
|||
old = __salt__["dism.installed_features"]()
|
||||
|
||||
if name in old:
|
||||
ret["comment"] = "The feature {} is already installed".format(name)
|
||||
ret["comment"] = f"The feature {name} is already installed"
|
||||
return ret
|
||||
|
||||
if __opts__["test"]:
|
||||
ret["changes"]["feature"] = "{} will be installed".format(name)
|
||||
ret["changes"]["feature"] = f"{name} will be installed"
|
||||
ret["result"] = None
|
||||
return ret
|
||||
|
||||
|
@ -199,14 +228,14 @@ def feature_installed(
|
|||
)
|
||||
|
||||
if status["retcode"] not in [0, 1641, 3010]:
|
||||
ret["comment"] = "Failed to install {}: {}".format(name, status["stdout"])
|
||||
ret["comment"] = f'Failed to install {name}: {status["stdout"]}'
|
||||
ret["result"] = False
|
||||
|
||||
new = __salt__["dism.installed_features"]()
|
||||
changes = salt.utils.data.compare_lists(old, new)
|
||||
|
||||
if changes:
|
||||
ret["comment"] = "Installed {}".format(name)
|
||||
ret["comment"] = f"Installed {name}"
|
||||
ret["changes"] = status
|
||||
ret["changes"]["feature"] = changes
|
||||
|
||||
|
@ -218,18 +247,26 @@ def feature_removed(name, remove_payload=False, image=None, restart=False):
|
|||
Disables a feature.
|
||||
|
||||
Args:
|
||||
name (str): The feature to disable
|
||||
remove_payload (Optional[bool]): Remove the feature's payload. Must
|
||||
supply source when enabling in the future.
|
||||
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
|
||||
uninstall
|
||||
|
||||
name (str):
|
||||
The feature to disable
|
||||
|
||||
remove_payload (Optional[bool]):
|
||||
Remove the feature's payload. Must supply source when enabling in
|
||||
the future.
|
||||
|
||||
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 uninstall
|
||||
|
||||
Example:
|
||||
|
||||
Run ``dism.installed_features`` to get a list of installed features.
|
||||
This will help you get the proper name to use.
|
||||
This will help you get the proper name to use
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
|
@ -243,11 +280,11 @@ def feature_removed(name, remove_payload=False, image=None, restart=False):
|
|||
old = __salt__["dism.installed_features"]()
|
||||
|
||||
if name not in old:
|
||||
ret["comment"] = "The feature {} is already removed".format(name)
|
||||
ret["comment"] = f"The feature {name} is already removed"
|
||||
return ret
|
||||
|
||||
if __opts__["test"]:
|
||||
ret["changes"]["feature"] = "{} will be removed".format(name)
|
||||
ret["changes"]["feature"] = f"{name} will be removed"
|
||||
ret["result"] = None
|
||||
return ret
|
||||
|
||||
|
@ -255,14 +292,14 @@ def feature_removed(name, remove_payload=False, image=None, restart=False):
|
|||
status = __salt__["dism.remove_feature"](name, remove_payload, image, restart)
|
||||
|
||||
if status["retcode"] not in [0, 1641, 3010]:
|
||||
ret["comment"] = "Failed to remove {}: {}".format(name, status["stdout"])
|
||||
ret["comment"] = f'Failed to remove {name}: {status["stdout"]}'
|
||||
ret["result"] = False
|
||||
|
||||
new = __salt__["dism.installed_features"]()
|
||||
changes = salt.utils.data.compare_lists(old, new)
|
||||
|
||||
if changes:
|
||||
ret["comment"] = "Removed {}".format(name)
|
||||
ret["comment"] = f"Removed {name}"
|
||||
ret["changes"] = status
|
||||
ret["changes"]["feature"] = changes
|
||||
|
||||
|
@ -276,16 +313,24 @@ def package_installed(
|
|||
Install a package.
|
||||
|
||||
Args:
|
||||
name (str): The package to install. Can be a .cab file, a .msu file,
|
||||
or a folder
|
||||
ignore_check (Optional[bool]): Skip installation of the package if the
|
||||
applicability checks fail
|
||||
prevent_pending (Optional[bool]): Skip the installation of the package
|
||||
if there are pending online actions
|
||||
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 install
|
||||
|
||||
name (str):
|
||||
The package to install. Can be a .cab file, a .msu file, or a folder
|
||||
|
||||
ignore_check (Optional[bool]):
|
||||
Skip installation of the package if the applicability checks fail
|
||||
|
||||
prevent_pending (Optional[bool]):
|
||||
Skip the installation of the package if there are pending online
|
||||
actions
|
||||
|
||||
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 install
|
||||
|
||||
Example:
|
||||
|
||||
|
@ -303,7 +348,7 @@ def package_installed(
|
|||
ret["result"] = None
|
||||
else:
|
||||
ret["result"] = False
|
||||
ret["comment"] = "Package path {} does not exist".format(name)
|
||||
ret["comment"] = f"Package path {name} does not exist"
|
||||
return ret
|
||||
|
||||
old = __salt__["dism.installed_packages"]()
|
||||
|
@ -312,13 +357,14 @@ def package_installed(
|
|||
package_info = __salt__["dism.package_info"](name)
|
||||
|
||||
if package_info["Package Identity"] in old:
|
||||
ret["comment"] = "The package {} is already installed: {}".format(
|
||||
name, package_info["Package Identity"]
|
||||
ret["comment"] = (
|
||||
f"The package {name} is already installed: "
|
||||
f'{package_info["Package Identity"]}'
|
||||
)
|
||||
return ret
|
||||
|
||||
if __opts__["test"]:
|
||||
ret["changes"]["package"] = "{} will be installed".format(name)
|
||||
ret["changes"]["package"] = f"{name} will be installed"
|
||||
ret["result"] = None
|
||||
return ret
|
||||
|
||||
|
@ -328,14 +374,93 @@ def package_installed(
|
|||
)
|
||||
|
||||
if status["retcode"] not in [0, 1641, 3010]:
|
||||
ret["comment"] = "Failed to install {}: {}".format(name, status["stdout"])
|
||||
ret["comment"] = f'Failed to install {name}: {status["stdout"]}'
|
||||
ret["result"] = False
|
||||
|
||||
new = __salt__["dism.installed_packages"]()
|
||||
changes = salt.utils.data.compare_lists(old, new)
|
||||
|
||||
if changes:
|
||||
ret["comment"] = "Installed {}".format(name)
|
||||
ret["comment"] = f"Installed {name}"
|
||||
ret["changes"] = status
|
||||
ret["changes"]["package"] = changes
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
def provisioned_package_installed(name, image=None, restart=False):
|
||||
"""
|
||||
Provision a package on a Windows image.
|
||||
|
||||
.. versionadded:: 3007.0
|
||||
|
||||
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
|
||||
|
||||
|
@ -347,15 +472,20 @@ def package_removed(name, image=None, restart=False):
|
|||
Uninstall a package
|
||||
|
||||
Args:
|
||||
name (str): The full path to the package. Can be either a .cab file or a
|
||||
folder. Should point to the original source of the package, not to
|
||||
where the file is installed. This can also be the name of a package as listed in
|
||||
``dism.installed_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
|
||||
uninstall
|
||||
|
||||
name (str):
|
||||
The full path to the package. Can be either a .cab file or a folder.
|
||||
Should point to the original source of the package, not to where the
|
||||
file is installed. This can also be the name of a package as listed
|
||||
in ``dism.installed_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 uninstall
|
||||
|
||||
Example:
|
||||
|
||||
|
@ -379,7 +509,7 @@ def package_removed(name, image=None, restart=False):
|
|||
ret["result"] = None
|
||||
else:
|
||||
ret["result"] = False
|
||||
ret["comment"] = "Package path {} does not exist".format(name)
|
||||
ret["comment"] = f"Package path {name} does not exist"
|
||||
return ret
|
||||
|
||||
old = __salt__["dism.installed_packages"]()
|
||||
|
@ -393,11 +523,11 @@ def package_removed(name, image=None, restart=False):
|
|||
"Package Identity" not in package_info
|
||||
or package_info["Package Identity"] not in old
|
||||
):
|
||||
ret["comment"] = "The package {} is already removed".format(name)
|
||||
ret["comment"] = f"The package {name} is already removed"
|
||||
return ret
|
||||
|
||||
if __opts__["test"]:
|
||||
ret["changes"]["package"] = "{} will be removed".format(name)
|
||||
ret["changes"]["package"] = f"{name} will be removed"
|
||||
ret["result"] = None
|
||||
return ret
|
||||
|
||||
|
@ -405,14 +535,14 @@ def package_removed(name, image=None, restart=False):
|
|||
status = __salt__["dism.remove_package"](name, image, restart)
|
||||
|
||||
if status["retcode"] not in [0, 1641, 3010]:
|
||||
ret["comment"] = "Failed to remove {}: {}".format(name, status["stdout"])
|
||||
ret["comment"] = f'Failed to remove {name}: {status["stdout"]}'
|
||||
ret["result"] = False
|
||||
|
||||
new = __salt__["dism.installed_packages"]()
|
||||
changes = salt.utils.data.compare_lists(old, new)
|
||||
|
||||
if changes:
|
||||
ret["comment"] = "Removed {}".format(name)
|
||||
ret["comment"] = f"Removed {name}"
|
||||
ret["changes"] = status
|
||||
ret["changes"]["package"] = changes
|
||||
|
||||
|
@ -426,13 +556,17 @@ def kb_removed(name, image=None, restart=False):
|
|||
.. versionadded:: 3006.0
|
||||
|
||||
Args:
|
||||
name (str): The name of the KB. Can be with or without the KB at the
|
||||
beginning.
|
||||
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
|
||||
uninstall
|
||||
|
||||
name (str):
|
||||
The name of the KB. Can be with or without the KB at the beginning
|
||||
|
||||
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 uninstall
|
||||
|
||||
Example:
|
||||
|
||||
|
@ -454,11 +588,11 @@ def kb_removed(name, image=None, restart=False):
|
|||
|
||||
# If pkg_name is None, the package is not installed
|
||||
if pkg_name is None:
|
||||
ret["comment"] = "{} is not installed".format(name)
|
||||
ret["comment"] = f"{name} is not installed"
|
||||
return ret
|
||||
|
||||
if __opts__["test"]:
|
||||
ret["changes"]["package"] = "{} will be removed".format(name)
|
||||
ret["changes"]["package"] = f"{name} will be removed"
|
||||
ret["result"] = None
|
||||
return ret
|
||||
|
||||
|
@ -469,7 +603,7 @@ def kb_removed(name, image=None, restart=False):
|
|||
status = __salt__["dism.remove_kb"](kb=name, image=image, restart=restart)
|
||||
|
||||
if status["retcode"] not in [0, 1641, 3010]:
|
||||
ret["comment"] = "Failed to remove {}: {}".format(name, status["stdout"])
|
||||
ret["comment"] = f'Failed to remove {name}: {status["stdout"]}'
|
||||
ret["result"] = False
|
||||
return ret
|
||||
|
||||
|
@ -477,7 +611,7 @@ def kb_removed(name, image=None, restart=False):
|
|||
changes = salt.utils.data.compare_lists(old, new)
|
||||
|
||||
if changes:
|
||||
ret["comment"] = "Removed {}".format(name)
|
||||
ret["comment"] = f"Removed {name}"
|
||||
ret["changes"] = status
|
||||
ret["changes"]["package"] = changes
|
||||
|
||||
|
|
|
@ -28,7 +28,7 @@ def installed(
|
|||
restart=False,
|
||||
source=None,
|
||||
exclude=None,
|
||||
**kwargs
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
Install the windows feature. To install a single feature, use the ``name``
|
||||
|
@ -137,7 +137,7 @@ def installed(
|
|||
for feature in features:
|
||||
|
||||
if feature not in old:
|
||||
ret["changes"][feature] = "Will be installed recurse={}".format(recurse)
|
||||
ret["changes"][feature] = f"Will be installed recurse={recurse}"
|
||||
elif recurse:
|
||||
ret["changes"][feature] = "Already installed but might install sub-features"
|
||||
else:
|
||||
|
@ -168,13 +168,13 @@ def installed(
|
|||
for feature in status["Features"]:
|
||||
# Features that failed to install or be removed
|
||||
if not status["Features"][feature].get("Success", True):
|
||||
fail_feat.append("- {}".format(feature))
|
||||
fail_feat.append(f"- {feature}")
|
||||
# Features that installed
|
||||
elif "(exclude)" not in status["Features"][feature]["Message"]:
|
||||
new_feat.append("- {}".format(feature))
|
||||
new_feat.append(f"- {feature}")
|
||||
# Show items that were removed because they were part of `exclude`
|
||||
elif "(exclude)" in status["Features"][feature]["Message"]:
|
||||
rem_feat.append("- {}".format(feature))
|
||||
rem_feat.append(f"- {feature}")
|
||||
|
||||
if fail_feat:
|
||||
fail_feat.insert(0, "Failed to install the following:")
|
||||
|
@ -302,10 +302,10 @@ def removed(name, features=None, remove_payload=False, restart=False):
|
|||
# feature is already uninstalled
|
||||
if not status["Features"][feature].get("Success", True):
|
||||
# Show items that failed to uninstall
|
||||
fail_feat.append("- {}".format(feature))
|
||||
fail_feat.append(f"- {feature}")
|
||||
else:
|
||||
# Show items that uninstalled
|
||||
rem_feat.append("- {}".format(feature))
|
||||
rem_feat.append(f"- {feature}")
|
||||
|
||||
if fail_feat:
|
||||
fail_feat.insert(0, "Failed to remove the following:")
|
||||
|
|
63
salt/utils/win_pwsh.py
Normal file
63
salt/utils/win_pwsh.py
Normal file
|
@ -0,0 +1,63 @@
|
|||
import salt.modules.cmdmod
|
||||
import salt.utils.json
|
||||
import salt.utils.platform
|
||||
from salt.exceptions import CommandExecutionError
|
||||
|
||||
__virtualname__ = "win_pwsh"
|
||||
|
||||
|
||||
def __virtual__():
|
||||
"""
|
||||
Only load if windows
|
||||
"""
|
||||
if not salt.utils.platform.is_windows():
|
||||
return False, "This utility will only run on Windows"
|
||||
|
||||
return __virtualname__
|
||||
|
||||
|
||||
def run_dict(cmd, cwd=None):
|
||||
"""
|
||||
Execute the powershell command and return the data as a dictionary
|
||||
|
||||
Args:
|
||||
|
||||
cmd (str): The powershell command to run
|
||||
|
||||
cwd (str): The current working directory
|
||||
|
||||
Returns:
|
||||
dict: A dictionary containing the output of the powershell command
|
||||
|
||||
Raises:
|
||||
CommandExecutionError:
|
||||
If an error is encountered or the command does not complete
|
||||
successfully
|
||||
"""
|
||||
if "convertto-json" not in cmd.lower():
|
||||
cmd = f"{cmd} | ConvertTo-Json"
|
||||
if "progresspreference" not in cmd.lower():
|
||||
cmd = f"$ProgressPreference = 'SilentlyContinue'; {cmd}"
|
||||
ret = salt.modules.cmdmod.run_all(cmd=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(f"Issue executing PowerShell 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
|
216
tests/pytests/unit/modules/test_win_appx.py
Normal file
216
tests/pytests/unit/modules/test_win_appx.py
Normal file
|
@ -0,0 +1,216 @@
|
|||
import pytest
|
||||
|
||||
import salt.modules.win_appx as win_appx
|
||||
from tests.support.mock import MagicMock, patch
|
||||
|
||||
pytestmark = [
|
||||
pytest.mark.windows_whitelisted,
|
||||
pytest.mark.skip_unless_on_windows,
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def configure_loader_modules():
|
||||
return {win_appx: {}}
|
||||
|
||||
|
||||
def test__pkg_list_empty():
|
||||
assert win_appx._pkg_list("") is None
|
||||
|
||||
|
||||
def test__pkg_list_single():
|
||||
raw = {
|
||||
"Name": "MicrosoftTeams",
|
||||
"Version": "22042.702.1226.2352",
|
||||
"PackageFullName": "MicrosoftTeams_22042.702.1226.2352_x64__8wekyb3d8bbwe",
|
||||
"PackageFamilyName": "MicrosoftTeams_8wekyb3d8bbwe",
|
||||
}
|
||||
assert win_appx._pkg_list(raw=raw) == ["MicrosoftTeams"]
|
||||
|
||||
|
||||
def test__pkg_list_multiple():
|
||||
raw = [
|
||||
{
|
||||
"Name": "MicrosoftTeams",
|
||||
"Version": "22042.702.1226.2352",
|
||||
"PackageFullName": "MicrosoftTeams_22042.702.1226.2352_x64__8wekyb3d8bbwe",
|
||||
"PackageFamilyName": "MicrosoftTeams_8wekyb3d8bbwe",
|
||||
},
|
||||
{
|
||||
"Name": "Microsoft.BingWeather",
|
||||
"Version": "4.53.51361.0",
|
||||
"PackageFullName": "Microsoft.BingWeather_4.53.51361.0_x64__8wekyb3d8bbwe",
|
||||
"PackageFamilyName": "Microsoft.BingWeather_8wekyb3d8bbwe",
|
||||
},
|
||||
]
|
||||
assert win_appx._pkg_list(raw=raw) == ["MicrosoftTeams", "Microsoft.BingWeather"]
|
||||
|
||||
|
||||
def test__pkg_list_single_field():
|
||||
raw = {
|
||||
"Name": "MicrosoftTeams",
|
||||
"Version": "22042.702.1226.2352",
|
||||
"PackageFullName": "MicrosoftTeams_22042.702.1226.2352_x64__8wekyb3d8bbwe",
|
||||
"PackageFamilyName": "MicrosoftTeams_8wekyb3d8bbwe",
|
||||
}
|
||||
assert win_appx._pkg_list(raw=raw, field="PackageFamilyName") == [
|
||||
"MicrosoftTeams_8wekyb3d8bbwe"
|
||||
]
|
||||
|
||||
|
||||
def test__pkg_list_multiple_field():
|
||||
raw = [
|
||||
{
|
||||
"Name": "MicrosoftTeams",
|
||||
"Version": "22042.702.1226.2352",
|
||||
"PackageFullName": "MicrosoftTeams_22042.702.1226.2352_x64__8wekyb3d8bbwe",
|
||||
"PackageFamilyName": "MicrosoftTeams_8wekyb3d8bbwe",
|
||||
},
|
||||
{
|
||||
"Name": "Microsoft.BingWeather",
|
||||
"Version": "4.53.51361.0",
|
||||
"PackageFullName": "Microsoft.BingWeather_4.53.51361.0_x64__8wekyb3d8bbwe",
|
||||
"PackageFamilyName": "Microsoft.BingWeather_8wekyb3d8bbwe",
|
||||
},
|
||||
]
|
||||
assert win_appx._pkg_list(raw=raw, field="PackageFamilyName") == [
|
||||
"MicrosoftTeams_8wekyb3d8bbwe",
|
||||
"Microsoft.BingWeather_8wekyb3d8bbwe",
|
||||
]
|
||||
|
||||
|
||||
def test_list():
|
||||
mock_run_dict = MagicMock()
|
||||
with patch("salt.utils.win_pwsh.run_dict", mock_run_dict):
|
||||
win_appx.list_("*test*")
|
||||
cmd = [
|
||||
"Get-AppxPackage -AllUsers -PackageTypeFilter Bundle -Name *test*",
|
||||
'Where-Object {$_.name -notlike "Microsoft.WindowsStore*"}',
|
||||
"Where-Object -Property IsFramework -eq $false",
|
||||
"Where-Object -Property NonRemovable -eq $false",
|
||||
"Sort-Object Name",
|
||||
]
|
||||
mock_run_dict.assert_called_once_with(" | ".join(cmd))
|
||||
|
||||
|
||||
def test_list_field_none():
|
||||
mock_run_dict = MagicMock()
|
||||
with patch("salt.utils.win_pwsh.run_dict", mock_run_dict):
|
||||
win_appx.list_("*test*", field=None)
|
||||
cmd = [
|
||||
"Get-AppxPackage -AllUsers -PackageTypeFilter Bundle -Name *test*",
|
||||
'Where-Object {$_.name -notlike "Microsoft.WindowsStore*"}',
|
||||
"Where-Object -Property IsFramework -eq $false",
|
||||
"Where-Object -Property NonRemovable -eq $false",
|
||||
"Sort-Object Name",
|
||||
"Select Name, Version, PackageFullName, PackageFamilyName, IsBundle, IsFramework",
|
||||
]
|
||||
mock_run_dict.assert_called_once_with(" | ".join(cmd))
|
||||
|
||||
|
||||
def test_list_other_options_flipped():
|
||||
mock_run_dict = MagicMock()
|
||||
with patch("salt.utils.win_pwsh.run_dict", mock_run_dict):
|
||||
win_appx.list_("*test*", include_store=True, frameworks=True, bundles=False)
|
||||
cmd = [
|
||||
"Get-AppxPackage -AllUsers -Name *test*",
|
||||
"Where-Object -Property NonRemovable -eq $false",
|
||||
"Sort-Object Name",
|
||||
]
|
||||
mock_run_dict.assert_called_once_with(" | ".join(cmd))
|
||||
|
||||
|
||||
def test_remove():
|
||||
mock_run_dict = MagicMock()
|
||||
mock_list_return = {
|
||||
"Name": "Microsoft.BingWeather",
|
||||
"PackageFullName": "Microsoft.BingWeather_full_name",
|
||||
"IsBundle": True,
|
||||
}
|
||||
mock_list = MagicMock(return_value=mock_list_return)
|
||||
with patch("salt.utils.win_pwsh.run_dict", mock_run_dict), patch.object(
|
||||
win_appx, "list_", mock_list
|
||||
):
|
||||
assert win_appx.remove("*test*") is True
|
||||
cmd = "Remove-AppxPackage -AllUsers -Package Microsoft.BingWeather_full_name"
|
||||
mock_run_dict.assert_called_with(cmd)
|
||||
|
||||
|
||||
def test_remove_deprovision_only():
|
||||
mock_run_dict = MagicMock()
|
||||
mock_list_return = {
|
||||
"Name": "Microsoft.BingWeather",
|
||||
"PackageFullName": "Microsoft.BingWeather_full_name",
|
||||
"IsBundle": True,
|
||||
}
|
||||
mock_list = MagicMock(return_value=mock_list_return)
|
||||
with patch("salt.utils.win_pwsh.run_dict", mock_run_dict), patch.object(
|
||||
win_appx, "list_", mock_list
|
||||
):
|
||||
assert win_appx.remove("*test*", deprovision_only=True) is True
|
||||
cmd = "Remove-AppxProvisionedPackage -Online -PackageName Microsoft.BingWeather_full_name"
|
||||
mock_run_dict.assert_called_with(cmd)
|
||||
|
||||
|
||||
def test_remove_non_bundle():
|
||||
mock_run_dict = MagicMock()
|
||||
mock_list_return = [
|
||||
{
|
||||
"Name": "Microsoft.BingWeather",
|
||||
"PackageFullName": "Microsoft.BingWeather_non_bundle",
|
||||
"IsBundle": False,
|
||||
},
|
||||
{
|
||||
"Name": "Microsoft.BingWeather",
|
||||
"PackageFullName": "Microsoft.BingWeather_bundle",
|
||||
"IsBundle": True,
|
||||
},
|
||||
]
|
||||
mock_list = MagicMock(side_effect=mock_list_return)
|
||||
with patch("salt.utils.win_pwsh.run_dict", mock_run_dict), patch.object(
|
||||
win_appx, "list_", mock_list
|
||||
):
|
||||
assert win_appx.remove("*test*", deprovision_only=True) is True
|
||||
cmd = "Remove-AppxProvisionedPackage -Online -PackageName Microsoft.BingWeather_bundle"
|
||||
mock_run_dict.assert_called_with(cmd)
|
||||
|
||||
|
||||
def test_remove_not_found_empty_dict():
|
||||
mock_run_dict = MagicMock()
|
||||
mock_list_return = {}
|
||||
mock_list = MagicMock(return_value=mock_list_return)
|
||||
with patch("salt.utils.win_pwsh.run_dict", mock_run_dict), patch.object(
|
||||
win_appx, "list_", mock_list
|
||||
):
|
||||
assert win_appx.remove("*test*", deprovision_only=True) is None
|
||||
|
||||
|
||||
def test_remove_not_found_none():
|
||||
mock_run_dict = MagicMock()
|
||||
mock_list_return = None
|
||||
mock_list = MagicMock(return_value=mock_list_return)
|
||||
with patch("salt.utils.win_pwsh.run_dict", mock_run_dict), patch.object(
|
||||
win_appx, "list_", mock_list
|
||||
):
|
||||
assert win_appx.remove("*test*", deprovision_only=True) is None
|
||||
|
||||
|
||||
def test_list_deprovisioned():
|
||||
mock_list_keys = MagicMock(return_value=["Deprovisioned1", "Deprovisioned2"])
|
||||
with patch("salt.utils.win_reg.list_keys", mock_list_keys):
|
||||
expected = ["Deprovisioned1", "Deprovisioned2"]
|
||||
assert win_appx.list_deprovisioned() == expected
|
||||
|
||||
|
||||
def test_list_deprovisioned_query():
|
||||
mock_list_keys = MagicMock(return_value=["Deprovisioned1", "Deprovisioned2"])
|
||||
with patch("salt.utils.win_reg.list_keys", mock_list_keys):
|
||||
expected = ["Deprovisioned1"]
|
||||
assert win_appx.list_deprovisioned(query="*ed1*") == expected
|
||||
|
||||
|
||||
def test_install():
|
||||
mock_dism = MagicMock(return_value={"retcode": 0})
|
||||
with patch.dict(win_appx.__salt__, {"dism.add_provisioned_package": mock_dism}):
|
||||
assert win_appx.install("C:\\Test.appx") is True
|
||||
mock_dism.assert_called_once_with("C:\\Test.appx")
|
|
@ -46,9 +46,9 @@ def test_install():
|
|||
}
|
||||
|
||||
mock_reboot = MagicMock(return_value=True)
|
||||
with patch.object(
|
||||
win_servermanager, "_pshell_json", return_value=mock_out
|
||||
), patch.dict(win_servermanager.__salt__, {"system.reboot": mock_reboot}):
|
||||
with patch("salt.utils.win_pwsh.run_dict", return_value=mock_out), patch.dict(
|
||||
win_servermanager.__salt__, {"system.reboot": mock_reboot}
|
||||
):
|
||||
result = win_servermanager.install("XPS-Viewer")
|
||||
assert result == expected
|
||||
|
||||
|
@ -90,9 +90,9 @@ def test_install_restart():
|
|||
}
|
||||
|
||||
mock_reboot = MagicMock(return_value=True)
|
||||
with patch.object(
|
||||
win_servermanager, "_pshell_json", return_value=mock_out
|
||||
), patch.dict(win_servermanager.__salt__, {"system.reboot": mock_reboot}):
|
||||
with patch("salt.utils.win_pwsh.run_dict", return_value=mock_out), patch.dict(
|
||||
win_servermanager.__salt__, {"system.reboot": mock_reboot}
|
||||
):
|
||||
result = win_servermanager.install("XPS-Viewer", restart=True)
|
||||
mock_reboot.assert_called_once()
|
||||
assert result == expected
|
||||
|
|
245
tests/pytests/unit/modules/test_win_wusa.py
Normal file
245
tests/pytests/unit/modules/test_win_wusa.py
Normal file
|
@ -0,0 +1,245 @@
|
|||
"""
|
||||
Test the win_wusa execution module
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
import salt.modules.win_wusa as win_wusa
|
||||
from salt.exceptions import CommandExecutionError
|
||||
from tests.support.mock import MagicMock, patch
|
||||
|
||||
pytestmark = [
|
||||
pytest.mark.windows_whitelisted,
|
||||
pytest.mark.skip_unless_on_windows,
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def configure_loader_modules():
|
||||
return {win_wusa: {}}
|
||||
|
||||
|
||||
def test_is_installed_false():
|
||||
"""
|
||||
test is_installed function when the KB is not installed
|
||||
"""
|
||||
mock_retcode = MagicMock(return_value=1)
|
||||
with patch.dict(win_wusa.__salt__, {"cmd.retcode": mock_retcode}):
|
||||
assert win_wusa.is_installed("KB123456") is False
|
||||
|
||||
|
||||
def test_is_installed_true():
|
||||
"""
|
||||
test is_installed function when the KB is installed
|
||||
"""
|
||||
mock_retcode = MagicMock(return_value=0)
|
||||
with patch.dict(win_wusa.__salt__, {"cmd.retcode": mock_retcode}):
|
||||
assert win_wusa.is_installed("KB123456") is True
|
||||
|
||||
|
||||
def test_list():
|
||||
"""
|
||||
test list function
|
||||
"""
|
||||
ret = [{"HotFixID": "KB123456"}, {"HotFixID": "KB123457"}]
|
||||
mock_all = MagicMock(return_value=ret)
|
||||
with patch("salt.utils.win_pwsh.run_dict", mock_all):
|
||||
expected = ["KB123456", "KB123457"]
|
||||
returned = win_wusa.list_()
|
||||
assert returned == expected
|
||||
|
||||
|
||||
def test_install():
|
||||
"""
|
||||
test install function
|
||||
"""
|
||||
mock_retcode = MagicMock(return_value=0)
|
||||
path = "C:\\KB123456.msu"
|
||||
with patch.dict(win_wusa.__salt__, {"cmd.retcode": mock_retcode}):
|
||||
assert win_wusa.install(path) is True
|
||||
mock_retcode.assert_called_once_with(
|
||||
["wusa.exe", path, "/quiet", "/norestart"], ignore_retcode=True
|
||||
)
|
||||
|
||||
|
||||
def test_install_restart():
|
||||
"""
|
||||
test install function with restart=True
|
||||
"""
|
||||
mock_retcode = MagicMock(return_value=0)
|
||||
path = "C:\\KB123456.msu"
|
||||
with patch.dict(win_wusa.__salt__, {"cmd.retcode": mock_retcode}):
|
||||
assert win_wusa.install(path, restart=True) is True
|
||||
mock_retcode.assert_called_once_with(
|
||||
["wusa.exe", path, "/quiet", "/forcerestart"], ignore_retcode=True
|
||||
)
|
||||
|
||||
|
||||
def test_install_already_installed():
|
||||
"""
|
||||
test install function when KB already installed
|
||||
"""
|
||||
retcode = 2359302
|
||||
mock_retcode = MagicMock(return_value=retcode)
|
||||
path = "C:\\KB123456.msu"
|
||||
name = "KB123456.msu"
|
||||
with patch.dict(win_wusa.__salt__, {"cmd.retcode": mock_retcode}):
|
||||
with pytest.raises(CommandExecutionError) as excinfo:
|
||||
win_wusa.install(path)
|
||||
mock_retcode.assert_called_once_with(
|
||||
["wusa.exe", path, "/quiet", "/norestart"], ignore_retcode=True
|
||||
)
|
||||
assert (
|
||||
f"{name} is already installed. Additional info follows:\n\n{retcode}"
|
||||
== excinfo.value.message
|
||||
)
|
||||
|
||||
|
||||
def test_install_reboot_needed():
|
||||
"""
|
||||
test install function when KB need a reboot
|
||||
"""
|
||||
retcode = 3010
|
||||
mock_retcode = MagicMock(return_value=retcode)
|
||||
path = "C:\\KB123456.msu"
|
||||
name = "KB123456.msu"
|
||||
with patch.dict(win_wusa.__salt__, {"cmd.retcode": mock_retcode}):
|
||||
with pytest.raises(CommandExecutionError) as excinfo:
|
||||
win_wusa.install(path)
|
||||
mock_retcode.assert_called_once_with(
|
||||
["wusa.exe", path, "/quiet", "/norestart"], ignore_retcode=True
|
||||
)
|
||||
assert (
|
||||
f"{name} correctly installed but server reboot is needed to complete installation. Additional info follows:\n\n{retcode}"
|
||||
== excinfo.value.message
|
||||
)
|
||||
|
||||
|
||||
def test_install_error_87():
|
||||
"""
|
||||
test install function when error 87 returned
|
||||
"""
|
||||
retcode = 87
|
||||
mock_retcode = MagicMock(return_value=retcode)
|
||||
path = "C:\\KB123456.msu"
|
||||
with patch.dict(win_wusa.__salt__, {"cmd.retcode": mock_retcode}):
|
||||
with pytest.raises(CommandExecutionError) as excinfo:
|
||||
win_wusa.install(path)
|
||||
mock_retcode.assert_called_once_with(
|
||||
["wusa.exe", path, "/quiet", "/norestart"], ignore_retcode=True
|
||||
)
|
||||
assert (
|
||||
f"Unknown error. Additional info follows:\n\n{retcode}" == excinfo.value.message
|
||||
)
|
||||
|
||||
|
||||
def test_install_error_other():
|
||||
"""
|
||||
test install function on other unknown error
|
||||
"""
|
||||
mock_retcode = MagicMock(return_value=1234)
|
||||
path = "C:\\KB123456.msu"
|
||||
with patch.dict(win_wusa.__salt__, {"cmd.retcode": mock_retcode}):
|
||||
with pytest.raises(CommandExecutionError) as excinfo:
|
||||
win_wusa.install(path)
|
||||
mock_retcode.assert_called_once_with(
|
||||
["wusa.exe", path, "/quiet", "/norestart"], ignore_retcode=True
|
||||
)
|
||||
assert "Unknown error: 1234" == excinfo.value.message
|
||||
|
||||
|
||||
def test_uninstall_kb():
|
||||
"""
|
||||
test uninstall function passing kb name
|
||||
"""
|
||||
mock_retcode = MagicMock(return_value=0)
|
||||
kb = "KB123456"
|
||||
with patch.dict(win_wusa.__salt__, {"cmd.retcode": mock_retcode}), patch(
|
||||
"os.path.exists", MagicMock(return_value=False)
|
||||
):
|
||||
assert win_wusa.uninstall(kb) is True
|
||||
mock_retcode.assert_called_once_with(
|
||||
[
|
||||
"wusa.exe",
|
||||
"/uninstall",
|
||||
"/quiet",
|
||||
f"/kb:{kb[2:]}",
|
||||
"/norestart",
|
||||
],
|
||||
ignore_retcode=True,
|
||||
)
|
||||
|
||||
|
||||
def test_uninstall_path():
|
||||
"""
|
||||
test uninstall function passing full path to .msu file
|
||||
"""
|
||||
mock_retcode = MagicMock(return_value=0)
|
||||
path = "C:\\KB123456.msu"
|
||||
with patch.dict(win_wusa.__salt__, {"cmd.retcode": mock_retcode}), patch(
|
||||
"os.path.exists", MagicMock(return_value=True)
|
||||
):
|
||||
assert win_wusa.uninstall(path) is True
|
||||
mock_retcode.assert_called_once_with(
|
||||
["wusa.exe", "/uninstall", "/quiet", path, "/norestart"],
|
||||
ignore_retcode=True,
|
||||
)
|
||||
|
||||
|
||||
def test_uninstall_path_restart():
|
||||
"""
|
||||
test uninstall function with full path and restart=True
|
||||
"""
|
||||
mock_retcode = MagicMock(return_value=0)
|
||||
path = "C:\\KB123456.msu"
|
||||
with patch.dict(win_wusa.__salt__, {"cmd.retcode": mock_retcode}), patch(
|
||||
"os.path.exists", MagicMock(return_value=True)
|
||||
):
|
||||
assert win_wusa.uninstall(path, restart=True) is True
|
||||
mock_retcode.assert_called_once_with(
|
||||
["wusa.exe", "/uninstall", "/quiet", path, "/forcerestart"],
|
||||
ignore_retcode=True,
|
||||
)
|
||||
|
||||
|
||||
def test_uninstall_already_uninstalled():
|
||||
"""
|
||||
test uninstall function when KB already uninstalled
|
||||
"""
|
||||
retcode = 2359303
|
||||
mock_retcode = MagicMock(return_value=retcode)
|
||||
kb = "KB123456"
|
||||
with patch.dict(win_wusa.__salt__, {"cmd.retcode": mock_retcode}):
|
||||
with pytest.raises(CommandExecutionError) as excinfo:
|
||||
win_wusa.uninstall(kb)
|
||||
mock_retcode.assert_called_once_with(
|
||||
[
|
||||
"wusa.exe",
|
||||
"/uninstall",
|
||||
"/quiet",
|
||||
f"/kb:{kb[2:]}",
|
||||
"/norestart",
|
||||
],
|
||||
ignore_retcode=True,
|
||||
)
|
||||
assert (
|
||||
f"{kb} not installed. Additional info follows:\n\n{retcode}"
|
||||
== excinfo.value.message
|
||||
)
|
||||
|
||||
|
||||
def test_uninstall_path_error_other():
|
||||
"""
|
||||
test uninstall function with unknown error
|
||||
"""
|
||||
mock_retcode = MagicMock(return_value=1234)
|
||||
path = "C:\\KB123456.msu"
|
||||
with patch.dict(win_wusa.__salt__, {"cmd.retcode": mock_retcode}), patch(
|
||||
"os.path.exists", MagicMock(return_value=True)
|
||||
), pytest.raises(CommandExecutionError) as excinfo:
|
||||
win_wusa.uninstall(path)
|
||||
mock_retcode.assert_called_once_with(
|
||||
["wusa.exe", "/uninstall", "/quiet", path, "/norestart"],
|
||||
ignore_retcode=True,
|
||||
)
|
||||
assert "Unknown error: 1234" == excinfo.value.message
|
|
@ -354,7 +354,6 @@ def test_configurable_test_state_test():
|
|||
ret = test.configurable_test_state(mock_name)
|
||||
assert ret == mock_ret
|
||||
|
||||
with patch.dict(test.__opts__, {"test": True}):
|
||||
mock_ret = {
|
||||
"name": mock_name,
|
||||
"changes": mock_changes,
|
||||
|
@ -364,7 +363,6 @@ def test_configurable_test_state_test():
|
|||
ret = test.configurable_test_state(mock_name, comment=mock_comment)
|
||||
assert ret == mock_ret
|
||||
|
||||
with patch.dict(test.__opts__, {"test": True}):
|
||||
mock_ret = {
|
||||
"name": mock_name,
|
||||
"changes": mock_changes,
|
||||
|
@ -376,7 +374,6 @@ def test_configurable_test_state_test():
|
|||
)
|
||||
assert ret == mock_ret
|
||||
|
||||
with patch.dict(test.__opts__, {"test": True}):
|
||||
mock_ret = {
|
||||
"name": mock_name,
|
||||
"changes": {},
|
||||
|
@ -388,6 +385,30 @@ def test_configurable_test_state_test():
|
|||
)
|
||||
assert ret == mock_ret
|
||||
|
||||
# normal test mode operation doesn't allow False result
|
||||
mock_ret = {
|
||||
"name": mock_name,
|
||||
"changes": {},
|
||||
"result": True,
|
||||
"comment": "This is a test",
|
||||
}
|
||||
ret = test.configurable_test_state(
|
||||
mock_name, changes=False, result=False, allow_test_mode_failure=False
|
||||
)
|
||||
assert ret == mock_ret
|
||||
|
||||
# test allow False result in test mode
|
||||
mock_ret = {
|
||||
"name": mock_name,
|
||||
"changes": {},
|
||||
"result": False,
|
||||
"comment": "This is a test",
|
||||
}
|
||||
ret = test.configurable_test_state(
|
||||
mock_name, changes=False, result=False, allow_test_mode_failure=True
|
||||
)
|
||||
assert ret == mock_ret
|
||||
|
||||
|
||||
def test_mod_watch():
|
||||
"""
|
||||
|
|
86
tests/pytests/unit/states/test_win_appx.py
Normal file
86
tests/pytests/unit/states/test_win_appx.py
Normal file
|
@ -0,0 +1,86 @@
|
|||
import pytest
|
||||
|
||||
import salt.states.win_appx as win_appx
|
||||
from tests.support.mock import MagicMock, patch
|
||||
|
||||
pytestmark = [
|
||||
pytest.mark.windows_whitelisted,
|
||||
pytest.mark.skip_unless_on_windows,
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def configure_loader_modules():
|
||||
return {win_appx: {}}
|
||||
|
||||
|
||||
def test_absent_missing():
|
||||
expected = {
|
||||
"comment": "No apps found matching query: *candy*",
|
||||
"changes": {},
|
||||
"name": "remove_candy",
|
||||
"result": True,
|
||||
}
|
||||
mock_list = MagicMock(return_value=["package1", "package2"])
|
||||
with patch.dict(win_appx.__salt__, {"appx.list": mock_list}):
|
||||
result = win_appx.absent("remove_candy", "*candy*")
|
||||
assert result == expected
|
||||
|
||||
|
||||
def test_absent_test_true():
|
||||
expected = {
|
||||
"comment": "The following apps will be removed:\n- king.com.CandyCrush",
|
||||
"changes": {},
|
||||
"name": "remove_candy",
|
||||
"result": None,
|
||||
}
|
||||
mock_list = MagicMock(return_value=["package1", "king.com.CandyCrush"])
|
||||
with patch.dict(win_appx.__salt__, {"appx.list": mock_list}):
|
||||
with patch.dict(win_appx.__opts__, {"test": True}):
|
||||
result = win_appx.absent("remove_candy", "*candy*")
|
||||
assert result == expected
|
||||
|
||||
|
||||
def test_absent_missing_after_test():
|
||||
expected = {
|
||||
"comment": "No apps found matching query: *candy*",
|
||||
"changes": {},
|
||||
"name": "remove_candy",
|
||||
"result": False,
|
||||
}
|
||||
mock_list = MagicMock(return_value=["package1", "king.com.CandyCrush"])
|
||||
with patch.dict(
|
||||
win_appx.__salt__,
|
||||
{
|
||||
"appx.list": mock_list,
|
||||
"appx.remove": MagicMock(return_value=None),
|
||||
},
|
||||
):
|
||||
with patch.dict(win_appx.__opts__, {"test": False}):
|
||||
result = win_appx.absent("remove_candy", "*candy*")
|
||||
assert result == expected
|
||||
|
||||
|
||||
def test_absent():
|
||||
expected = {
|
||||
"comment": "Removed apps matching query: *candy*",
|
||||
"changes": {"old": ["king.com.CandyCrush"]},
|
||||
"name": "remove_candy",
|
||||
"result": True,
|
||||
}
|
||||
mock_list = MagicMock(
|
||||
side_effect=(
|
||||
["package1", "king.com.CandyCrush"],
|
||||
["package1"],
|
||||
),
|
||||
)
|
||||
with patch.dict(
|
||||
win_appx.__salt__,
|
||||
{
|
||||
"appx.list": mock_list,
|
||||
"appx.remove": MagicMock(return_value=True),
|
||||
},
|
||||
):
|
||||
with patch.dict(win_appx.__opts__, {"test": False}):
|
||||
result = win_appx.absent("remove_candy", "*candy*")
|
||||
assert result == expected
|
106
tests/pytests/unit/utils/test_win_pwsh.py
Normal file
106
tests/pytests/unit/utils/test_win_pwsh.py
Normal file
|
@ -0,0 +1,106 @@
|
|||
import pytest
|
||||
|
||||
import salt.utils.win_pwsh as win_pwsh
|
||||
from salt.exceptions import CommandExecutionError
|
||||
from tests.support.mock import MagicMock, patch
|
||||
|
||||
pytestmark = [
|
||||
pytest.mark.windows_whitelisted,
|
||||
pytest.mark.skip_unless_on_windows,
|
||||
]
|
||||
|
||||
|
||||
def test_run_dict():
|
||||
"""
|
||||
Tests the run_dict function
|
||||
"""
|
||||
result = win_pwsh.run_dict("Get-Item C:\\Windows")
|
||||
assert result["Name"] == "Windows"
|
||||
assert result["FullName"] == "C:\\Windows"
|
||||
|
||||
|
||||
def test_run_dict_json_string():
|
||||
"""
|
||||
Tests the run_dict function with json string
|
||||
"""
|
||||
ret = {
|
||||
"pid": 1,
|
||||
"retcode": 0,
|
||||
"stderr": "",
|
||||
"stdout": '[{"HotFixID": "KB123456"}, {"HotFixID": "KB123457"}]',
|
||||
}
|
||||
mock_all = MagicMock(return_value=ret)
|
||||
with patch("salt.modules.cmdmod.run_all", mock_all):
|
||||
result = win_pwsh.run_dict("Junk-Command")
|
||||
assert result == [{"HotFixID": "KB123456"}, {"HotFixID": "KB123457"}]
|
||||
|
||||
|
||||
def test_run_dict_empty_return():
|
||||
"""
|
||||
Tests the run_dict function with json string
|
||||
"""
|
||||
ret = {
|
||||
"pid": 1,
|
||||
"retcode": 0,
|
||||
"stderr": "",
|
||||
"stdout": "",
|
||||
}
|
||||
mock_all = MagicMock(return_value=ret)
|
||||
with patch("salt.modules.cmdmod.run_all", mock_all):
|
||||
result = win_pwsh.run_dict("Junk-Command")
|
||||
assert result == {}
|
||||
|
||||
|
||||
def test_run_dict_stderr():
|
||||
ret = {
|
||||
"pid": 1,
|
||||
"retcode": 1,
|
||||
"stderr": "This is an error",
|
||||
"stdout": "",
|
||||
}
|
||||
mock_all = MagicMock(return_value=ret)
|
||||
with patch("salt.modules.cmdmod.run_all", mock_all):
|
||||
with pytest.raises(CommandExecutionError) as exc_info:
|
||||
win_pwsh.run_dict("Junk-Command")
|
||||
assert "This is an error" in exc_info.value.message
|
||||
|
||||
|
||||
def test_run_dict_missing_retcode():
|
||||
ret = {
|
||||
"pid": 1,
|
||||
"stderr": "",
|
||||
"stdout": "",
|
||||
}
|
||||
mock_all = MagicMock(return_value=ret)
|
||||
with patch("salt.modules.cmdmod.run_all", mock_all):
|
||||
with pytest.raises(CommandExecutionError) as exc_info:
|
||||
win_pwsh.run_dict("Junk-Command")
|
||||
assert "Issue executing PowerShell" in exc_info.value.message
|
||||
|
||||
|
||||
def test_run_dict_retcode_not_zero():
|
||||
ret = {
|
||||
"pid": 1,
|
||||
"retcode": 1,
|
||||
"stderr": "",
|
||||
"stdout": "",
|
||||
}
|
||||
mock_all = MagicMock(return_value=ret)
|
||||
with patch("salt.modules.cmdmod.run_all", mock_all):
|
||||
with pytest.raises(CommandExecutionError) as exc_info:
|
||||
win_pwsh.run_dict("Junk-Command")
|
||||
assert "Issue executing PowerShell" in exc_info.value.message
|
||||
|
||||
|
||||
def test_run_dict_invalid_json():
|
||||
ret = {
|
||||
"pid": 1,
|
||||
"retcode": 0,
|
||||
"stderr": "",
|
||||
"stdout": "Invalid Json",
|
||||
}
|
||||
mock_all = MagicMock(return_value=ret)
|
||||
with patch("salt.modules.cmdmod.run_all", mock_all):
|
||||
with pytest.raises(CommandExecutionError) as exc_info:
|
||||
win_pwsh.run_dict("Junk-Command")
|
||||
assert "No JSON results from PowerShell" in exc_info.value.message
|
|
@ -1,242 +0,0 @@
|
|||
"""
|
||||
Test the win_wusa execution module
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
import salt.modules.win_wusa as win_wusa
|
||||
from salt.exceptions import CommandExecutionError
|
||||
from tests.support.mixins import LoaderModuleMockMixin
|
||||
from tests.support.mock import MagicMock, patch
|
||||
from tests.support.unit import TestCase
|
||||
|
||||
|
||||
@pytest.mark.skip_unless_on_windows
|
||||
class WinWusaTestCase(TestCase, LoaderModuleMockMixin):
|
||||
"""
|
||||
test the functions in the win_wusa execution module
|
||||
"""
|
||||
|
||||
def setup_loader_modules(self):
|
||||
return {win_wusa: {}}
|
||||
|
||||
def test_is_installed_false(self):
|
||||
"""
|
||||
test is_installed function when the KB is not installed
|
||||
"""
|
||||
mock_retcode = MagicMock(return_value=1)
|
||||
with patch.dict(win_wusa.__salt__, {"cmd.retcode": mock_retcode}):
|
||||
self.assertFalse(win_wusa.is_installed("KB123456"))
|
||||
|
||||
def test_is_installed_true(self):
|
||||
"""
|
||||
test is_installed function when the KB is installed
|
||||
"""
|
||||
mock_retcode = MagicMock(return_value=0)
|
||||
with patch.dict(win_wusa.__salt__, {"cmd.retcode": mock_retcode}):
|
||||
self.assertTrue(win_wusa.is_installed("KB123456"))
|
||||
|
||||
def test_list(self):
|
||||
"""
|
||||
test list function
|
||||
"""
|
||||
ret = {
|
||||
"pid": 1,
|
||||
"retcode": 0,
|
||||
"stderr": "",
|
||||
"stdout": '[{"HotFixID": "KB123456"}, {"HotFixID": "KB123457"}]',
|
||||
}
|
||||
mock_all = MagicMock(return_value=ret)
|
||||
with patch.dict(win_wusa.__salt__, {"cmd.run_all": mock_all}):
|
||||
expected = ["KB123456", "KB123457"]
|
||||
returned = win_wusa.list()
|
||||
self.assertListEqual(expected, returned)
|
||||
|
||||
def test_install(self):
|
||||
"""
|
||||
test install function
|
||||
"""
|
||||
mock_retcode = MagicMock(return_value=0)
|
||||
path = "C:\\KB123456.msu"
|
||||
with patch.dict(win_wusa.__salt__, {"cmd.retcode": mock_retcode}):
|
||||
self.assertTrue(win_wusa.install(path))
|
||||
mock_retcode.assert_called_once_with(
|
||||
["wusa.exe", path, "/quiet", "/norestart"], ignore_retcode=True
|
||||
)
|
||||
|
||||
def test_install_restart(self):
|
||||
"""
|
||||
test install function with restart=True
|
||||
"""
|
||||
mock_retcode = MagicMock(return_value=0)
|
||||
path = "C:\\KB123456.msu"
|
||||
with patch.dict(win_wusa.__salt__, {"cmd.retcode": mock_retcode}):
|
||||
self.assertTrue(win_wusa.install(path, restart=True))
|
||||
mock_retcode.assert_called_once_with(
|
||||
["wusa.exe", path, "/quiet", "/forcerestart"], ignore_retcode=True
|
||||
)
|
||||
|
||||
def test_install_already_installed(self):
|
||||
"""
|
||||
test install function when KB already installed
|
||||
"""
|
||||
retcode = 2359302
|
||||
mock_retcode = MagicMock(return_value=retcode)
|
||||
path = "C:\\KB123456.msu"
|
||||
name = "KB123456.msu"
|
||||
with patch.dict(win_wusa.__salt__, {"cmd.retcode": mock_retcode}):
|
||||
with self.assertRaises(CommandExecutionError) as excinfo:
|
||||
win_wusa.install(path)
|
||||
mock_retcode.assert_called_once_with(
|
||||
["wusa.exe", path, "/quiet", "/norestart"], ignore_retcode=True
|
||||
)
|
||||
self.assertEqual(
|
||||
"{} is already installed. Additional info follows:\n\n{}".format(
|
||||
name, retcode
|
||||
),
|
||||
excinfo.exception.strerror,
|
||||
)
|
||||
|
||||
def test_install_reboot_needed(self):
|
||||
"""
|
||||
test install function when KB need a reboot
|
||||
"""
|
||||
retcode = 3010
|
||||
mock_retcode = MagicMock(return_value=retcode)
|
||||
path = "C:\\KB123456.msu"
|
||||
name = "KB123456.msu"
|
||||
with patch.dict(win_wusa.__salt__, {"cmd.retcode": mock_retcode}):
|
||||
with self.assertRaises(CommandExecutionError) as excinfo:
|
||||
win_wusa.install(path)
|
||||
mock_retcode.assert_called_once_with(
|
||||
["wusa.exe", path, "/quiet", "/norestart"], ignore_retcode=True
|
||||
)
|
||||
self.assertEqual(
|
||||
"{} correctly installed but server reboot is needed to complete"
|
||||
" installation. Additional info follows:\n\n{}".format(name, retcode),
|
||||
excinfo.exception.strerror,
|
||||
)
|
||||
|
||||
def test_install_error_87(self):
|
||||
"""
|
||||
test install function when error 87 returned
|
||||
"""
|
||||
retcode = 87
|
||||
mock_retcode = MagicMock(return_value=retcode)
|
||||
path = "C:\\KB123456.msu"
|
||||
with patch.dict(win_wusa.__salt__, {"cmd.retcode": mock_retcode}):
|
||||
with self.assertRaises(CommandExecutionError) as excinfo:
|
||||
win_wusa.install(path)
|
||||
mock_retcode.assert_called_once_with(
|
||||
["wusa.exe", path, "/quiet", "/norestart"], ignore_retcode=True
|
||||
)
|
||||
self.assertEqual(
|
||||
"Unknown error. Additional info follows:\n\n{}".format(retcode),
|
||||
excinfo.exception.strerror,
|
||||
)
|
||||
|
||||
def test_install_error_other(self):
|
||||
"""
|
||||
test install function on other unknown error
|
||||
"""
|
||||
mock_retcode = MagicMock(return_value=1234)
|
||||
path = "C:\\KB123456.msu"
|
||||
with patch.dict(win_wusa.__salt__, {"cmd.retcode": mock_retcode}):
|
||||
with self.assertRaises(CommandExecutionError) as excinfo:
|
||||
win_wusa.install(path)
|
||||
mock_retcode.assert_called_once_with(
|
||||
["wusa.exe", path, "/quiet", "/norestart"], ignore_retcode=True
|
||||
)
|
||||
self.assertEqual("Unknown error: 1234", excinfo.exception.strerror)
|
||||
|
||||
def test_uninstall_kb(self):
|
||||
"""
|
||||
test uninstall function passing kb name
|
||||
"""
|
||||
mock_retcode = MagicMock(return_value=0)
|
||||
kb = "KB123456"
|
||||
with patch.dict(win_wusa.__salt__, {"cmd.retcode": mock_retcode}), patch(
|
||||
"os.path.exists", MagicMock(return_value=False)
|
||||
):
|
||||
self.assertTrue(win_wusa.uninstall(kb))
|
||||
mock_retcode.assert_called_once_with(
|
||||
[
|
||||
"wusa.exe",
|
||||
"/uninstall",
|
||||
"/quiet",
|
||||
"/kb:{}".format(kb[2:]),
|
||||
"/norestart",
|
||||
],
|
||||
ignore_retcode=True,
|
||||
)
|
||||
|
||||
def test_uninstall_path(self):
|
||||
"""
|
||||
test uninstall function passing full path to .msu file
|
||||
"""
|
||||
mock_retcode = MagicMock(return_value=0)
|
||||
path = "C:\\KB123456.msu"
|
||||
with patch.dict(win_wusa.__salt__, {"cmd.retcode": mock_retcode}), patch(
|
||||
"os.path.exists", MagicMock(return_value=True)
|
||||
):
|
||||
self.assertTrue(win_wusa.uninstall(path))
|
||||
mock_retcode.assert_called_once_with(
|
||||
["wusa.exe", "/uninstall", "/quiet", path, "/norestart"],
|
||||
ignore_retcode=True,
|
||||
)
|
||||
|
||||
def test_uninstall_path_restart(self):
|
||||
"""
|
||||
test uninstall function with full path and restart=True
|
||||
"""
|
||||
mock_retcode = MagicMock(return_value=0)
|
||||
path = "C:\\KB123456.msu"
|
||||
with patch.dict(win_wusa.__salt__, {"cmd.retcode": mock_retcode}), patch(
|
||||
"os.path.exists", MagicMock(return_value=True)
|
||||
):
|
||||
self.assertTrue(win_wusa.uninstall(path, restart=True))
|
||||
mock_retcode.assert_called_once_with(
|
||||
["wusa.exe", "/uninstall", "/quiet", path, "/forcerestart"],
|
||||
ignore_retcode=True,
|
||||
)
|
||||
|
||||
def test_uninstall_already_uninstalled(self):
|
||||
"""
|
||||
test uninstall function when KB already uninstalled
|
||||
"""
|
||||
retcode = 2359303
|
||||
mock_retcode = MagicMock(return_value=retcode)
|
||||
kb = "KB123456"
|
||||
with patch.dict(win_wusa.__salt__, {"cmd.retcode": mock_retcode}):
|
||||
with self.assertRaises(CommandExecutionError) as excinfo:
|
||||
win_wusa.uninstall(kb)
|
||||
mock_retcode.assert_called_once_with(
|
||||
[
|
||||
"wusa.exe",
|
||||
"/uninstall",
|
||||
"/quiet",
|
||||
"/kb:{}".format(kb[2:]),
|
||||
"/norestart",
|
||||
],
|
||||
ignore_retcode=True,
|
||||
)
|
||||
self.assertEqual(
|
||||
"{} not installed. Additional info follows:\n\n{}".format(kb, retcode),
|
||||
excinfo.exception.strerror,
|
||||
)
|
||||
|
||||
def test_uninstall_path_error_other(self):
|
||||
"""
|
||||
test uninstall function with unknown error
|
||||
"""
|
||||
mock_retcode = MagicMock(return_value=1234)
|
||||
path = "C:\\KB123456.msu"
|
||||
with patch.dict(win_wusa.__salt__, {"cmd.retcode": mock_retcode}), patch(
|
||||
"os.path.exists", MagicMock(return_value=True)
|
||||
), self.assertRaises(CommandExecutionError) as excinfo:
|
||||
win_wusa.uninstall(path)
|
||||
mock_retcode.assert_called_once_with(
|
||||
["wusa.exe", "/uninstall", "/quiet", path, "/norestart"],
|
||||
ignore_retcode=True,
|
||||
)
|
||||
self.assertEqual("Unknown error: 1234", excinfo.exception.strerror)
|
Loading…
Add table
Reference in a new issue