1#!/usr/bin/python
2# -*- coding: utf-8 -*-
3#
4# Copyright: (c) 2019, F5 Networks Inc.
5# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
6
7from __future__ import absolute_import, division, print_function
8__metaclass__ = type
9
10
11ANSIBLE_METADATA = {'metadata_version': '1.1',
12                    'status': ['preview'],
13                    'supported_by': 'certified'}
14
15DOCUMENTATION = r'''
16---
17module: bigip_asm_dos_application
18short_description: Manage application settings for DOS profile
19description:
20  - Manages Application settings for ASM/AFM DOS profile.
21version_added: 2.9
22options:
23  profile:
24    description:
25      - Specifies the name of the profile to manage application settings in.
26    type: str
27    required: True
28  rtbh_duration:
29    description:
30      - Specifies the duration of the RTBH BGP route advertisement, in seconds.
31      - The accepted range is between 0 and 4294967295 inclusive.
32    type: int
33  rtbh_enable:
34    description:
35      - Specifies whether to enable Remote Triggered Black Hole C(RTBH) of attacking IPs by advertising BGP routes.
36    type: bool
37  scrubbing_duration:
38    description:
39      - Specifies the duration of the Traffic Scrubbing BGP route advertisement, in seconds.
40      - The accepted range is between 0 and 4294967295 inclusive.
41    type: int
42  scrubbing_enable:
43    description:
44      - Specifies whether to enable Traffic Scrubbing during attacks by advertising BGP routes.
45    type: bool
46  single_page_application:
47    description:
48      - Specifies, when C(yes), that the system supports a Single Page Applications.
49    type: bool
50  trigger_irule:
51    description:
52      - Specifies, when C(yes), that the system activates an Application DoS iRule event.
53    type: bool
54  geolocations:
55    description:
56      - Manages the geolocations countries whitelist, blacklist.
57    type: dict
58    suboptions:
59      whitelist:
60        description:
61          - A list of countries to be put on whitelist, must not have overlapping elements with C(blacklist).
62        type: list
63      blacklist:
64        description:
65          - A list of countries to be put on blacklist, must not have overlapping elements with C(whitelist).
66        type: list
67  heavy_urls:
68    description:
69      - Manages Heavy URL protection.
70      - Heavy URLs are a small number of site URLs that might consume considerable server resources per request.
71    type: dict
72    suboptions:
73      auto_detect:
74        description:
75          - Enables or disables automatic heavy URL detection.
76        type: bool
77      latency_threshold:
78        description:
79          - Specifies the latency threshold for automatic heavy URL detection.
80          - The accepted range is between 0 and 4294967295 milliseconds inclusive.
81        type: int
82      exclude:
83        description:
84          - Specifies a list of URLs or wildcards to exclude from the heavy URLs.
85        type: list
86      include:
87        description:
88          - Configures additional URLs to include in the heavy URLs that were auto detected.
89        type: list
90        suboptions:
91          url:
92            description:
93              - Specifies the URL to be added to the list of heavy URLs, in addition to the automatically detected ones.
94            type: str
95          threshold:
96            description:
97              - Specifies the threshold of requests per second, where the URL in question is considered under attack.
98              - The accepted range is between 1 and 4294967295 inclusive, or C(auto).
99            type: str
100  mobile_detection:
101    description:
102      - Configures detection of mobile applications built with the Anti-Bot Mobile SDK and defines how requests
103        from these mobile application clients are handled.
104    type: dict
105    suboptions:
106      enabled:
107        description:
108          - When C(yes), requests from mobile applications built with Anti-Bot Mobile SDK will be detected and handled
109            according to the parameters set.
110          - When C(no), these requests will be handled like any other request which may let attacks in, or cause false
111            positives.
112        type: bool
113      allow_android_rooted_device:
114        description:
115          - When C(yes) device will allow traffic from rooted Android devices.
116        type: bool
117      allow_any_android_package:
118        description:
119          - When C(yes) allows any application publisher.
120          - A publisher is identified by the certificate used to sign the application.
121        type: bool
122      allow_any_ios_package:
123        description:
124          - When C(yes) allows any iOS package.
125          - A package name is the unique identifier of the mobile application.
126        type: bool
127      allow_jailbroken_devices:
128        description:
129          - When C(yes) allows traffic from jailbroken iOS devices.
130        type: bool
131      allow_emulators:
132        description:
133          - When C(yes) allows traffic from applications run on emulators.
134        type: bool
135      client_side_challenge_mode:
136        description:
137          - Action to take when a CAPTCHA or Client Side Integrity challenge needs to be presented.
138          - The mobile application user will not see a CAPTCHA challenge and the mobile application will not be
139            presented with the Client Side Integrity challenge. The such options for mobile applications are C(pass)
140            or C(cshui).
141          - When C(pass) the traffic is passed without incident.
142          - When C(cshui) the SDK checks for human interactions with the screen in the last few seconds.
143            If none are detected, the traffic is blocked.
144        type: str
145        choices:
146          - pass
147          - cshui
148      ios_allowed_package_names:
149        description:
150          - Specifies the names of iOS packages to allow traffic on.
151          - This option has no effect when C(allow_any_ios_package) is set to C(yes).
152        type: list
153      android_publishers:
154        description:
155          - This option has no effect when C(allow_any_android_package) is set to C(yes).
156          - Specifies the allowed publisher certificates for android applications.
157          - The publisher certificate needs to be installed on the BIG-IP beforehand.
158          - "The certificate name located on a different partition than the one specified
159            in C(partition) parameter needs to be provided in C(full_path) format C(/Foo/cert.crt)."
160        type: list
161  partition:
162    description:
163      - Device partition to manage resources on.
164    type: str
165    default: Common
166  state:
167    description:
168      - When C(state) is C(present), ensures that the Application object exists.
169      - When C(state) is C(absent), ensures that the Application object is removed.
170    type: str
171    choices:
172      - present
173      - absent
174    default: present
175notes:
176  - Requires BIG-IP >= 13.1.0
177extends_documentation_fragment: f5
178author:
179  - Wojciech Wypior (@wojtek0806)
180'''
181
182EXAMPLES = r'''
183- name: Create an ASM dos application profile
184  bigip_asm_dos_application:
185    profile: dos_foo
186    geolocations:
187      blacklist:
188        - Afghanistan
189        - Andora
190      whitelist:
191        - Cuba
192    heavy_urls:
193      auto_detect: yes
194      latency_threshold: 1000
195    rtbh_duration: 3600
196    rtbh_enable: yes
197    single_page_application: yes
198    provider:
199      password: secret
200      server: lb.mydomain.com
201      user: admin
202  delegate_to: localhost
203
204- name: Update an ASM dos application profile
205  bigip_asm_dos_application:
206    profile: dos_foo
207    mobile_detection:
208      enabled: yes
209      allow_any_ios_package: yes
210      allow_emulators: yes
211    provider:
212      password: secret
213      server: lb.mydomain.com
214      user: admin
215  delegate_to: localhost
216
217- name: Remove an ASM dos application profile
218  bigip_asm_dos_application:
219    profile: dos_foo
220    state: absent
221    provider:
222      password: secret
223      server: lb.mydomain.com
224      user: admin
225  delegate_to: localhost
226'''
227
228RETURN = r'''
229rtbh_enable:
230  description: Enables Remote Triggered Black Hole of attacking IPs.
231  returned: changed
232  type: bool
233  sample: no
234rtbh_duration:
235  description: The duration of the RTBH BGP route advertisement.
236  returned: changed
237  type: int
238  sample: 3600
239scrubbing_enable:
240  description: Enables Traffic Scrubbing during attacks.
241  returned: changed
242  type: bool
243  sample: yes
244scrubbing_duration:
245  description: The duration of the Traffic Scrubbing BGP route advertisement.
246  returned: changed
247  type: int
248  sample: 3600
249single_page_application:
250  description: Enables support of a Single Page Applications.
251  returned: changed
252  type: bool
253  sample: no
254trigger_irule:
255  description: Activates an Application DoS iRule event.
256  returned: changed
257  type: bool
258  sample: yes
259geolocations:
260  description: Specifies geolocations countries whitelist, blacklist.
261  type: complex
262  returned: changed
263  contains:
264    whitelist:
265      description: A list of countries to be put on whitelist.
266      returned: changed
267      type: list
268      sample: ['United States, United Kingdom']
269    blacklist:
270      description: A list of countries to be put on blacklist.
271      returned: changed
272      type: list
273      sample: ['Russia', 'Germany']
274  sample: hash/dictionary of values
275heavy_urls:
276  description: Manages Heavy URL protection.
277  type: complex
278  returned: changed
279  contains:
280    auto_detect:
281      description: Enables or disables automatic heavy URL detection.
282      returned: changed
283      type: bool
284      sample: yes
285    latency_threshold:
286      description: Specifies the latency threshold for automatic heavy URL detection.
287      returned: changed
288      type: int
289      sample: 2000
290    exclude:
291      description: Specifies a list of URLs or wildcards to exclude from the heavy URLs.
292      returned: changed
293      type: list
294      sample: ['/exclude.html', '/exclude2.html']
295    include:
296      description: Configures additional URLs to include in the heavy URLs.
297      type: complex
298      returned: changed
299      contains:
300        url:
301          description: The URL to be added to the list of heavy URLs.
302          returned: changed
303          type: str
304          sample: /include.html
305        threshold:
306          description: The threshold of requests per second
307          returned: changed
308          type: str
309          sample: auto
310      sample: hash/dictionary of values
311  sample: hash/dictionary of values
312mobile_detection:
313  description: Configures detection of mobile applications built with the Anti-Bot Mobile SDK.
314  type: complex
315  returned: changed
316  contains:
317    enable:
318      description: Enables or disables automatic mobile detection.
319      returned: changed
320      type: bool
321      sample: yes
322    allow_android_rooted_device:
323      description: Allows traffic from rooted Android devices.
324      returned: changed
325      type: bool
326      sample: no
327    allow_any_android_package:
328      description: Allows any application publisher.
329      returned: changed
330      type: bool
331      sample: no
332    allow_any_ios_package:
333      description: Allows any iOS package.
334      returned: changed
335      type: bool
336      sample: yes
337    allow_jailbroken_devices:
338      description: Allows traffic from jailbroken iOS devices.
339      returned: changed
340      type: bool
341      sample: no
342    allow_emulators:
343      description: Allows traffic from applications run on emulators.
344      returned: changed
345      type: bool
346      sample: yes
347    client_side_challenge_mode:
348      description: Action to take when a CAPTCHA or Client Side Integrity challenge needs to be presented.
349      returned: changed
350      type: str
351      sample: pass
352    ios_allowed_package_names:
353      description: The names of iOS packages to allow traffic on.
354      returned: changed
355      type: list
356      sample: ['package1','package2']
357    android_publishers:
358      description: The allowed publisher certificates for android applications.
359      returned: changed
360      type: list
361      sample: ['/Common/cert1.crt', '/Common/cert2.crt']
362  sample: hash/dictionary of values
363'''
364from ansible.module_utils.basic import AnsibleModule
365from ansible.module_utils.basic import env_fallback
366from distutils.version import LooseVersion
367
368try:
369    from library.module_utils.network.f5.bigip import F5RestClient
370    from library.module_utils.network.f5.common import F5ModuleError
371    from library.module_utils.network.f5.common import AnsibleF5Parameters
372    from library.module_utils.network.f5.common import fq_name
373    from library.module_utils.network.f5.common import transform_name
374    from library.module_utils.network.f5.common import flatten_boolean
375    from library.module_utils.network.f5.common import f5_argument_spec
376    from library.module_utils.network.f5.compare import compare_complex_list
377    from library.module_utils.network.f5.compare import cmp_simple_list
378    from library.module_utils.network.f5.icontrol import tmos_version
379    from library.module_utils.network.f5.icontrol import module_provisioned
380except ImportError:
381    from ansible.module_utils.network.f5.bigip import F5RestClient
382    from ansible.module_utils.network.f5.common import F5ModuleError
383    from ansible.module_utils.network.f5.common import AnsibleF5Parameters
384    from ansible.module_utils.network.f5.common import fq_name
385    from ansible.module_utils.network.f5.common import transform_name
386    from ansible.module_utils.network.f5.common import flatten_boolean
387    from ansible.module_utils.network.f5.common import f5_argument_spec
388    from ansible.module_utils.network.f5.compare import compare_complex_list
389    from ansible.module_utils.network.f5.compare import cmp_simple_list
390    from ansible.module_utils.network.f5.icontrol import tmos_version
391    from ansible.module_utils.network.f5.icontrol import module_provisioned
392
393
394class Parameters(AnsibleF5Parameters):
395    api_map = {
396        'rtbhDurationSec': 'rtbh_duration',
397        'rtbhEnable': 'rtbh_enable',
398        'scrubbingDurationSec': 'scrubbing_duration',
399        'scrubbingEnable': 'scrubbing_enable',
400        'singlePageApplication': 'single_page_application',
401        'triggerIrule': 'trigger_irule',
402        'heavyUrls': 'heavy_urls',
403        'mobileDetection': 'mobile_detection',
404    }
405
406    api_attributes = [
407        'geolocations',
408        'rtbhDurationSec',
409        'rtbhEnable',
410        'scrubbingDurationSec',
411        'scrubbingEnable',
412        'singlePageApplication',
413        'triggerIrule',
414        'heavyUrls',
415        'mobileDetection',
416    ]
417
418    returnables = [
419        'rtbh_duration',
420        'rtbh_enable',
421        'scrubbing_duration',
422        'scrubbing_enable',
423        'single_page_application',
424        'trigger_irule',
425        'enable_mobile_detection',
426        'allow_android_rooted_device',
427        'allow_any_android_package',
428        'allow_any_ios_package',
429        'allow_jailbroken_devices',
430        'allow_emulators',
431        'client_side_challenge_mode',
432        'ios_allowed_package_names',
433        'android_publishers',
434        'auto_detect',
435        'latency_threshold',
436        'hw_url_exclude',
437        'hw_url_include',
438        'geo_blacklist',
439        'geo_whitelist',
440    ]
441
442    updatables = [
443        'rtbh_duration',
444        'rtbh_enable',
445        'scrubbing_duration',
446        'scrubbing_enable',
447        'single_page_application',
448        'trigger_irule',
449        'enable_mobile_detection',
450        'allow_android_rooted_device',
451        'allow_any_android_package',
452        'allow_any_ios_package',
453        'allow_jailbroken_devices',
454        'allow_emulators',
455        'client_side_challenge_mode',
456        'ios_allowed_package_names',
457        'android_publishers',
458        'auto_detect',
459        'latency_threshold',
460        'hw_url_exclude',
461        'hw_url_include',
462        'geo_blacklist',
463        'geo_whitelist',
464    ]
465
466
467class ApiParameters(Parameters):
468    @property
469    def enable_mobile_detection(self):
470        if self._values['mobile_detection'] is None:
471            return None
472        return self._values['mobile_detection']['enabled']
473
474    @property
475    def allow_android_rooted_device(self):
476        if self._values['mobile_detection'] is None:
477            return None
478        return self._values['mobile_detection']['allowAndroidRootedDevice']
479
480    @property
481    def allow_any_android_package(self):
482        if self._values['mobile_detection'] is None:
483            return None
484        return self._values['mobile_detection']['allowAnyAndroidPackage']
485
486    @property
487    def allow_any_ios_package(self):
488        if self._values['mobile_detection'] is None:
489            return None
490        return self._values['mobile_detection']['allowAnyIosPackage']
491
492    @property
493    def allow_jailbroken_devices(self):
494        if self._values['mobile_detection'] is None:
495            return None
496        return self._values['mobile_detection']['allowJailbrokenDevices']
497
498    @property
499    def allow_emulators(self):
500        if self._values['mobile_detection'] is None:
501            return None
502        return self._values['mobile_detection']['allowEmulators']
503
504    @property
505    def client_side_challenge_mode(self):
506        if self._values['mobile_detection'] is None:
507            return None
508        return self._values['mobile_detection']['clientSideChallengeMode']
509
510    @property
511    def ios_allowed_package_names(self):
512        if self._values['mobile_detection'] is None:
513            return None
514        return self._values['mobile_detection'].get('iosAllowedPackageNames', None)
515
516    @property
517    def android_publishers(self):
518        if self._values['mobile_detection'] is None or 'androidPublishers' not in self._values['mobile_detection']:
519            return None
520        result = [fq_name(publisher['partition'], publisher['name'])
521                  for publisher in self._values['mobile_detection']['androidPublishers']]
522        return result
523
524    @property
525    def auto_detect(self):
526        if self._values['heavy_urls'] is None:
527            return None
528        return self._values['heavy_urls']['automaticDetection']
529
530    @property
531    def latency_threshold(self):
532        if self._values['heavy_urls'] is None:
533            return None
534        return self._values['heavy_urls']['latencyThreshold']
535
536    @property
537    def hw_url_exclude(self):
538        if self._values['heavy_urls'] is None:
539            return None
540        return self._values['heavy_urls'].get('exclude', None)
541
542    @property
543    def hw_url_include(self):
544        if self._values['heavy_urls'] is None:
545            return None
546        return self._values['heavy_urls'].get('includeList', None)
547
548    @property
549    def geo_blacklist(self):
550        if self._values['geolocations'] is None:
551            return None
552        result = list()
553        for item in self._values['geolocations']:
554            if 'blackListed' in item and item['blackListed'] is True:
555                result.append(item['name'])
556        if result:
557            return result
558
559    @property
560    def geo_whitelist(self):
561        if self._values['geolocations'] is None:
562            return None
563        result = list()
564        for item in self._values['geolocations']:
565            if 'whiteListed' in item and item['whiteListed'] is True:
566                result.append(item['name'])
567        if result:
568            return result
569
570
571class ModuleParameters(Parameters):
572    @property
573    def rtbh_duration(self):
574        if self._values['rtbh_duration'] is None:
575            return None
576        if 0 <= self._values['rtbh_duration'] <= 4294967295:
577            return self._values['rtbh_duration']
578        raise F5ModuleError(
579            "Valid 'rtbh_duration' must be in range 0 - 4294967295 seconds."
580        )
581
582    @property
583    def rtbh_enable(self):
584        result = flatten_boolean(self._values['rtbh_enable'])
585        if result == 'yes':
586            return 'enabled'
587        if result == 'no':
588            return 'disabled'
589        return result
590
591    @property
592    def scrubbing_duration(self):
593        if self._values['scrubbing_duration'] is None:
594            return None
595        if 0 <= self._values['scrubbing_duration'] <= 4294967295:
596            return self._values['scrubbing_duration']
597        raise F5ModuleError(
598            "Valid 'scrubbing_duration' must be in range 0 - 4294967295 seconds."
599        )
600
601    @property
602    def scrubbing_enable(self):
603        result = flatten_boolean(self._values['scrubbing_enable'])
604        if result == 'yes':
605            return 'enabled'
606        if result == 'no':
607            return 'disabled'
608        return result
609
610    @property
611    def single_page_application(self):
612        result = flatten_boolean(self._values['single_page_application'])
613        if result == 'yes':
614            return 'enabled'
615        if result == 'no':
616            return 'disabled'
617        return result
618
619    @property
620    def trigger_irule(self):
621        result = flatten_boolean(self._values['trigger_irule'])
622        if result == 'yes':
623            return 'enabled'
624        if result == 'no':
625            return 'disabled'
626        return result
627
628    @property
629    def enable_mobile_detection(self):
630        if self._values['mobile_detection'] is None:
631            return None
632        result = flatten_boolean(self._values['mobile_detection']['enabled'])
633        if result == 'yes':
634            return 'enabled'
635        if result == 'no':
636            return 'disabled'
637        return result
638
639    @property
640    def allow_android_rooted_device(self):
641        if self._values['mobile_detection'] is None:
642            return None
643        result = flatten_boolean(self._values['mobile_detection']['allow_android_rooted_device'])
644        if result == 'yes':
645            return 'true'
646        if result == 'no':
647            return 'false'
648        return result
649
650    @property
651    def allow_any_android_package(self):
652        if self._values['mobile_detection'] is None:
653            return None
654        result = flatten_boolean(self._values['mobile_detection']['allow_any_android_package'])
655        if result == 'yes':
656            return 'true'
657        if result == 'no':
658            return 'false'
659        return result
660
661    @property
662    def allow_any_ios_package(self):
663        if self._values['mobile_detection'] is None:
664            return None
665        result = flatten_boolean(self._values['mobile_detection']['allow_any_ios_package'])
666        if result == 'yes':
667            return 'true'
668        if result == 'no':
669            return 'false'
670        return result
671
672    @property
673    def allow_jailbroken_devices(self):
674        if self._values['mobile_detection'] is None:
675            return None
676        result = flatten_boolean(self._values['mobile_detection']['allow_jailbroken_devices'])
677        if result == 'yes':
678            return 'true'
679        if result == 'no':
680            return 'false'
681        return result
682
683    @property
684    def allow_emulators(self):
685        if self._values['mobile_detection'] is None:
686            return None
687        result = flatten_boolean(self._values['mobile_detection']['allow_emulators'])
688        if result == 'yes':
689            return 'true'
690        if result == 'no':
691            return 'false'
692        return result
693
694    @property
695    def client_side_challenge_mode(self):
696        if self._values['mobile_detection'] is None:
697            return None
698        return self._values['mobile_detection']['client_side_challenge_mode']
699
700    @property
701    def ios_allowed_package_names(self):
702        if self._values['mobile_detection'] is None:
703            return None
704        return self._values['mobile_detection']['ios_allowed_package_names']
705
706    @property
707    def android_publishers(self):
708        if self._values['mobile_detection'] is None or self._values['mobile_detection']['android_publishers'] is None:
709            return None
710        result = [fq_name(self.partition, item) for item in self._values['mobile_detection']['android_publishers']]
711        return result
712
713    @property
714    def auto_detect(self):
715        if self._values['heavy_urls'] is None:
716            return None
717        result = flatten_boolean(self._values['heavy_urls']['auto_detect'])
718        if result == 'yes':
719            return 'enabled'
720        if result == 'no':
721            return 'disabled'
722        return result
723
724    @property
725    def latency_threshold(self):
726        if self._values['heavy_urls'] is None or self._values['heavy_urls']['latency_threshold'] is None:
727            return None
728        if 0 <= self._values['heavy_urls']['latency_threshold'] <= 4294967295:
729            return self._values['heavy_urls']['latency_threshold']
730        raise F5ModuleError(
731            "Valid 'latency_threshold' must be in range 0 - 4294967295 milliseconds."
732        )
733
734    @property
735    def hw_url_exclude(self):
736        if self._values['heavy_urls'] is None:
737            return None
738        return self._values['heavy_urls']['exclude']
739
740    @property
741    def hw_url_include(self):
742        if self._values['heavy_urls'] is None or self._values['heavy_urls']['include'] is None:
743            return None
744        result = list()
745        for item in self._values['heavy_urls']['include']:
746            element = dict()
747            element['url'] = self._correct_url(item['url'])
748            element['name'] = 'URL{0}'.format(self._correct_url(item['url']))
749            if 'threshold' in item:
750                element['threshold'] = self._validate_threshold(item['threshold'])
751            result.append(element)
752        return result
753
754    def _validate_threshold(self, item):
755        if item == 'auto':
756            return item
757        if 1 <= int(item) <= 4294967295:
758            return item
759        raise F5ModuleError(
760            "Valid 'url threshold' must be in range 1 - 4294967295 requests per second or 'auto'."
761        )
762
763    def _correct_url(self, item):
764        if item.startswith('/'):
765            return item
766        return "/{0}".format(item)
767
768    @property
769    def geo_blacklist(self):
770        if self._values['geolocations'] is None:
771            return None
772        whitelist = self.geo_whitelist
773        blacklist = self._values['geolocations']['blacklist']
774        if whitelist and blacklist:
775            if not set(whitelist).isdisjoint(set(blacklist)):
776                raise F5ModuleError('Cannot specify the same element in blacklist and whitelist.')
777        return blacklist
778
779    @property
780    def geo_whitelist(self):
781        if self._values['geolocations'] is None:
782            return None
783        return self._values['geolocations']['whitelist']
784
785
786class Changes(Parameters):
787    def to_return(self):
788        result = {}
789        try:
790            for returnable in self.returnables:
791                result[returnable] = getattr(self, returnable)
792            result = self._filter_params(result)
793        except Exception:
794            pass
795        return result
796
797
798class UsableChanges(Changes):
799    @property
800    def geolocations(self):
801        if self._values['geo_blacklist'] is None and self._values['geo_whitelist'] is None:
802            return None
803        result = list()
804        if self._values['geo_blacklist']:
805            for item in self._values['geo_blacklist']:
806                element = dict()
807                element['name'] = item
808                element['blackListed'] = True
809                result.append(element)
810        if self._values['geo_whitelist']:
811            for item in self._values['geo_whitelist']:
812                element = dict()
813                element['name'] = item
814                element['whiteListed'] = True
815                result.append(element)
816        if result:
817            return result
818
819    @property
820    def heavy_urls(self):
821        tmp = dict()
822        tmp['automaticDetection'] = self._values['auto_detect']
823        tmp['latencyThreshold'] = self._values['latency_threshold']
824        tmp['exclude'] = self._values['hw_url_exclude']
825        tmp['includeList'] = self._values['hw_url_include']
826        result = self._filter_params(tmp)
827        if result:
828            return result
829
830    @property
831    def mobile_detection(self):
832        tmp = dict()
833        tmp['enabled'] = self._values['enable_mobile_detection']
834        tmp['allowAndroidRootedDevice'] = self._values['allow_android_rooted_device']
835        tmp['allowAnyAndroidPackage'] = self._values['allow_any_android_package']
836        tmp['allowAnyIosPackage'] = self._values['allow_any_ios_package']
837        tmp['allowJailbrokenDevices'] = self._values['allow_jailbroken_devices']
838        tmp['allowEmulators'] = self._values['allow_emulators']
839        tmp['clientSideChallengeMode'] = self._values['client_side_challenge_mode']
840        tmp['iosAllowedPackageNames'] = self._values['ios_allowed_package_names']
841        tmp['androidPublishers'] = self._values['android_publishers']
842        result = self._filter_params(tmp)
843        if result:
844            return result
845
846
847class ReportableChanges(Changes):
848    returnables = [
849        'rtbh_duration',
850        'rtbh_enable',
851        'scrubbing_duration',
852        'scrubbing_enable',
853        'single_page_application',
854        'trigger_irule',
855        'heavy_urls',
856        'mobile_detection',
857        'geolocations',
858    ]
859
860    def _convert_include_list(self, items):
861        result = list()
862        for item in items:
863            element = dict()
864            element['url'] = item['url']
865            if 'threshold' in item:
866                element['threshold'] = item['threshold']
867            result.append(element)
868        if result:
869            return result
870
871    @property
872    def geolocations(self):
873        tmp = dict()
874        tmp['blacklist'] = self._values['geo_blacklist']
875        tmp['whitelist'] = self._values['geo_whitelist']
876        result = self._filter_params(tmp)
877        if result:
878            return result
879
880    @property
881    def heavy_urls(self):
882        tmp = dict()
883        tmp['auto_detect'] = flatten_boolean(self._values['auto_detect'])
884        tmp['latency_threshold'] = self._values['latency_threshold']
885        tmp['exclude'] = self._values['hw_url_exclude']
886        tmp['include'] = self._convert_include_list(self._values['hw_url_include'])
887        result = self._filter_params(tmp)
888        if result:
889            return result
890
891    @property
892    def mobile_detection(self):
893        tmp = dict()
894        tmp['enabled'] = flatten_boolean(self._values['enable_mobile_detection'])
895        tmp['allow_android_rooted_device'] = flatten_boolean(self._values['allow_android_rooted_device'])
896        tmp['allow_any_android_package'] = flatten_boolean(self._values['allow_any_android_package'])
897        tmp['allow_any_ios_package'] = flatten_boolean(self._values['allow_any_ios_package'])
898        tmp['allow_jailbroken_devices'] = flatten_boolean(self._values['allow_jailbroken_devices'])
899        tmp['allow_emulators'] = flatten_boolean(self._values['allow_emulators'])
900        tmp['client_side_challenge_mode'] = self._values['client_side_challenge_mode']
901        tmp['ios_allowed_package_names'] = self._values['ios_allowed_package_names']
902        tmp['android_publishers'] = self._values['android_publishers']
903        result = self._filter_params(tmp)
904        if result:
905            return result
906
907    @property
908    def rtbh_enable(self):
909        result = flatten_boolean(self._values['rtbh_enable'])
910        return result
911
912    @property
913    def scrubbing_enable(self):
914        result = flatten_boolean(self._values['scrubbing_enable'])
915        return result
916
917    @property
918    def single_page_application(self):
919        result = flatten_boolean(self._values['single_page_application'])
920        return result
921
922    @property
923    def trigger_irule(self):
924        result = flatten_boolean(self._values['trigger_irule'])
925        return result
926
927
928class Difference(object):
929    def __init__(self, want, have=None):
930        self.want = want
931        self.have = have
932
933    def compare(self, param):
934        try:
935            result = getattr(self, param)
936            return result
937        except AttributeError:
938            return self.__default(param)
939
940    def __default(self, param):
941        attr1 = getattr(self.want, param)
942        try:
943            attr2 = getattr(self.have, param)
944            if attr1 != attr2:
945                return attr1
946        except AttributeError:
947            return attr1
948
949    @property
950    def hw_url_include(self):
951        if self.want.hw_url_include is None:
952            return None
953        if self.have.hw_url_include is None and self.want.hw_url_include == []:
954            return None
955        if self.have.hw_url_include is None:
956            return self.want.hw_url_include
957
958        wants = self.want.hw_url_include
959        haves = list()
960        # First we remove extra keys in have for the same elements
961        for want in wants:
962            for have in self.have.hw_url_include:
963                if want['url'] == have['url']:
964                    entry = self._filter_have(want, have)
965                    haves.append(entry)
966        # Next we do compare the lists as normal
967        result = compare_complex_list(wants, haves)
968        return result
969
970    def _filter_have(self, want, have):
971        to_check = set(want.keys()).intersection(set(have.keys()))
972        result = dict()
973        for k in list(to_check):
974            result[k] = have[k]
975        return result
976
977    @property
978    def hw_url_exclude(self):
979        result = cmp_simple_list(self.want.hw_url_exclude, self.have.hw_url_exclude)
980        return result
981
982    @property
983    def geo_blacklist(self):
984        result = cmp_simple_list(self.want.geo_blacklist, self.have.geo_blacklist)
985        return result
986
987    @property
988    def geo_whitelist(self):
989        result = cmp_simple_list(self.want.geo_whitelist, self.have.geo_whitelist)
990        return result
991
992    @property
993    def android_publishers(self):
994        result = cmp_simple_list(self.want.android_publishers, self.have.android_publishers)
995        return result
996
997    @property
998    def ios_allowed_package_names(self):
999        result = cmp_simple_list(self.want.ios_allowed_package_names, self.have.ios_allowed_package_names)
1000        return result
1001
1002
1003class ModuleManager(object):
1004    def __init__(self, *args, **kwargs):
1005        self.module = kwargs.get('module', None)
1006        self.client = F5RestClient(**self.module.params)
1007        self.want = ModuleParameters(params=self.module.params)
1008        self.have = ApiParameters()
1009        self.changes = UsableChanges()
1010
1011    def _set_changed_options(self):
1012        changed = {}
1013        for key in Parameters.returnables:
1014            if getattr(self.want, key) is not None:
1015                changed[key] = getattr(self.want, key)
1016        if changed:
1017            self.changes = UsableChanges(params=changed)
1018
1019    def _update_changed_options(self):
1020        diff = Difference(self.want, self.have)
1021        updatables = Parameters.updatables
1022        changed = dict()
1023        for k in updatables:
1024            change = diff.compare(k)
1025            if change is None:
1026                continue
1027            else:
1028                if isinstance(change, dict):
1029                    changed.update(change)
1030                else:
1031                    changed[k] = change
1032        if changed:
1033            self.changes = UsableChanges(params=changed)
1034            return True
1035        return False
1036
1037    def _announce_deprecations(self, result):
1038        warnings = result.pop('__warnings', [])
1039        for warning in warnings:
1040            self.client.module.deprecate(
1041                msg=warning['msg'],
1042                version=warning['version']
1043            )
1044
1045    def exec_module(self):
1046        if not module_provisioned(self.client, 'asm'):
1047            raise F5ModuleError(
1048                "ASM must be provisioned to use this module."
1049            )
1050
1051        if self.version_less_than_13_1():
1052            raise F5ModuleError('Module supported on TMOS versions 13.1.x and above')
1053
1054        changed = False
1055        result = dict()
1056        state = self.want.state
1057
1058        if state == "present":
1059            changed = self.present()
1060        elif state == "absent":
1061            changed = self.absent()
1062
1063        reportable = ReportableChanges(params=self.changes.to_return())
1064        changes = reportable.to_return()
1065        result.update(**changes)
1066        result.update(dict(changed=changed))
1067        self._announce_deprecations(result)
1068        return result
1069
1070    def version_less_than_13_1(self):
1071        version = tmos_version(self.client)
1072        if LooseVersion(version) < LooseVersion('13.1.0'):
1073            return True
1074        return False
1075
1076    def present(self):
1077        if self.exists():
1078            return self.update()
1079        else:
1080            return self.create()
1081
1082    def absent(self):
1083        if self.exists():
1084            return self.remove()
1085        return False
1086
1087    def should_update(self):
1088        result = self._update_changed_options()
1089        if result:
1090            return True
1091        return False
1092
1093    def update(self):
1094        self.have = self.read_current_from_device()
1095        if not self.should_update():
1096            return False
1097        if self.module.check_mode:
1098            return True
1099        self.update_on_device()
1100        return True
1101
1102    def remove(self):
1103        if self.module.check_mode:
1104            return True
1105        self.remove_from_device()
1106        if self.exists():
1107            raise F5ModuleError("Failed to delete the resource.")
1108        return True
1109
1110    def create(self):
1111        self._set_changed_options()
1112        if self.module.check_mode:
1113            return True
1114        self.create_on_device()
1115        return True
1116
1117    def profile_exists(self):
1118        uri = "https://{0}:{1}/mgmt/tm/security/dos/profile/{2}/".format(
1119            self.client.provider['server'],
1120            self.client.provider['server_port'],
1121            transform_name(self.want.partition, self.want.profile),
1122        )
1123        resp = self.client.api.get(uri)
1124        try:
1125            response = resp.json()
1126        except ValueError:
1127            return False
1128        if resp.status == 404 or 'code' in response and response['code'] == 404:
1129            return False
1130        return True
1131
1132    def exists(self):
1133        if not self.profile_exists():
1134            raise F5ModuleError(
1135                'Specified DOS profile: {0} on partition: {1} does not exist.'.format(
1136                    self.want.profile, self.want.partition)
1137            )
1138        uri = "https://{0}:{1}/mgmt/tm/security/dos/profile/{2}/application/{3}".format(
1139            self.client.provider['server'],
1140            self.client.provider['server_port'],
1141            transform_name(self.want.partition, self.want.profile),
1142            self.want.profile
1143        )
1144        resp = self.client.api.get(uri)
1145        try:
1146            response = resp.json()
1147        except ValueError:
1148            return False
1149        if resp.status == 404 or 'code' in response and response['code'] == 404:
1150            return False
1151        return True
1152
1153    def create_on_device(self):
1154        params = self.changes.api_params()
1155        params['name'] = self.want.profile
1156        uri = "https://{0}:{1}/mgmt/tm/security/dos/profile/{2}/application/".format(
1157            self.client.provider['server'],
1158            self.client.provider['server_port'],
1159            transform_name(self.want.partition, self.want.profile),
1160        )
1161        resp = self.client.api.post(uri, json=params)
1162        try:
1163            response = resp.json()
1164        except ValueError as ex:
1165            raise F5ModuleError(str(ex))
1166
1167        if 'code' in response and response['code'] in [400, 409]:
1168            if 'message' in response:
1169                raise F5ModuleError(response['message'])
1170            else:
1171                raise F5ModuleError(resp.content)
1172        return True
1173
1174    def update_on_device(self):
1175        params = self.changes.api_params()
1176        uri = "https://{0}:{1}/mgmt/tm/security/dos/profile/{2}/application/{3}".format(
1177            self.client.provider['server'],
1178            self.client.provider['server_port'],
1179            transform_name(self.want.partition, self.want.profile),
1180            self.want.profile
1181        )
1182        resp = self.client.api.patch(uri, json=params)
1183        try:
1184            response = resp.json()
1185        except ValueError as ex:
1186            raise F5ModuleError(str(ex))
1187
1188        if 'code' in response and response['code'] == 400:
1189            if 'message' in response:
1190                raise F5ModuleError(response['message'])
1191            else:
1192                raise F5ModuleError(resp.content)
1193
1194    def remove_from_device(self):
1195        uri = "https://{0}:{1}/mgmt/tm/security/dos/profile/{2}/application/{3}".format(
1196            self.client.provider['server'],
1197            self.client.provider['server_port'],
1198            transform_name(self.want.partition, self.want.profile),
1199            self.want.profile
1200        )
1201        response = self.client.api.delete(uri)
1202        if response.status == 200:
1203            return True
1204        raise F5ModuleError(response.content)
1205
1206    def read_current_from_device(self):
1207        uri = "https://{0}:{1}/mgmt/tm/security/dos/profile/{2}/application/{3}".format(
1208            self.client.provider['server'],
1209            self.client.provider['server_port'],
1210            transform_name(self.want.partition, self.want.profile),
1211            self.want.profile
1212        )
1213        resp = self.client.api.get(uri)
1214        try:
1215            response = resp.json()
1216        except ValueError as ex:
1217            raise F5ModuleError(str(ex))
1218
1219        if 'code' in response and response['code'] == 400:
1220            if 'message' in response:
1221                raise F5ModuleError(response['message'])
1222            else:
1223                raise F5ModuleError(resp.content)
1224        return ApiParameters(params=response)
1225
1226
1227class ArgumentSpec(object):
1228    def __init__(self):
1229        self.supports_check_mode = True
1230        argument_spec = dict(
1231            profile=dict(
1232                required=True,
1233            ),
1234            geolocations=dict(
1235                type='dict',
1236                options=dict(
1237                    blacklist=dict(type='list'),
1238                    whitelist=dict(type='list'),
1239                ),
1240            ),
1241            heavy_urls=dict(
1242                type='dict',
1243                options=dict(
1244                    auto_detect=dict(type='bool'),
1245                    latency_threshold=dict(type='int'),
1246                    exclude=dict(type='list'),
1247                    include=dict(
1248                        type='list',
1249                        elements='dict',
1250                        options=dict(
1251                            url=dict(required=True),
1252                            threshold=dict(),
1253                        ),
1254                    )
1255                ),
1256            ),
1257            mobile_detection=dict(
1258                type='dict',
1259                options=dict(
1260                    enabled=dict(type='bool'),
1261                    allow_android_rooted_device=dict(type='bool'),
1262                    allow_any_android_package=dict(type='bool'),
1263                    allow_any_ios_package=dict(type='bool'),
1264                    allow_jailbroken_devices=dict(type='bool'),
1265                    allow_emulators=dict(type='bool'),
1266                    client_side_challenge_mode=dict(choices=['cshui', 'pass']),
1267                    ios_allowed_package_names=dict(type='list'),
1268                    android_publishers=dict(type='list')
1269                )
1270            ),
1271            rtbh_duration=dict(type='int'),
1272            rtbh_enable=dict(type='bool'),
1273            scrubbing_duration=dict(type='int'),
1274            scrubbing_enable=dict(type='bool'),
1275            single_page_application=dict(type='bool'),
1276            trigger_irule=dict(type='bool'),
1277            partition=dict(
1278                default='Common',
1279                fallback=(env_fallback, ['F5_PARTITION'])
1280            ),
1281            state=dict(
1282                default='present',
1283                choices=['present', 'absent']
1284            )
1285        )
1286        self.argument_spec = {}
1287        self.argument_spec.update(f5_argument_spec)
1288        self.argument_spec.update(argument_spec)
1289
1290
1291def main():
1292    spec = ArgumentSpec()
1293
1294    module = AnsibleModule(
1295        argument_spec=spec.argument_spec,
1296        supports_check_mode=spec.supports_check_mode,
1297    )
1298
1299    try:
1300        mm = ModuleManager(module=module)
1301        results = mm.exec_module()
1302        module.exit_json(**results)
1303    except F5ModuleError as ex:
1304        module.fail_json(msg=str(ex))
1305
1306
1307if __name__ == '__main__':
1308    main()
1309