From 77afc32621bd76804b32a6d8e60e713cfc4c87a4 Mon Sep 17 00:00:00 2001 From: Tim Rupp Date: Sat, 10 Nov 2018 22:12:19 -0800 Subject: [PATCH] Adds new parameters to bigip_profile_http (#48528) --- .../modules/network/f5/bigip_profile_http.py | 264 ++++++++++++++++-- .../network/f5/test_bigip_profile_http.py | 20 +- 2 files changed, 250 insertions(+), 34 deletions(-) diff --git a/lib/ansible/modules/network/f5/bigip_profile_http.py b/lib/ansible/modules/network/f5/bigip_profile_http.py index 27e7285b40..47d12e4e48 100644 --- a/lib/ansible/modules/network/f5/bigip_profile_http.py +++ b/lib/ansible/modules/network/f5/bigip_profile_http.py @@ -7,6 +7,7 @@ from __future__ import absolute_import, division, print_function __metaclass__ = type + ANSIBLE_METADATA = {'metadata_version': '1.1', 'status': ['preview'], 'supported_by': 'certified'} @@ -94,6 +95,53 @@ options: choices: - always - on_create + header_erase: + description: + - The name of a header, in an HTTP request, which the system removes from request. + - To remove the entry completely a value of C(none) or C('') should be set. + - The format of the header must be in C(KEY:VALUE) format, otherwise error is raised. + - When creating a new profile, if this parameter is not specified, the + default is provided by the parent profile. + version_added: 2.8 + header_insert: + description: + - A string that the system inserts as a header in an HTTP request. + - To remove the entry completely a value of C(none) or C('') should be set. + - The format of the header must be in C(KEY:VALUE) format, otherwise error is raised. + - When creating a new profile, if this parameter is not specified, the + default is provided by the parent profile. + version_added: 2.8 + server_agent_name: + description: + - Specifies the string used as the server name in traffic generated by LTM. + - To remove the entry completely a value of C(none) or C('') should be set. + - When creating a new profile, if this parameter is not specified, the + default is provided by the parent profile. + version_added: 2.8 + include_subdomains: + description: + - When set to C(yes), applies the HSTS policy to the HSTS host and its sub-domains. + - When creating a new profile, if this parameter is not specified, the + default is provided by the parent profile. + type: bool + version_added: 2.8 + maximum_age: + description: + - Specifies the maximum length of time, in seconds, that HSTS functionality + requests that clients only use HTTPS to connect to the current host and + any sub-domains of the current host's domain name. + - The accepted value range is C(0 - 4294967295) seconds, a value of C(0) seconds + re-enables plaintext HTTP access, while specifying C(indefinite) will set it to the maximum value. + - When creating a new profile, if this parameter is not specified, the + default is provided by the parent profile. + version_added: 2.8 + hsts_mode: + description: + - When set to C(yes), enables the HSTS settings. + - When creating a new profile, if this parameter is not specified, the + default is provided by the parent profile. + type: bool + version_added: 2.8 partition: description: - Device partition to manage resources on. @@ -189,6 +237,7 @@ try: from library.module_utils.network.f5.common import transform_name from library.module_utils.network.f5.common import exit_json from library.module_utils.network.f5.common import fail_json + from library.module_utils.network.f5.urls import check_header_validity except ImportError: from ansible.module_utils.network.f5.bigip import F5RestClient from ansible.module_utils.network.f5.common import F5ModuleError @@ -200,6 +249,7 @@ except ImportError: from ansible.module_utils.network.f5.common import transform_name from ansible.module_utils.network.f5.common import exit_json from ansible.module_utils.network.f5.common import fail_json + from ansible.module_utils.network.f5.urls import check_header_validity class Parameters(AnsibleF5Parameters): @@ -211,7 +261,12 @@ class Parameters(AnsibleF5Parameters): 'encryptCookieSecret': 'encrypt_cookie_secret', 'proxyType': 'proxy_type', 'explicitProxy': 'explicit_proxy', - + 'headerErase': 'header_erase', + 'headerInsert': 'header_insert', + 'serverAgentName': 'server_agent_name', + 'includeSubdomains': 'include_subdomains', + 'maximumAge': 'maximum_age', + 'mode': 'hsts_mode', } api_attributes = [ @@ -223,6 +278,10 @@ class Parameters(AnsibleF5Parameters): 'encryptCookieSecret', 'proxyType', 'explicitProxy', + 'headerErase', + 'headerInsert', + 'hsts', + 'serverAgentName', ] returnables = [ @@ -234,6 +293,12 @@ class Parameters(AnsibleF5Parameters): 'proxy_type', 'explicit_proxy', 'dns_resolver', + 'hsts_mode', + 'maximum_age', + 'include_subdomains', + 'server_agent_name', + 'header_erase', + 'header_insert', ] updatables = [ @@ -244,6 +309,12 @@ class Parameters(AnsibleF5Parameters): 'encrypt_cookie_secret', 'proxy_type', 'dns_resolver', + 'hsts_mode', + 'maximum_age', + 'include_subdomains', + 'server_agent_name', + 'header_erase', + 'header_insert', ] @@ -262,6 +333,24 @@ class ApiParameters(Parameters): if 'dnsResolverReference' in self._values['explicit_proxy']: return self._values['explicit_proxy']['dnsResolverReference'] + @property + def include_subdomains(self): + if self._values['hsts'] is None: + return None + return self._values['hsts']['includeSubdomains'] + + @property + def hsts_mode(self): + if self._values['hsts'] is None: + return None + return self._values['hsts']['mode'] + + @property + def maximum_age(self): + if self._values['hsts'] is None: + return None + return self._values['hsts']['maximumAge'] + class ModuleParameters(Parameters): @property @@ -327,6 +416,56 @@ class ModuleParameters(Parameters): ) return result + @property + def include_subdomains(self): + result = flatten_boolean(self._values['include_subdomains']) + if result is None: + return None + if result == 'yes': + return 'enabled' + return 'disabled' + + @property + def maximum_age(self): + if self._values['maximum_age'] is None: + return None + if self._values['maximum_age'] == 'indefinite': + return 4294967295 + if 0 <= int(self._values['maximum_age']) <= 4294967295: + return int(self._values['maximum_age']) + raise F5ModuleError( + "Valid 'maximum_age' must be in range 0 - 4294967295, or 'indefinite'." + ) + + @property + def hsts_mode(self): + result = flatten_boolean(self._values['hsts_mode']) + if result is None: + return None + if result == 'yes': + return 'enabled' + return 'disabled' + + @property + def header_erase(self): + header_erase = self._values['header_erase'] + if header_erase is None: + return None + if header_erase in ['none', '']: + return self._values['header_erase'] + check_header_validity(header_erase) + return header_erase + + @property + def header_insert(self): + header_insert = self._values['header_insert'] + if header_insert is None: + return None + if header_insert in ['none', '']: + return self._values['header_insert'] + check_header_validity(header_insert) + return header_insert + class Changes(Parameters): def to_return(self): @@ -352,6 +491,19 @@ class UsableChanges(Changes): return None return result + @property + def hsts(self): + result = dict() + if self._values['hsts_mode'] is not None: + result['mode'] = self._values['hsts_mode'] + if self._values['maximum_age'] is not None: + result['maximumAge'] = self._values['maximum_age'] + if self._values['include_subdomains'] is not None: + result['includeSubdomains'] = self._values['include_subdomains'] + if not result: + return None + return result + class ReportableChanges(Changes): @property @@ -362,6 +514,30 @@ class ReportableChanges(Changes): return 'yes' return 'no' + @property + def hsts_mode(self): + if self._values['hsts_mode'] is None: + return None + elif self._values['hsts_mode'] == 'enabled': + return 'yes' + return 'no' + + @property + def include_subdomains(self): + if self._values['include_subdomains'] is None: + return None + elif self._values['include_subdomains'] == 'enabled': + return 'yes' + return 'no' + + @property + def maximum_age(self): + if self._values['maximum_age'] is None: + return None + if self._values['maximum_age'] == 4294967295: + return 'indefinite' + return int(self._values['maximum_age']) + class Difference(object): def __init__(self, want, have=None): @@ -407,16 +583,45 @@ class Difference(object): if self.have.dns_resolver is None: return self.want.dns_resolver + @property + def header_erase(self): + if self.want.header_erase is None: + return None + if self.want.header_erase in ['none', '']: + if self.have.header_erase in [None, 'none']: + return None + if self.want.header_erase != self.have.header_erase: + return self.want.header_erase + + @property + def header_insert(self): + if self.want.header_insert is None: + return None + if self.want.header_insert in ['none', '']: + if self.have.header_insert in [None, 'none']: + return None + if self.want.header_insert != self.have.header_insert: + return self.want.header_insert + + @property + def server_agent_name(self): + if self.want.server_agent_name is None: + return None + if self.want.server_agent_name in ['none', '']: + if self.have.server_agent_name in [None, 'none']: + return None + if self.want.server_agent_name != self.have.server_agent_name: + return self.want.server_agent_name + @property def encrypt_cookies(self): if self.want.encrypt_cookies is None: return None - if self.have.encrypt_cookies == [] and self.want.encrypt_cookies == []: - return None - if self.have.encrypt_cookies is not None and self.want.encrypt_cookies == []: - return self.want.encrypt_cookies - if self.have.encrypt_cookies is None: - return self.want.encrypt_cookies + if self.have.encrypt_cookies in [None, []]: + if not self.want.encrypt_cookies: + return None + else: + return self.want.encrypt_cookies if set(self.want.encrypt_cookies) != set(self.have.encrypt_cookies): return self.want.encrypt_cookies @@ -477,7 +682,6 @@ class ModuleManager(object): changed = self.present() elif state == "absent": changed = self.absent() - reportable = ReportableChanges(params=self.changes.to_return()) changes = reportable.to_return() result.update(**changes) @@ -499,20 +703,10 @@ class ModuleManager(object): else: return self.create() - def exists(self): - uri = "https://{0}:{1}/mgmt/tm/ltm/profile/http/{2}".format( - self.client.provider['server'], - self.client.provider['server_port'], - transform_name(self.want.partition, self.want.name) - ) - resp = self.client.api.get(uri) - try: - response = resp.json() - except ValueError: - return False - if resp.status == 404 or 'code' in response and response['code'] == 404: - return False - return True + def absent(self): + if self.exists(): + return self.remove() + return False def update(self): self.have = self.read_current_from_device() @@ -538,6 +732,21 @@ class ModuleManager(object): self.create_on_device() return True + def exists(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/profile/http/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError: + return False + if resp.status == 404 or 'code' in response and response['code'] == 404: + return False + return True + def create_on_device(self): params = self.changes.api_params() params['name'] = self.want.name @@ -578,11 +787,6 @@ class ModuleManager(object): else: raise F5ModuleError(resp.content) - def absent(self): - if self.exists(): - return self.remove() - return False - def remove_from_device(self): uri = "https://{0}:{1}/mgmt/tm/ltm/profile/http/{2}".format( self.client.provider['server'], @@ -644,6 +848,12 @@ class ArgumentSpec(object): default='always', choices=['always', 'on_create'] ), + header_erase=dict(), + header_insert=dict(), + server_agent_name=dict(), + hsts_mode=dict(type='bool'), + maximum_age=dict(), + include_subdomains=dict(type='bool'), state=dict( default='present', choices=['present', 'absent'] diff --git a/test/units/modules/network/f5/test_bigip_profile_http.py b/test/units/modules/network/f5/test_bigip_profile_http.py index 0aa9a8ccb7..0a58eba217 100644 --- a/test/units/modules/network/f5/test_bigip_profile_http.py +++ b/test/units/modules/network/f5/test_bigip_profile_http.py @@ -14,8 +14,6 @@ from nose.plugins.skip import SkipTest if sys.version_info < (2, 7): raise SkipTest("F5 Ansible modules require Python >= 2.7") -from units.compat import unittest -from units.compat.mock import Mock from ansible.module_utils.basic import AnsibleModule try: @@ -23,17 +21,25 @@ try: from library.modules.bigip_profile_http import ModuleParameters from library.modules.bigip_profile_http import ModuleManager from library.modules.bigip_profile_http import ArgumentSpec - from library.module_utils.network.f5.common import F5ModuleError - from library.module_utils.network.f5.common import iControlUnexpectedHTTPError - from test.unit.modules.utils import set_module_args + + # In Ansible 2.8, Ansible changed import paths. + from test.units.compat import unittest + from test.units.compat.mock import Mock + from test.units.compat.mock import patch + + from test.units.modules.utils import set_module_args except ImportError: try: from ansible.modules.network.f5.bigip_profile_http import ApiParameters from ansible.modules.network.f5.bigip_profile_http import ModuleParameters from ansible.modules.network.f5.bigip_profile_http import ModuleManager from ansible.modules.network.f5.bigip_profile_http import ArgumentSpec - from ansible.module_utils.network.f5.common import F5ModuleError - from ansible.module_utils.network.f5.common import iControlUnexpectedHTTPError + + # Ansible 2.8 imports + from units.compat import unittest + from units.compat.mock import Mock + from units.compat.mock import patch + from units.modules.utils import set_module_args except ImportError: raise SkipTest("F5 Ansible modules require the f5-sdk Python library")