From ccfa7226dd6c1e46f69abe5310a00ac2facd681e Mon Sep 17 00:00:00 2001 From: Thomas Lemarchand Date: Thu, 30 Aug 2018 11:15:20 +0200 Subject: [PATCH 1/4] Added raw prefix to regex string. --- salt/modules/win_iis.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/salt/modules/win_iis.py b/salt/modules/win_iis.py index 661f1d8a5c..3c420db518 100644 --- a/salt/modules/win_iis.py +++ b/salt/modules/win_iis.py @@ -160,6 +160,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 wit 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. From 1da593a8f9233ae392f1abfc34d515502d0c87ea Mon Sep 17 00:00:00 2001 From: Thomas Lemarchand Date: Thu, 30 Aug 2018 17:57:08 +0200 Subject: [PATCH 2/4] Simplify documentation (removed jinja variable ) --- salt/states/win_iis.py | 122 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 122 insertions(+) diff --git a/salt/states/win_iis.py b/salt/states/win_iis.py index e1ea140bf5..1da0ae4e81 100644 --- a/salt/states/win_iis.py +++ b/salt/states/win_iis.py @@ -865,3 +865,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 From 4187b3396ca6504256004b03d480a960962f3798 Mon Sep 17 00:00:00 2001 From: Thomas Lemarchand Date: Fri, 31 Aug 2018 17:51:40 +0200 Subject: [PATCH 3/4] Fix doc typo --- salt/modules/win_iis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/salt/modules/win_iis.py b/salt/modules/win_iis.py index 3c420db518..abe02b714a 100644 --- a/salt/modules/win_iis.py +++ b/salt/modules/win_iis.py @@ -173,7 +173,7 @@ def _collection_match_to_index(pspath, colfilter, name, match): def _prepare_settings(pspath, settings): ''' - Prepare settings before execution wit get or set functions. + Prepare settings before execution with get or set functions. Removes settings with a match parameter when index is not found. ''' prepared_settings = [] From ab6365de61aaae6aebe9f5395111631a6deeafa3 Mon Sep 17 00:00:00 2001 From: Tyler Johnson Date: Tue, 29 Oct 2019 16:01:44 -0600 Subject: [PATCH 4/4] Various improvements and fixes to win_iis --- salt/modules/win_iis.py | 178 +++++++++++++++- salt/states/win_iis.py | 21 +- tests/unit/modules/test_win_iis.py | 325 +++++++++++++++++++++-------- tests/unit/states/test_win_iis.py | 114 ++++++++++ 4 files changed, 539 insertions(+), 99 deletions(-) create mode 100644 tests/unit/states/test_win_iis.py diff --git a/salt/modules/win_iis.py b/salt/modules/win_iis.py index abe02b714a..73dc2305da 100644 --- a/salt/modules/win_iis.py +++ b/salt/modules/win_iis.py @@ -13,12 +13,14 @@ Microsoft IIS site management via WebAdministration powershell module from __future__ import absolute_import, print_function, unicode_literals import decimal import logging +import re import os +import yaml # Import salt libs import salt.utils.json import salt.utils.platform -from salt.ext.six.moves import range +from salt.ext.six.moves import range, map from salt.exceptions import SaltInvocationError, CommandExecutionError from salt.ext import six @@ -178,12 +180,20 @@ def _prepare_settings(pspath, settings): ''' prepared_settings = [] for setting in settings: + if setting.get('name', None) is None: + log.warning('win_iis: Setting has no name: {}'.format(setting)) + continue + if setting.get('filter', None) is None: + log.warning('win_iis: Setting has no filter: {}'.format(setting)) + continue 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: + if index == -1: + log.warning('win_iis: No match found for setting: {}'.format(setting)) + else: setting['name'] = setting['name'].replace(match.group(1), str(index)) prepared_settings.append(setting) else: @@ -2016,3 +2026,167 @@ 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'}]" + ''' + 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', "'{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 = [] + 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'] = 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 diff --git a/salt/states/win_iis.py b/salt/states/win_iis.py index 1da0ae4e81..b8098c8c85 100644 --- a/salt/states/win_iis.py +++ b/salt/states/win_iis.py @@ -11,6 +11,7 @@ from Microsoft IIS. # Import python libs from __future__ import absolute_import, unicode_literals, print_function +from salt.ext.six.moves import map # Define the module's virtual name @@ -944,13 +945,15 @@ def webconfiguration_settings(name, settings=None): 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) + 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']))) + # If this is a new setting and not an update to an existing setting + if len(current_settings_list) <= idx: + ret_settings['changes'][setting['filter'] + '.' + setting['name']] = {'old': {}, + 'new': settings_list[idx]['value']} + elif ((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']} @@ -963,19 +966,17 @@ def webconfiguration_settings(name, settings=None): ret['changes'] = ret_settings return ret - __salt__['win_iis.set_webconfiguration_settings'](name=name, settings=settings_list) + success = __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) + 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) + ret_settings['changes'].get(setting['filter'] + '.' + setting['name'], None) if ret_settings['failures']: ret['comment'] = 'Some settings failed to change.' @@ -984,6 +985,6 @@ def webconfiguration_settings(name, settings=None): else: ret['comment'] = 'Set settings to contain the provided values.' ret['changes'] = ret_settings['changes'] - ret['result'] = True + ret['result'] = success return ret diff --git a/tests/unit/modules/test_win_iis.py b/tests/unit/modules/test_win_iis.py index 2107245206..a215632124 100644 --- a/tests/unit/modules/test_win_iis.py +++ b/tests/unit/modules/test_win_iis.py @@ -131,9 +131,9 @@ class WinIisTestCase(TestCase, LoaderModuleMockMixin): ''' with patch('salt.modules.win_iis._srvmgr', MagicMock(return_value={'retcode': 0})), \ - patch('salt.modules.win_iis.list_apppools', - MagicMock(return_value=dict())), \ - patch.dict(win_iis.__salt__): + patch('salt.modules.win_iis.list_apppools', + MagicMock(return_value=dict())), \ + patch.dict(win_iis.__salt__): self.assertTrue(win_iis.create_apppool('MyTestPool')) def test_list_apppools(self): @@ -141,8 +141,8 @@ class WinIisTestCase(TestCase, LoaderModuleMockMixin): Test - List all configured IIS application pools. ''' with patch.dict(win_iis.__salt__), \ - patch('salt.modules.win_iis._srvmgr', - MagicMock(return_value=LIST_APPPOOLS_SRVMGR)): + patch('salt.modules.win_iis._srvmgr', + MagicMock(return_value=LIST_APPPOOLS_SRVMGR)): self.assertEqual(win_iis.list_apppools(), APPPOOL_LIST) def test_remove_apppool(self): @@ -150,12 +150,12 @@ class WinIisTestCase(TestCase, LoaderModuleMockMixin): Test - Remove an IIS application pool. ''' with patch.dict(win_iis.__salt__), \ - patch('salt.modules.win_iis._srvmgr', - MagicMock(return_value={'retcode': 0})), \ - patch('salt.modules.win_iis.list_apppools', - MagicMock(return_value={'MyTestPool': { - 'applications': list(), - 'state': 'Started'}})): + patch('salt.modules.win_iis._srvmgr', + MagicMock(return_value={'retcode': 0})), \ + patch('salt.modules.win_iis.list_apppools', + MagicMock(return_value={'MyTestPool': { + 'applications': list(), + 'state': 'Started'}})): self.assertTrue(win_iis.remove_apppool('MyTestPool')) def test_restart_apppool(self): @@ -163,8 +163,8 @@ class WinIisTestCase(TestCase, LoaderModuleMockMixin): Test - Restart an IIS application pool. ''' with patch.dict(win_iis.__salt__), \ - patch('salt.modules.win_iis._srvmgr', - MagicMock(return_value={'retcode': 0})): + patch('salt.modules.win_iis._srvmgr', + MagicMock(return_value={'retcode': 0})): self.assertTrue(win_iis.restart_apppool('MyTestPool')) def test_create_site(self): @@ -175,12 +175,12 @@ class WinIisTestCase(TestCase, LoaderModuleMockMixin): 'apppool': 'MyTestPool', 'hostheader': 'mytestsite.local', 'ipaddress': '*', 'port': 80, 'protocol': 'http'} with patch.dict(win_iis.__salt__), \ - patch('salt.modules.win_iis._srvmgr', - MagicMock(return_value={'retcode': 0})), \ - patch('salt.modules.win_iis.list_sites', - MagicMock(return_value=dict())), \ - patch('salt.modules.win_iis.list_apppools', - MagicMock(return_value=dict())): + patch('salt.modules.win_iis._srvmgr', + MagicMock(return_value={'retcode': 0})), \ + patch('salt.modules.win_iis.list_sites', + MagicMock(return_value=dict())), \ + patch('salt.modules.win_iis.list_apppools', + MagicMock(return_value=dict())): self.assertTrue(win_iis.create_site(**kwargs)) def test_create_site_failed(self): @@ -191,12 +191,12 @@ class WinIisTestCase(TestCase, LoaderModuleMockMixin): 'apppool': 'MyTestPool', 'hostheader': 'mytestsite.local', 'ipaddress': '*', 'port': 80, 'protocol': 'invalid-protocol-name'} with patch.dict(win_iis.__salt__), \ - patch('salt.modules.win_iis._srvmgr', - MagicMock(return_value={'retcode': 0})), \ - patch('salt.modules.win_iis.list_sites', - MagicMock(return_value=dict())), \ - patch('salt.modules.win_iis.list_apppools', - MagicMock(return_value=dict())): + patch('salt.modules.win_iis._srvmgr', + MagicMock(return_value={'retcode': 0})), \ + patch('salt.modules.win_iis.list_sites', + MagicMock(return_value=dict())), \ + patch('salt.modules.win_iis.list_apppools', + MagicMock(return_value=dict())): self.assertRaises(SaltInvocationError, win_iis.create_site, **kwargs) def test_remove_site(self): @@ -204,10 +204,10 @@ class WinIisTestCase(TestCase, LoaderModuleMockMixin): Test - Delete a website from IIS. ''' with patch.dict(win_iis.__salt__), \ - patch('salt.modules.win_iis._srvmgr', - MagicMock(return_value={'retcode': 0})), \ - patch('salt.modules.win_iis.list_sites', - MagicMock(return_value=SITE_LIST)): + patch('salt.modules.win_iis._srvmgr', + MagicMock(return_value={'retcode': 0})), \ + patch('salt.modules.win_iis.list_sites', + MagicMock(return_value=SITE_LIST)): self.assertTrue(win_iis.remove_site('MyTestSite')) def test_create_app(self): @@ -217,11 +217,11 @@ class WinIisTestCase(TestCase, LoaderModuleMockMixin): kwargs = {'name': 'testApp', 'site': 'MyTestSite', 'sourcepath': r'C:\inetpub\apps\testApp', 'apppool': 'MyTestPool'} with patch.dict(win_iis.__salt__), \ - patch('os.path.isdir', MagicMock(return_value=True)), \ - patch('salt.modules.win_iis._srvmgr', - MagicMock(return_value={'retcode': 0})), \ - patch('salt.modules.win_iis.list_apps', - MagicMock(return_value=APP_LIST)): + patch('os.path.isdir', MagicMock(return_value=True)), \ + patch('salt.modules.win_iis._srvmgr', + MagicMock(return_value={'retcode': 0})), \ + patch('salt.modules.win_iis.list_apps', + MagicMock(return_value=APP_LIST)): self.assertTrue(win_iis.create_app(**kwargs)) def test_list_apps(self): @@ -229,8 +229,8 @@ class WinIisTestCase(TestCase, LoaderModuleMockMixin): Test - Get all configured IIS applications for the specified site. ''' with patch.dict(win_iis.__salt__), \ - patch('salt.modules.win_iis._srvmgr', - MagicMock(return_value=LIST_APPS_SRVMGR)): + patch('salt.modules.win_iis._srvmgr', + MagicMock(return_value=LIST_APPS_SRVMGR)): self.assertEqual(win_iis.list_apps('MyTestSite'), APP_LIST) def test_remove_app(self): @@ -239,10 +239,10 @@ class WinIisTestCase(TestCase, LoaderModuleMockMixin): ''' kwargs = {'name': 'otherApp', 'site': 'MyTestSite'} with patch.dict(win_iis.__salt__), \ - patch('salt.modules.win_iis._srvmgr', - MagicMock(return_value={'retcode': 0})), \ - patch('salt.modules.win_iis.list_apps', - MagicMock(return_value=APP_LIST)): + patch('salt.modules.win_iis._srvmgr', + MagicMock(return_value={'retcode': 0})), \ + patch('salt.modules.win_iis.list_apps', + MagicMock(return_value=APP_LIST)): self.assertTrue(win_iis.remove_app(**kwargs)) def test_create_binding(self): @@ -252,10 +252,10 @@ class WinIisTestCase(TestCase, LoaderModuleMockMixin): kwargs = {'site': 'MyTestSite', 'hostheader': '', 'ipaddress': '*', 'port': 80, 'protocol': 'http', 'sslflags': 0} with patch.dict(win_iis.__salt__), \ - patch('salt.modules.win_iis._srvmgr', - MagicMock(return_value={'retcode': 0})), \ - patch('salt.modules.win_iis.list_bindings', - MagicMock(return_value=BINDING_LIST)): + patch('salt.modules.win_iis._srvmgr', + MagicMock(return_value={'retcode': 0})), \ + patch('salt.modules.win_iis.list_bindings', + MagicMock(return_value=BINDING_LIST)): self.assertTrue(win_iis.create_binding(**kwargs)) def test_create_binding_failed(self): @@ -265,10 +265,10 @@ class WinIisTestCase(TestCase, LoaderModuleMockMixin): kwargs = {'site': 'MyTestSite', 'hostheader': '', 'ipaddress': '*', 'port': 80, 'protocol': 'invalid-protocol-name', 'sslflags': 999} with patch.dict(win_iis.__salt__), \ - patch('salt.modules.win_iis._srvmgr', - MagicMock(return_value={'retcode': 0})), \ - patch('salt.modules.win_iis.list_bindings', - MagicMock(return_value=BINDING_LIST)): + patch('salt.modules.win_iis._srvmgr', + MagicMock(return_value={'retcode': 0})), \ + patch('salt.modules.win_iis.list_bindings', + MagicMock(return_value=BINDING_LIST)): self.assertRaises(SaltInvocationError, win_iis.create_binding, **kwargs) def test_list_bindings(self): @@ -276,8 +276,8 @@ class WinIisTestCase(TestCase, LoaderModuleMockMixin): Test - Get all configured IIS bindings for the specified site. ''' with patch.dict(win_iis.__salt__), \ - patch('salt.modules.win_iis.list_sites', - MagicMock(return_value=SITE_LIST)): + patch('salt.modules.win_iis.list_sites', + MagicMock(return_value=SITE_LIST)): self.assertEqual(win_iis.list_bindings('MyTestSite'), BINDING_LIST) def test_remove_binding(self): @@ -287,10 +287,10 @@ class WinIisTestCase(TestCase, LoaderModuleMockMixin): kwargs = {'site': 'MyTestSite', 'hostheader': 'myothertestsite.local', 'ipaddress': '*', 'port': 443} with patch.dict(win_iis.__salt__), \ - patch('salt.modules.win_iis._srvmgr', - MagicMock(return_value={'retcode': 0})), \ - patch('salt.modules.win_iis.list_bindings', - MagicMock(return_value=BINDING_LIST)): + patch('salt.modules.win_iis._srvmgr', + MagicMock(return_value={'retcode': 0})), \ + patch('salt.modules.win_iis.list_bindings', + MagicMock(return_value=BINDING_LIST)): self.assertTrue(win_iis.remove_binding(**kwargs)) def test_create_vdir(self): @@ -300,12 +300,12 @@ class WinIisTestCase(TestCase, LoaderModuleMockMixin): kwargs = {'name': 'TestVdir', 'site': 'MyTestSite', 'sourcepath': r'C:\inetpub\vdirs\TestVdir'} with patch.dict(win_iis.__salt__), \ - patch('os.path.isdir', - MagicMock(return_value=True)), \ - patch('salt.modules.win_iis._srvmgr', - MagicMock(return_value={'retcode': 0})), \ - patch('salt.modules.win_iis.list_vdirs', - MagicMock(return_value=VDIR_LIST)): + patch('os.path.isdir', + MagicMock(return_value=True)), \ + patch('salt.modules.win_iis._srvmgr', + MagicMock(return_value={'retcode': 0})), \ + patch('salt.modules.win_iis.list_vdirs', + MagicMock(return_value=VDIR_LIST)): self.assertTrue(win_iis.create_vdir(**kwargs)) def test_list_vdirs(self): @@ -318,8 +318,8 @@ class WinIisTestCase(TestCase, LoaderModuleMockMixin): } } with patch.dict(win_iis.__salt__), \ - patch('salt.modules.win_iis._srvmgr', - MagicMock(return_value=LIST_VDIRS_SRVMGR)): + patch('salt.modules.win_iis._srvmgr', + MagicMock(return_value=LIST_VDIRS_SRVMGR)): self.assertEqual(win_iis.list_vdirs('MyTestSite'), vdirs) def test_remove_vdir(self): @@ -328,10 +328,10 @@ class WinIisTestCase(TestCase, LoaderModuleMockMixin): ''' kwargs = {'name': 'TestOtherVdir', 'site': 'MyTestSite'} with patch.dict(win_iis.__salt__), \ - patch('salt.modules.win_iis._srvmgr', - MagicMock(return_value={'retcode': 0})), \ - patch('salt.modules.win_iis.list_vdirs', - MagicMock(return_value=VDIR_LIST)): + patch('salt.modules.win_iis._srvmgr', + MagicMock(return_value={'retcode': 0})), \ + patch('salt.modules.win_iis.list_vdirs', + MagicMock(return_value=VDIR_LIST)): self.assertTrue(win_iis.remove_vdir(**kwargs)) def test_create_cert_binding(self): @@ -342,15 +342,15 @@ class WinIisTestCase(TestCase, LoaderModuleMockMixin): 'site': 'MyTestSite', 'hostheader': 'mytestsite.local', 'ipaddress': '*', 'port': 443} with patch.dict(win_iis.__salt__), \ - patch('salt.modules.win_iis._list_certs', - MagicMock(return_value={'9988776655443322111000AAABBBCCCDDDEEEFFF': None})), \ - patch('salt.modules.win_iis._srvmgr', - MagicMock(return_value={'retcode': 0, 'stdout': 10})), \ - patch('salt.utils.json.loads', MagicMock(return_value=[{'MajorVersion': 10, 'MinorVersion': 0}])), \ - patch('salt.modules.win_iis.list_bindings', - MagicMock(return_value=BINDING_LIST)), \ - patch('salt.modules.win_iis.list_cert_bindings', - MagicMock(return_value={CERT_BINDING_INFO: BINDING_LIST[CERT_BINDING_INFO]})): + patch('salt.modules.win_iis._list_certs', + MagicMock(return_value={'9988776655443322111000AAABBBCCCDDDEEEFFF': None})), \ + patch('salt.modules.win_iis._srvmgr', + MagicMock(return_value={'retcode': 0, 'stdout': 10})), \ + patch('salt.utils.json.loads', MagicMock(return_value=[{'MajorVersion': 10, 'MinorVersion': 0}])), \ + patch('salt.modules.win_iis.list_bindings', + MagicMock(return_value=BINDING_LIST)), \ + patch('salt.modules.win_iis.list_cert_bindings', + MagicMock(return_value={CERT_BINDING_INFO: BINDING_LIST[CERT_BINDING_INFO]})): self.assertTrue(win_iis.create_cert_binding(**kwargs)) def test_list_cert_bindings(self): @@ -359,8 +359,8 @@ class WinIisTestCase(TestCase, LoaderModuleMockMixin): ''' key = '*:443:mytestsite.local' with patch.dict(win_iis.__salt__), \ - patch('salt.modules.win_iis.list_sites', - MagicMock(return_value=SITE_LIST)): + patch('salt.modules.win_iis.list_sites', + MagicMock(return_value=SITE_LIST)): self.assertEqual(win_iis.list_cert_bindings('MyTestSite'), {key: BINDING_LIST[key]}) @@ -372,10 +372,10 @@ class WinIisTestCase(TestCase, LoaderModuleMockMixin): 'site': 'MyOtherTestSite', 'hostheader': 'myothertestsite.local', 'ipaddress': '*', 'port': 443} with patch.dict(win_iis.__salt__), \ - patch('salt.modules.win_iis._srvmgr', - MagicMock(return_value={'retcode': 0})), \ - patch('salt.modules.win_iis.list_cert_bindings', - MagicMock(return_value={CERT_BINDING_INFO: BINDING_LIST[CERT_BINDING_INFO]})): + patch('salt.modules.win_iis._srvmgr', + MagicMock(return_value={'retcode': 0})), \ + patch('salt.modules.win_iis.list_cert_bindings', + MagicMock(return_value={CERT_BINDING_INFO: BINDING_LIST[CERT_BINDING_INFO]})): self.assertTrue(win_iis.remove_cert_binding(**kwargs)) def test_get_container_setting(self): @@ -385,8 +385,8 @@ class WinIisTestCase(TestCase, LoaderModuleMockMixin): kwargs = {'name': 'MyTestSite', 'container': 'AppPools', 'settings': ['managedPipelineMode']} with patch.dict(win_iis.__salt__), \ - patch('salt.modules.win_iis._srvmgr', - MagicMock(return_value=CONTAINER_SETTING)): + patch('salt.modules.win_iis._srvmgr', + MagicMock(return_value=CONTAINER_SETTING)): self.assertEqual(win_iis.get_container_setting(**kwargs), {'managedPipelineMode': 'Integrated'}) @@ -397,8 +397,159 @@ class WinIisTestCase(TestCase, LoaderModuleMockMixin): kwargs = {'name': 'MyTestSite', 'container': 'AppPools', 'settings': {'managedPipelineMode': 'Integrated'}} with patch.dict(win_iis.__salt__), \ - patch('salt.modules.win_iis._srvmgr', - MagicMock(return_value={'retcode': 0})), \ - patch('salt.modules.win_iis.get_container_setting', - MagicMock(return_value={'managedPipelineMode': 'Integrated'})): + patch('salt.modules.win_iis._srvmgr', + MagicMock(return_value={'retcode': 0})), \ + patch('salt.modules.win_iis.get_container_setting', + MagicMock(return_value={'managedPipelineMode': 'Integrated'})): self.assertTrue(win_iis.set_container_setting(**kwargs)) + + def test__collection_match_to_index(self): + bad_match = {'key_0': 'value'} + first_match = {'key_1': 'value'} + second_match = {'key_2': 'value'} + collection = [first_match, second_match] + settings = [{'name': 'enabled', 'value': collection}] + with patch.dict(win_iis.__salt__), \ + patch('salt.modules.win_iis.get_webconfiguration_settings', + MagicMock(return_value=settings)): + ret = win_iis._collection_match_to_index('pspath', 'colfilter', 'name', bad_match) + self.assertEqual(ret, -1) + ret = win_iis._collection_match_to_index('pspath', 'colfilter', 'name', first_match) + self.assertEqual(ret, 0) + ret = win_iis._collection_match_to_index('pspath', 'colfilter', 'name', second_match) + self.assertEqual(ret, 1) + + def test__prepare_settings(self): + simple_setting = {'name': 'value', 'filter': 'value'} + collection_setting = {'name': 'Collection[{yaml:\n\tdata}]', 'filter': 'value'} + with patch.dict(win_iis.__salt__), \ + patch('salt.modules.win_iis._collection_match_to_index', + MagicMock(return_value=0)): + ret = win_iis._prepare_settings('pspath', [ + simple_setting, collection_setting, {'invalid': 'setting'}, {'name': 'filter-less_setting'} + ]) + self.assertEqual(ret, [simple_setting, collection_setting]) + + @patch('salt.modules.win_iis.log') + def test_get_webconfiguration_settings_empty(self, mock_log): + ret = win_iis.get_webconfiguration_settings('name', settings=[]) + mock_log.warning.assert_called_once_with('No settings provided') + self.assertEqual(ret, {}) + + def test_get_webconfiguration_settings(self): + # Setup + name = 'IIS' + collection_setting = {'name': 'Collection[{yaml:\n\tdata}]', 'filter': 'value'} + filter_setting = {'name': 'enabled', + 'filter': 'system.webServer / security / authentication / anonymousAuthentication'} + settings = [collection_setting, filter_setting] + + ps_cmd = ['$Settings = New-Object System.Collections.ArrayList;', ] + for setting in settings: + ps_cmd.extend([ + "$Property = Get-WebConfigurationProperty -PSPath '{}'".format(name), + "-Name '{name}' -Filter '{filter}' -ErrorAction Stop;".format( + filter=setting['filter'], name=setting['name']), + 'if (([String]::IsNullOrEmpty($Property) -eq $False) -and', + "($Property.GetType()).Name -eq 'ConfigurationAttribute') {", + '$Property = $Property | Select-Object', + '-ExpandProperty Value };', + "$Settings.add(@{{filter='{filter}';name='{name}';value=[String] $Property}})| Out-Null;".format( + filter=setting['filter'], name=setting['name']), + '$Property = $Null;', + ]) + ps_cmd.append('$Settings') + + # Execute + with patch.dict(win_iis.__salt__), \ + patch('salt.modules.win_iis._prepare_settings', + MagicMock(return_value=settings)), \ + patch('salt.modules.win_iis._srvmgr', + MagicMock(return_value={'retcode': 0, 'stdout': '{}'})): + ret = win_iis.get_webconfiguration_settings(name, settings=settings) + + # Verify + win_iis._srvmgr.assert_called_with(cmd=ps_cmd, return_json=True) + self.assertEqual(ret, {}) + + @patch('salt.modules.win_iis.log') + def test_set_webconfiguration_settings_empty(self, mock_log): + ret = win_iis.set_webconfiguration_settings('name', settings=[]) + mock_log.warning.assert_called_once_with('No settings provided') + self.assertEqual(ret, False) + + @patch('salt.modules.win_iis.log') + def test_set_webconfiguration_settings_no_changes(self, mock_log): + # Setup + name = 'IIS' + setting = { + 'name': 'Collection[{yaml:\n\tdata}]', + 'filter': 'system.webServer / security / authentication / anonymousAuthentication', + 'value': [] + } + settings = [setting] + + # Execute + with patch.dict(win_iis.__salt__), \ + patch('salt.modules.win_iis._prepare_settings', + MagicMock(return_value=settings)), \ + patch('salt.modules.win_iis._srvmgr', + MagicMock(return_value={'retcode': 0, 'stdout': '{}'})), \ + patch('salt.modules.win_iis.get_webconfiguration_settings', + MagicMock(return_value=settings)): + ret = win_iis.set_webconfiguration_settings(name, settings=settings) + + # Verify + mock_log.debug.assert_called_with('Settings already contain the provided values.') + self.assertEqual(ret, True) + + @patch('salt.modules.win_iis.log') + def test_set_webconfiguration_settings_failed(self, mock_log): + # Setup + name = 'IIS' + setting = { + 'name': 'Collection[{yaml:\n\tdata}]', + 'filter': 'system.webServer / security / authentication / anonymousAuthentication', + 'value': [] + } + settings = [setting] + + # Execute + with patch.dict(win_iis.__salt__), \ + patch('salt.modules.win_iis._prepare_settings', + MagicMock(return_value=settings)), \ + patch('salt.modules.win_iis._srvmgr', + MagicMock(return_value={'retcode': 0, 'stdout': '{}'})), \ + patch('salt.modules.win_iis.get_webconfiguration_settings', + MagicMock(side_effect=[[], [{'value': 'unexpected_change!'}]])): + + ret = win_iis.set_webconfiguration_settings(name, settings=settings) + + # Verify + self.assertEqual(ret, False) + mock_log.error.assert_called_with('Failed to change settings: %s', settings) + + @patch('salt.modules.win_iis.log') + def test_set_webconfiguration_settings(self, mock_log): + # Setup + name = 'IIS' + setting = { + 'name': 'Collection[{yaml:\n\tdata}]', + 'filter': 'system.webServer / security / authentication / anonymousAuthentication', + 'value': [] + } + settings = [setting] + + # Execute + with patch.dict(win_iis.__salt__), \ + patch('salt.modules.win_iis._prepare_settings', + MagicMock(return_value=settings)), \ + patch('salt.modules.win_iis._srvmgr', + MagicMock(return_value={'retcode': 0, 'stdout': '{}'})), \ + patch('salt.modules.win_iis.get_webconfiguration_settings', + MagicMock(side_effect=[[], settings])): + ret = win_iis.set_webconfiguration_settings(name, settings=settings) + + # Verify + self.assertEqual(ret, True) + mock_log.debug.assert_called_with('Settings configured successfully: %s', settings) diff --git a/tests/unit/states/test_win_iis.py b/tests/unit/states/test_win_iis.py new file mode 100644 index 0000000000..3f27a4e122 --- /dev/null +++ b/tests/unit/states/test_win_iis.py @@ -0,0 +1,114 @@ +# -*- coding: utf-8 -*- +''' + :synopsis: Unit Tests for Windows iis Module 'state.win_iis' + :platform: Windows + .. versionadded:: 2019.2.2 +''' + +# Import Python Libs +from __future__ import absolute_import, unicode_literals, print_function + +# Import Salt Libs +import salt.states.win_iis as win_iis + +# Import Salt Testing Libs +from tests.support.mixins import LoaderModuleMockMixin +from tests.support.unit import TestCase, skipIf +from tests.support.mock import ( + MagicMock, + patch, + NO_MOCK, + NO_MOCK_REASON, +) + + +@skipIf(NO_MOCK, NO_MOCK_REASON) +class WinIisTestCase(TestCase, LoaderModuleMockMixin): + ''' + Test cases for salt.states.win_pki + ''' + + def setup_loader_modules(self): + return {win_iis: {}} + + def __base_webconfiguration_ret(self, comment='', changes=None, name='', result=None): + return { + 'name': name, + 'changes': changes if changes else {}, + 'comment': comment, + 'result': result, + } + + def test_webconfiguration_settings_no_settings(self): + name = 'IIS' + settings = {} + expected_ret = self.__base_webconfiguration_ret(name=name, comment='No settings to change provided.', + result=True) + actual_ret = win_iis.webconfiguration_settings(name, settings) + self.assertEqual(expected_ret, actual_ret) + + def test_webconfiguration_settings_collection_failure(self): + name = 'IIS:\\' + settings = { + 'system.applicationHost/sites': { + 'Collection[{name: site0}].logFile.directory': 'C:\\logs\\iis\\site0', + }, + } + old_settings = [ + {'filter': 'system.applicationHost/sites', 'name': 'Collection[{name: site0}].logFile.directory', + 'value': 'C:\\logs\\iis\\old_site'}] + current_settings = old_settings + new_settings = old_settings + expected_ret = self.__base_webconfiguration_ret( + name=name, + result=False, + changes={ + 'changes': {old_settings[0]['filter'] + '.' + old_settings[0]['name']: { + 'old': old_settings[0]['value'], + 'new': settings[old_settings[0]['filter']][old_settings[0]['name']], + }}, + 'failures': {old_settings[0]['filter'] + '.' + old_settings[0]['name']: { + 'old': old_settings[0]['value'], + 'new': new_settings[0]['value'], + }}, + }, + comment='Some settings failed to change.' + ) + with patch.dict(win_iis.__salt__, { + 'win_iis.get_webconfiguration_settings': MagicMock( + side_effect=[old_settings, current_settings, new_settings]), + 'win_iis.set_webconfiguration_settings': MagicMock(return_value=True), + }), patch.dict(win_iis.__opts__, {'test': False}): + actual_ret = win_iis.webconfiguration_settings(name, settings) + self.assertEqual(expected_ret, actual_ret) + + def test_webconfiguration_settings_collection(self): + name = 'IIS:\\' + settings = { + 'system.applicationHost/sites': { + 'Collection[{name: site0}].logFile.directory': 'C:\\logs\\iis\\site0', + }, + } + old_settings = [ + {'filter': 'system.applicationHost/sites', 'name': 'Collection[{name: site0}].logFile.directory', + 'value': 'C:\\logs\\iis\\old_site'}] + current_settings = [ + {'filter': 'system.applicationHost/sites', 'name': 'Collection[{name: site0}].logFile.directory', + 'value': 'C:\\logs\\iis\\site0'}] + new_settings = current_settings + expected_ret = self.__base_webconfiguration_ret( + name=name, + result=True, + changes={old_settings[0]['filter'] + '.' + old_settings[0]['name']: { + 'old': old_settings[0]['value'], + 'new': new_settings[0]['value'], + }}, + comment='Set settings to contain the provided values.' + ) + with patch.dict(win_iis.__salt__, { + 'win_iis.get_webconfiguration_settings': MagicMock( + side_effect=[old_settings, current_settings, new_settings]), + 'win_iis.set_webconfiguration_settings': MagicMock(return_value=True), + }), patch.dict(win_iis.__opts__, {'test': False}): + actual_ret = win_iis.webconfiguration_settings(name, settings) + self.assertEqual(expected_ret, actual_ret)