diff --git a/lib/ansible/modules/network/f5/bigip_asm_dos_application.py b/lib/ansible/modules/network/f5/bigip_asm_dos_application.py
new file mode 100644
index 00000000000..a7838f427a6
--- /dev/null
+++ b/lib/ansible/modules/network/f5/bigip_asm_dos_application.py
@@ -0,0 +1,1308 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright: (c) 2019, F5 Networks Inc.
+# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+ANSIBLE_METADATA = {'metadata_version': '1.1',
+ 'status': ['preview'],
+ 'supported_by': 'certified'}
+
+DOCUMENTATION = r'''
+---
+module: bigip_asm_dos_application
+short_description: Manage application settings for DOS profile
+description:
+ - Manages Application settings for ASM/AFM DOS profile.
+version_added: 2.9
+options:
+ profile:
+ description:
+ - Specifies the name of the profile to manage application settings in.
+ type: str
+ required: True
+ rtbh_duration:
+ description:
+ - Specifies the duration of the RTBH BGP route advertisement, in seconds.
+ - The accepted range is between 0 and 4294967295 inclusive.
+ type: int
+ rtbh_enable:
+ description:
+ - Specifies whether to enable Remote Triggered Black Hole C(RTBH) of attacking IPs by advertising BGP routes.
+ type: bool
+ scrubbing_duration:
+ description:
+ - Specifies the duration of the Traffic Scrubbing BGP route advertisement, in seconds.
+ - The accepted range is between 0 and 4294967295 inclusive.
+ type: int
+ scrubbing_enable:
+ description:
+ - Specifies whether to enable Traffic Scrubbing during attacks by advertising BGP routes.
+ type: bool
+ single_page_application:
+ description:
+ - Specifies, when C(yes), that the system supports a Single Page Applications.
+ type: bool
+ trigger_irule:
+ description:
+ - Specifies, when C(yes), that the system activates an Application DoS iRule event.
+ type: bool
+ geolocations:
+ description:
+ - Manages the geolocations countries whitelist, blacklist.
+ type: dict
+ suboptions:
+ whitelist:
+ description:
+ - A list of countries to be put on whitelist, must not have overlapping elements with C(blacklist).
+ type: list
+ blacklist:
+ description:
+ - A list of countries to be put on blacklist, must not have overlapping elements with C(whitelist).
+ type: list
+ heavy_urls:
+ description:
+ - Manages Heavy URL protection.
+ - Heavy URLs are a small number of site URLs that might consume considerable server resources per request.
+ type: dict
+ suboptions:
+ auto_detect:
+ description:
+ - Enables or disables automatic heavy URL detection.
+ type: bool
+ latency_threshold:
+ description:
+ - Specifies the latency threshold for automatic heavy URL detection.
+ - The accepted range is between 0 and 4294967295 miliseconds inclusive.
+ type: int
+ exclude:
+ description:
+ - Specifies a list of URLs or wildcards to exclude from the heavy URLs.
+ type: list
+ include:
+ description:
+ - Configures additional URLs to include in the heavy URLs that were auto detected.
+ type: list
+ suboptions:
+ url:
+ description:
+ - Specifies the URL to be added to the list of heavy URLs, in addition to the automatically detected ones.
+ type: str
+ threshold:
+ description:
+ - Specifies the threshold of requests per second, where the URL in question is considered under attack.
+ - The accepted range is between 1 and 4294967295 inclusive, or C(auto).
+ type: str
+ mobile_detection:
+ description:
+ - Configures detection of mobile applications built with the Anti-Bot Mobile SDK and defines how requests
+ from these mobile application clients are handled.
+ type: dict
+ suboptions:
+ enabled:
+ description:
+ - When C(yes), requests from mobile applications built with Anti-Bot Mobile SDK will be detected and handled
+ according to the parameters set.
+ - When C(no), these requests will be handled like any other request which may let attacks in, or cause false
+ positives.
+ type: bool
+ allow_android_rooted_device:
+ description:
+ - When C(yes) device will allow traffic from rooted Android devices.
+ type: bool
+ allow_any_android_package:
+ description:
+ - When C(yes) allows any application publisher.
+ - A publisher is identified by the certificate used to sign the application.
+ type: bool
+ allow_any_ios_package:
+ description:
+ - When C(yes) allows any iOS package.
+ - A package name is the unique identifier of the mobile application.
+ type: bool
+ allow_jailbroken_devices:
+ description:
+ - When C(yes) allows traffic from jailbroken iOS devices.
+ type: bool
+ allow_emulators:
+ description:
+ - When C(yes) allows traffic from applications run on emulators.
+ type: bool
+ client_side_challenge_mode:
+ description:
+ - Action to take when a CAPTCHA or Client Side Integrity challenge needs to be presented.
+ - The mobile application user will not see a CAPTCHA challenge and the mobile application will not be
+ presented with the Client Side Integrity challenge. The such options for mobile applications are C(pass)
+ or C(cshui).
+ - When C(pass) the traffic is passed without incident.
+ - When C(cshui) the SDK checks for human interactions with the screen in the last few seconds.
+ If none are detected, the traffic is blocked.
+ type: str
+ choices:
+ - pass
+ - cshui
+ ios_allowed_package_names:
+ description:
+ - Specifies the names of iOS packages to allow traffic on.
+ - This option has no effect when C(allow_any_ios_package) is set to C(yes).
+ type: list
+ android_publishers:
+ description:
+ - This option has no effect when C(allow_any_android_package) is set to C(yes).
+ - Specifies the allowed publisher certificates for android applications.
+ - The publisher certificate needs to be installed on the BIG-IP beforehand.
+ - "The certificate name located on a different partition than the one specified
+ in C(partition) parameter needs to be provided in C(full_path) format C(/Foo/cert.crt)."
+ type: list
+ partition:
+ description:
+ - Device partition to manage resources on.
+ type: str
+ default: Common
+ state:
+ description:
+ - When C(state) is C(present), ensures that the Application object exists.
+ - When C(state) is C(absent), ensures that the Application object is removed.
+ type: str
+ choices:
+ - present
+ - absent
+ default: present
+notes:
+ - Requires BIG-IP >= 13.1.0
+extends_documentation_fragment: f5
+author:
+ - Wojciech Wypior (@wojtek0806)
+'''
+
+EXAMPLES = r'''
+- name: Create an ASM dos application profile
+ bigip_asm_dos_application:
+ profile: dos_foo
+ geolocations:
+ blacklist:
+ - Afghanistan
+ - Andora
+ whitelist:
+ - Cuba
+ heavy_urls:
+ auto_detect: yes
+ latency_threshold: 1000
+ rtbh_duration: 3600
+ rtbh_enable: yes
+ single_page_application: yes
+ provider:
+ password: secret
+ server: lb.mydomain.com
+ user: admin
+ delegate_to: localhost
+
+- name: Update an ASM dos application profile
+ bigip_asm_dos_application:
+ profile: dos_foo
+ mobile_detection:
+ enabled: yes
+ allow_any_ios_package: yes
+ allow_emulators: yes
+ provider:
+ password: secret
+ server: lb.mydomain.com
+ user: admin
+ delegate_to: localhost
+
+- name: Remove an ASM dos application profile
+ bigip_asm_dos_application:
+ profile: dos_foo
+ state: absent
+ provider:
+ password: secret
+ server: lb.mydomain.com
+ user: admin
+ delegate_to: localhost
+'''
+
+RETURN = r'''
+rtbh_enable:
+ description: Enables Remote Triggered Black Hole of attacking IPs.
+ returned: changed
+ type: bool
+ sample: no
+rtbh_duration:
+ description: The duration of the RTBH BGP route advertisement.
+ returned: changed
+ type: int
+ sample: 3600
+scrubbing_enable:
+ description: Enables Traffic Scrubbing during attacks.
+ returned: changed
+ type: bool
+ sample: yes
+scrubbing_duration:
+ description: The duration of the Traffic Scrubbing BGP route advertisement.
+ returned: changed
+ type: int
+ sample: 3600
+single_page_application:
+ description: Enables support of a Single Page Applications.
+ returned: changed
+ type: bool
+ sample: no
+trigger_irule:
+ description: Activates an Application DoS iRule event.
+ returned: changed
+ type: bool
+ sample: yes
+geolocations:
+ description: Specifies geolocations countries whitelist, blacklist.
+ type: complex
+ returned: changed
+ contains:
+ whitelist:
+ description: A list of countries to be put on whitelist.
+ returned: changed
+ type: list
+ sample: ['United States, United Kingdom']
+ blacklist:
+ description: A list of countries to be put on blacklist.
+ returned: changed
+ type: list
+ sample: ['Russia', 'Germany']
+ sample: hash/dictionary of values
+heavy_urls:
+ description: Manages Heavy URL protection.
+ type: complex
+ returned: changed
+ contains:
+ auto_detect:
+ description: Enables or disables automatic heavy URL detection.
+ returned: changed
+ type: bool
+ sample: yes
+ latency_threshold:
+ description: Specifies the latency threshold for automatic heavy URL detection.
+ returned: changed
+ type: int
+ sample: 2000
+ exclude:
+ description: Specifies a list of URLs or wildcards to exclude from the heavy URLs.
+ returned: changed
+ type: list
+ sample: ['/exclude.html', '/exclude2.html']
+ include:
+ description: Configures additional URLs to include in the heavy URLs.
+ type: complex
+ returned: changed
+ contains:
+ url:
+ description: The URL to be added to the list of heavy URLs.
+ returned: changed
+ type: str
+ sample: /include.html
+ threshold:
+ description: The threshold of requests per second
+ returned: changed
+ type: str
+ sample: auto
+ sample: hash/dictionary of values
+ sample: hash/dictionary of values
+mobile_detection:
+ description: Configures detection of mobile applications built with the Anti-Bot Mobile SDK.
+ type: complex
+ returned: changed
+ contains:
+ enable:
+ description: Enables or disables automatic mobile detection.
+ returned: changed
+ type: bool
+ sample: yes
+ allow_android_rooted_device:
+ description: Allows traffic from rooted Android devices.
+ returned: changed
+ type: bool
+ sample: no
+ allow_any_android_package:
+ description: Allows any application publisher.
+ returned: changed
+ type: bool
+ sample: no
+ allow_any_ios_package:
+ description: Allows any iOS package.
+ returned: changed
+ type: bool
+ sample: yes
+ allow_jailbroken_devices:
+ description: Allows traffic from jailbroken iOS devices.
+ returned: changed
+ type: bool
+ sample: no
+ allow_emulators:
+ description: Allows traffic from applications run on emulators.
+ returned: changed
+ type: bool
+ sample: yes
+ client_side_challenge_mode:
+ description: Action to take when a CAPTCHA or Client Side Integrity challenge needs to be presented.
+ returned: changed
+ type: str
+ sample: pass
+ ios_allowed_package_names:
+ description: The names of iOS packages to allow traffic on.
+ returned: changed
+ type: list
+ sample: ['package1','package2']
+ android_publishers:
+ description: The allowed publisher certificates for android applications.
+ returned: changed
+ type: list
+ sample: ['/Common/cert1.crt', '/Common/cert2.crt']
+ sample: hash/dictionary of values
+'''
+from ansible.module_utils.basic import AnsibleModule
+from ansible.module_utils.basic import env_fallback
+from distutils.version import LooseVersion
+
+try:
+ from library.module_utils.network.f5.bigip import F5RestClient
+ from library.module_utils.network.f5.common import F5ModuleError
+ from library.module_utils.network.f5.common import AnsibleF5Parameters
+ from library.module_utils.network.f5.common import fq_name
+ from library.module_utils.network.f5.common import transform_name
+ from library.module_utils.network.f5.common import flatten_boolean
+ from library.module_utils.network.f5.common import f5_argument_spec
+ from library.module_utils.network.f5.compare import compare_complex_list
+ from library.module_utils.network.f5.compare import cmp_simple_list
+ from library.module_utils.network.f5.icontrol import tmos_version
+ from library.module_utils.network.f5.icontrol import module_provisioned
+except ImportError:
+ from ansible.module_utils.network.f5.bigip import F5RestClient
+ from ansible.module_utils.network.f5.common import F5ModuleError
+ from ansible.module_utils.network.f5.common import AnsibleF5Parameters
+ from ansible.module_utils.network.f5.common import fq_name
+ from ansible.module_utils.network.f5.common import transform_name
+ from ansible.module_utils.network.f5.common import flatten_boolean
+ from ansible.module_utils.network.f5.common import f5_argument_spec
+ from ansible.module_utils.network.f5.compare import compare_complex_list
+ from ansible.module_utils.network.f5.compare import cmp_simple_list
+ from ansible.module_utils.network.f5.icontrol import tmos_version
+ from ansible.module_utils.network.f5.icontrol import module_provisioned
+
+
+class Parameters(AnsibleF5Parameters):
+ api_map = {
+ 'rtbhDurationSec': 'rtbh_duration',
+ 'rtbhEnable': 'rtbh_enable',
+ 'scrubbingDurationSec': 'scrubbing_duration',
+ 'scrubbingEnable': 'scrubbing_enable',
+ 'singlePageApplication': 'single_page_application',
+ 'triggerIrule': 'trigger_irule',
+ 'heavyUrls': 'heavy_urls',
+ 'mobileDetection': 'mobile_detection',
+ }
+
+ api_attributes = [
+ 'geolocations',
+ 'rtbhDurationSec',
+ 'rtbhEnable',
+ 'scrubbingDurationSec',
+ 'scrubbingEnable',
+ 'singlePageApplication',
+ 'triggerIrule',
+ 'heavyUrls',
+ 'mobileDetection',
+ ]
+
+ returnables = [
+ 'rtbh_duration',
+ 'rtbh_enable',
+ 'scrubbing_duration',
+ 'scrubbing_enable',
+ 'single_page_application',
+ 'trigger_irule',
+ 'enable_mobile_detection',
+ 'allow_android_rooted_device',
+ 'allow_any_android_package',
+ 'allow_any_ios_package',
+ 'allow_jailbroken_devices',
+ 'allow_emulators',
+ 'client_side_challenge_mode',
+ 'ios_allowed_package_names',
+ 'android_publishers',
+ 'auto_detect',
+ 'latency_threshold',
+ 'hw_url_exclude',
+ 'hw_url_include',
+ 'geo_blacklist',
+ 'geo_whitelist',
+ ]
+
+ updatables = [
+ 'rtbh_duration',
+ 'rtbh_enable',
+ 'scrubbing_duration',
+ 'scrubbing_enable',
+ 'single_page_application',
+ 'trigger_irule',
+ 'enable_mobile_detection',
+ 'allow_android_rooted_device',
+ 'allow_any_android_package',
+ 'allow_any_ios_package',
+ 'allow_jailbroken_devices',
+ 'allow_emulators',
+ 'client_side_challenge_mode',
+ 'ios_allowed_package_names',
+ 'android_publishers',
+ 'auto_detect',
+ 'latency_threshold',
+ 'hw_url_exclude',
+ 'hw_url_include',
+ 'geo_blacklist',
+ 'geo_whitelist',
+ ]
+
+
+class ApiParameters(Parameters):
+ @property
+ def enable_mobile_detection(self):
+ if self._values['mobile_detection'] is None:
+ return None
+ return self._values['mobile_detection']['enabled']
+
+ @property
+ def allow_android_rooted_device(self):
+ if self._values['mobile_detection'] is None:
+ return None
+ return self._values['mobile_detection']['allowAndroidRootedDevice']
+
+ @property
+ def allow_any_android_package(self):
+ if self._values['mobile_detection'] is None:
+ return None
+ return self._values['mobile_detection']['allowAnyAndroidPackage']
+
+ @property
+ def allow_any_ios_package(self):
+ if self._values['mobile_detection'] is None:
+ return None
+ return self._values['mobile_detection']['allowAnyIosPackage']
+
+ @property
+ def allow_jailbroken_devices(self):
+ if self._values['mobile_detection'] is None:
+ return None
+ return self._values['mobile_detection']['allowJailbrokenDevices']
+
+ @property
+ def allow_emulators(self):
+ if self._values['mobile_detection'] is None:
+ return None
+ return self._values['mobile_detection']['allowEmulators']
+
+ @property
+ def client_side_challenge_mode(self):
+ if self._values['mobile_detection'] is None:
+ return None
+ return self._values['mobile_detection']['clientSideChallengeMode']
+
+ @property
+ def ios_allowed_package_names(self):
+ if self._values['mobile_detection'] is None:
+ return None
+ return self._values['mobile_detection'].get('iosAllowedPackageNames', None)
+
+ @property
+ def android_publishers(self):
+ if self._values['mobile_detection'] is None or 'androidPublishers' not in self._values['mobile_detection']:
+ return None
+ result = [fq_name(publisher['partition'], publisher['name'])
+ for publisher in self._values['mobile_detection']['androidPublishers']]
+ return result
+
+ @property
+ def auto_detect(self):
+ if self._values['heavy_urls'] is None:
+ return None
+ return self._values['heavy_urls']['automaticDetection']
+
+ @property
+ def latency_threshold(self):
+ if self._values['heavy_urls'] is None:
+ return None
+ return self._values['heavy_urls']['latencyThreshold']
+
+ @property
+ def hw_url_exclude(self):
+ if self._values['heavy_urls'] is None:
+ return None
+ return self._values['heavy_urls'].get('exclude', None)
+
+ @property
+ def hw_url_include(self):
+ if self._values['heavy_urls'] is None:
+ return None
+ return self._values['heavy_urls'].get('includeList', None)
+
+ @property
+ def geo_blacklist(self):
+ if self._values['geolocations'] is None:
+ return None
+ result = list()
+ for item in self._values['geolocations']:
+ if 'blackListed' in item and item['blackListed'] is True:
+ result.append(item['name'])
+ if result:
+ return result
+
+ @property
+ def geo_whitelist(self):
+ if self._values['geolocations'] is None:
+ return None
+ result = list()
+ for item in self._values['geolocations']:
+ if 'whiteListed' in item and item['whiteListed'] is True:
+ result.append(item['name'])
+ if result:
+ return result
+
+
+class ModuleParameters(Parameters):
+ @property
+ def rtbh_duration(self):
+ if self._values['rtbh_duration'] is None:
+ return None
+ if 0 <= self._values['rtbh_duration'] <= 4294967295:
+ return self._values['rtbh_duration']
+ raise F5ModuleError(
+ "Valid 'rtbh_duration' must be in range 0 - 4294967295 seconds."
+ )
+
+ @property
+ def rtbh_enable(self):
+ result = flatten_boolean(self._values['rtbh_enable'])
+ if result == 'yes':
+ return 'enabled'
+ if result == 'no':
+ return 'disabled'
+ return result
+
+ @property
+ def scrubbing_duration(self):
+ if self._values['scrubbing_duration'] is None:
+ return None
+ if 0 <= self._values['scrubbing_duration'] <= 4294967295:
+ return self._values['scrubbing_duration']
+ raise F5ModuleError(
+ "Valid 'scrubbing_duration' must be in range 0 - 4294967295 seconds."
+ )
+
+ @property
+ def scrubbing_enable(self):
+ result = flatten_boolean(self._values['scrubbing_enable'])
+ if result == 'yes':
+ return 'enabled'
+ if result == 'no':
+ return 'disabled'
+ return result
+
+ @property
+ def single_page_application(self):
+ result = flatten_boolean(self._values['single_page_application'])
+ if result == 'yes':
+ return 'enabled'
+ if result == 'no':
+ return 'disabled'
+ return result
+
+ @property
+ def trigger_irule(self):
+ result = flatten_boolean(self._values['trigger_irule'])
+ if result == 'yes':
+ return 'enabled'
+ if result == 'no':
+ return 'disabled'
+ return result
+
+ @property
+ def enable_mobile_detection(self):
+ if self._values['mobile_detection'] is None:
+ return None
+ result = flatten_boolean(self._values['mobile_detection']['enabled'])
+ if result == 'yes':
+ return 'enabled'
+ if result == 'no':
+ return 'disabled'
+ return result
+
+ @property
+ def allow_android_rooted_device(self):
+ if self._values['mobile_detection'] is None:
+ return None
+ result = flatten_boolean(self._values['mobile_detection']['allow_android_rooted_device'])
+ if result == 'yes':
+ return 'true'
+ if result == 'no':
+ return 'false'
+ return result
+
+ @property
+ def allow_any_android_package(self):
+ if self._values['mobile_detection'] is None:
+ return None
+ result = flatten_boolean(self._values['mobile_detection']['allow_any_android_package'])
+ if result == 'yes':
+ return 'true'
+ if result == 'no':
+ return 'false'
+ return result
+
+ @property
+ def allow_any_ios_package(self):
+ if self._values['mobile_detection'] is None:
+ return None
+ result = flatten_boolean(self._values['mobile_detection']['allow_any_ios_package'])
+ if result == 'yes':
+ return 'true'
+ if result == 'no':
+ return 'false'
+ return result
+
+ @property
+ def allow_jailbroken_devices(self):
+ if self._values['mobile_detection'] is None:
+ return None
+ result = flatten_boolean(self._values['mobile_detection']['allow_jailbroken_devices'])
+ if result == 'yes':
+ return 'true'
+ if result == 'no':
+ return 'false'
+ return result
+
+ @property
+ def allow_emulators(self):
+ if self._values['mobile_detection'] is None:
+ return None
+ result = flatten_boolean(self._values['mobile_detection']['allow_emulators'])
+ if result == 'yes':
+ return 'true'
+ if result == 'no':
+ return 'false'
+ return result
+
+ @property
+ def client_side_challenge_mode(self):
+ if self._values['mobile_detection'] is None:
+ return None
+ return self._values['mobile_detection']['client_side_challenge_mode']
+
+ @property
+ def ios_allowed_package_names(self):
+ if self._values['mobile_detection'] is None:
+ return None
+ return self._values['mobile_detection']['ios_allowed_package_names']
+
+ @property
+ def android_publishers(self):
+ if self._values['mobile_detection'] is None or self._values['mobile_detection']['android_publishers'] is None:
+ return None
+ result = [fq_name(self.partition, item) for item in self._values['mobile_detection']['android_publishers']]
+ return result
+
+ @property
+ def auto_detect(self):
+ if self._values['heavy_urls'] is None:
+ return None
+ result = flatten_boolean(self._values['heavy_urls']['auto_detect'])
+ if result == 'yes':
+ return 'enabled'
+ if result == 'no':
+ return 'disabled'
+ return result
+
+ @property
+ def latency_threshold(self):
+ if self._values['heavy_urls'] is None or self._values['heavy_urls']['latency_threshold'] is None:
+ return None
+ if 0 <= self._values['heavy_urls']['latency_threshold'] <= 4294967295:
+ return self._values['heavy_urls']['latency_threshold']
+ raise F5ModuleError(
+ "Valid 'latency_threshold' must be in range 0 - 4294967295 milliseconds."
+ )
+
+ @property
+ def hw_url_exclude(self):
+ if self._values['heavy_urls'] is None:
+ return None
+ return self._values['heavy_urls']['exclude']
+
+ @property
+ def hw_url_include(self):
+ if self._values['heavy_urls'] is None or self._values['heavy_urls']['include'] is None:
+ return None
+ result = list()
+ for item in self._values['heavy_urls']['include']:
+ element = dict()
+ element['url'] = self._correct_url(item['url'])
+ element['name'] = 'URL{0}'.format(self._correct_url(item['url']))
+ if 'threshold' in item:
+ element['threshold'] = self._validate_threshold(item['threshold'])
+ result.append(element)
+ return result
+
+ def _validate_threshold(self, item):
+ if item == 'auto':
+ return item
+ if 1 <= int(item) <= 4294967295:
+ return item
+ raise F5ModuleError(
+ "Valid 'url threshold' must be in range 1 - 4294967295 requests per second or 'auto'."
+ )
+
+ def _correct_url(self, item):
+ if item.startswith('/'):
+ return item
+ return "/{0}".format(item)
+
+ @property
+ def geo_blacklist(self):
+ if self._values['geolocations'] is None:
+ return None
+ whitelist = self.geo_whitelist
+ blacklist = self._values['geolocations']['blacklist']
+ if whitelist and blacklist:
+ if not set(whitelist).isdisjoint(set(blacklist)):
+ raise F5ModuleError('Cannot specify the same element in blacklist and whitelist.')
+ return blacklist
+
+ @property
+ def geo_whitelist(self):
+ if self._values['geolocations'] is None:
+ return None
+ return self._values['geolocations']['whitelist']
+
+
+class Changes(Parameters):
+ def to_return(self):
+ result = {}
+ try:
+ for returnable in self.returnables:
+ result[returnable] = getattr(self, returnable)
+ result = self._filter_params(result)
+ except Exception:
+ pass
+ return result
+
+
+class UsableChanges(Changes):
+ @property
+ def geolocations(self):
+ if self._values['geo_blacklist'] is None and self._values['geo_whitelist'] is None:
+ return None
+ result = list()
+ if self._values['geo_blacklist']:
+ for item in self._values['geo_blacklist']:
+ element = dict()
+ element['name'] = item
+ element['blackListed'] = True
+ result.append(element)
+ if self._values['geo_whitelist']:
+ for item in self._values['geo_whitelist']:
+ element = dict()
+ element['name'] = item
+ element['whiteListed'] = True
+ result.append(element)
+ if result:
+ return result
+
+ @property
+ def heavy_urls(self):
+ tmp = dict()
+ tmp['automaticDetection'] = self._values['auto_detect']
+ tmp['latencyThreshold'] = self._values['latency_threshold']
+ tmp['exclude'] = self._values['hw_url_exclude']
+ tmp['includeList'] = self._values['hw_url_include']
+ result = self._filter_params(tmp)
+ if result:
+ return result
+
+ @property
+ def mobile_detection(self):
+ tmp = dict()
+ tmp['enabled'] = self._values['enable_mobile_detection']
+ tmp['allowAndroidRootedDevice'] = self._values['allow_android_rooted_device']
+ tmp['allowAnyAndroidPackage'] = self._values['allow_any_android_package']
+ tmp['allowAnyIosPackage'] = self._values['allow_any_ios_package']
+ tmp['allowJailbrokenDevices'] = self._values['allow_jailbroken_devices']
+ tmp['allowEmulators'] = self._values['allow_emulators']
+ tmp['clientSideChallengeMode'] = self._values['client_side_challenge_mode']
+ tmp['iosAllowedPackageNames'] = self._values['ios_allowed_package_names']
+ tmp['androidPublishers'] = self._values['android_publishers']
+ result = self._filter_params(tmp)
+ if result:
+ return result
+
+
+class ReportableChanges(Changes):
+ returnables = [
+ 'rtbh_duration',
+ 'rtbh_enable',
+ 'scrubbing_duration',
+ 'scrubbing_enable',
+ 'single_page_application',
+ 'trigger_irule',
+ 'heavy_urls',
+ 'mobile_detection',
+ 'geolocations',
+ ]
+
+ def _convert_include_list(self, items):
+ result = list()
+ for item in items:
+ element = dict()
+ element['url'] = item['url']
+ if 'threshold' in item:
+ element['threshold'] = item['threshold']
+ result.append(element)
+ if result:
+ return result
+
+ @property
+ def geolocations(self):
+ tmp = dict()
+ tmp['blacklist'] = self._values['geo_blacklist']
+ tmp['whitelist'] = self._values['geo_whitelist']
+ result = self._filter_params(tmp)
+ if result:
+ return result
+
+ @property
+ def heavy_urls(self):
+ tmp = dict()
+ tmp['auto_detect'] = flatten_boolean(self._values['auto_detect'])
+ tmp['latency_threshold'] = self._values['latency_threshold']
+ tmp['exclude'] = self._values['hw_url_exclude']
+ tmp['include'] = self._convert_include_list(self._values['hw_url_include'])
+ result = self._filter_params(tmp)
+ if result:
+ return result
+
+ @property
+ def mobile_detection(self):
+ tmp = dict()
+ tmp['enabled'] = flatten_boolean(self._values['enable_mobile_detection'])
+ tmp['allow_android_rooted_device'] = flatten_boolean(self._values['allow_android_rooted_device'])
+ tmp['allow_any_android_package'] = flatten_boolean(self._values['allow_any_android_package'])
+ tmp['allow_any_ios_package'] = flatten_boolean(self._values['allow_any_ios_package'])
+ tmp['allow_jailbroken_devices'] = flatten_boolean(self._values['allow_jailbroken_devices'])
+ tmp['allow_emulators'] = flatten_boolean(self._values['allow_emulators'])
+ tmp['client_side_challenge_mode'] = self._values['client_side_challenge_mode']
+ tmp['ios_allowed_package_names'] = self._values['ios_allowed_package_names']
+ tmp['android_publishers'] = self._values['android_publishers']
+ result = self._filter_params(tmp)
+ if result:
+ return result
+
+ @property
+ def rtbh_enable(self):
+ result = flatten_boolean(self._values['rtbh_enable'])
+ return result
+
+ @property
+ def scrubbing_enable(self):
+ result = flatten_boolean(self._values['scrubbing_enable'])
+ return result
+
+ @property
+ def single_page_application(self):
+ result = flatten_boolean(self._values['single_page_application'])
+ return result
+
+ @property
+ def trigger_irule(self):
+ result = flatten_boolean(self._values['trigger_irule'])
+ return result
+
+
+class Difference(object):
+ def __init__(self, want, have=None):
+ self.want = want
+ self.have = have
+
+ def compare(self, param):
+ try:
+ result = getattr(self, param)
+ return result
+ except AttributeError:
+ return self.__default(param)
+
+ def __default(self, param):
+ attr1 = getattr(self.want, param)
+ try:
+ attr2 = getattr(self.have, param)
+ if attr1 != attr2:
+ return attr1
+ except AttributeError:
+ return attr1
+
+ @property
+ def hw_url_include(self):
+ if self.want.hw_url_include is None:
+ return None
+ if self.have.hw_url_include is None and self.want.hw_url_include == []:
+ return None
+ if self.have.hw_url_include is None:
+ return self.want.hw_url_include
+
+ wants = self.want.hw_url_include
+ haves = list()
+ # First we remove extra keys in have for the same elements
+ for want in wants:
+ for have in self.have.hw_url_include:
+ if want['url'] == have['url']:
+ entry = self._filter_have(want, have)
+ haves.append(entry)
+ # Next we do compare the lists as normal
+ result = compare_complex_list(wants, haves)
+ return result
+
+ def _filter_have(self, want, have):
+ to_check = set(want.keys()).intersection(set(have.keys()))
+ result = dict()
+ for k in list(to_check):
+ result[k] = have[k]
+ return result
+
+ @property
+ def hw_url_exclude(self):
+ result = cmp_simple_list(self.want.hw_url_exclude, self.have.hw_url_exclude)
+ return result
+
+ @property
+ def geo_blacklist(self):
+ result = cmp_simple_list(self.want.geo_blacklist, self.have.geo_blacklist)
+ return result
+
+ @property
+ def geo_whitelist(self):
+ result = cmp_simple_list(self.want.geo_whitelist, self.have.geo_whitelist)
+ return result
+
+ @property
+ def android_publishers(self):
+ result = cmp_simple_list(self.want.android_publishers, self.have.android_publishers)
+ return result
+
+ @property
+ def ios_allowed_package_names(self):
+ result = cmp_simple_list(self.want.ios_allowed_package_names, self.have.ios_allowed_package_names)
+ return result
+
+
+class ModuleManager(object):
+ def __init__(self, *args, **kwargs):
+ self.module = kwargs.get('module', None)
+ self.client = F5RestClient(**self.module.params)
+ self.want = ModuleParameters(params=self.module.params)
+ self.have = ApiParameters()
+ self.changes = UsableChanges()
+
+ def _set_changed_options(self):
+ changed = {}
+ for key in Parameters.returnables:
+ if getattr(self.want, key) is not None:
+ changed[key] = getattr(self.want, key)
+ if changed:
+ self.changes = UsableChanges(params=changed)
+
+ def _update_changed_options(self):
+ diff = Difference(self.want, self.have)
+ updatables = Parameters.updatables
+ changed = dict()
+ for k in updatables:
+ change = diff.compare(k)
+ if change is None:
+ continue
+ else:
+ if isinstance(change, dict):
+ changed.update(change)
+ else:
+ changed[k] = change
+ if changed:
+ self.changes = UsableChanges(params=changed)
+ return True
+ return False
+
+ def _announce_deprecations(self, result):
+ warnings = result.pop('__warnings', [])
+ for warning in warnings:
+ self.client.module.deprecate(
+ msg=warning['msg'],
+ version=warning['version']
+ )
+
+ def exec_module(self):
+ if not module_provisioned(self.client, 'asm'):
+ raise F5ModuleError(
+ "ASM must be provisioned to use this module."
+ )
+
+ if self.version_less_than_13_1():
+ raise F5ModuleError('Module supported on TMOS versions 13.1.x and above')
+
+ changed = False
+ result = dict()
+ state = self.want.state
+
+ if state == "present":
+ changed = self.present()
+ elif state == "absent":
+ changed = self.absent()
+
+ reportable = ReportableChanges(params=self.changes.to_return())
+ changes = reportable.to_return()
+ result.update(**changes)
+ result.update(dict(changed=changed))
+ self._announce_deprecations(result)
+ return result
+
+ def version_less_than_13_1(self):
+ version = tmos_version(self.client)
+ if LooseVersion(version) < LooseVersion('13.1.0'):
+ return True
+ return False
+
+ def present(self):
+ if self.exists():
+ return self.update()
+ else:
+ return self.create()
+
+ def absent(self):
+ if self.exists():
+ return self.remove()
+ return False
+
+ def should_update(self):
+ result = self._update_changed_options()
+ if result:
+ return True
+ return False
+
+ def update(self):
+ self.have = self.read_current_from_device()
+ if not self.should_update():
+ return False
+ if self.module.check_mode:
+ return True
+ self.update_on_device()
+ return True
+
+ def remove(self):
+ if self.module.check_mode:
+ return True
+ self.remove_from_device()
+ if self.exists():
+ raise F5ModuleError("Failed to delete the resource.")
+ return True
+
+ def create(self):
+ self._set_changed_options()
+ if self.module.check_mode:
+ return True
+ self.create_on_device()
+ return True
+
+ def profile_exists(self):
+ uri = "https://{0}:{1}/mgmt/tm/security/dos/profile/{2}/".format(
+ self.client.provider['server'],
+ self.client.provider['server_port'],
+ transform_name(self.want.partition, self.want.profile),
+ )
+ 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 exists(self):
+ if not self.profile_exists():
+ raise F5ModuleError(
+ 'Specified DOS profile: {0} on partition: {1} does not exist.'.format(
+ self.want.profile, self.want.partition)
+ )
+ uri = "https://{0}:{1}/mgmt/tm/security/dos/profile/{2}/application/{3}".format(
+ self.client.provider['server'],
+ self.client.provider['server_port'],
+ transform_name(self.want.partition, self.want.profile),
+ self.want.profile
+ )
+ 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.profile
+ uri = "https://{0}:{1}/mgmt/tm/security/dos/profile/{2}/application/".format(
+ self.client.provider['server'],
+ self.client.provider['server_port'],
+ transform_name(self.want.partition, self.want.profile),
+ )
+ resp = self.client.api.post(uri, json=params)
+ try:
+ response = resp.json()
+ except ValueError as ex:
+ raise F5ModuleError(str(ex))
+
+ if 'code' in response and response['code'] in [400, 409]:
+ if 'message' in response:
+ raise F5ModuleError(response['message'])
+ else:
+ raise F5ModuleError(resp.content)
+ return True
+
+ def update_on_device(self):
+ params = self.changes.api_params()
+ uri = "https://{0}:{1}/mgmt/tm/security/dos/profile/{2}/application/{3}".format(
+ self.client.provider['server'],
+ self.client.provider['server_port'],
+ transform_name(self.want.partition, self.want.profile),
+ self.want.profile
+ )
+ resp = self.client.api.patch(uri, json=params)
+ try:
+ response = resp.json()
+ except ValueError as ex:
+ raise F5ModuleError(str(ex))
+
+ if 'code' in response and response['code'] == 400:
+ if 'message' in response:
+ raise F5ModuleError(response['message'])
+ else:
+ raise F5ModuleError(resp.content)
+
+ def remove_from_device(self):
+ uri = "https://{0}:{1}/mgmt/tm/security/dos/profile/{2}/application/{3}".format(
+ self.client.provider['server'],
+ self.client.provider['server_port'],
+ transform_name(self.want.partition, self.want.profile),
+ self.want.profile
+ )
+ response = self.client.api.delete(uri)
+ if response.status == 200:
+ return True
+ raise F5ModuleError(response.content)
+
+ def read_current_from_device(self):
+ uri = "https://{0}:{1}/mgmt/tm/security/dos/profile/{2}/application/{3}".format(
+ self.client.provider['server'],
+ self.client.provider['server_port'],
+ transform_name(self.want.partition, self.want.profile),
+ self.want.profile
+ )
+ resp = self.client.api.get(uri)
+ try:
+ response = resp.json()
+ except ValueError as ex:
+ raise F5ModuleError(str(ex))
+
+ if 'code' in response and response['code'] == 400:
+ if 'message' in response:
+ raise F5ModuleError(response['message'])
+ else:
+ raise F5ModuleError(resp.content)
+ return ApiParameters(params=response)
+
+
+class ArgumentSpec(object):
+ def __init__(self):
+ self.supports_check_mode = True
+ argument_spec = dict(
+ profile=dict(
+ required=True,
+ ),
+ geolocations=dict(
+ type='dict',
+ options=dict(
+ blacklist=dict(type='list'),
+ whitelist=dict(type='list'),
+ ),
+ ),
+ heavy_urls=dict(
+ type='dict',
+ options=dict(
+ auto_detect=dict(type='bool'),
+ latency_threshold=dict(type='int'),
+ exclude=dict(type='list'),
+ include=dict(
+ type='list',
+ elements='dict',
+ options=dict(
+ url=dict(required=True),
+ threshold=dict(),
+ ),
+ )
+ ),
+ ),
+ mobile_detection=dict(
+ type='dict',
+ options=dict(
+ enabled=dict(type='bool'),
+ allow_android_rooted_device=dict(type='bool'),
+ allow_any_android_package=dict(type='bool'),
+ allow_any_ios_package=dict(type='bool'),
+ allow_jailbroken_devices=dict(type='bool'),
+ allow_emulators=dict(type='bool'),
+ client_side_challenge_mode=dict(choices=['cshui', 'pass']),
+ ios_allowed_package_names=dict(type='list'),
+ android_publishers=dict(type='list')
+ )
+ ),
+ rtbh_duration=dict(type='int'),
+ rtbh_enable=dict(type='bool'),
+ scrubbing_duration=dict(type='int'),
+ scrubbing_enable=dict(type='bool'),
+ single_page_application=dict(type='bool'),
+ trigger_irule=dict(type='bool'),
+ partition=dict(
+ default='Common',
+ fallback=(env_fallback, ['F5_PARTITION'])
+ ),
+ state=dict(
+ default='present',
+ choices=['present', 'absent']
+ )
+ )
+ self.argument_spec = {}
+ self.argument_spec.update(f5_argument_spec)
+ self.argument_spec.update(argument_spec)
+
+
+def main():
+ spec = ArgumentSpec()
+
+ module = AnsibleModule(
+ argument_spec=spec.argument_spec,
+ supports_check_mode=spec.supports_check_mode,
+ )
+
+ try:
+ mm = ModuleManager(module=module)
+ results = mm.exec_module()
+ module.exit_json(**results)
+ except F5ModuleError as ex:
+ module.fail_json(msg=str(ex))
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/units/modules/network/f5/fixtures/load_asm_dos.json b/test/units/modules/network/f5/fixtures/load_asm_dos.json
new file mode 100644
index 00000000000..cab7b71ff25
--- /dev/null
+++ b/test/units/modules/network/f5/fixtures/load_asm_dos.json
@@ -0,0 +1,276 @@
+{
+ "kind": "tm:security:dos:profile:application:applicationstate",
+ "name": "test",
+ "fullPath": "test",
+ "generation": 1442,
+ "selfLink": "https://localhost/mgmt/tm/security/dos/profile/~Common~test/application/test?ver=13.1.1.4",
+ "botDefense": {
+ "browserLegitCaptcha": "enabled",
+ "browserLegitEnabled": "enabled",
+ "crossDomainRequests": "allow-all",
+ "gracePeriod": 300,
+ "mode": "disabled"
+ },
+ "botSignatures": {
+ "check": "disabled",
+ "categories": [
+ {
+ "name": "DOS Tool",
+ "partition": "Common",
+ "action": "block",
+ "nameReference": {
+ "link": "https://localhost/mgmt/tm/security/dos/bot-signature-category/~Common~DOS%20Tool?ver=13.1.1.4"
+ }
+ },
+ {
+ "name": "E-Mail Collector",
+ "partition": "Common",
+ "action": "block",
+ "nameReference": {
+ "link": "https://localhost/mgmt/tm/security/dos/bot-signature-category/~Common~E-Mail%20Collector?ver=13.1.1.4"
+ }
+ },
+ {
+ "name": "Exploit Tool",
+ "partition": "Common",
+ "action": "block",
+ "nameReference": {
+ "link": "https://localhost/mgmt/tm/security/dos/bot-signature-category/~Common~Exploit%20Tool?ver=13.1.1.4"
+ }
+ },
+ {
+ "name": "Network Scanner",
+ "partition": "Common",
+ "action": "block",
+ "nameReference": {
+ "link": "https://localhost/mgmt/tm/security/dos/bot-signature-category/~Common~Network%20Scanner?ver=13.1.1.4"
+ }
+ },
+ {
+ "name": "Search Engine",
+ "partition": "Common",
+ "action": "report",
+ "nameReference": {
+ "link": "https://localhost/mgmt/tm/security/dos/bot-signature-category/~Common~Search%20Engine?ver=13.1.1.4"
+ }
+ },
+ {
+ "name": "Spam Bot",
+ "partition": "Common",
+ "action": "block",
+ "nameReference": {
+ "link": "https://localhost/mgmt/tm/security/dos/bot-signature-category/~Common~Spam%20Bot?ver=13.1.1.4"
+ }
+ },
+ {
+ "name": "Spyware",
+ "partition": "Common",
+ "action": "block",
+ "nameReference": {
+ "link": "https://localhost/mgmt/tm/security/dos/bot-signature-category/~Common~Spyware?ver=13.1.1.4"
+ }
+ },
+ {
+ "name": "Vulnerability Scanner",
+ "partition": "Common",
+ "action": "block",
+ "nameReference": {
+ "link": "https://localhost/mgmt/tm/security/dos/bot-signature-category/~Common~Vulnerability%20Scanner?ver=13.1.1.4"
+ }
+ },
+ {
+ "name": "Web Spider",
+ "partition": "Common",
+ "action": "block",
+ "nameReference": {
+ "link": "https://localhost/mgmt/tm/security/dos/bot-signature-category/~Common~Web%20Spider?ver=13.1.1.4"
+ }
+ },
+ {
+ "name": "Webserver Stress Tool",
+ "partition": "Common",
+ "action": "block",
+ "nameReference": {
+ "link": "https://localhost/mgmt/tm/security/dos/bot-signature-category/~Common~Webserver%20Stress%20Tool?ver=13.1.1.4"
+ }
+ }
+ ]
+ },
+ "captchaResponse": {
+ "failure": {
+ "body": "You have entered an invalid answer for the question. Please, try again.\n
\n%DOSL7.captcha.image% %DOSL7.captcha.change%\n
\nWhat code is in the image\\?\n%DOSL7.captcha.solution%\n
\n%DOSL7.captcha.submit%\n
\n
\nYour support ID is: %DOSL7.captcha.support_id%.",
+ "type": "default"
+ },
+ "first": {
+ "body": "This question is for testing whether you are a human visitor and to prevent automated spam submission.\n
\n%DOSL7.captcha.image% %DOSL7.captcha.change%\n
\nWhat code is in the image\\?\n%DOSL7.captcha.solution%\n
\n%DOSL7.captcha.submit%\n
\n
\nYour support ID is: %DOSL7.captcha.support_id%.",
+ "type": "default"
+ }
+ },
+ "heavyUrls": {
+ "automaticDetection": "enabled",
+ "exclude": [
+ "/exclude.html"
+ ],
+ "latencyThreshold": 1000,
+ "protection": "disabled",
+ "includeList": [
+ {
+ "name": "URL/test.htm",
+ "threshold": "auto",
+ "url": "/test.htm"
+ },
+ {
+ "name": "URL/testy.htm",
+ "threshold": "auto",
+ "url": "/testy.htm"
+ }
+ ]
+ },
+ "mobileDetection": {
+ "allowAndroidRootedDevice": "false",
+ "allowAnyAndroidPackage": "false",
+ "allowAnyIosPackage": "false",
+ "allowEmulators": "true",
+ "allowJailbrokenDevices": "true",
+ "clientSideChallengeMode": "pass",
+ "enabled": "disabled",
+ "iosAllowedPackageNames": [
+ "foobarapp"
+ ],
+ "androidPublishers": [
+ {
+ "name": "ca-bundle.crt",
+ "partition": "Common",
+ "nameReference": {
+ "link": "https://localhost/mgmt/tm/sys/file/ssl-cert/~Common~ca-bundle.crt?ver=13.1.1.4"
+ }
+ }
+ ]
+ },
+ "rtbhDurationSec": 300,
+ "rtbhEnable": "enabled",
+ "scrubbingDurationSec": 60,
+ "scrubbingEnable": "enabled",
+ "singlePageApplication": "enabled",
+ "stressBased": {
+ "behavioral": {
+ "dosDetection": "disabled",
+ "mitigationMode": "none",
+ "signatures": "disabled",
+ "signaturesApprovedOnly": "disabled"
+ },
+ "deEscalationPeriod": 7200,
+ "deviceCaptchaChallenge": "disabled",
+ "deviceClientSideDefense": "disabled",
+ "deviceMaximumAutoTps": 5000,
+ "deviceMaximumTps": 200,
+ "deviceMinimumAutoTps": 5,
+ "deviceMinimumTps": 40,
+ "deviceRateLimiting": "disabled",
+ "deviceRequestBlockingMode": "rate-limit",
+ "deviceTpsIncreaseRate": 500,
+ "escalationPeriod": 120,
+ "geoCaptchaChallenge": "disabled",
+ "geoClientSideDefense": "disabled",
+ "geoMaximumAutoTps": 20000,
+ "geoMinimumAutoTps": 50,
+ "geoMinimumShare": 10,
+ "geoRateLimiting": "disabled",
+ "geoRequestBlockingMode": "rate-limit",
+ "geoShareIncreaseRate": 500,
+ "ipCaptchaChallenge": "disabled",
+ "ipClientSideDefense": "disabled",
+ "ipMaximumAutoTps": 5000,
+ "ipMaximumTps": 200,
+ "ipMinimumAutoTps": 5,
+ "ipMinimumTps": 40,
+ "ipRateLimiting": "enabled",
+ "ipRequestBlockingMode": "rate-limit",
+ "ipTpsIncreaseRate": 500,
+ "mode": "off",
+ "siteCaptchaChallenge": "disabled",
+ "siteClientSideDefense": "disabled",
+ "siteMaximumAutoTps": 20000,
+ "siteMaximumTps": 10000,
+ "siteMinimumAutoTps": 50,
+ "siteMinimumTps": 2000,
+ "siteRateLimiting": "disabled",
+ "siteTpsIncreaseRate": 500,
+ "thresholdsMode": "manual",
+ "urlCaptchaChallenge": "disabled",
+ "urlClientSideDefense": "disabled",
+ "urlEnableHeavy": "enabled",
+ "urlMaximumAutoTps": 5000,
+ "urlMaximumTps": 1000,
+ "urlMinimumAutoTps": 50,
+ "urlMinimumTps": 200,
+ "urlRateLimiting": "enabled",
+ "urlTpsIncreaseRate": 500
+ },
+ "tcpDump": {
+ "maximumDuration": 30,
+ "maximumSize": 10,
+ "recordTraffic": "disabled",
+ "repetitionInterval": "120"
+ },
+ "tpsBased": {
+ "deEscalationPeriod": 7200,
+ "deviceCaptchaChallenge": "disabled",
+ "deviceClientSideDefense": "disabled",
+ "deviceMaximumAutoTps": 5000,
+ "deviceMaximumTps": 200,
+ "deviceMinimumAutoTps": 5,
+ "deviceMinimumTps": 40,
+ "deviceRateLimiting": "disabled",
+ "deviceRequestBlockingMode": "rate-limit",
+ "deviceTpsIncreaseRate": 500,
+ "escalationPeriod": 120,
+ "geoCaptchaChallenge": "disabled",
+ "geoClientSideDefense": "disabled",
+ "geoMaximumAutoTps": 20000,
+ "geoMinimumAutoTps": 50,
+ "geoMinimumShare": 10,
+ "geoRateLimiting": "disabled",
+ "geoRequestBlockingMode": "rate-limit",
+ "geoShareIncreaseRate": 500,
+ "ipCaptchaChallenge": "disabled",
+ "ipClientSideDefense": "disabled",
+ "ipMaximumAutoTps": 5000,
+ "ipMaximumTps": 200,
+ "ipMinimumAutoTps": 5,
+ "ipMinimumTps": 40,
+ "ipRateLimiting": "enabled",
+ "ipRequestBlockingMode": "rate-limit",
+ "ipTpsIncreaseRate": 500,
+ "mode": "off",
+ "siteCaptchaChallenge": "disabled",
+ "siteClientSideDefense": "disabled",
+ "siteMaximumAutoTps": 20000,
+ "siteMaximumTps": 10000,
+ "siteMinimumAutoTps": 50,
+ "siteMinimumTps": 2000,
+ "siteRateLimiting": "disabled",
+ "siteTpsIncreaseRate": 500,
+ "thresholdsMode": "manual",
+ "urlCaptchaChallenge": "disabled",
+ "urlClientSideDefense": "disabled",
+ "urlEnableHeavy": "enabled",
+ "urlMaximumAutoTps": 5000,
+ "urlMaximumTps": 1000,
+ "urlMinimumAutoTps": 50,
+ "urlMinimumTps": 200,
+ "urlRateLimiting": "enabled",
+ "urlTpsIncreaseRate": 500
+ },
+ "triggerIrule": "enabled",
+ "geolocations": [
+ {
+ "name": "Afghanistan",
+ "blackListed": true
+ },
+ {
+ "name": "Aland Islands",
+ "whiteListed": true
+ }
+ ]
+}
diff --git a/test/units/modules/network/f5/test_bigip_asm_dos_application.py b/test/units/modules/network/f5/test_bigip_asm_dos_application.py
new file mode 100644
index 00000000000..bd9a99125de
--- /dev/null
+++ b/test/units/modules/network/f5/test_bigip_asm_dos_application.py
@@ -0,0 +1,279 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright: (c) 2019, F5 Networks Inc.
+# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import os
+import json
+import pytest
+import sys
+
+if sys.version_info < (2, 7):
+ pytestmark = pytest.mark.skip("F5 Ansible modules require Python >= 2.7")
+
+from ansible.module_utils.basic import AnsibleModule
+
+try:
+ from library.modules.bigip_asm_dos_application import ApiParameters
+ from library.modules.bigip_asm_dos_application import ModuleParameters
+ from library.modules.bigip_asm_dos_application import ModuleManager
+ from library.modules.bigip_asm_dos_application import ArgumentSpec
+
+ # 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:
+ from ansible.modules.network.f5.bigip_asm_dos_application import ApiParameters
+ from ansible.modules.network.f5.bigip_asm_dos_application import ModuleParameters
+ from ansible.modules.network.f5.bigip_asm_dos_application import ModuleManager
+ from ansible.modules.network.f5.bigip_asm_dos_application import ArgumentSpec
+
+ # 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
+
+
+fixture_path = os.path.join(os.path.dirname(__file__), 'fixtures')
+fixture_data = {}
+
+
+def load_fixture(name):
+ path = os.path.join(fixture_path, name)
+
+ if path in fixture_data:
+ return fixture_data[path]
+
+ with open(path) as f:
+ data = f.read()
+
+ try:
+ data = json.loads(data)
+ except Exception:
+ pass
+
+ fixture_data[path] = data
+ return data
+
+
+class TestParameters(unittest.TestCase):
+ def test_module_parameters(self):
+ args = dict(
+ profile='dos_foo',
+ geolocations=dict(
+ blacklist=['Argentina', 'Montenegro'],
+ whitelist=['France', 'Belgium']
+ ),
+ heavy_urls=dict(
+ auto_detect=True,
+ latency_threshold=3000,
+ exclude=['/exclude1.html', '/exclude2.html'],
+ include=[dict(url='include1.html', threshold='auto'),
+ dict(url='include2.html', threshold='2000')],
+ ),
+ mobile_detection=dict(
+ enabled=True,
+ allow_android_rooted_device=True,
+ allow_any_android_package=True,
+ allow_any_ios_package=True,
+ allow_jailbroken_devices=True,
+ allow_emulators=True,
+ client_side_challenge_mode='cshui',
+ ios_allowed_package_names=['foo', 'bar'],
+ android_publishers=['cert1.crt', 'cert2.crt']
+ ),
+ rtbh_duration=180,
+ rtbh_enable=True,
+ scrubbing_duration=360,
+ scrubbing_enable=True,
+ single_page_application=True,
+ trigger_irule=False,
+ partition='Common'
+ )
+
+ p = ModuleParameters(params=args)
+ assert p.profile == 'dos_foo'
+ assert p.geo_whitelist == ['France', 'Belgium']
+ assert p.geo_blacklist == ['Argentina', 'Montenegro']
+ assert p.auto_detect == 'enabled'
+ assert p.latency_threshold == 3000
+ assert p.hw_url_exclude == ['/exclude1.html', '/exclude2.html']
+ assert dict(name='URL/include1.html', threshold='auto', url='/include1.html') in p.hw_url_include
+ assert dict(name='URL/include2.html', threshold='2000', url='/include2.html') in p.hw_url_include
+ assert p.allow_android_rooted_device == 'true'
+ assert p.enable_mobile_detection == 'enabled'
+ assert p.allow_any_android_package == 'true'
+ assert p.allow_any_ios_package == 'true'
+ assert p.allow_jailbroken_devices == 'true'
+ assert p.allow_emulators == 'true'
+ assert p.client_side_challenge_mode == 'cshui'
+ assert p.ios_allowed_package_names == ['foo', 'bar']
+ assert p.android_publishers == ['/Common/cert1.crt', '/Common/cert2.crt']
+ assert p.rtbh_duration == 180
+ assert p.rtbh_enable == 'enabled'
+ assert p.scrubbing_duration == 360
+ assert p.scrubbing_enable == 'enabled'
+ assert p.single_page_application == 'enabled'
+ assert p.trigger_irule == 'disabled'
+
+ def test_api_parameters(self):
+ args = load_fixture('load_asm_dos.json')
+
+ p = ApiParameters(params=args)
+ assert p.geo_whitelist == ['Aland Islands']
+ assert p.geo_blacklist == ['Afghanistan']
+ assert p.auto_detect == 'enabled'
+ assert p.latency_threshold == 1000
+ assert p.hw_url_exclude == ['/exclude.html']
+ assert dict(name='URL/test.htm', threshold='auto', url='/test.htm') in p.hw_url_include
+ assert dict(name='URL/testy.htm', threshold='auto', url='/testy.htm') in p.hw_url_include
+ assert p.allow_android_rooted_device == 'false'
+ assert p.enable_mobile_detection == 'disabled'
+ assert p.allow_any_android_package == 'false'
+ assert p.allow_any_ios_package == 'false'
+ assert p.allow_jailbroken_devices == 'true'
+ assert p.allow_emulators == 'true'
+ assert p.client_side_challenge_mode == 'pass'
+ assert p.ios_allowed_package_names == ['foobarapp']
+ assert p.android_publishers == ['/Common/ca-bundle.crt']
+ assert p.rtbh_duration == 300
+ assert p.rtbh_enable == 'enabled'
+ assert p.scrubbing_duration == 60
+ assert p.scrubbing_enable == 'enabled'
+ assert p.single_page_application == 'enabled'
+ assert p.trigger_irule == 'enabled'
+
+
+class TestManager(unittest.TestCase):
+ def setUp(self):
+ self.spec = ArgumentSpec()
+ try:
+ self.p1 = patch('library.modules.bigip_asm_dos_application.module_provisioned')
+ self.m1 = self.p1.start()
+ self.m1.return_value = True
+ except Exception:
+ self.p1 = patch('ansible.modules.network.f5.bigip_asm_dos_application.module_provisioned')
+ self.m1 = self.p1.start()
+ self.m1.return_value = True
+
+ def tearDown(self):
+ self.p1.stop()
+
+ def test_create_asm_dos_profile(self, *args):
+ set_module_args(dict(
+ profile='dos_foo',
+ geolocations=dict(
+ blacklist=['Argentina', 'Montenegro'],
+ whitelist=['France', 'Belgium']
+ ),
+ heavy_urls=dict(
+ auto_detect=True,
+ latency_threshold=3000,
+ exclude=['/exclude1.html', '/exclude2.html'],
+ include=[dict(url='include1.html', threshold='auto'),
+ dict(url='include2.html', threshold='2000')]
+ ),
+ mobile_detection=dict(
+ enabled=True,
+ allow_android_rooted_device=True,
+ allow_any_android_package=True,
+ allow_any_ios_package=True,
+ allow_jailbroken_devices=True,
+ allow_emulators=True,
+ client_side_challenge_mode='cshui',
+ ios_allowed_package_names=['foo', 'bar'],
+ android_publishers=['cert1.crt', 'cert2.crt']
+ ),
+ rtbh_duration=180,
+ rtbh_enable=True,
+ scrubbing_duration=360,
+ scrubbing_enable=True,
+ single_page_application=True,
+ trigger_irule=False,
+ partition='Common',
+ provider=dict(
+ server='localhost',
+ password='password',
+ user='admin'
+ )
+ ))
+
+ module = AnsibleModule(
+ argument_spec=self.spec.argument_spec,
+ supports_check_mode=self.spec.supports_check_mode,
+ )
+
+ # Override methods in the specific type of manager
+ mm = ModuleManager(module=module)
+ mm.exists = Mock(return_value=False)
+ mm.create_on_device = Mock(return_value=True)
+ mm.version_less_than_13_1 = Mock(return_value=False)
+
+ results = mm.exec_module()
+
+ assert results['changed'] is True
+ assert results['geolocations'] == dict(blacklist=['Argentina', 'Montenegro'], whitelist=['France', 'Belgium'])
+ assert results['heavy_urls'] == dict(auto_detect='yes', latency_threshold=3000,
+ exclude=['/exclude1.html', '/exclude2.html'],
+ include=[dict(url='/include1.html', threshold='auto'),
+ dict(url='/include2.html', threshold='2000')]
+ )
+ assert results['mobile_detection'] == dict(enabled='yes', allow_android_rooted_device='yes',
+ allow_any_android_package='yes', allow_any_ios_package='yes',
+ allow_jailbroken_devices='yes', allow_emulators='yes',
+ client_side_challenge_mode='cshui',
+ ios_allowed_package_names=['foo', 'bar'],
+ android_publishers=['/Common/cert1.crt', '/Common/cert2.crt']
+ )
+ assert results['rtbh_duration'] == 180
+ assert results['rtbh_enable'] == 'yes'
+ assert results['scrubbing_duration'] == 360
+ assert results['scrubbing_enable'] == 'yes'
+ assert results['single_page_application'] == 'yes'
+ assert results['trigger_irule'] == 'no'
+
+ def test_update_asm_dos_profile(self, *args):
+ set_module_args(dict(
+ profile='test',
+ heavy_urls=dict(
+ latency_threshold=3000,
+ exclude=['/exclude1.html', '/exclude2.html'],
+ include=[dict(url='include1.html', threshold='auto'),
+ dict(url='include2.html', threshold='2000')]
+ ),
+ provider=dict(
+ server='localhost',
+ password='password',
+ user='admin'
+ )
+ ))
+
+ current = ApiParameters(params=load_fixture('load_asm_dos.json'))
+
+ module = AnsibleModule(
+ argument_spec=self.spec.argument_spec,
+ supports_check_mode=self.spec.supports_check_mode,
+ )
+
+ # Override methods in the specific type of manager
+ mm = ModuleManager(module=module)
+ mm.exists = Mock(return_value=True)
+ mm.update_on_device = Mock(return_value=True)
+ mm.read_current_from_device = Mock(return_value=current)
+ mm.version_less_than_13_1 = Mock(return_value=False)
+
+ results = mm.exec_module()
+
+ assert results['changed'] is True
+ assert results['heavy_urls'] == dict(latency_threshold=3000, exclude=['/exclude1.html', '/exclude2.html'],
+ include=[dict(url='/include1.html', threshold='auto'),
+ dict(url='/include2.html', threshold='2000')]
+ )