1#!/usr/bin/python
2# -*- coding: utf-8 -*-
3#
4# Copyright: (c) 2017, 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_gtm_monitor_firepass
18short_description: Manages F5 BIG-IP GTM FirePass monitors
19description:
20  - Manages F5 BIG-IP GTM FirePass monitors.
21version_added: 2.6
22options:
23  name:
24    description:
25      - Monitor name.
26    type: str
27    required: True
28  parent:
29    description:
30      - The parent template of this monitor template. Once this value has
31        been set, it cannot be changed. By default, this value is the C(tcp)
32        parent on the C(Common) partition.
33    type: str
34    default: /Common/firepass_gtm
35  ip:
36    description:
37      - IP address part of the IP/port definition. If this parameter is not
38        provided when creating a new monitor, then the default value will be
39        '*'.
40      - If this value is an IP address, then a C(port) number must be specified.
41    type: str
42  port:
43    description:
44      - Port address part of the IP/port definition. If this parameter is not
45        provided when creating a new monitor, then the default value will be
46        '*'. Note that if specifying an IP address, a value between 1 and 65535
47        must be specified.
48    type: str
49  interval:
50    description:
51      - The interval specifying how frequently the monitor instance of this
52        template will run.
53      - If this parameter is not provided when creating a new monitor, then
54        the default value will be 30.
55      - This value B(must) be less than the C(timeout) value.
56    type: int
57  timeout:
58    description:
59      - The number of seconds in which the node or service must respond to
60        the monitor request. If the target responds within the set time
61        period, it is considered up. If the target does not respond within
62        the set time period, it is considered down. You can change this
63        number to any number you want, however, it should be 3 times the
64        interval number of seconds plus 1 second.
65      - If this parameter is not provided when creating a new monitor, then
66        the default value will be 90.
67    type: int
68  partition:
69    description:
70      - Device partition to manage resources on.
71    type: str
72    default: Common
73  state:
74    description:
75      - When C(present), ensures that the monitor exists.
76      - When C(absent), ensures the monitor is removed.
77    type: str
78    choices:
79      - present
80      - absent
81    default: present
82  probe_timeout:
83    description:
84      - Specifies the number of seconds after which the system times out the probe request
85        to the system.
86      - When creating a new monitor, if this parameter is not provided, then the default
87        value will be C(5).
88    type: int
89  ignore_down_response:
90    description:
91      - Specifies that the monitor allows more than one probe attempt per interval.
92      - When C(yes), specifies that the monitor ignores down responses for the duration of
93        the monitor timeout. Once the monitor timeout is reached without the system receiving
94        an up response, the system marks the object down.
95      - When C(no), specifies that the monitor immediately marks an object down when it
96        receives a down response.
97      - When creating a new monitor, if this parameter is not provided, then the default
98        value will be C(no).
99    type: bool
100  target_username:
101    description:
102      - Specifies the user name, if the monitored target requires authentication.
103    type: str
104  target_password:
105    description:
106      - Specifies the password, if the monitored target requires authentication.
107    type: str
108  update_password:
109    description:
110      - C(always) will update passwords if the C(target_password) is specified.
111      - C(on_create) will only set the password for newly created monitors.
112    type: str
113    choices:
114      - always
115      - on_create
116    default: always
117  cipher_list:
118    description:
119      - Specifies the list of ciphers for this monitor.
120      - The items in the cipher list are separated with the colon C(:) symbol.
121      - When creating a new monitor, if this parameter is not specified, the default
122        list is C(HIGH:!ADH).
123    type: str
124  max_load_average:
125    description:
126      - Specifies the number that the monitor uses to mark the Secure Access Manager
127        system up or down.
128      - The system compares the Max Load Average setting against a one-minute average
129        of the Secure Access Manager system load.
130      - When the Secure Access Manager system-load average falls within the specified
131        Max Load Average, the monitor marks the Secure Access Manager system up.
132      - When the average exceeds the setting, the monitor marks the system down.
133      - When creating a new monitor, if this parameter is not specified, the default
134        is C(12).
135    type: int
136  concurrency_limit:
137    description:
138      - Specifies the maximum percentage of licensed connections currently in use under
139        which the monitor marks the Secure Access Manager system up.
140      - As an example, a setting of 95 percent means that the monitor marks the Secure
141        Access Manager system up until 95 percent of licensed connections are in use.
142      - When the number of in-use licensed connections exceeds 95 percent, the monitor
143        marks the Secure Access Manager system down.
144      - When creating a new monitor, if this parameter is not specified, the default is C(95).
145    type: int
146extends_documentation_fragment: f5
147author:
148  - Tim Rupp (@caphrim007)
149  - Wojciech Wypior (@wojtek0806)
150'''
151
152EXAMPLES = r'''
153- name: Create a GTM FirePass monitor
154  bigip_gtm_monitor_firepass:
155    name: my_monitor
156    ip: 1.1.1.1
157    port: 80
158    state: present
159    provider:
160      user: admin
161      password: secret
162      server: lb.mydomain.com
163  delegate_to: localhost
164
165- name: Remove FirePass Monitor
166  bigip_gtm_monitor_firepass:
167    name: my_monitor
168    state: absent
169    provider:
170      user: admin
171      password: secret
172      server: lb.mydomain.com
173  delegate_to: localhost
174
175- name: Add FirePass monitor for all addresses, port 514
176  bigip_gtm_monitor_firepass:
177    name: my_monitor
178    port: 514
179    provider:
180      user: admin
181      password: secret
182      server: lb.mydomain.com
183  delegate_to: localhost
184'''
185
186RETURN = r'''
187parent:
188  description: New parent template of the monitor.
189  returned: changed
190  type: str
191  sample: firepass_gtm
192ip:
193  description: The new IP of IP/port definition.
194  returned: changed
195  type: str
196  sample: 10.12.13.14
197port:
198  description: The new port the monitor checks the resource on.
199  returned: changed
200  type: str
201  sample: 8080
202interval:
203  description: The new interval in which to run the monitor check.
204  returned: changed
205  type: int
206  sample: 2
207timeout:
208  description: The new timeout in which the remote system must respond to the monitor.
209  returned: changed
210  type: int
211  sample: 10
212ignore_down_response:
213  description: Whether to ignore the down response or not.
214  returned: changed
215  type: bool
216  sample: True
217probe_timeout:
218  description: The new timeout in which the system will timeout the monitor probe.
219  returned: changed
220  type: int
221  sample: 10
222cipher_list:
223  description: The new value for the cipher list.
224  returned: changed
225  type: str
226  sample: +3DES:+kEDH
227max_load_average:
228  description: The new value for the max load average.
229  returned: changed
230  type: int
231  sample: 12
232concurrency_limit:
233  description: The new value for the concurrency limit.
234  returned: changed
235  type: int
236  sample: 95
237'''
238
239from ansible.module_utils.basic import AnsibleModule
240from ansible.module_utils.basic import env_fallback
241
242try:
243    from library.module_utils.network.f5.bigip import F5RestClient
244    from library.module_utils.network.f5.common import F5ModuleError
245    from library.module_utils.network.f5.common import AnsibleF5Parameters
246    from library.module_utils.network.f5.common import fq_name
247    from library.module_utils.network.f5.common import f5_argument_spec
248    from library.module_utils.network.f5.common import transform_name
249    from library.module_utils.network.f5.icontrol import module_provisioned
250    from library.module_utils.network.f5.ipaddress import is_valid_ip
251except ImportError:
252    from ansible.module_utils.network.f5.bigip import F5RestClient
253    from ansible.module_utils.network.f5.common import F5ModuleError
254    from ansible.module_utils.network.f5.common import AnsibleF5Parameters
255    from ansible.module_utils.network.f5.common import fq_name
256    from ansible.module_utils.network.f5.common import f5_argument_spec
257    from ansible.module_utils.network.f5.common import transform_name
258    from ansible.module_utils.network.f5.icontrol import module_provisioned
259    from ansible.module_utils.network.f5.ipaddress import is_valid_ip
260
261
262class Parameters(AnsibleF5Parameters):
263    api_map = {
264        'defaultsFrom': 'parent',
265        'ignoreDownResponse': 'ignore_down_response',
266        'probeTimeout': 'probe_timeout',
267        'username': 'target_username',
268        'password': 'target_password',
269        'cipherlist': 'cipher_list',
270        'concurrencyLimit': 'concurrency_limit',
271        'maxLoadAverage': 'max_load_average',
272    }
273
274    api_attributes = [
275        'defaultsFrom',
276        'interval',
277        'timeout',
278        'destination',
279        'probeTimeout',
280        'ignoreDownResponse',
281        'username',
282        'password',
283        'cipherlist',
284        'concurrencyLimit',
285        'maxLoadAverage',
286    ]
287
288    returnables = [
289        'parent',
290        'ip',
291        'port',
292        'interval',
293        'timeout',
294        'probe_timeout',
295        'ignore_down_response',
296        'cipher_list',
297        'max_load_average',
298        'concurrency_limit',
299    ]
300
301    updatables = [
302        'destination',
303        'interval',
304        'timeout',
305        'probe_timeout',
306        'ignore_down_response',
307        'ip',
308        'port',
309        'target_username',
310        'target_password',
311        'cipher_list',
312        'max_load_average',
313        'concurrency_limit',
314    ]
315
316
317class ApiParameters(Parameters):
318    @property
319    def ip(self):
320        ip, port = self._values['destination'].split(':')
321        return ip
322
323    @property
324    def port(self):
325        ip, port = self._values['destination'].split(':')
326        try:
327            return int(port)
328        except ValueError:
329            return port
330
331    @property
332    def ignore_down_response(self):
333        if self._values['ignore_down_response'] is None:
334            return None
335        if self._values['ignore_down_response'] == 'disabled':
336            return False
337        return True
338
339
340class ModuleParameters(Parameters):
341    @property
342    def interval(self):
343        if self._values['interval'] is None:
344            return None
345        if 1 > int(self._values['interval']) > 86400:
346            raise F5ModuleError(
347                "Interval value must be between 1 and 86400"
348            )
349        return int(self._values['interval'])
350
351    @property
352    def timeout(self):
353        if self._values['timeout'] is None:
354            return None
355        return int(self._values['timeout'])
356
357    @property
358    def ip(self):  # lgtm [py/similar-function]
359        if self._values['ip'] is None:
360            return None
361        if self._values['ip'] in ['*', '0.0.0.0']:
362            return '*'
363        elif is_valid_ip(self._values['ip']):
364            return self._values['ip']
365        else:
366            raise F5ModuleError(
367                "The provided 'ip' parameter is not an IP address."
368            )
369
370    @property
371    def parent(self):
372        if self._values['parent'] is None:
373            return None
374        result = fq_name(self.partition, self._values['parent'])
375        return result
376
377    @property
378    def port(self):
379        if self._values['port'] is None:
380            return None
381        elif self._values['port'] == '*':
382            return '*'
383        return int(self._values['port'])
384
385    @property
386    def destination(self):
387        if self.ip is None and self.port is None:
388            return None
389        destination = '{0}:{1}'.format(self.ip, self.port)
390        return destination
391
392    @destination.setter
393    def destination(self, value):
394        ip, port = value.split(':')
395        self._values['ip'] = ip
396        self._values['port'] = port
397
398    @property
399    def probe_timeout(self):
400        if self._values['probe_timeout'] is None:
401            return None
402        return int(self._values['probe_timeout'])
403
404    @property
405    def max_load_average(self):
406        if self._values['max_load_average'] is None:
407            return None
408        return int(self._values['max_load_average'])
409
410    @property
411    def concurrency_limit(self):
412        if self._values['concurrency_limit'] is None:
413            return None
414        return int(self._values['concurrency_limit'])
415
416
417class Changes(Parameters):
418    def to_return(self):
419        result = {}
420        try:
421            for returnable in self.returnables:
422                result[returnable] = getattr(self, returnable)
423            result = self._filter_params(result)
424        except Exception:
425            pass
426        return result
427
428
429class UsableChanges(Changes):
430    @property
431    def ignore_down_response(self):
432        if self._values['ignore_down_response'] is None:
433            return None
434        elif self._values['ignore_down_response'] is True:
435            return 'enabled'
436        return 'disabled'
437
438
439class ReportableChanges(Changes):
440    @property
441    def ip(self):
442        ip, port = self._values['destination'].split(':')
443        return ip
444
445    @property
446    def port(self):
447        ip, port = self._values['destination'].split(':')
448        return int(port)
449
450    @property
451    def ignore_down_response(self):
452        if self._values['ignore_down_response'] == 'enabled':
453            return True
454        return False
455
456
457class Difference(object):
458    def __init__(self, want, have=None):
459        self.want = want
460        self.have = have
461
462    def compare(self, param):
463        try:
464            result = getattr(self, param)
465            return result
466        except AttributeError:
467            return self.__default(param)
468
469    def __default(self, param):
470        attr1 = getattr(self.want, param)
471        try:
472            attr2 = getattr(self.have, param)
473            if attr1 != attr2:
474                return attr1
475        except AttributeError:
476            return attr1
477
478    @property
479    def parent(self):
480        if self.want.parent != self.have.parent:
481            raise F5ModuleError(
482                "The parent monitor cannot be changed"
483            )
484
485    @property
486    def destination(self):
487        if self.want.ip is None and self.want.port is None:
488            return None
489        if self.want.port is None:
490            self.want.update({'port': self.have.port})
491        if self.want.ip is None:
492            self.want.update({'ip': self.have.ip})
493
494        if self.want.port in [None, '*'] and self.want.ip != '*':
495            raise F5ModuleError(
496                "Specifying an IP address requires that a port number be specified"
497            )
498
499        if self.want.destination != self.have.destination:
500            return self.want.destination
501
502    @property
503    def interval(self):
504        if self.want.timeout is not None and self.want.interval is not None:
505            if self.want.interval >= self.want.timeout:
506                raise F5ModuleError(
507                    "Parameter 'interval' must be less than 'timeout'."
508                )
509        elif self.want.timeout is not None:
510            if self.have.interval >= self.want.timeout:
511                raise F5ModuleError(
512                    "Parameter 'interval' must be less than 'timeout'."
513                )
514        elif self.want.interval is not None:
515            if self.want.interval >= self.have.timeout:
516                raise F5ModuleError(
517                    "Parameter 'interval' must be less than 'timeout'."
518                )
519        if self.want.interval != self.have.interval:
520            return self.want.interval
521
522    @property
523    def target_password(self):
524        if self.want.target_password != self.have.target_password:
525            if self.want.update_password == 'always':
526                result = self.want.target_password
527                return result
528
529
530class ModuleManager(object):
531    def __init__(self, *args, **kwargs):
532        self.module = kwargs.get('module', None)
533        self.client = F5RestClient(**self.module.params)
534        self.want = ModuleParameters(params=self.module.params)
535        self.have = ApiParameters()
536        self.changes = UsableChanges()
537
538    def _set_changed_options(self):
539        changed = {}
540        for key in Parameters.returnables:
541            if getattr(self.want, key) is not None:
542                changed[key] = getattr(self.want, key)
543        if changed:
544            self.changes = UsableChanges(params=changed)
545
546    def _update_changed_options(self):
547        diff = Difference(self.want, self.have)
548        updatables = Parameters.updatables
549        changed = dict()
550        for k in updatables:
551            change = diff.compare(k)
552            if change is None:
553                continue
554            else:
555                if isinstance(change, dict):
556                    changed.update(change)
557                else:
558                    changed[k] = change
559        if changed:
560            self.changes = UsableChanges(params=changed)
561            return True
562        return False
563
564    def _announce_deprecations(self, result):
565        warnings = result.pop('__warnings', [])
566        for warning in warnings:
567            self.client.module.deprecate(
568                msg=warning['msg'],
569                version=warning['version']
570            )
571
572    def _set_default_creation_values(self):
573        if self.want.timeout is None:
574            self.want.update({'timeout': 90})
575        if self.want.interval is None:
576            self.want.update({'interval': 30})
577        if self.want.probe_timeout is None:
578            self.want.update({'probe_timeout': 5})
579        if self.want.ip is None:
580            self.want.update({'ip': '*'})
581        if self.want.port is None:
582            self.want.update({'port': '*'})
583        if self.want.ignore_down_response is None:
584            self.want.update({'ignore_down_response': False})
585        if self.want.cipher_list is None:
586            self.want.update({'cipher_list': 'HIGH:!ADH'})
587        if self.want.max_load_average is None:
588            self.want.update({'max_load_average': 12})
589        if self.want.concurrency_limit is None:
590            self.want.update({'concurrency_limit': 95})
591
592    def exec_module(self):
593        if not module_provisioned(self.client, 'gtm'):
594            raise F5ModuleError(
595                "GTM must be provisioned to use this module."
596            )
597        changed = False
598        result = dict()
599        state = self.want.state
600
601        if state == "present":
602            changed = self.present()
603        elif state == "absent":
604            changed = self.absent()
605
606        reportable = ReportableChanges(params=self.changes.to_return())
607        changes = reportable.to_return()
608        result.update(**changes)
609        result.update(dict(changed=changed))
610        self._announce_deprecations(result)
611        return result
612
613    def present(self):
614        if self.exists():
615            return self.update()
616        else:
617            return self.create()
618
619    def absent(self):
620        if self.exists():
621            return self.remove()
622        return False
623
624    def should_update(self):
625        result = self._update_changed_options()
626        if result:
627            return True
628        return False
629
630    def update(self):
631        self.have = self.read_current_from_device()
632        if not self.should_update():
633            return False
634        if self.module.check_mode:
635            return True
636        self.update_on_device()
637        return True
638
639    def remove(self):
640        if self.module.check_mode:
641            return True
642        self.remove_from_device()
643        if self.exists():
644            raise F5ModuleError("Failed to delete the resource.")
645        return True
646
647    def create(self):
648        self._set_default_creation_values()
649        self._set_changed_options()
650        if self.module.check_mode:
651            return True
652        self.create_on_device()
653        return True
654
655    def exists(self):
656        uri = "https://{0}:{1}/mgmt/tm/gtm/monitor/firepass/{2}".format(
657            self.client.provider['server'],
658            self.client.provider['server_port'],
659            transform_name(self.want.partition, self.want.name),
660        )
661        resp = self.client.api.get(uri)
662        try:
663            response = resp.json()
664        except ValueError:
665            return False
666        if resp.status == 404 or 'code' in response and response['code'] == 404:
667            return False
668        return True
669
670    def create_on_device(self):
671        params = self.changes.api_params()
672        params['name'] = self.want.name
673        params['partition'] = self.want.partition
674        uri = "https://{0}:{1}/mgmt/tm/gtm/monitor/firepass/".format(
675            self.client.provider['server'],
676            self.client.provider['server_port'],
677        )
678        resp = self.client.api.post(uri, json=params)
679        try:
680            response = resp.json()
681        except ValueError as ex:
682            raise F5ModuleError(str(ex))
683
684        if 'code' in response and response['code'] in [400, 403]:
685            if 'message' in response:
686                raise F5ModuleError(response['message'])
687            else:
688                raise F5ModuleError(resp.content)
689        return response['selfLink']
690
691    def update_on_device(self):
692        params = self.changes.api_params()
693        uri = "https://{0}:{1}/mgmt/tm/gtm/monitor/firepass/{2}".format(
694            self.client.provider['server'],
695            self.client.provider['server_port'],
696            transform_name(self.want.partition, self.want.name),
697        )
698        resp = self.client.api.patch(uri, json=params)
699        try:
700            response = resp.json()
701        except ValueError as ex:
702            raise F5ModuleError(str(ex))
703
704        if 'code' in response and response['code'] == 400:
705            if 'message' in response:
706                raise F5ModuleError(response['message'])
707            else:
708                raise F5ModuleError(resp.content)
709
710    def remove_from_device(self):
711        uri = "https://{0}:{1}/mgmt/tm/gtm/monitor/firepass/{2}".format(
712            self.client.provider['server'],
713            self.client.provider['server_port'],
714            transform_name(self.want.partition, self.want.name),
715        )
716        response = self.client.api.delete(uri)
717        if response.status == 200:
718            return True
719        raise F5ModuleError(response.content)
720
721    def read_current_from_device(self):
722        uri = "https://{0}:{1}/mgmt/tm/gtm/monitor/firepass/{2}".format(
723            self.client.provider['server'],
724            self.client.provider['server_port'],
725            transform_name(self.want.partition, self.want.name),
726        )
727        resp = self.client.api.get(uri)
728        try:
729            response = resp.json()
730        except ValueError as ex:
731            raise F5ModuleError(str(ex))
732
733        if 'code' in response and response['code'] == 400:
734            if 'message' in response:
735                raise F5ModuleError(response['message'])
736            else:
737                raise F5ModuleError(resp.content)
738        return ApiParameters(params=response)
739
740
741class ArgumentSpec(object):
742    def __init__(self):
743        self.supports_check_mode = True
744        argument_spec = dict(
745            name=dict(required=True),
746            parent=dict(default='/Common/firepass_gtm'),
747            ip=dict(),
748            port=dict(),
749            interval=dict(type='int'),
750            timeout=dict(type='int'),
751            ignore_down_response=dict(type='bool'),
752            probe_timeout=dict(type='int'),
753            target_username=dict(),
754            target_password=dict(no_log=True),
755            cipher_list=dict(),
756            update_password=dict(
757                default='always',
758                choices=['always', 'on_create']
759            ),
760            max_load_average=dict(type='int'),
761            concurrency_limit=dict(type='int'),
762            state=dict(
763                default='present',
764                choices=['present', 'absent']
765            ),
766            partition=dict(
767                default='Common',
768                fallback=(env_fallback, ['F5_PARTITION'])
769            )
770        )
771        self.argument_spec = {}
772        self.argument_spec.update(f5_argument_spec)
773        self.argument_spec.update(argument_spec)
774
775
776def main():
777    spec = ArgumentSpec()
778
779    module = AnsibleModule(
780        argument_spec=spec.argument_spec,
781        supports_check_mode=spec.supports_check_mode,
782    )
783
784    try:
785        mm = ModuleManager(module=module)
786        results = mm.exec_module()
787        module.exit_json(**results)
788    except F5ModuleError as ex:
789        module.fail_json(msg=str(ex))
790
791
792if __name__ == '__main__':
793    main()
794