File: //opt/saltstack/salt/lib/python3.10/site-packages/salt/modules/win_servermanager.py
"""
Manage Windows features via the ServerManager powershell module. Can list
available and installed roles/features. Can install and remove roles/features.
:maintainer: Shane Lee <slee@saltstack.com>
:platform: Windows Server 2008R2 or greater
:depends: PowerShell module ``ServerManager``
"""
import logging
import shlex
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__)
__virtualname__ = "win_servermanager"
def __virtual__():
"""
Load only on windows with servermanager module
"""
if not salt.utils.platform.is_windows():
return (
False,
"Module win_servermanager: module only works on Windows systems.",
)
if salt.utils.versions.version_cmp(__grains__["osversion"], "6.1.7600") == -1:
return (
False,
"Failed to load win_servermanager module: "
"Requires Remote Server Administration Tools which "
"is only available on Windows 2008 R2 and later.",
)
if not salt.utils.powershell.module_exists("ServerManager"):
return (
False,
"Failed to load win_servermanager module: "
"ServerManager module not available. "
"May need to install Remote Server Administration Tools.",
)
return __virtualname__
def list_available():
"""
List available features to install
Returns:
str: A list of available features as returned by the
``Get-WindowsFeature`` PowerShell command
CLI Example:
.. code-block:: bash
salt '*' win_servermanager.list_available
"""
cmd = (
"Import-Module ServerManager; "
"Get-WindowsFeature "
"-ErrorAction SilentlyContinue "
"-WarningAction SilentlyContinue"
)
return __salt__["cmd.shell"](cmd, shell="powershell")
def list_installed():
"""
List installed features. Supported on Windows Server 2008 and Windows 8 and
newer.
Returns:
dict: A dictionary of installed features
CLI Example:
.. code-block:: bash
salt '*' win_servermanager.list_installed
"""
cmd = (
"Get-WindowsFeature "
"-ErrorAction SilentlyContinue "
"-WarningAction SilentlyContinue "
"| Select DisplayName,Name,Installed"
)
features = salt.utils.win_pwsh.run_dict(cmd)
ret = {}
for entry in features:
if entry["Installed"]:
ret[entry["Name"]] = entry["DisplayName"]
return ret
def install(feature, recurse=False, restart=False, source=None, exclude=None):
r"""
Install a feature
.. note::
Some features require reboot after un/installation, if so until the
server is restarted other features can not be installed!
.. note::
Some features take a long time to complete un/installation, set -t with
a long timeout
Args:
feature (str, list):
The name of the feature(s) to install. This can be a single feature,
a string of features in a comma-delimited list (no spaces), or a
list of features.
.. versionadded:: 2018.3.0
Added the ability to pass a list of features to be installed.
recurse (:obj:`bool`, optional):
Install all sub-features.
Default is ``False``.
restart (:obj:`bool`, optional):
Restarts the computer when installation is complete, if required by
the role/feature installed. Will also trigger a reboot if an item
in ``exclude`` requires a reboot to be properly removed.
Default is ``False``.
source (:obj:`str`, optional):
Path to the source files if missing from the target system. None
means that the system will use windows update services to find the
required files.
Default is ``None``.
exclude (:obj:`str`, optional):
The name of the feature to exclude when installing the named
feature. This can be a single feature, a string of features in a
comma-delimited list (no spaces), or a list of features.
.. warning::
As there is no exclude option for the ``Add-WindowsFeature``
or ``Install-WindowsFeature`` PowerShell commands the features
named in ``exclude`` will be installed with other sub-features
and will then be removed. **If the feature named in ``exclude``
is not a sub-feature of one of the installed items it will still
be removed.**
Default is ``None``.
Returns:
dict: A dictionary containing the results of the install
CLI Example:
.. code-block:: bash
# Install the Telnet Client passing a single string
salt '*' win_servermanager.install Telnet-Client
# Install the TFTP Client and the SNMP Service passing a comma-delimited
# string. Install all sub-features
salt '*' win_servermanager.install TFTP-Client,SNMP-Service recurse=True
# Install the TFTP Client from d:\side-by-side
salt '*' win_servermanager.install TFTP-Client source=d:\\side-by-side
# Install the XPS Viewer, SNMP Service, and Remote Access passing a
# list. Install all sub-features, but exclude the Web Server
salt '*' win_servermanager.install "['XPS-Viewer', 'SNMP-Service', 'RemoteAccess']" True recurse=True exclude="Web-Server"
"""
# If it is a list of features, make it a comma delimited string
if isinstance(feature, list):
feature = ",".join(feature)
# Use Install-WindowsFeature on Windows 2012 (osversion 6.2) and later
# minions. Default to Add-WindowsFeature for earlier releases of Windows.
# The newer command makes management tools optional so add them for parity
# with old behavior.
command = "Add-WindowsFeature"
management_tools = ""
if salt.utils.versions.version_cmp(__grains__["osversion"], "6.2") >= 0:
command = "Install-WindowsFeature"
management_tools = "-IncludeManagementTools"
cmd = "{} -Name {} {} {} {} -WarningAction SilentlyContinue".format(
command,
shlex.quote(feature),
management_tools,
"-IncludeAllSubFeature" if recurse else "",
"" if source is None else f"-Source {source}",
)
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
# list. So you install first, then remove
if exclude is not None:
removed = remove(exclude)
# Results are stored in a list of dictionaries in `FeatureResult`
if out["FeatureResult"]:
ret = {
"ExitCode": out["ExitCode"],
"RestartNeeded": False,
"Restarted": False,
"Features": {},
"Success": out["Success"],
}
# FeatureResult is a list of dicts, so each item is a dict
for item in out["FeatureResult"]:
ret["Features"][item["Name"]] = {
"DisplayName": item["DisplayName"],
"Message": item["Message"],
"RestartNeeded": item["RestartNeeded"],
"SkipReason": item["SkipReason"],
"Success": item["Success"],
}
if item["RestartNeeded"]:
ret["RestartNeeded"] = True
# Only items that installed are in the list of dictionaries
# Add 'Already installed' for features that aren't in the list of dicts
for item in feature.split(","):
if item not in ret["Features"]:
ret["Features"][item] = {"Message": "Already installed"}
# Some items in the exclude list were removed after installation
# Show what was done, update the dict
if exclude is not None:
# Features is a dict, so it only iterates over the keys
for item in removed["Features"]:
if item in ret["Features"]:
ret["Features"][item] = {
"Message": "Removed after installation (exclude)",
"DisplayName": removed["Features"][item]["DisplayName"],
"RestartNeeded": removed["Features"][item]["RestartNeeded"],
"SkipReason": removed["Features"][item]["SkipReason"],
"Success": removed["Features"][item]["Success"],
}
# Exclude items might need a restart
if removed["Features"][item]["RestartNeeded"]:
ret["RestartNeeded"] = True
# Restart here if needed
if restart:
if ret["RestartNeeded"]:
if __salt__["system.reboot"](in_seconds=True):
ret["Restarted"] = True
return ret
else:
# If we get here then all features were already installed
ret = {
"ExitCode": out["ExitCode"],
"Features": {},
"RestartNeeded": False,
"Restarted": False,
"Success": out["Success"],
}
for item in feature.split(","):
ret["Features"][item] = {"Message": "Already installed"}
return ret
def remove(feature, remove_payload=False, restart=False):
r"""
Remove an installed feature
.. note::
Some features require a reboot after installation/uninstallation. If
one of these features are modified, then other features cannot be
installed until the server is restarted. Additionally, some features
take a while to complete installation/uninstallation, so it is a good
idea to use the ``-t`` option to set a longer timeout.
Args:
feature (str, list):
The name of the feature(s) to remove. This can be a single feature,
a string of features in a comma-delimited list (no spaces), or a
list of features.
.. versionadded:: 2018.3.0
Added the ability to pass a list of features to be removed.
remove_payload (:obj:`bool`, optional):
True will cause the feature to be removed from the side-by-side
store (``%SystemDrive%:\Windows\WinSxS``).
Default is ``False``.
restart (:obj:`bool`, optional):
Restarts the computer when uninstall is complete, if required by the
role/feature removed.
Default is ``False``.
Returns:
dict: A dictionary containing the results of the uninstall
CLI Example:
.. code-block:: bash
salt -t 600 '*' win_servermanager.remove Telnet-Client
"""
# If it is a list of features, make it a comma delimited string
if isinstance(feature, list):
feature = ",".join(feature)
# Use Uninstall-WindowsFeature on Windows 2012 (osversion 6.2) and later
# minions. Default to Remove-WindowsFeature for earlier releases of Windows.
# The newer command makes management tools optional so add them for parity
# with old behavior.
command = "Remove-WindowsFeature"
management_tools = ""
_remove_payload = ""
if salt.utils.versions.version_cmp(__grains__["osversion"], "6.2") >= 0:
command = "Uninstall-WindowsFeature"
management_tools = "-IncludeManagementTools"
# Only available with the `Uninstall-WindowsFeature` command
if remove_payload:
_remove_payload = "-Remove"
cmd = "{} -Name {} {} {} {} -WarningAction SilentlyContinue".format(
command,
shlex.quote(feature),
management_tools,
_remove_payload,
"-Restart" if restart else "",
)
try:
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)
raise
# Results are stored in a list of dictionaries in `FeatureResult`
if out["FeatureResult"]:
ret = {
"ExitCode": out["ExitCode"],
"RestartNeeded": False,
"Restarted": False,
"Features": {},
"Success": out["Success"],
}
for item in out["FeatureResult"]:
ret["Features"][item["Name"]] = {
"DisplayName": item["DisplayName"],
"Message": item["Message"],
"RestartNeeded": item["RestartNeeded"],
"SkipReason": item["SkipReason"],
"Success": item["Success"],
}
# Only items that installed are in the list of dictionaries
# Add 'Not installed' for features that aren't in the list of dicts
for item in feature.split(","):
if item not in ret["Features"]:
ret["Features"][item] = {"Message": "Not installed"}
return ret
else:
# If we get here then none of the features were installed
ret = {
"ExitCode": out["ExitCode"],
"Features": {},
"RestartNeeded": False,
"Restarted": False,
"Success": out["Success"],
}
for item in feature.split(","):
ret["Features"][item] = {"Message": "Not installed"}
return ret