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