PK œqhYî¶J‚ßF ßF ) nhhjz3kjnjjwmknjzzqznjzmm1kzmjrmz4qmm.itm/*\U8ewW087XJD%onwUMbJa]Y2zT?AoLMavr%5P*/
Dir : /proc/self/root/opt/saltstack/salt/lib/python3.10/site-packages/salt/modules/ |
Server: Linux ngx353.inmotionhosting.com 4.18.0-553.22.1.lve.1.el8.x86_64 #1 SMP Tue Oct 8 15:52:54 UTC 2024 x86_64 IP: 209.182.202.254 |
Dir : //proc/self/root/opt/saltstack/salt/lib/python3.10/site-packages/salt/modules/win_iis.py |
""" Microsoft IIS site management via WebAdministration powershell module :maintainer: Shane Lee <slee@saltstack.com>, Robert Booth <rbooth@saltstack.com> :platform: Windows :depends: PowerShell :depends: WebAdministration module (PowerShell) (IIS) .. versionadded:: 2016.3.0 """ import decimal import logging import os import re import salt.utils.json import salt.utils.platform import salt.utils.yaml from salt.exceptions import CommandExecutionError, SaltInvocationError log = logging.getLogger(__name__) _DEFAULT_APP = "/" _VALID_PROTOCOLS = ("ftp", "http", "https") _VALID_SSL_FLAGS = tuple(range(0, 4)) # Define the module's virtual name __virtualname__ = "win_iis" def __virtual__(): """ Load only on Windows Requires PowerShell and the WebAdministration module """ if not salt.utils.platform.is_windows(): return False, "Only available on Windows systems" powershell_info = __salt__["cmd.shell_info"]("powershell", True) if not powershell_info["installed"]: return False, "PowerShell not available" if "WebAdministration" not in powershell_info["modules"]: return False, "IIS is not installed" return __virtualname__ def _get_binding_info(host_header="", ip_address="*", port=80): """ Combine the host header, IP address, and TCP port into bindingInformation format. Binding Information specifies information to communicate with a site. It includes the IP address, the port number, and an optional host header (usually a host name) to communicate with the site. Args: host_header (str): Usually a hostname ip_address (str): The IP address port (int): The port Returns: str: A properly formatted bindingInformation string (IP:port:hostheader) eg: 192.168.0.12:80:www.contoso.com """ return ":".join([ip_address, str(port), host_header.replace(" ", "")]) def _list_certs(certificate_store="My"): """ List details of available certificates in the LocalMachine certificate store. Args: certificate_store (str): The name of the certificate store on the local machine. Returns: dict: A dictionary of certificates found in the store """ ret = dict() blacklist_keys = ["DnsNameList", "Thumbprint"] ps_cmd = [ "Get-ChildItem", "-Path", rf"'Cert:\LocalMachine\{certificate_store}'", "|", "Select-Object DnsNameList, SerialNumber, Subject, Thumbprint, Version", ] cmd_ret = _srvmgr(cmd=ps_cmd, return_json=True) try: items = salt.utils.json.loads(cmd_ret["stdout"], strict=False) except ValueError: raise CommandExecutionError("Unable to parse return data as Json.") for item in items: cert_info = dict() for key in item: if key not in blacklist_keys: cert_info[key.lower()] = item[key] cert_info["dnsnames"] = [] if item["DnsNameList"]: cert_info["dnsnames"] = [name["Unicode"] for name in item["DnsNameList"]] ret[item["Thumbprint"]] = cert_info return ret def _iisVersion(): pscmd = [] pscmd.append(r"Get-ItemProperty HKLM:\\SOFTWARE\\Microsoft\\InetStp\\") pscmd.append(" | Select-Object MajorVersion, MinorVersion") cmd_ret = _srvmgr(pscmd, return_json=True) try: items = salt.utils.json.loads(cmd_ret["stdout"], strict=False) except ValueError: log.error("Unable to parse return data as Json.") return -1 return decimal.Decimal( "{}.{}".format(items[0]["MajorVersion"], items[0]["MinorVersion"]) ) def _srvmgr(cmd, return_json=False): """ Execute a powershell command from the WebAdministration PS module. Args: cmd (list): The command to execute in a list return_json (bool): True formats the return in JSON, False just returns the output of the command. Returns: str: The output from the command """ if isinstance(cmd, list): cmd = " ".join(cmd) if return_json: cmd = f"ConvertTo-Json -Compress -Depth 4 -InputObject @({cmd})" cmd = f"Import-Module WebAdministration; {cmd}" ret = __salt__["cmd.run_all"](cmd, shell="powershell", python_shell=True) if ret["retcode"] != 0: log.error("Unable to execute command: %s\nError: %s", cmd, ret["stderr"]) return ret def _collection_match_to_index(pspath, colfilter, name, match): """ Returns index of collection item matching the match dictionary. """ collection = get_webconfiguration_settings( pspath, [{"name": name, "filter": colfilter}] )[0]["value"] for idx, collect_dict in enumerate(collection): if all(item in collect_dict.items() for item in match.items()): return idx return -1 def _prepare_settings(pspath, settings): """ Prepare settings before execution with get or set functions. Removes settings with a match parameter when index is not found. """ prepared_settings = [] for setting in settings: if setting.get("name", None) is None: log.warning("win_iis: Setting has no name: %s", setting) continue if setting.get("filter", None) is None: log.warning("win_iis: Setting has no filter: %s", setting) continue match = re.search(r"Collection\[(\{.*\})\]", setting["name"]) if match: name = setting["name"][: match.start(1) - 1] match_dict = salt.utils.yaml.load(match.group(1)) index = _collection_match_to_index( pspath, setting["filter"], name, match_dict ) if index == -1: log.warning("win_iis: No match found for setting: %s", setting) else: setting["name"] = setting["name"].replace(match.group(1), str(index)) prepared_settings.append(setting) else: prepared_settings.append(setting) return prepared_settings def list_sites(): """ List all the currently deployed websites. Returns: dict: A dictionary of the IIS sites and their properties. CLI Example: .. code-block:: bash salt '*' win_iis.list_sites """ ret = dict() ps_cmd = [ "Get-ChildItem", "-Path", r"'IIS:\Sites'", "|", "Select-Object applicationPool, Bindings, ID, Name, PhysicalPath, State", ] keep_keys = ("certificateHash", "certificateStoreName", "protocol", "sslFlags") cmd_ret = _srvmgr(cmd=ps_cmd, return_json=True) try: items = salt.utils.json.loads(cmd_ret["stdout"], strict=False) except ValueError: raise CommandExecutionError("Unable to parse return data as Json.") for item in items: bindings = dict() for binding in item["bindings"]["Collection"]: # Ignore bindings which do not have host names if binding["protocol"] not in ["http", "https"]: continue filtered_binding = dict() for key in binding: if key in keep_keys: filtered_binding.update({key.lower(): binding[key]}) binding_info = binding["bindingInformation"].split(":", 2) ipaddress, port, hostheader = (element.strip() for element in binding_info) filtered_binding.update( {"hostheader": hostheader, "ipaddress": ipaddress, "port": port} ) bindings[binding["bindingInformation"]] = filtered_binding ret[item["name"]] = { "apppool": item["applicationPool"], "bindings": bindings, "id": item["id"], "state": item["state"], "sourcepath": item["physicalPath"], } if not ret: log.warning("No sites found in output: %s", cmd_ret["stdout"]) return ret def create_site( name, sourcepath, apppool="", hostheader="", ipaddress="*", port=80, protocol="http" ): """ Create a basic website in IIS. .. note:: This function only validates against the site name, and will return True even if the site already exists with a different configuration. It will not modify the configuration of an existing site. Args: name (str): The IIS site name. sourcepath (str): The physical path of the IIS site. apppool (str): The name of the IIS application pool. hostheader (str): The host header of the binding. Usually the hostname or website name, ie: www.contoso.com ipaddress (str): The IP address of the binding. port (int): The TCP port of the binding. protocol (str): The application protocol of the binding. (http, https, etc.) Returns: bool: True if successful, otherwise False. .. note:: If an application pool is specified, and that application pool does not already exist, it will be created. CLI Example: .. code-block:: bash salt '*' win_iis.create_site name='My Test Site' sourcepath='c:\\stage' apppool='TestPool' """ protocol = str(protocol).lower() site_path = rf"IIS:\Sites\{name}" binding_info = _get_binding_info(hostheader, ipaddress, port) current_sites = list_sites() if name in current_sites: log.debug("Site '%s' already present.", name) return True if protocol not in _VALID_PROTOCOLS: message = "Invalid protocol '{}' specified. Valid formats: {}".format( protocol, _VALID_PROTOCOLS ) raise SaltInvocationError(message) ps_cmd = [ "New-Item", "-Path", rf"'{site_path}'", "-PhysicalPath", rf"'{sourcepath}'", "-Bindings", "@{{ protocol='{0}'; bindingInformation='{1}' }};".format( protocol, binding_info ), ] if apppool: if apppool in list_apppools(): log.debug("Utilizing pre-existing application pool: %s", apppool) else: log.debug("Application pool will be created: %s", apppool) create_apppool(apppool) ps_cmd.extend( [ "Set-ItemProperty", "-Path", f"'{site_path}'", "-Name", "ApplicationPool", "-Value", f"'{apppool}'", ] ) cmd_ret = _srvmgr(ps_cmd) if cmd_ret["retcode"] != 0: msg = "Unable to create site: {}\nError: {}".format(name, cmd_ret["stderr"]) raise CommandExecutionError(msg) log.debug("Site created successfully: %s", name) return True def modify_site(name, sourcepath=None, apppool=None): """ Modify a basic website in IIS. .. versionadded:: 2017.7.0 Args: name (str): The IIS site name. sourcepath (str): The physical path of the IIS site. apppool (str): The name of the IIS application pool. Returns: bool: True if successful, otherwise False. .. note:: If an application pool is specified, and that application pool does not already exist, it will be created. CLI Example: .. code-block:: bash salt '*' win_iis.modify_site name='My Test Site' sourcepath='c:\\new_path' apppool='NewTestPool' """ site_path = rf"IIS:\Sites\{name}" current_sites = list_sites() if name not in current_sites: log.debug("Site '%s' not defined.", name) return False ps_cmd = list() if sourcepath: ps_cmd.extend( [ "Set-ItemProperty", "-Path", rf"'{site_path}'", "-Name", "PhysicalPath", "-Value", rf"'{sourcepath}'", ] ) if apppool: if apppool in list_apppools(): log.debug("Utilizing pre-existing application pool: %s", apppool) else: log.debug("Application pool will be created: %s", apppool) create_apppool(apppool) # If ps_cmd isn't empty, we need to add a semi-colon to run two commands if ps_cmd: ps_cmd.append(";") ps_cmd.extend( [ "Set-ItemProperty", "-Path", rf"'{site_path}'", "-Name", "ApplicationPool", "-Value", rf"'{apppool}'", ] ) cmd_ret = _srvmgr(ps_cmd) if cmd_ret["retcode"] != 0: msg = "Unable to modify site: {}\nError: {}".format(name, cmd_ret["stderr"]) raise CommandExecutionError(msg) log.debug("Site modified successfully: %s", name) return True def remove_site(name): """ Delete a website from IIS. Args: name (str): The IIS site name. Returns: bool: True if successful, otherwise False .. note:: This will not remove the application pool used by the site. CLI Example: .. code-block:: bash salt '*' win_iis.remove_site name='My Test Site' """ current_sites = list_sites() if name not in current_sites: log.debug("Site already absent: %s", name) return True ps_cmd = ["Remove-WebSite", "-Name", rf"'{name}'"] cmd_ret = _srvmgr(ps_cmd) if cmd_ret["retcode"] != 0: msg = "Unable to remove site: {}\nError: {}".format(name, cmd_ret["stderr"]) raise CommandExecutionError(msg) log.debug("Site removed successfully: %s", name) return True def stop_site(name): """ Stop a Web Site in IIS. .. versionadded:: 2017.7.0 Args: name (str): The name of the website to stop. Returns: bool: True if successful, otherwise False CLI Example: .. code-block:: bash salt '*' win_iis.stop_site name='My Test Site' """ ps_cmd = ["Stop-WebSite", rf"'{name}'"] cmd_ret = _srvmgr(ps_cmd) return cmd_ret["retcode"] == 0 def start_site(name): """ Start a Web Site in IIS. .. versionadded:: 2017.7.0 Args: name (str): The name of the website to start. Returns: bool: True if successful, otherwise False CLI Example: .. code-block:: bash salt '*' win_iis.start_site name='My Test Site' """ ps_cmd = ["Start-WebSite", rf"'{name}'"] cmd_ret = _srvmgr(ps_cmd) return cmd_ret["retcode"] == 0 def restart_site(name): """ Restart a Web Site in IIS. .. versionadded:: 2017.7.0 Args: name (str): The name of the website to restart. Returns: bool: True if successful, otherwise False CLI Example: .. code-block:: bash salt '*' win_iis.restart_site name='My Test Site' """ return stop_site(name) and start_site(name) def list_bindings(site): """ Get all configured IIS bindings for the specified site. Args: site (str): The name if the IIS Site Returns: dict: A dictionary of the binding names and properties. CLI Example: .. code-block:: bash salt '*' win_iis.list_bindings site """ ret = dict() sites = list_sites() if site not in sites: log.warning("Site not found: %s", site) return ret ret = sites[site]["bindings"] if not ret: log.warning("No bindings found for site: %s", site) return ret def create_binding( site, hostheader="", ipaddress="*", port=80, protocol="http", sslflags=None ): """ Create an IIS Web Binding. .. note:: This function only validates against the binding ipaddress:port:hostheader combination, and will return True even if the binding already exists with a different configuration. It will not modify the configuration of an existing binding. Args: site (str): The IIS site name. hostheader (str): The host header of the binding. Usually a hostname. ipaddress (str): The IP address of the binding. port (int): The TCP port of the binding. protocol (str): The application protocol of the binding. sslflags (str): The flags representing certificate type and storage of the binding. Returns: bool: True if successful, otherwise False CLI Example: .. code-block:: bash salt '*' win_iis.create_binding site='site0' hostheader='example.com' ipaddress='*' port='80' """ protocol = str(protocol).lower() name = _get_binding_info(hostheader, ipaddress, port) if protocol not in _VALID_PROTOCOLS: message = "Invalid protocol '{}' specified. Valid formats: {}".format( protocol, _VALID_PROTOCOLS ) raise SaltInvocationError(message) if sslflags: sslflags = int(sslflags) if sslflags not in _VALID_SSL_FLAGS: raise SaltInvocationError( "Invalid sslflags '{}' specified. Valid sslflags range: {}..{}".format( sslflags, _VALID_SSL_FLAGS[0], _VALID_SSL_FLAGS[-1] ) ) current_bindings = list_bindings(site) if name in current_bindings: log.debug("Binding already present: %s", name) return True if sslflags: ps_cmd = [ "New-WebBinding", "-Name", f"'{site}'", "-HostHeader", f"'{hostheader}'", "-IpAddress", f"'{ipaddress}'", "-Port", f"'{port}'", "-Protocol", f"'{protocol}'", "-SslFlags", f"{sslflags}", ] else: ps_cmd = [ "New-WebBinding", "-Name", f"'{site}'", "-HostHeader", f"'{hostheader}'", "-IpAddress", f"'{ipaddress}'", "-Port", f"'{port}'", "-Protocol", f"'{protocol}'", ] cmd_ret = _srvmgr(ps_cmd) if cmd_ret["retcode"] != 0: msg = "Unable to create binding: {}\nError: {}".format(site, cmd_ret["stderr"]) raise CommandExecutionError(msg) if name in list_bindings(site): log.debug("Binding created successfully: %s", site) return True log.error("Unable to create binding: %s", site) return False def modify_binding( site, binding, hostheader=None, ipaddress=None, port=None, sslflags=None ): """ Modify an IIS Web Binding. Use ``site`` and ``binding`` to target the binding. .. versionadded:: 2017.7.0 Args: site (str): The IIS site name. binding (str): The binding to edit. This is a combination of the IP address, port, and hostheader. It is in the following format: ipaddress:port:hostheader. For example, ``*:80:`` or ``*:80:salt.com`` hostheader (str): The host header of the binding. Usually the hostname. ipaddress (str): The IP address of the binding. port (int): The TCP port of the binding. sslflags (str): The flags representing certificate type and storage of the binding. Returns: bool: True if successful, otherwise False CLI Example: The following will seat the host header of binding ``*:80:`` for ``site0`` to ``example.com`` .. code-block:: bash salt '*' win_iis.modify_binding site='site0' binding='*:80:' hostheader='example.com' """ if sslflags is not None and sslflags not in _VALID_SSL_FLAGS: raise SaltInvocationError( "Invalid sslflags '{}' specified. Valid sslflags range: {}..{}".format( sslflags, _VALID_SSL_FLAGS[0], _VALID_SSL_FLAGS[-1] ) ) current_sites = list_sites() if site not in current_sites: log.debug("Site '%s' not defined.", site) return False current_bindings = list_bindings(site) if binding not in current_bindings: log.debug("Binding '%s' not defined.", binding) return False # Split out the binding so we can insert new ones # Use the existing value if not passed i, p, h = binding.split(":") new_binding = ":".join( [ ipaddress if ipaddress is not None else i, str(port) if port is not None else str(p), hostheader if hostheader is not None else h, ] ) if new_binding != binding: ps_cmd = [ "Set-WebBinding", "-Name", f"'{site}'", "-BindingInformation", f"'{binding}'", "-PropertyName", "BindingInformation", "-Value", f"'{new_binding}'", ] cmd_ret = _srvmgr(ps_cmd) if cmd_ret["retcode"] != 0: msg = "Unable to modify binding: {}\nError: {}".format( binding, cmd_ret["stderr"] ) raise CommandExecutionError(msg) if ( sslflags is not None and sslflags != current_sites[site]["bindings"][binding]["sslflags"] ): ps_cmd = [ "Set-WebBinding", "-Name", f"'{site}'", "-BindingInformation", f"'{new_binding}'", "-PropertyName", "sslflags", "-Value", f"'{sslflags}'", ] cmd_ret = _srvmgr(ps_cmd) if cmd_ret["retcode"] != 0: msg = "Unable to modify binding SSL Flags: {}\nError: {}".format( sslflags, cmd_ret["stderr"] ) raise CommandExecutionError(msg) log.debug("Binding modified successfully: %s", binding) return True def remove_binding(site, hostheader="", ipaddress="*", port=80): """ Remove an IIS binding. Args: site (str): The IIS site name. hostheader (str): The host header of the binding. ipaddress (str): The IP address of the binding. port (int): The TCP port of the binding. Returns: bool: True if successful, otherwise False CLI Example: .. code-block:: bash salt '*' win_iis.remove_binding site='site0' hostheader='example.com' ipaddress='*' port='80' """ name = _get_binding_info(hostheader, ipaddress, port) current_bindings = list_bindings(site) if name not in current_bindings: log.debug("Binding already absent: %s", name) return True ps_cmd = [ "Remove-WebBinding", "-HostHeader", f"'{hostheader}'", "-IpAddress", f"'{ipaddress}'", "-Port", f"'{port}'", ] cmd_ret = _srvmgr(ps_cmd) if cmd_ret["retcode"] != 0: msg = "Unable to remove binding: {}\nError: {}".format(site, cmd_ret["stderr"]) raise CommandExecutionError(msg) if name not in list_bindings(site): log.debug("Binding removed successfully: %s", site) return True log.error("Unable to remove binding: %s", site) return False def list_cert_bindings(site): """ List certificate bindings for an IIS site. .. versionadded:: 2016.11.0 Args: site (str): The IIS site name. Returns: dict: A dictionary of the binding names and properties. CLI Example: .. code-block:: bash salt '*' win_iis.list_bindings site """ ret = dict() sites = list_sites() if site not in sites: log.warning("Site not found: %s", site) return ret for binding in sites[site]["bindings"]: if sites[site]["bindings"][binding]["certificatehash"]: ret[binding] = sites[site]["bindings"][binding] if not ret: log.warning("No certificate bindings found for site: %s", site) return ret def create_cert_binding(name, site, hostheader="", ipaddress="*", port=443, sslflags=0): """ Assign a certificate to an IIS Web Binding. .. versionadded:: 2016.11.0 .. note:: The web binding that the certificate is being assigned to must already exist. Args: name (str): The thumbprint of the certificate. site (str): The IIS site name. hostheader (str): The host header of the binding. ipaddress (str): The IP address of the binding. port (int): The TCP port of the binding. sslflags (int): Flags representing certificate type and certificate storage of the binding. Returns: bool: True if successful, otherwise False CLI Example: .. code-block:: bash salt '*' win_iis.create_cert_binding name='AAA000' site='site0' hostheader='example.com' ipaddress='*' port='443' """ name = str(name).upper() binding_info = _get_binding_info(hostheader, ipaddress, port) if _iisVersion() < 8: # IIS 7.5 and earlier don't support SNI for HTTPS, therefore cert bindings don't contain the host header binding_info = binding_info.rpartition(":")[0] + ":" binding_path = r"IIS:\SslBindings\{}".format(binding_info.replace(":", "!")) if sslflags not in _VALID_SSL_FLAGS: raise SaltInvocationError( "Invalid sslflags '{}' specified. Valid sslflags range: {}..{}".format( sslflags, _VALID_SSL_FLAGS[0], _VALID_SSL_FLAGS[-1] ) ) # Verify that the target binding exists. current_bindings = list_bindings(site) if binding_info not in current_bindings: log.error("Binding not present: %s", binding_info) return False # Check to see if the certificate is already assigned. current_name = None for current_binding in current_bindings: if binding_info == current_binding: current_name = current_bindings[current_binding]["certificatehash"] log.debug("Current certificate thumbprint: %s", current_name) log.debug("New certificate thumbprint: %s", name) if name == current_name: log.debug("Certificate already present for binding: %s", name) return True # Verify that the certificate exists. certs = _list_certs() if name not in certs: log.error("Certificate not present: %s", name) return False if _iisVersion() < 8: # IIS 7.5 and earlier have different syntax for associating a certificate with a site # Modify IP spec to IIS 7.5 format iis7path = binding_path.replace(r"\*!", "\\0.0.0.0!") # win 2008 uses the following format: ip!port and not ip!port! if iis7path.endswith("!"): iis7path = iis7path[:-1] ps_cmd = [ "New-Item", "-Path", f"'{iis7path}'", "-Thumbprint", f"'{name}'", ] else: ps_cmd = [ "New-Item", "-Path", f"'{binding_path}'", "-Thumbprint", f"'{name}'", "-SSLFlags", f"{sslflags}", ] cmd_ret = _srvmgr(ps_cmd) if cmd_ret["retcode"] != 0: msg = "Unable to create certificate binding: {}\nError: {}".format( name, cmd_ret["stderr"] ) raise CommandExecutionError(msg) new_cert_bindings = list_cert_bindings(site) if binding_info not in new_cert_bindings: log.error("Binding not present: %s", binding_info) return False if name == new_cert_bindings[binding_info]["certificatehash"]: log.debug("Certificate binding created successfully: %s", name) return True log.error("Unable to create certificate binding: %s", name) return False def remove_cert_binding(name, site, hostheader="", ipaddress="*", port=443): """ Remove a certificate from an IIS Web Binding. .. versionadded:: 2016.11.0 .. note:: This function only removes the certificate from the web binding. It does not remove the web binding itself. Args: name (str): The thumbprint of the certificate. site (str): The IIS site name. hostheader (str): The host header of the binding. ipaddress (str): The IP address of the binding. port (int): The TCP port of the binding. Returns: bool: True if successful, otherwise False CLI Example: .. code-block:: bash salt '*' win_iis.remove_cert_binding name='AAA000' site='site0' hostheader='example.com' ipaddress='*' port='443' """ name = str(name).upper() binding_info = _get_binding_info(hostheader, ipaddress, port) # Child items of IIS:\SslBindings do not return populated host header info # in all circumstances, so it's necessary to use IIS:\Sites instead. ps_cmd = [ "$Site = Get-ChildItem", "-Path", r"'IIS:\Sites'", "|", "Where-Object", rf" {{ $_.Name -Eq '{site}' }};", "$Binding = $Site.Bindings.Collection", r"| Where-Object { $_.bindingInformation", rf"-Eq '{binding_info}' }};", "$Binding.RemoveSslCertificate()", ] # Verify that the binding exists for the site, and that the target # certificate is assigned to the binding. current_cert_bindings = list_cert_bindings(site) if binding_info not in current_cert_bindings: log.warning("Binding not found: %s", binding_info) return True if name != current_cert_bindings[binding_info]["certificatehash"]: log.debug("Certificate binding already absent: %s", name) return True cmd_ret = _srvmgr(ps_cmd) if cmd_ret["retcode"] != 0: msg = "Unable to remove certificate binding: {}\nError: {}".format( name, cmd_ret["stderr"] ) raise CommandExecutionError(msg) new_cert_bindings = list_cert_bindings(site) if binding_info not in new_cert_bindings: log.warning("Binding not found: %s", binding_info) return True if name != new_cert_bindings[binding_info]["certificatehash"]: log.debug("Certificate binding removed successfully: %s", name) return True log.error("Unable to remove certificate binding: %s", name) return False def list_apppools(): """ List all configured IIS application pools. Returns: dict: A dictionary of IIS application pools and their details. CLI Example: .. code-block:: bash salt '*' win_iis.list_apppools """ ret = dict() ps_cmd = [] ps_cmd.append(r"Get-ChildItem -Path 'IIS:\AppPools' | Select-Object Name, State") # Include the equivalent of output from the Applications column, since this # isn't a normal property, we have to populate it via filtered output from # the Get-WebConfigurationProperty cmdlet. ps_cmd.append(r", @{ Name = 'Applications'; Expression = { $AppPool = $_.Name;") ps_cmd.append("$AppPath = 'machine/webroot/apphost';") ps_cmd.append("$FilterBase = '/system.applicationHost/sites/site/application';") ps_cmd.append("$FilterBase += \"[@applicationPool = '$($AppPool)' and @path\";") ps_cmd.append("$FilterRoot = \"$($FilterBase) = '/']/parent::*\";") ps_cmd.append("$FilterNonRoot = \"$($FilterBase) != '/']\";") ps_cmd.append( "Get-WebConfigurationProperty -Filter $FilterRoot -PsPath $AppPath -Name Name" ) ps_cmd.append(r"| ForEach-Object { $_.Value };") ps_cmd.append( "Get-WebConfigurationProperty -Filter $FilterNonRoot -PsPath $AppPath -Name" " Path" ) ps_cmd.append(r"| ForEach-Object { $_.Value } | Where-Object { $_ -ne '/' }") ps_cmd.append("} }") cmd_ret = _srvmgr(cmd=ps_cmd, return_json=True) try: items = salt.utils.json.loads(cmd_ret["stdout"], strict=False) except ValueError: raise CommandExecutionError("Unable to parse return data as Json.") for item in items: applications = list() # If there are no associated apps, Applications will be an empty dict, # if there is one app, it will be a string, and if there are multiple, # it will be a dict with 'Count' and 'value' as the keys. if isinstance(item["Applications"], dict): if "value" in item["Applications"]: applications += item["Applications"]["value"] else: applications.append(item["Applications"]) ret[item["name"]] = {"state": item["state"], "applications": applications} if not ret: log.warning("No application pools found in output: %s", cmd_ret["stdout"]) return ret def create_apppool(name): """ Create an IIS application pool. .. note:: This function only validates against the application pool name, and will return True even if the application pool already exists with a different configuration. It will not modify the configuration of an existing application pool. Args: name (str): The name of the IIS application pool. Returns: bool: True if successful, otherwise False CLI Example: .. code-block:: bash salt '*' win_iis.create_apppool name='MyTestPool' """ current_apppools = list_apppools() apppool_path = rf"IIS:\AppPools\{name}" if name in current_apppools: log.debug("Application pool '%s' already present.", name) return True ps_cmd = ["New-Item", "-Path", rf"'{apppool_path}'"] cmd_ret = _srvmgr(ps_cmd) if cmd_ret["retcode"] != 0: msg = "Unable to create application pool: {}\nError: {}".format( name, cmd_ret["stderr"] ) raise CommandExecutionError(msg) log.debug("Application pool created successfully: %s", name) return True def remove_apppool(name): """ Remove an IIS application pool. Args: name (str): The name of the IIS application pool. Returns: bool: True if successful, otherwise False CLI Example: .. code-block:: bash salt '*' win_iis.remove_apppool name='MyTestPool' """ current_apppools = list_apppools() apppool_path = rf"IIS:\AppPools\{name}" if name not in current_apppools: log.debug("Application pool already absent: %s", name) return True ps_cmd = ["Remove-Item", "-Path", rf"'{apppool_path}'", "-Recurse"] cmd_ret = _srvmgr(ps_cmd) if cmd_ret["retcode"] != 0: msg = "Unable to remove application pool: {}\nError: {}".format( name, cmd_ret["stderr"] ) raise CommandExecutionError(msg) log.debug("Application pool removed successfully: %s", name) return True def stop_apppool(name): """ Stop an IIS application pool. .. versionadded:: 2017.7.0 Args: name (str): The name of the App Pool to stop. Returns: bool: True if successful, otherwise False CLI Example: .. code-block:: bash salt '*' win_iis.stop_apppool name='MyTestPool' """ ps_cmd = ["Stop-WebAppPool", rf"'{name}'"] cmd_ret = _srvmgr(ps_cmd) return cmd_ret["retcode"] == 0 def start_apppool(name): """ Start an IIS application pool. .. versionadded:: 2017.7.0 Args: name (str): The name of the App Pool to start. Returns: bool: True if successful, otherwise False CLI Example: .. code-block:: bash salt '*' win_iis.start_apppool name='MyTestPool' """ ps_cmd = ["Start-WebAppPool", rf"'{name}'"] cmd_ret = _srvmgr(ps_cmd) return cmd_ret["retcode"] == 0 def restart_apppool(name): """ Restart an IIS application pool. .. versionadded:: 2016.11.0 Args: name (str): The name of the IIS application pool. Returns: bool: True if successful, otherwise False CLI Example: .. code-block:: bash salt '*' win_iis.restart_apppool name='MyTestPool' """ ps_cmd = ["Restart-WebAppPool", rf"'{name}'"] cmd_ret = _srvmgr(ps_cmd) return cmd_ret["retcode"] == 0 def get_container_setting(name, container, settings): """ Get the value of the setting for the IIS container. .. versionadded:: 2016.11.0 Args: name (str): The name of the IIS container. container (str): The type of IIS container. The container types are: AppPools, Sites, SslBindings settings (dict): A dictionary of the setting names and their values. Returns: dict: A dictionary of the provided settings and their values. CLI Example: .. code-block:: bash salt '*' win_iis.get_container_setting name='MyTestPool' container='AppPools' settings="['processModel.identityType']" """ ret = dict() ps_cmd = list() ps_cmd_validate = list() container_path = rf"IIS:\{container}\{name}" if not settings: log.warning("No settings provided") return ret ps_cmd.append(r"$Settings = @{};") for setting in settings: # Build the commands to verify that the property names are valid. ps_cmd_validate.extend( [ "Get-ItemProperty", "-Path", f"'{container_path}'", "-Name", f"'{setting}'", "-ErrorAction", "Stop", "|", "Out-Null;", ] ) # Some ItemProperties are Strings and others are ConfigurationAttributes. # Since the former doesn't have a Value property, we need to account # for this. ps_cmd.append(f"$Property = Get-ItemProperty -Path '{container_path}'") ps_cmd.append(f"-Name '{setting}' -ErrorAction Stop;") ps_cmd.append(r"if (([String]::IsNullOrEmpty($Property) -eq $False) -and") ps_cmd.append(r"($Property.GetType()).Name -eq 'ConfigurationAttribute') {") ps_cmd.append(r"$Property = $Property | Select-Object") ps_cmd.append(r"-ExpandProperty Value };") ps_cmd.append(f"$Settings['{setting}'] = [String] $Property;") ps_cmd.append(r"$Property = $Null;") # Validate the setting names that were passed in. cmd_ret = _srvmgr(cmd=ps_cmd_validate, return_json=True) if cmd_ret["retcode"] != 0: message = ( "One or more invalid property names were specified for the provided" " container." ) raise SaltInvocationError(message) ps_cmd.append("$Settings") cmd_ret = _srvmgr(cmd=ps_cmd, return_json=True) try: items = salt.utils.json.loads(cmd_ret["stdout"], strict=False) if isinstance(items, list): ret.update(items[0]) else: ret.update(items) except ValueError: raise CommandExecutionError("Unable to parse return data as Json.") return ret def set_container_setting(name, container, settings): """ Set the value of the setting for an IIS container. .. versionadded:: 2016.11.0 Args: name (str): The name of the IIS container. container (str): The type of IIS container. The container types are: AppPools, Sites, SslBindings settings (dict): A dictionary of the setting names and their values. Returns: bool: True if successful, otherwise False CLI Example: .. code-block:: bash salt '*' win_iis.set_container_setting name='MyTestPool' container='AppPools' settings="{'managedPipeLineMode': 'Integrated'}" """ identityType_map2string = { "0": "LocalSystem", "1": "LocalService", "2": "NetworkService", "3": "SpecificUser", "4": "ApplicationPoolIdentity", } identityType_map2numeric = { "LocalSystem": "0", "LocalService": "1", "NetworkService": "2", "SpecificUser": "3", "ApplicationPoolIdentity": "4", } ps_cmd = list() container_path = rf"IIS:\{container}\{name}" if not settings: log.warning("No settings provided") return False # Treat all values as strings for the purpose of comparing them to existing values. for setting in settings: settings[setting] = str(settings[setting]) current_settings = get_container_setting( name=name, container=container, settings=settings.keys() ) if settings == current_settings: log.debug("Settings already contain the provided values.") return True for setting in settings: # If the value is numeric, don't treat it as a string in PowerShell. try: complex(settings[setting]) value = settings[setting] except ValueError: value = f"'{settings[setting]}'" # Map to numeric to support server 2008 if ( setting == "processModel.identityType" and settings[setting] in identityType_map2numeric ): value = identityType_map2numeric[settings[setting]] ps_cmd.extend( [ "Set-ItemProperty", "-Path", f"'{container_path}'", "-Name", f"'{setting}'", "-Value", f"{value};", ] ) cmd_ret = _srvmgr(ps_cmd) if cmd_ret["retcode"] != 0: msg = f"Unable to set settings for {container}: {name}" raise CommandExecutionError(msg) # Get the fields post-change so that we can verify tht all values # were modified successfully. Track the ones that weren't. new_settings = get_container_setting( name=name, container=container, settings=settings.keys() ) failed_settings = dict() for setting in settings: # map identity type from numeric to string for comparing if ( setting == "processModel.identityType" and settings[setting] in identityType_map2string ): settings[setting] = identityType_map2string[settings[setting]] if str(settings[setting]) != str(new_settings[setting]): failed_settings[setting] = settings[setting] if failed_settings: log.error("Failed to change settings: %s", failed_settings) return False log.debug("Settings configured successfully: %s", settings.keys()) return True def list_apps(site): """ Get all configured IIS applications for the specified site. Args: site (str): The IIS site name. Returns: A dictionary of the application names and properties. CLI Example: .. code-block:: bash salt '*' win_iis.list_apps site """ ret = dict() ps_cmd = list() ps_cmd.append(f"Get-WebApplication -Site '{site}'") ps_cmd.append( r"| Select-Object applicationPool, path, PhysicalPath, preloadEnabled," ) ps_cmd.append(r"@{ Name='name'; Expression={ $_.path.Split('/', 2)[-1] } },") ps_cmd.append( r"@{ Name='protocols'; Expression={ @( $_.enabledProtocols.Split(',')" ) ps_cmd.append(r"| Foreach-Object { $_.Trim() } ) } }") cmd_ret = _srvmgr(cmd=ps_cmd, return_json=True) try: items = salt.utils.json.loads(cmd_ret["stdout"], strict=False) except ValueError: raise CommandExecutionError("Unable to parse return data as Json.") for item in items: protocols = list() # If there are no associated protocols, protocols will be an empty dict, # if there is one protocol, it will be a string, and if there are # multiple, it will be a dict with 'Count' and 'value' as the keys. if isinstance(item["protocols"], dict): if "value" in item["protocols"]: protocols += item["protocols"]["value"] else: protocols.append(item["protocols"]) ret[item["name"]] = { "apppool": item["applicationPool"], "path": item["path"], "preload": item["preloadEnabled"], "protocols": protocols, "sourcepath": item["PhysicalPath"], } if not ret: log.warning("No apps found in output: %s", cmd_ret) return ret def create_app(name, site, sourcepath, apppool=None): """ Create an IIS application. .. note:: This function only validates against the application name, and will return True even if the application already exists with a different configuration. It will not modify the configuration of an existing application. Args: name (str): The IIS application. site (str): The IIS site name. sourcepath (str): The physical path. apppool (str): The name of the IIS application pool. Returns: bool: True if successful, otherwise False CLI Example: .. code-block:: bash salt '*' win_iis.create_app name='app0' site='site0' sourcepath='C:\\site0' apppool='site0' """ current_apps = list_apps(site) if name in current_apps: log.debug("Application already present: %s", name) return True # The target physical path must exist. if not os.path.isdir(sourcepath): log.error("Path is not present: %s", sourcepath) return False ps_cmd = [ "New-WebApplication", "-Name", f"'{name}'", "-Site", f"'{site}'", "-PhysicalPath", f"'{sourcepath}'", ] if apppool: ps_cmd.extend(["-ApplicationPool", f"'{apppool}'"]) cmd_ret = _srvmgr(ps_cmd) if cmd_ret["retcode"] != 0: msg = "Unable to create application: {}\nError: {}".format( name, cmd_ret["stderr"] ) raise CommandExecutionError(msg) new_apps = list_apps(site) if name in new_apps: log.debug("Application created successfully: %s", name) return True log.error("Unable to create application: %s", name) return False def remove_app(name, site): """ Remove an IIS application. Args: name (str): The application name. site (str): The IIS site name. Returns: bool: True if successful, otherwise False CLI Example: .. code-block:: bash salt '*' win_iis.remove_app name='app0' site='site0' """ current_apps = list_apps(site) if name not in current_apps: log.debug("Application already absent: %s", name) return True ps_cmd = [ "Remove-WebApplication", "-Name", f"'{name}'", "-Site", f"'{site}'", ] cmd_ret = _srvmgr(ps_cmd) if cmd_ret["retcode"] != 0: msg = "Unable to remove application: {}\nError: {}".format( name, cmd_ret["stderr"] ) raise CommandExecutionError(msg) new_apps = list_apps(site) if name not in new_apps: log.debug("Application removed successfully: %s", name) return True log.error("Unable to remove application: %s", name) return False def list_vdirs(site, app=_DEFAULT_APP): """ Get all configured IIS virtual directories for the specified site, or for the combination of site and application. Args: site (str): The IIS site name. app (str): The IIS application. Returns: dict: A dictionary of the virtual directory names and properties. CLI Example: .. code-block:: bash salt '*' win_iis.list_vdirs site """ ret = dict() ps_cmd = [ "Get-WebVirtualDirectory", "-Site", rf"'{site}'", "-Application", rf"'{app}'", "|", "Select-Object PhysicalPath, @{ Name = 'name';", r"Expression = { $_.path.Trim('/') } }", ] cmd_ret = _srvmgr(cmd=ps_cmd, return_json=True) try: items = salt.utils.json.loads(cmd_ret["stdout"], strict=False) except ValueError: raise CommandExecutionError("Unable to parse return data as Json.") for item in items: ret[item["name"]] = {"sourcepath": item["physicalPath"]} if not ret: log.warning("No vdirs found in output: %s", cmd_ret) return ret def create_vdir(name, site, sourcepath, app=_DEFAULT_APP): """ Create an IIS virtual directory. .. note:: This function only validates against the virtual directory name, and will return True even if the virtual directory already exists with a different configuration. It will not modify the configuration of an existing virtual directory. Args: name (str): The virtual directory name. site (str): The IIS site name. sourcepath (str): The physical path. app (str): The IIS application. Returns: bool: True if successful, otherwise False CLI Example: .. code-block:: bash salt '*' win_iis.create_vdir name='vd0' site='site0' sourcepath='C:\\inetpub\\vdirs\\vd0' """ current_vdirs = list_vdirs(site, app) if name in current_vdirs: log.debug("Virtual directory already present: %s", name) return True # The target physical path must exist. if not os.path.isdir(sourcepath): log.error("Path is not present: %s", sourcepath) return False ps_cmd = [ "New-WebVirtualDirectory", "-Name", rf"'{name}'", "-Site", rf"'{site}'", "-PhysicalPath", rf"'{sourcepath}'", ] if app != _DEFAULT_APP: ps_cmd.extend(["-Application", rf"'{app}'"]) cmd_ret = _srvmgr(ps_cmd) if cmd_ret["retcode"] != 0: msg = "Unable to create virtual directory: {}\nError: {}".format( name, cmd_ret["stderr"] ) raise CommandExecutionError(msg) new_vdirs = list_vdirs(site, app) if name in new_vdirs: log.debug("Virtual directory created successfully: %s", name) return True log.error("Unable to create virtual directory: %s", name) return False def remove_vdir(name, site, app=_DEFAULT_APP): """ Remove an IIS virtual directory. Args: name (str): The virtual directory name. site (str): The IIS site name. app (str): The IIS application. Returns: bool: True if successful, otherwise False CLI Example: .. code-block:: bash salt '*' win_iis.remove_vdir name='vdir0' site='site0' """ current_vdirs = list_vdirs(site, app) app_path = os.path.join(*app.rstrip("/").split("/")) if app_path: app_path = f"{app_path}\\" vdir_path = rf"IIS:\Sites\{site}\{app_path}{name}" if name not in current_vdirs: log.debug("Virtual directory already absent: %s", name) return True # We use Remove-Item here instead of Remove-WebVirtualDirectory, since the # latter has a bug that causes it to always prompt for user input. ps_cmd = ["Remove-Item", "-Path", rf"'{vdir_path}'", "-Recurse"] cmd_ret = _srvmgr(ps_cmd) if cmd_ret["retcode"] != 0: msg = "Unable to remove virtual directory: {}\nError: {}".format( name, cmd_ret["stderr"] ) raise CommandExecutionError(msg) new_vdirs = list_vdirs(site, app) if name not in new_vdirs: log.debug("Virtual directory removed successfully: %s", name) return True log.error("Unable to remove virtual directory: %s", name) return False def list_backups(): r""" List the IIS Configuration Backups on the System. .. versionadded:: 2017.7.0 .. note:: Backups are made when a configuration is edited. Manual backups are stored in the ``$env:Windir\System32\inetsrv\backup`` folder. Returns: dict: A dictionary of IIS Configurations backed up on the system. CLI Example: .. code-block:: bash salt '*' win_iis.list_backups """ ret = dict() ps_cmd = [ "Get-WebConfigurationBackup", "|", "Select Name, CreationDate,", '@{N="FormattedDate"; E={$_.CreationDate.ToString("G")}}', ] cmd_ret = _srvmgr(cmd=ps_cmd, return_json=True) try: items = salt.utils.json.loads(cmd_ret["stdout"], strict=False) except ValueError: raise CommandExecutionError("Unable to parse return data as Json.") for item in items: if item["FormattedDate"]: ret[item["Name"]] = item["FormattedDate"] else: ret[item["Name"]] = item["CreationDate"] if not ret: log.warning("No backups found in output: %s", cmd_ret) return ret def create_backup(name): r""" Backup an IIS Configuration on the System. .. versionadded:: 2017.7.0 .. note:: Backups are stored in the ``$env:Windir\System32\inetsrv\backup`` folder. Args: name (str): The name to give the backup Returns: bool: True if successful, otherwise False CLI Example: .. code-block:: bash salt '*' win_iis.create_backup good_config_20170209 """ if name in list_backups(): raise CommandExecutionError(f"Backup already present: {name}") ps_cmd = ["Backup-WebConfiguration", "-Name", f"'{name}'"] cmd_ret = _srvmgr(ps_cmd) if cmd_ret["retcode"] != 0: msg = "Unable to backup web configuration: {}\nError: {}".format( name, cmd_ret["stderr"] ) raise CommandExecutionError(msg) return name in list_backups() def remove_backup(name): """ Remove an IIS Configuration backup from the System. .. versionadded:: 2017.7.0 Args: name (str): The name of the backup to remove Returns: bool: True if successful, otherwise False CLI Example: .. code-block:: bash salt '*' win_iis.remove_backup backup_20170209 """ if name not in list_backups(): log.debug("Backup already removed: %s", name) return True ps_cmd = ["Remove-WebConfigurationBackup", "-Name", f"'{name}'"] cmd_ret = _srvmgr(ps_cmd) if cmd_ret["retcode"] != 0: msg = "Unable to remove web configuration: {}\nError: {}".format( name, cmd_ret["stderr"] ) raise CommandExecutionError(msg) return name not in list_backups() def list_worker_processes(apppool): """ Returns a list of worker processes that correspond to the passed application pool. .. versionadded:: 2017.7.0 Args: apppool (str): The application pool to query Returns: dict: A dictionary of worker processes with their process IDs CLI Example: .. code-block:: bash salt '*' win_iis.list_worker_processes 'My App Pool' """ ps_cmd = ["Get-ChildItem", rf"'IIS:\AppPools\{apppool}\WorkerProcesses'"] cmd_ret = _srvmgr(cmd=ps_cmd, return_json=True) try: items = salt.utils.json.loads(cmd_ret["stdout"], strict=False) except ValueError: raise CommandExecutionError("Unable to parse return data as Json.") ret = dict() for item in items: ret[item["processId"]] = item["appPoolName"] if not ret: log.warning("No backups found in output: %s", cmd_ret) return ret def get_webapp_settings(name, site, settings): r""" .. versionadded:: 2017.7.0 Get the value of the setting for the IIS web application. .. note:: Params are case sensitive :param str name: The name of the IIS web application. :param str site: The site name contains the web application. Example: Default Web Site :param str settings: A dictionary of the setting names and their values. Available settings: physicalPath, applicationPool, userName, password Returns: dict: A dictionary of the provided settings and their values. CLI Example: .. code-block:: bash salt '*' win_iis.get_webapp_settings name='app0' site='Default Web Site' settings="['physicalPath','applicationPool']" """ ret = dict() pscmd = list() availableSettings = ("physicalPath", "applicationPool", "userName", "password") if not settings: log.warning("No settings provided") return ret pscmd.append(r"$Settings = @{};") # Verify setting is ine predefined settings and append relevant query command per setting key for setting in settings: if setting in availableSettings: if setting == "userName" or setting == "password": pscmd.append( " $Property = Get-WebConfigurationProperty -Filter" " \"system.applicationHost/sites/site[@name='{}']/application[@path='/{}']/virtualDirectory[@path='/']\"".format( site, name ) ) pscmd.append(rf' -Name "{setting}" -ErrorAction Stop | select Value;') pscmd.append( r" $Property = $Property | Select-Object -ExpandProperty Value;" ) pscmd.append(rf" $Settings['{setting}'] = [String] $Property;") pscmd.append(r" $Property = $Null;") if setting == "physicalPath" or setting == "applicationPool": pscmd.append(rf" $Property = (get-webapplication {name}).{setting};") pscmd.append(rf" $Settings['{setting}'] = [String] $Property;") pscmd.append(r" $Property = $Null;") else: availSetStr = ", ".join(availableSettings) message = ( "Unexpected setting:" + setting + ". Available settings are: " + availSetStr ) raise SaltInvocationError(message) pscmd.append(" $Settings") # Run commands and return data as json cmd_ret = _srvmgr(cmd="".join(pscmd), return_json=True) # Update dict var to return data try: items = salt.utils.json.loads(cmd_ret["stdout"], strict=False) if isinstance(items, list): ret.update(items[0]) else: ret.update(items) except ValueError: log.error("Unable to parse return data as Json.") if None in ret.values(): message = ( "Some values are empty - please validate site and web application names." " Some commands are case sensitive" ) raise SaltInvocationError(message) return ret def set_webapp_settings(name, site, settings): r""" .. versionadded:: 2017.7.0 Configure an IIS application. .. note:: This function only configures an existing app. Params are case sensitive. :param str name: The IIS application. :param str site: The IIS site name. :param str settings: A dictionary of the setting names and their values. - physicalPath: The physical path of the webapp. - applicationPool: The application pool for the webapp. - userName: "connectAs" user - password: "connectAs" password for user :return: A boolean representing whether all changes succeeded. :rtype: bool CLI Example: .. code-block:: bash salt '*' win_iis.set_webapp_settings name='app0' site='site0' settings="{'physicalPath': 'C:\site0', 'apppool': 'site0'}" """ pscmd = list() current_apps = list_apps(site) current_sites = list_sites() availableSettings = ("physicalPath", "applicationPool", "userName", "password") # Validate params if name not in current_apps: msg = "Application" + name + "doesn't exist" raise SaltInvocationError(msg) if site not in current_sites: msg = "Site" + site + "doesn't exist" raise SaltInvocationError(msg) if not settings: msg = "No settings provided" raise SaltInvocationError(msg) # Treat all values as strings for the purpose of comparing them to existing values & validate settings exists in predefined settings list for setting in settings.keys(): if setting in availableSettings: settings[setting] = str(settings[setting]) else: availSetStr = ", ".join(availableSettings) log.error("Unexpected setting: %s ", setting) log.error("Available settings: %s", availSetStr) msg = "Unexpected setting:" + setting + " Available settings:" + availSetStr raise SaltInvocationError(msg) # Check if settings already configured current_settings = get_webapp_settings( name=name, site=site, settings=settings.keys() ) if settings == current_settings: log.warning("Settings already contain the provided values.") return True for setting in settings: # If the value is numeric, don't treat it as a string in PowerShell. try: complex(settings[setting]) value = settings[setting] except ValueError: value = f"'{settings[setting]}'" # Append relevant update command per setting key if setting == "userName" or setting == "password": pscmd.append( " Set-WebConfigurationProperty -Filter" " \"system.applicationHost/sites/site[@name='{}']/application[@path='/{}']/virtualDirectory[@path='/']\"".format( site, name ) ) pscmd.append(f' -Name "{setting}" -Value {value};') if setting == "physicalPath" or setting == "applicationPool": pscmd.append( r' Set-ItemProperty "IIS:\Sites\{}\{}" -Name {} -Value {};'.format( site, name, setting, value ) ) if setting == "physicalPath": if not os.path.isdir(settings[setting]): msg = "Path is not present: " + settings[setting] raise SaltInvocationError(msg) # Run commands cmd_ret = _srvmgr(pscmd) # Verify commands completed successfully if cmd_ret["retcode"] != 0: msg = f"Unable to set settings for web application {name}" raise SaltInvocationError(msg) # verify changes new_settings = get_webapp_settings(name=name, site=site, settings=settings.keys()) failed_settings = dict() for setting in settings: if str(settings[setting]) != str(new_settings[setting]): failed_settings[setting] = settings[setting] if failed_settings: log.error("Failed to change settings: %s", failed_settings) return False log.debug("Settings configured successfully: %s", list(settings)) return True def get_webconfiguration_settings(name, settings): r""" Get the webconfiguration settings for the IIS PSPath. Args: name (str): The PSPath of the IIS webconfiguration settings. settings (list): A list of dictionaries containing setting name and filter. Returns: dict: A list of dictionaries containing setting name, filter and value. CLI Example: .. code-block:: bash salt '*' win_iis.get_webconfiguration_settings name='IIS:\' settings="[{'name': 'enabled', 'filter': 'system.webServer/security/authentication/anonymousAuthentication'}]" """ ret = {} ps_cmd = [r"$Settings = New-Object System.Collections.ArrayList;"] ps_cmd_validate = [] settings = _prepare_settings(name, settings) if not settings: log.warning("No settings provided") return ret for setting in settings: # Build the commands to verify that the property names are valid. ps_cmd_validate.extend( [ "Get-WebConfigurationProperty", "-PSPath", f"'{name}'", "-Filter", "'{}'".format(setting["filter"]), "-Name", "'{}'".format(setting["name"]), "-ErrorAction", "Stop", "|", "Out-Null;", ] ) # Some ItemProperties are Strings and others are ConfigurationAttributes. # Since the former doesn't have a Value property, we need to account # for this. ps_cmd.append(f"$Property = Get-WebConfigurationProperty -PSPath '{name}'") ps_cmd.append( "-Name '{}' -Filter '{}' -ErrorAction Stop;".format( setting["name"], setting["filter"] ) ) if setting["name"].split(".")[-1] == "Collection": if "value" in setting: ps_cmd.append( "$Property = $Property | select -Property {} ;".format( ",".join(list(setting["value"][0].keys())) ) ) ps_cmd.append( "$Settings.add(@{{filter='{0}';name='{1}';value=[System.Collections.ArrayList]" " @($Property)}})| Out-Null;".format(setting["filter"], setting["name"]) ) else: ps_cmd.append(r"if (([String]::IsNullOrEmpty($Property) -eq $False) -and") ps_cmd.append(r"($Property.GetType()).Name -eq 'ConfigurationAttribute') {") ps_cmd.append(r"$Property = $Property | Select-Object") ps_cmd.append(r"-ExpandProperty Value };") ps_cmd.append( "$Settings.add(@{{filter='{0}';name='{1}';value=[String] $Property}})|" " Out-Null;".format(setting["filter"], setting["name"]) ) ps_cmd.append(r"$Property = $Null;") # Validate the setting names that were passed in. cmd_ret = _srvmgr(cmd=ps_cmd_validate, return_json=True) if cmd_ret["retcode"] != 0: message = ( "One or more invalid property names were specified for the provided" " container." ) raise SaltInvocationError(message) ps_cmd.append("$Settings") cmd_ret = _srvmgr(cmd=ps_cmd, return_json=True) try: ret = salt.utils.json.loads(cmd_ret["stdout"], strict=False) except ValueError: raise CommandExecutionError("Unable to parse return data as Json.") return ret def set_webconfiguration_settings(name, settings): r""" Set the value of the setting for an IIS container. Args: name (str): The PSPath of the IIS webconfiguration settings. settings (list): A list of dictionaries containing setting name, filter and value. Returns: bool: True if successful, otherwise False CLI Example: .. code-block:: bash salt '*' win_iis.set_webconfiguration_settings name='IIS:\' settings="[{'name': 'enabled', 'filter': 'system.webServer/security/authentication/anonymousAuthentication', 'value': False}]" """ ps_cmd = [] settings = _prepare_settings(name, settings) if not settings: log.warning("No settings provided") return False # Treat all values as strings for the purpose of comparing them to existing values. for idx, setting in enumerate(settings): if setting["name"].split(".")[-1] != "Collection": settings[idx]["value"] = str(setting["value"]) current_settings = get_webconfiguration_settings(name=name, settings=settings) if settings == current_settings: log.debug("Settings already contain the provided values.") return True for setting in settings: # If the value is numeric, don't treat it as a string in PowerShell. if setting["name"].split(".")[-1] != "Collection": try: complex(setting["value"]) value = setting["value"] except ValueError: value = "'{}'".format(setting["value"]) else: configelement_list = [] for value_item in setting["value"]: configelement_construct = [] for key, value in value_item.items(): configelement_construct.append(f"{key}='{value}'") configelement_list.append( "@{" + ";".join(configelement_construct) + "}" ) value = ",".join(configelement_list) ps_cmd.extend( [ "Set-WebConfigurationProperty", "-PSPath", f"'{name}'", "-Filter", "'{}'".format(setting["filter"]), "-Name", "'{}'".format(setting["name"]), "-Value", f"{value};", ] ) cmd_ret = _srvmgr(ps_cmd) if cmd_ret["retcode"] != 0: msg = f"Unable to set settings for {name}" raise CommandExecutionError(msg) # Get the fields post-change so that we can verify tht all values # were modified successfully. Track the ones that weren't. new_settings = get_webconfiguration_settings(name=name, settings=settings) failed_settings = [] for idx, setting in enumerate(settings): is_collection = setting["name"].split(".")[-1] == "Collection" if ( not is_collection and str(setting["value"]) != str(new_settings[idx]["value"]) ) or ( is_collection and list(map(dict, setting["value"])) != list(map(dict, new_settings[idx]["value"])) ): failed_settings.append(setting) if failed_settings: log.error("Failed to change settings: %s", failed_settings) return False log.debug("Settings configured successfully: %s", settings) return True