salt/modules/win_iis.py
# -*- coding: utf-8 -*-
'''
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 python libs
from __future__ import absolute_import, print_function, unicode_literals
import decimal
import logging
import os
import re
import yaml
# Import salt libs
import salt.utils.json
import salt.utils.platform
from salt.ext.six.moves import range
from salt.exceptions import SaltInvocationError, CommandExecutionError
from salt.ext import six
from salt.ext.six.moves import map
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, six.text_type(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', r"'Cert:\LocalMachine\{0}'".format(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("{0}.{1}".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 = 'ConvertTo-Json -Compress -Depth 4 -InputObject @({0})' \
''.format(cmd)
cmd = 'Import-Module WebAdministration; {0}'.format(cmd)
ret = __salt__['cmd.run_all'](cmd, shell='powershell', python_shell=True)
if ret['retcode'] != 0:
msg = 'Unable to execute command: {0}\nError: {1}' \
''.format(cmd, ret['stderr'])
log.error(msg)
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:
match = re.search(r'Collection\[(\{.*\})\]', setting['name'])
if match:
name = setting['name'][:match.start(1)-1]
match_dict = yaml.load(match.group(1))
index = _collection_match_to_index(pspath, setting['filter'], name, match_dict)
if index != -1:
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, applicationDefaults, 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
# ApplicationDefaults
application_defaults = dict()
for attribute in item['applicationDefaults']['Attributes']:
application_defaults.update({attribute['Name']: attribute['Value']})
# ApplicationDefaults
ret[item['name']] = {'apppool': item['applicationPool'],
'bindings': bindings,
'applicationDefaults': application_defaults,
'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', preload=''):
'''
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.)
preload (bool): Whether preloading should be enabled
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' preload=True
'''
protocol = six.text_type(protocol).lower()
site_path = r'IIS:\Sites\{0}'.format(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 '{0}' specified. Valid formats:"
' {1}').format(protocol, _VALID_PROTOCOLS)
raise SaltInvocationError(message)
ps_cmd = ['New-Item',
'-Path', r"'{0}'".format(site_path),
'-PhysicalPath', r"'{0}'".format(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', "'{0}'".format(site_path),
'-Name', 'ApplicationPool',
'-Value', "'{0}';".format(apppool)])
if preload:
ps_cmd.extend(['Set-ItemProperty',
'-Path', "'{0}'".format(site_path),
'-Name', 'applicationDefaults.preloadEnabled',
'-Value', "{0};".format(preload)])
cmd_ret = _srvmgr(ps_cmd)
if cmd_ret['retcode'] != 0:
msg = 'Unable to create site: {0}\nError: {1}' \
''.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, preload=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.
preload (bool): Whether preloading should be enabled
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' preload=True
'''
site_path = r'IIS:\Sites\{0}'.format(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', r"'{0}'".format(site_path),
'-Name', 'PhysicalPath',
'-Value', r"'{0}'".format(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', r"'{0}'".format(site_path),
'-Name', 'ApplicationPool',
'-Value', r"'{0}'".format(apppool)])
if preload:
ps_cmd.extend(['Set-ItemProperty',
'-Path', "'{0}'".format(site_path),
'-Name', 'applicationDefaults.preloadEnabled',
'-Value', "{0};".format(preload)])
cmd_ret = _srvmgr(ps_cmd)
if cmd_ret['retcode'] != 0:
msg = 'Unable to modify site: {0}\nError: {1}' \
''.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', r"'{0}'".format(name)]
cmd_ret = _srvmgr(ps_cmd)
if cmd_ret['retcode'] != 0:
msg = 'Unable to remove site: {0}\nError: {1}' \
''.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', r"'{0}'".format(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', r"'{0}'".format(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 = six.text_type(protocol).lower()
name = _get_binding_info(hostheader, ipaddress, port)
if protocol not in _VALID_PROTOCOLS:
message = ("Invalid protocol '{0}' specified. Valid formats:"
' {1}').format(protocol, _VALID_PROTOCOLS)
raise SaltInvocationError(message)
if sslflags:
sslflags = int(sslflags)
if sslflags not in _VALID_SSL_FLAGS:
message = ("Invalid sslflags '{0}' specified. Valid sslflags range:"
' {1}..{2}').format(sslflags, _VALID_SSL_FLAGS[0], _VALID_SSL_FLAGS[-1])
raise SaltInvocationError(message)
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', "'{0}'".format(site),
'-HostHeader', "'{0}'".format(hostheader),
'-IpAddress', "'{0}'".format(ipaddress),
'-Port', "'{0}'".format(port),
'-Protocol', "'{0}'".format(protocol),
'-SslFlags', '{0}'.format(sslflags)]
else:
ps_cmd = ['New-WebBinding',
'-Name', "'{0}'".format(site),
'-HostHeader', "'{0}'".format(hostheader),
'-IpAddress', "'{0}'".format(ipaddress),
'-Port', "'{0}'".format(port),
'-Protocol', "'{0}'".format(protocol)]
cmd_ret = _srvmgr(ps_cmd)
if cmd_ret['retcode'] != 0:
msg = 'Unable to create binding: {0}\nError: {1}' \
''.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:
message = ("Invalid sslflags '{0}' specified. Valid sslflags range:"
' {1}..{2}').format(sslflags, _VALID_SSL_FLAGS[0], _VALID_SSL_FLAGS[-1])
raise SaltInvocationError(message)
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,
six.text_type(port) if port is not None else six.text_type(p),
hostheader if hostheader is not None else h])
if new_binding != binding:
ps_cmd = ['Set-WebBinding',
'-Name', "'{0}'".format(site),
'-BindingInformation', "'{0}'".format(binding),
'-PropertyName', 'BindingInformation',
'-Value', "'{0}'".format(new_binding)]
cmd_ret = _srvmgr(ps_cmd)
if cmd_ret['retcode'] != 0:
msg = 'Unable to modify binding: {0}\nError: {1}' \
''.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', "'{0}'".format(site),
'-BindingInformation', "'{0}'".format(new_binding),
'-PropertyName', 'sslflags',
'-Value', "'{0}'".format(sslflags)]
cmd_ret = _srvmgr(ps_cmd)
if cmd_ret['retcode'] != 0:
msg = 'Unable to modify binding SSL Flags: {0}\nError: {1}' \
''.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', "'{0}'".format(hostheader),
'-IpAddress', "'{0}'".format(ipaddress),
'-Port', "'{0}'".format(port)]
cmd_ret = _srvmgr(ps_cmd)
if cmd_ret['retcode'] != 0:
msg = 'Unable to remove binding: {0}\nError: {1}' \
''.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 = six.text_type(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\{0}".format(binding_info.replace(':', '!'))
if sslflags not in _VALID_SSL_FLAGS:
message = ("Invalid sslflags '{0}' specified. Valid sslflags range: "
"{1}..{2}").format(sslflags, _VALID_SSL_FLAGS[0],
_VALID_SSL_FLAGS[-1])
raise SaltInvocationError(message)
# 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', "'{0}'".format(iis7path),
'-Thumbprint', "'{0}'".format(name)]
else:
ps_cmd = ['New-Item',
'-Path', "'{0}'".format(binding_path),
'-Thumbprint', "'{0}'".format(name),
'-SSLFlags', '{0}'.format(sslflags)]
cmd_ret = _srvmgr(ps_cmd)
if cmd_ret['retcode'] != 0:
msg = 'Unable to create certificate binding: {0}\nError: {1}' \
''.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 = six.text_type(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', r" {{ $_.Name -Eq '{0}' }};".format(site),
'$Binding = $Site.Bindings.Collection',
r"| Where-Object { $_.bindingInformation",
r"-Eq '{0}' }};".format(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: {0}\nError: {1}' \
''.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 = r'IIS:\AppPools\{0}'.format(name)
if name in current_apppools:
log.debug("Application pool '%s' already present.", name)
return True
ps_cmd = ['New-Item', '-Path', r"'{0}'".format(apppool_path)]
cmd_ret = _srvmgr(ps_cmd)
if cmd_ret['retcode'] != 0:
msg = 'Unable to create application pool: {0}\nError: {1}' \
''.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 = r'IIS:\AppPools\{0}'.format(name)
if name not in current_apppools:
log.debug('Application pool already absent: %s', name)
return True
ps_cmd = ['Remove-Item', '-Path', r"'{0}'".format(apppool_path), '-Recurse']
cmd_ret = _srvmgr(ps_cmd)
if cmd_ret['retcode'] != 0:
msg = 'Unable to remove application pool: {0}\nError: {1}' \
''.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', r"'{0}'".format(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', r"'{0}'".format(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', r"'{0}'".format(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 = r"IIS:\{0}\{1}".format(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', "'{0}'".format(container_path),
'-Name', "'{0}'".format(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("$Property = Get-ItemProperty -Path '{0}'".format(container_path))
ps_cmd.append("-Name '{0}' -ErrorAction Stop;".format(setting))
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['{0}'] = [String] $Property;".format(setting))
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 = r"IIS:\{0}\{1}".format(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] = six.text_type(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 = "'{0}'".format(settings[setting])
# Map to numeric to support server 2008
if setting == 'processModel.identityType' and settings[setting] in identityType_map2numeric.keys():
value = identityType_map2numeric[settings[setting]]
ps_cmd.extend(['Set-ItemProperty',
'-Path', "'{0}'".format(container_path),
'-Name', "'{0}'".format(setting),
'-Value', '{0};'.format(value)])
cmd_ret = _srvmgr(ps_cmd)
if cmd_ret['retcode'] != 0:
msg = 'Unable to set settings for {0}: {1}'.format(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.keys():
settings[setting] = identityType_map2string[settings[setting]]
if six.text_type(settings[setting]) != six.text_type(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("Get-WebApplication -Site '{0}'".format(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', "'{0}'".format(name),
'-Site', "'{0}'".format(site),
'-PhysicalPath', "'{0}'".format(sourcepath)]
if apppool:
ps_cmd.extend(['-ApplicationPool', "'{0}'".format(apppool)])
cmd_ret = _srvmgr(ps_cmd)
if cmd_ret['retcode'] != 0:
msg = 'Unable to create application: {0}\nError: {1}' \
''.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', "'{0}'".format(name),
'-Site', "'{0}'".format(site)]
cmd_ret = _srvmgr(ps_cmd)
if cmd_ret['retcode'] != 0:
msg = 'Unable to remove application: {0}\nError: {1}' \
''.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', r"'{0}'".format(site),
'-Application', r"'{0}'".format(app),
'|', "Select-Object PhysicalPath, @{ Name = 'name';",
r"Expression = { $_.path.Split('/')[-1] } }"]
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', r"'{0}'".format(name),
'-Site', r"'{0}'".format(site),
'-PhysicalPath', r"'{0}'".format(sourcepath)]
if app != _DEFAULT_APP:
ps_cmd.extend(['-Application', r"'{0}'".format(app)])
cmd_ret = _srvmgr(ps_cmd)
if cmd_ret['retcode'] != 0:
msg = 'Unable to create virtual directory: {0}\nError: {1}' \
''.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 = '{0}\\'.format(app_path)
vdir_path = r'IIS:\Sites\{0}\{1}{2}'.format(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', r"'{0}'".format(vdir_path),
'-Recurse']
cmd_ret = _srvmgr(ps_cmd)
if cmd_ret['retcode'] != 0:
msg = 'Unable to remove virtual directory: {0}\nError: {1}' \
''.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('Backup already present: {0}'.format(name))
ps_cmd = ['Backup-WebConfiguration',
'-Name', "'{0}'".format(name)]
cmd_ret = _srvmgr(ps_cmd)
if cmd_ret['retcode'] != 0:
msg = 'Unable to backup web configuration: {0}\nError: {1}' \
''.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', "'{0}'".format(name)]
cmd_ret = _srvmgr(ps_cmd)
if cmd_ret['retcode'] != 0:
msg = 'Unable to remove web configuration: {0}\nError: {1}' \
''.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',
r"'IIS:\AppPools\{0}\WorkerProcesses'".format(apppool)]
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='{0}']/application[@path='/{1}']/virtualDirectory[@path='/']\"".format(site, name))
pscmd.append(r' -Name "{0}" -ErrorAction Stop | select Value;'.format(setting))
pscmd.append(r' $Property = $Property | Select-Object -ExpandProperty Value;')
pscmd.append(r" $Settings['{0}'] = [String] $Property;".format(setting))
pscmd.append(r' $Property = $Null;')
if setting == "physicalPath" or setting == "applicationPool":
pscmd.append(r" $Property = (get-webapplication {0}).{1};".format(name, setting))
pscmd.append(r" $Settings['{0}'] = [String] $Property;".format(setting))
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=six.text_type().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 six.viewvalues(ret):
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] = six.text_type(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 = "'{0}'".format(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='{0}']/application[@path='/{1}']/virtualDirectory[@path='/']\"".format(site, name))
pscmd.append(" -Name \"{0}\" -Value {1};".format(setting, value))
if setting == "physicalPath" or setting == "applicationPool":
pscmd.append(r' Set-ItemProperty "IIS:\Sites\{0}\{1}" -Name {2} -Value {3};'.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 = 'Unable to set settings for web application {0}'.format(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 six.text_type(settings[setting]) != six.text_type(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, location=''):
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.
location (str): The location of the settings (optional)
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 = []
ps_cmd_validate = []
if not settings:
log.warning('No settings provided')
return ret
settings = _prepare_settings(name, settings)
ps_cmd.append(r'$Settings = New-Object System.Collections.ArrayList;')
for setting in settings:
# Build the commands to verify that the property names are valid.
ps_cmd_validate.extend(['Get-WebConfigurationProperty',
'-PSPath', "'{0}'".format(name),
'-Filter', "'{0}'".format(setting['filter']),
'-Name', "'{0}'".format(setting['name']),
'-Location', "'{0}'".format(location),
'-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("$Property = Get-WebConfigurationProperty -PSPath '{0}'".format(name))
ps_cmd.append("-Name '{0}' -Filter '{1}' -Location '{2}' -ErrorAction Stop;".format(setting['name'], setting['filter'], location))
if setting['name'].split('.')[-1] == 'Collection':
if 'value' in setting:
ps_cmd.append("$Property = $Property | select -Property {0} ;"
.format(",".join(list(setting['value'][0].keys()))))
ps_cmd.append("$Settings.add(@{{filter='{0}';name='{1}';location='{2}';value=[System.Collections.ArrayList] @($Property)}})| Out-Null;"
.format(setting['filter'], setting['name'], location))
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}';location='{2}';value=[String] $Property}})| Out-Null;"
.format(setting['filter'], setting['name'], location))
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, location=''):
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.
location (str): The location of the settings (optional)
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 = []
if not settings:
log.warning('No settings provided')
return False
settings = _prepare_settings(name, settings)
# 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'] = six.text_type(setting['value'])
current_settings = get_webconfiguration_settings(
name=name, settings=settings, location=location)
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 = "'{0}'".format(setting['value'])
else:
configelement_list = []
for value_item in setting['value']:
configelement_construct = []
for key, value in value_item.items():
configelement_construct.append("{0}='{1}'".format(key, value))
configelement_list.append('@{' + ';'.join(configelement_construct) + '}')
value = ','.join(configelement_list)
ps_cmd.extend(['Set-WebConfigurationProperty',
'-PSPath', "'{0}'".format(name),
'-Filter', "'{0}'".format(setting['filter']),
'-Name', "'{0}'".format(setting['name']),
'-Location', "'{0}'".format(location),
'-Value', '{0};'.format(value)])
cmd_ret = _srvmgr(ps_cmd)
if cmd_ret['retcode'] != 0:
msg = 'Unable to set settings for {0}'.format(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, location=location)
failed_settings = []
for idx, setting in enumerate(settings):
is_collection = setting['name'].split('.')[-1] == 'Collection'
if ((not is_collection and six.text_type(setting['value']) != six.text_type(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