Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add webconfiguration settings management (module + state). #49399

Merged
merged 7 commits into from
Sep 2, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
202 changes: 202 additions & 0 deletions salt/modules/win_iis.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,16 @@
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__)

Expand Down Expand Up @@ -160,6 +163,37 @@ def _srvmgr(cmd, return_json=False):
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.
Expand Down Expand Up @@ -1985,3 +2019,171 @@ def set_webapp_settings(name, site, settings):

log.debug('Settings configured successfully: {0}'.format(settings.keys()))
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'}]"
'''
tlemarchand marked this conversation as resolved.
Show resolved Hide resolved
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']),
'-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}' -ErrorAction Stop;".format(setting['name'], setting['filter']))
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}';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 = []

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)

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']),
'-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)

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
124 changes: 124 additions & 0 deletions salt/states/win_iis.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
# Import python libs
from __future__ import absolute_import, unicode_literals, print_function

# Import salt libs
from salt.ext.six.moves import map

# Define the module's virtual name
__virtualname__ = 'win_iis'
Expand Down Expand Up @@ -865,3 +867,125 @@ def set_app(name, site, settings=None):
ret['result'] = True

return ret


def webconfiguration_settings(name, settings=None):
r'''
Set the value of webconfiguration settings.

:param str name: The name of the IIS PSPath containing the settings.
Possible PSPaths are :
MACHINE, MACHINE/WEBROOT, IIS:\, IIS:\Sites\sitename, ...
:param dict settings: Dictionaries of dictionaries.
You can match a specific item in a collection with this syntax inside a key:
'Collection[{name: site0}].logFile.directory'

Example of usage for the ``MACHINE/WEBROOT`` PSPath:

.. code-block:: yaml

MACHINE-WEBROOT-level-security:
win_iis.webconfiguration_settings:
- name: 'MACHINE/WEBROOT'
- settings:
system.web/authentication/forms:
requireSSL: True
protection: "All"
credentials.passwordFormat: "SHA1"
system.web/httpCookies:
httpOnlyCookies: True

Example of usage for the ``IIS:\Sites\site0`` PSPath:

.. code-block:: yaml

site0-IIS-Sites-level-security:
win_iis.webconfiguration_settings:
- name: 'IIS:\Sites\site0'
- settings:
system.webServer/httpErrors:
errorMode: "DetailedLocalOnly"
system.webServer/security/requestFiltering:
allowDoubleEscaping: False
verbs.Collection:
- verb: TRACE
allowed: False
fileExtensions.allowUnlisted: False

Example of usage for the ``IIS:\`` PSPath with a collection matching:

.. code-block:: yaml

site0-IIS-level-security:
win_iis.webconfiguration_settings:
- name: 'IIS:\'
- settings:
system.applicationHost/sites:
'Collection[{name: site0}].logFile.directory': 'C:\logs\iis\site0'

'''

ret = {'name': name,
'changes': {},
'comment': str(),
'result': None}

if not settings:
ret['comment'] = 'No settings to change provided.'
ret['result'] = True
return ret

ret_settings = {
'changes': {},
'failures': {},
}

settings_list = list()

for filter, filter_settings in settings.items():
for setting_name, value in filter_settings.items():
settings_list.append({'filter': filter, 'name': setting_name, 'value': value})

current_settings_list = __salt__['win_iis.get_webconfiguration_settings'](name=name,
settings=settings_list)
for idx, setting in enumerate(settings_list):

is_collection = setting['name'].split('.')[-1] == 'Collection'

if ((is_collection and list(map(dict, setting['value'])) != list(map(dict, current_settings_list[idx]['value'])))
or (not is_collection and str(setting['value']) != str(current_settings_list[idx]['value']))):
ret_settings['changes'][setting['filter'] + '.' + setting['name']] = {'old': current_settings_list[idx]['value'],
'new': settings_list[idx]['value']}
if not ret_settings['changes']:
ret['comment'] = 'Settings already contain the provided values.'
ret['result'] = True
return ret
elif __opts__['test']:
ret['comment'] = 'Settings will be changed.'
ret['changes'] = ret_settings
return ret

__salt__['win_iis.set_webconfiguration_settings'](name=name, settings=settings_list)

new_settings_list = __salt__['win_iis.get_webconfiguration_settings'](name=name,
settings=settings_list)
for idx, setting in enumerate(settings_list):

is_collection = setting['name'].split('.')[-1] == 'Collection'

if ((is_collection and setting['value'] != new_settings_list[idx]['value'])
or (not is_collection and str(setting['value']) != str(new_settings_list[idx]['value']))):
ret_settings['failures'][setting['filter'] + '.' + setting['name']] = {'old': current_settings_list[idx]['value'],
'new': new_settings_list[idx]['value']}
ret_settings['changes'].pop(setting['filter'] + '.' + setting['name'], None)

if ret_settings['failures']:
ret['comment'] = 'Some settings failed to change.'
ret['changes'] = ret_settings
ret['result'] = False
else:
ret['comment'] = 'Set settings to contain the provided values.'
ret['changes'] = ret_settings['changes']
ret['result'] = True

return ret