1#!/usr/local/bin/python3.8
2# -*- coding: utf-8 -*-
3#
4# Copyright: (c) 2018, 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
10DOCUMENTATION = r'''
11---
12module: bigip_device_auth_ldap
13short_description: Manage LDAP device authentication settings on BIG-IP
14description:
15  - Manage LDAP device authentication settings on BIG-IP.
16version_added: "1.0.0"
17options:
18  servers:
19    description:
20      - Specifies the LDAP servers the system must use to obtain
21        authentication information. You must specify a server when you
22        create an LDAP configuration object.
23    type: list
24    elements: str
25  port:
26    description:
27      - Specifies the port the system uses for access to the remote host server.
28      - When configuring LDAP device authentication for the first time, the default
29        port is C(389) if this parameter is not specified.
30    type: int
31  remote_directory_tree:
32    description:
33      - Specifies the file location (tree) of the user authentication database on the
34        server.
35    type: str
36  scope:
37    description:
38      - Specifies the level of the remote Active Directory or LDAP directory the
39        system should search for the user authentication.
40    type: str
41    choices:
42      - sub
43      - one
44      - base
45  bind_dn:
46    description:
47      - Specifies the distinguished name for the Active Directory or LDAP server user
48        ID.
49      - The BIG-IP client authentication module does not support Active Directory or
50        LDAP servers that do not perform bind referral when authenticating referred
51        accounts.
52      - Therefore, if you plan to use Active Directory or LDAP as your authentication
53        source and want to use referred accounts, make sure your servers perform bind
54        referral.
55    type: str
56  bind_password:
57    description:
58      - Specifies a password for the Active Directory or LDAP server user ID.
59    type: str
60  user_template:
61    description:
62      - Specifies the distinguished name of the user who is logging on.
63      - You specify the template as a variable that the system replaces with user-specific
64        information during the logon attempt.
65      - For example, you could specify a user template such as C(%s@siterequest.com) or
66        C(uxml:id=%s,ou=people,dc=siterequest,dc=com).
67      - When a user attempts to log on, the system replaces C(%s) with the name the user
68        specified in the Basic Authentication dialog box, and passes that as the
69        distinguished name for the bind operation.
70      - The system passes the associated password as the password for the bind operation.
71      - This field can contain only one C(%s) and cannot contain any other format
72        specifiers.
73    type: str
74  check_member_attr:
75    description:
76      - Checks the member attribute of the user in the remote LDAP or AD group.
77    type: bool
78  ssl:
79    description:
80      - Specifies whether the system uses an SSL port to communicate with the LDAP server.
81    type: str
82    choices:
83      - "yes"
84      - "no"
85      - start-tls
86  ca_cert:
87    description:
88      - Specifies the name of an SSL certificate from a certificate authority (CA).
89      - To remove this value, use the reserved value C(none).
90    type: str
91    aliases: [ ssl_ca_cert ]
92  client_key:
93    description:
94      - Specifies the name of an SSL client key.
95      - To remove this value, use the reserved value C(none).
96    type: str
97    aliases: [ ssl_client_key ]
98  client_cert:
99    description:
100      - Specifies the name of an SSL client certificate.
101      - To remove this value, use the reserved value C(none).
102    type: str
103    aliases: [ ssl_client_cert ]
104  validate_certs:
105    description:
106      - Specifies whether the system checks an SSL peer, as a result of which the
107        system requires and verifies the server certificate.
108    type: bool
109    aliases: [ ssl_check_peer ]
110  login_ldap_attr:
111    description:
112      - Specifies the LDAP directory attribute containing the local user name that is
113        associated with the selected directory entry.
114      - If this parameter is not specified, when configuring LDAP device authentication for the first time,
115        the default port is C(samaccountname).
116    type: str
117  fallback_to_local:
118    description:
119      - Specifies the system uses the Local authentication method if the remote
120        authentication method is not available.
121      - Option only available on C(TMOS 13.0.0) and above.
122    type: bool
123  state:
124    description:
125      - When C(present), ensures the device authentication method exists.
126      - When C(absent), ensures the device authentication method does not exist.
127      - When C(state) equal to (absent), before you can delete the LDAP configuration, the system must set auth to
128        some alternative. The system ships with a system auth called C(local), therefore the system authentication type
129        will be set to that value on the device upon removal of LDAP configuration.
130    type: str
131    choices:
132      - present
133      - absent
134    default: present
135  update_password:
136    description:
137      - C(always) will always update the C(bind_password).
138      - C(on_create) will only set the C(bind_password) for newly created authentication
139        mechanisms.
140    type: str
141    choices:
142      - always
143      - on_create
144    default: always
145  use_for_auth:
146    description:
147      - Specifies whether or not this auth source is put in use on the system.
148      - If C(yes), the module sets the current system auth type to the value of C(ldap).
149      - If C(no), the module sets the authentication type to C(local), similar behavior to when C(state) is C(absent),
150        without removing the configured LDAP resource.
151    type: bool
152extends_documentation_fragment: f5networks.f5_modules.f5
153author:
154  - Tim Rupp (@caphrim007)
155  - Wojciech Wypior (@wojtek0806)
156'''
157
158EXAMPLES = r'''
159- name: Create an LDAP authentication object
160  bigip_device_auth_ldap:
161    name: foo
162    provider:
163      password: secret
164      server: lb.mydomain.com
165      user: admin
166  delegate_to: localhost
167'''
168
169RETURN = r'''
170servers:
171  description: LDAP servers used by the system to obtain authentication information.
172  returned: changed
173  type: list
174  sample: ['192.168.1.1', '192.168.1.2']
175port:
176  description: The port the system uses for access to the remote LDAP server.
177  returned: changed
178  type: int
179  sample: 389
180remote_directory_tree:
181  description: File location (tree) of the user authentication database on the server.
182  returned: changed
183  type: str
184  sample: "CN=Users,DC=FOOBAR,DC=LOCAL"
185scope:
186  description: The level of the remote Active Directory or LDAP directory searched for user authentication.
187  returned: changed
188  type: str
189  sample: base
190bind_dn:
191  description: The distinguished name for the Active Directory or LDAP server user ID.
192  returned: changed
193  type: str
194  sample: "user@foobar.local"
195user_template:
196  description: The distinguished name of the user who is logging on.
197  returned: changed
198  type: str
199  sample: "uid=%s,ou=people,dc=foobar,dc=local"
200check_member_attr:
201  description: The user's member attribute in the remote LDAP or AD group.
202  returned: changed
203  type: bool
204  sample: yes
205ssl:
206  description: Specifies whether the system uses an SSL port to communicate with the LDAP server.
207  returned: changed
208  type: str
209  sample: start-tls
210ca_cert:
211  description: The name of an SSL certificate from a certificate authority.
212  returned: changed
213  type: str
214  sample: My-Trusted-CA-Bundle.crt
215client_key:
216  description: The name of an SSL client key.
217  returned: changed
218  type: str
219  sample: MyKey.key
220client_cert:
221  description: The name of an SSL client certificate.
222  returned: changed
223  type: str
224  sample: MyCert.crt
225validate_certs:
226  description: Indicates if the system checks an SSL peer.
227  returned: changed
228  type: bool
229  sample: yes
230login_ldap_attr:
231  description: The LDAP directory attribute containing the local user name associated with the selected directory entry.
232  returned: changed
233  type: str
234  sample: samaccountname
235fallback_to_local:
236  description: Specifies the system uses the Local authentication method as fallback
237  returned: changed
238  type: bool
239  sample: yes
240'''
241from datetime import datetime
242from ansible.module_utils.basic import AnsibleModule
243
244from ..module_utils.bigip import F5RestClient
245from ..module_utils.common import (
246    F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, flatten_boolean, fq_name
247)
248from ..module_utils.compare import cmp_str_with_none
249from ..module_utils.icontrol import tmos_version
250from ..module_utils.teem import send_teem
251
252
253class Parameters(AnsibleF5Parameters):
254    api_map = {
255        'bindDn': 'bind_dn',
256        'bindPw': 'bind_password',
257        'userTemplate': 'user_template',
258        'fallback': 'fallback_to_local',
259        'loginAttribute': 'login_ldap_attr',
260        'sslCheckPeer': 'validate_certs',
261        'sslClientCert': 'client_cert',
262        'sslClientKey': 'client_key',
263        'sslCaCertFile': 'ca_cert',
264        'checkRolesGroup': 'check_member_attr',
265        'searchBaseDn': 'remote_directory_tree',
266    }
267
268    api_attributes = [
269        'bindDn',
270        'bindPw',
271        'checkRolesGroup',
272        'loginAttribute',
273        'port',
274        'scope',
275        'searchBaseDn',
276        'servers',
277        'ssl',
278        'sslCaCertFile',
279        'sslCheckPeer',
280        'sslClientCert',
281        'sslClientKey',
282        'userTemplate',
283    ]
284
285    returnables = [
286        'bind_dn',
287        'bind_password',
288        'check_member_attr',
289        'fallback_to_local',
290        'login_ldap_attr',
291        'port',
292        'remote_directory_tree',
293        'scope',
294        'servers',
295        'ssl',
296        'ca_cert',
297        'validate_certs',
298        'client_cert',
299        'client_key',
300        'user_template',
301    ]
302
303    updatables = [
304        'bind_dn',
305        'bind_password',
306        'check_member_attr',
307        'fallback_to_local',
308        'login_ldap_attr',
309        'port',
310        'remote_directory_tree',
311        'scope',
312        'servers',
313        'ssl',
314        'ca_cert',
315        'validate_certs',
316        'client_cert',
317        'client_key',
318        'user_template',
319        'auth_source',
320    ]
321
322    @property
323    def ca_cert(self):
324        if self._values['ca_cert'] is None:
325            return None
326        elif self._values['ca_cert'] in ['none', '']:
327            return ''
328        return fq_name(self.partition, self._values['ca_cert'])
329
330    @property
331    def client_key(self):
332        if self._values['client_key'] is None:
333            return None
334        elif self._values['client_key'] in ['none', '']:
335            return ''
336        return fq_name(self.partition, self._values['client_key'])
337
338    @property
339    def client_cert(self):
340        if self._values['client_cert'] is None:
341            return None
342        elif self._values['client_cert'] in ['none', '']:
343            return ''
344        return fq_name(self.partition, self._values['client_cert'])
345
346    @property
347    def validate_certs(self):
348        return flatten_boolean(self._values['validate_certs'])
349
350    @property
351    def check_member_attr(self):
352        return flatten_boolean(self._values['check_member_attr'])
353
354    @property
355    def login_ldap_attr(self):
356        if self._values['login_ldap_attr'] is None:
357            return None
358        elif self._values['login_ldap_attr'] in ['none', '']:
359            return ''
360        return self._values['login_ldap_attr']
361
362    @property
363    def user_template(self):
364        if self._values['user_template'] is None:
365            return None
366        elif self._values['user_template'] in ['none', '']:
367            return ''
368        return self._values['user_template']
369
370    @property
371    def ssl(self):
372        if self._values['ssl'] is None:
373            return None
374        elif self._values['ssl'] == 'start-tls':
375            return 'start-tls'
376        return flatten_boolean(self._values['ssl'])
377
378    @property
379    def fallback_to_local(self):
380        return flatten_boolean(self._values['fallback_to_local'])
381
382
383class ApiParameters(Parameters):
384    pass
385
386
387class ModuleParameters(Parameters):
388    @property
389    def use_for_auth(self):
390        return flatten_boolean(self._values['use_for_auth'])
391
392    @property
393    def auth_source(self):
394        if self._values['use_for_auth'] is None:
395            return None
396        if self.use_for_auth == 'yes':
397            return 'ldap'
398        if self.use_for_auth == 'no':
399            return 'local'
400
401
402class Changes(Parameters):
403    def to_return(self):
404        result = {}
405        try:
406            for returnable in self.returnables:
407                result[returnable] = getattr(self, returnable)
408            result = self._filter_params(result)
409        except Exception:
410            raise
411        return result
412
413
414class UsableChanges(Changes):
415    @property
416    def validate_certs(self):
417        if self._values['validate_certs'] is None:
418            return None
419        elif self._values['validate_certs'] == 'yes':
420            return 'enabled'
421        return 'disabled'
422
423    @property
424    def fallback_to_local(self):
425        if self._values['fallback_to_local'] is None:
426            return None
427        elif self._values['fallback_to_local'] == 'yes':
428            return 'true'
429        return 'false'
430
431    @property
432    def check_member_attr(self):
433        if self._values['check_member_attr'] is None:
434            return None
435        elif self._values['check_member_attr'] == 'yes':
436            return 'enabled'
437        return 'disabled'
438
439    @property
440    def ssl(self):
441        if self._values['ssl'] is None:
442            return None
443        elif self._values['ssl'] == 'start-tls':
444            return 'start-tls'
445        elif self._values['ssl'] == 'yes':
446            return 'enabled'
447        return 'disabled'
448
449
450class ReportableChanges(Changes):
451    @property
452    def bind_password(self):
453        return None
454
455    @property
456    def validate_certs(self):
457        return flatten_boolean(self._values['validate_certs'])
458
459    @property
460    def check_member_attr(self):
461        return flatten_boolean(self._values['check_member_attr'])
462
463    @property
464    def ssl(self):
465        if self._values['ssl'] is None:
466            return None
467        elif self._values['ssl'] == 'start-tls':
468            return 'start-tls'
469        return flatten_boolean(self._values['ssl'])
470
471
472class Difference(object):
473    def __init__(self, want, have=None):
474        self.want = want
475        self.have = have
476
477    def compare(self, param):
478        try:
479            result = getattr(self, param)
480            return result
481        except AttributeError:
482            return self.__default(param)
483
484    def __default(self, param):
485        attr1 = getattr(self.want, param)
486        try:
487            attr2 = getattr(self.have, param)
488            if attr1 != attr2:
489                return attr1
490        except AttributeError:
491            return attr1
492
493    @property
494    def login_ldap_attr(self):
495        return cmp_str_with_none(self.want.login_ldap_attr, self.have.login_ldap_attr)
496
497    @property
498    def user_template(self):
499        return cmp_str_with_none(self.want.user_template, self.have.user_template)
500
501    @property
502    def ca_cert(self):
503        return cmp_str_with_none(self.want.ca_cert, self.have.ca_cert)
504
505    @property
506    def client_key(self):
507        return cmp_str_with_none(self.want.client_key, self.have.client_key)
508
509    @property
510    def client_cert(self):
511        return cmp_str_with_none(self.want.client_cert, self.have.client_cert)
512
513    @property
514    def bind_password(self):
515        if self.want.bind_password != self.have.bind_password and self.want.update_password == 'always':
516            return self.want.bind_password
517
518
519class ModuleManager(object):
520    def __init__(self, *args, **kwargs):
521        self.module = kwargs.get('module', None)
522        self.client = F5RestClient(**self.module.params)
523        self.want = ModuleParameters(params=self.module.params)
524        self.have = ApiParameters()
525        self.changes = UsableChanges()
526
527    def _set_changed_options(self):
528        changed = {}
529        for key in Parameters.returnables:
530            if getattr(self.want, key) is not None:
531                changed[key] = getattr(self.want, key)
532        if changed:
533            self.changes = UsableChanges(params=changed)
534
535    def _update_changed_options(self):
536        diff = Difference(self.want, self.have)
537        updatables = Parameters.updatables
538        changed = dict()
539        for k in updatables:
540            change = diff.compare(k)
541            if change is None:
542                continue
543            else:
544                if isinstance(change, dict):
545                    changed.update(change)
546                else:
547                    changed[k] = change
548        if changed:
549            self.changes = UsableChanges(params=changed)
550            return True
551        return False
552
553    def _announce_deprecations(self, result):
554        warnings = result.pop('__warnings', [])
555        for warning in warnings:
556            self.client.module.deprecate(
557                msg=warning['msg'],
558                version=warning['version']
559            )
560
561    def update_auth_source_on_device(self, source):
562        """Set the system auth source.
563
564        Configuring the authentication source is only one step in the process of setting
565        up an auth source. The other step is to inform the system of the auth source
566        you want to use.
567
568        This method is used for situations where
569
570        * The ``use_for_auth`` parameter is set to ``yes``
571        * The ``use_for_auth`` parameter is set to ``no``
572        * The ``state`` parameter is set to ``absent``
573
574        When ``state`` equal to ``absent``, before you can delete the LDAP configuration,
575        you must set the system auth to "something else". The system ships with a system
576        auth called "local", so this is the logical "something else" to use.
577
578        When ``use_for_auth`` is no, the same situation applies as when ``state`` equal
579        to ``absent`` is done above.
580
581        When ``use_for_auth`` is ``yes``, this method will set the current system auth
582        state to the value of source_type.
583
584        Arguments:
585            source (string): The source that you want to set on the device.
586        """
587        params = dict(
588            type=source
589        )
590        uri = 'https://{0}:{1}/mgmt/tm/auth/source/'.format(
591            self.client.provider['server'],
592            self.client.provider['server_port']
593        )
594
595        resp = self.client.api.patch(uri, json=params)
596        try:
597            response = resp.json()
598        except ValueError as ex:
599            raise F5ModuleError(str(ex))
600
601        if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]:
602            return True
603        raise F5ModuleError(resp.content)
604
605    def update_fallback_on_device(self, fallback):
606        params = dict(
607            fallback=fallback
608        )
609        uri = 'https://{0}:{1}/mgmt/tm/auth/source/'.format(
610            self.client.provider['server'],
611            self.client.provider['server_port']
612        )
613
614        resp = self.client.api.patch(uri, json=params)
615        try:
616            response = resp.json()
617        except ValueError as ex:
618            raise F5ModuleError(str(ex))
619
620        if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]:
621            return True
622        raise F5ModuleError(resp.content)
623
624    def exec_module(self):
625        start = datetime.now().isoformat()
626        version = tmos_version(self.client)
627        changed = False
628        result = dict()
629        state = self.want.state
630
631        if state == "present":
632            changed = self.present()
633        elif state == "absent":
634            changed = self.absent()
635
636        reportable = ReportableChanges(params=self.changes.to_return())
637        changes = reportable.to_return()
638        result.update(**changes)
639        result.update(dict(changed=changed))
640        self._announce_deprecations(result)
641        send_teem(start, self.client, self.module, version)
642        return result
643
644    def present(self):
645        if self.exists():
646            return self.update()
647        else:
648            return self.create()
649
650    def absent(self):
651        if self.exists():
652            return self.remove()
653        return False
654
655    def should_update(self):
656        result = self._update_changed_options()
657        if result:
658            return True
659        return False
660
661    def update(self):
662        self.have = self.read_current_from_device()
663        if not self.should_update():
664            return False
665        if self.module.check_mode:
666            return True
667        self.update_on_device()
668        if self.want.fallback_to_local == 'yes':
669            self.update_fallback_on_device('true')
670        elif self.want.fallback_to_local == 'no':
671            self.update_fallback_on_device('false')
672        if self.want.use_for_auth and self.changes.auth_source:
673            self.update_auth_source_on_device(self.changes.auth_source)
674        return True
675
676    def remove(self):
677        if self.module.check_mode:
678            return True
679        self.update_auth_source_on_device('local')
680        self.remove_from_device()
681        if self.exists():
682            raise F5ModuleError("Failed to delete the resource.")
683        return True
684
685    def create(self):
686        self._set_changed_options()
687        if self.module.check_mode:
688            return True
689        self.create_on_device()
690        if self.want.fallback_to_local == 'yes':
691            self.update_fallback_on_device('true')
692        elif self.want.fallback_to_local == 'no':
693            self.update_fallback_on_device('false')
694        if self.want.use_for_auth:
695            self.update_auth_source_on_device(self.want.auth_source)
696        return True
697
698    def exists(self):
699        errors = [401, 403, 409, 500, 501, 502, 503, 504]
700        uri = "https://{0}:{1}/mgmt/tm/auth/ldap/{2}".format(
701            self.client.provider['server'],
702            self.client.provider['server_port'],
703            transform_name('Common', 'system-auth')
704        )
705        resp = self.client.api.get(uri)
706        try:
707            response = resp.json()
708        except ValueError as ex:
709            raise F5ModuleError(str(ex))
710
711        if resp.status == 404 or 'code' in response and response['code'] == 404:
712            return False
713        if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]:
714            return True
715
716        if resp.status in errors or 'code' in response and response['code'] in errors:
717            if 'message' in response:
718                raise F5ModuleError(response['message'])
719            else:
720                raise F5ModuleError(resp.content)
721
722    def create_on_device(self):
723        params = self.changes.api_params()
724        params['name'] = 'system-auth'
725        params['partition'] = 'Common'
726        uri = "https://{0}:{1}/mgmt/tm/auth/ldap/".format(
727            self.client.provider['server'],
728            self.client.provider['server_port'],
729        )
730        resp = self.client.api.post(uri, json=params)
731        try:
732            response = resp.json()
733        except ValueError as ex:
734            raise F5ModuleError(str(ex))
735
736        if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]:
737            return True
738        raise F5ModuleError(resp.content)
739
740    def update_on_device(self):
741        params = self.changes.api_params()
742        if not params:
743            return
744        uri = "https://{0}:{1}/mgmt/tm/auth/ldap/{2}".format(
745            self.client.provider['server'],
746            self.client.provider['server_port'],
747            transform_name('Common', 'system-auth')
748        )
749        resp = self.client.api.patch(uri, json=params)
750        try:
751            response = resp.json()
752        except ValueError as ex:
753            raise F5ModuleError(str(ex))
754
755        if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]:
756            return True
757        raise F5ModuleError(resp.content)
758
759    def remove_from_device(self):
760        uri = "https://{0}:{1}/mgmt/tm/auth/ldap/{2}".format(
761            self.client.provider['server'],
762            self.client.provider['server_port'],
763            transform_name('Common', 'system-auth')
764        )
765        response = self.client.api.delete(uri)
766
767        if response.status in [200, 201]:
768            return True
769        raise F5ModuleError(response.content)
770
771    def read_current_from_device(self):
772        uri = "https://{0}:{1}/mgmt/tm/auth/ldap/{2}".format(
773            self.client.provider['server'],
774            self.client.provider['server_port'],
775            transform_name('Common', 'system-auth')
776        )
777        resp = self.client.api.get(uri)
778        try:
779            response = resp.json()
780        except ValueError as ex:
781            raise F5ModuleError(str(ex))
782
783        if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]:
784            response.update(self.read_current_auth_source_from_device())
785            return ApiParameters(params=response)
786        raise F5ModuleError(resp.content)
787
788    def read_current_auth_source_from_device(self):
789        uri = "https://{0}:{1}/mgmt/tm/auth/source".format(
790            self.client.provider['server'],
791            self.client.provider['server_port'],
792        )
793        resp = self.client.api.get(uri)
794        try:
795            response = resp.json()
796        except ValueError as ex:
797            raise F5ModuleError(str(ex))
798        result = {}
799        if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]:
800            if 'fallback' in response:
801                result['fallback'] = response['fallback']
802            if 'type' in response:
803                result['auth_source'] = response['type']
804            return result
805        raise F5ModuleError(resp.content)
806
807
808class ArgumentSpec(object):
809    def __init__(self):
810        self.supports_check_mode = True
811        argument_spec = dict(
812            servers=dict(
813                type='list',
814                elements='str',
815            ),
816            port=dict(type='int'),
817            remote_directory_tree=dict(),
818            scope=dict(
819                choices=['sub', 'one', 'base']
820            ),
821            bind_dn=dict(),
822            bind_password=dict(no_log=True),
823            user_template=dict(),
824            check_member_attr=dict(type='bool'),
825            ssl=dict(
826                choices=['yes', 'no', 'start-tls']
827            ),
828            ca_cert=dict(aliases=['ssl_ca_cert']),
829            client_key=dict(aliases=['ssl_client_key']),
830            client_cert=dict(aliases=['ssl_client_cert']),
831            validate_certs=dict(type='bool', aliases=['ssl_check_peer']),
832            login_ldap_attr=dict(),
833            fallback_to_local=dict(type='bool'),
834            use_for_auth=dict(type='bool'),
835            update_password=dict(
836                default='always',
837                choices=['always', 'on_create']
838            ),
839            state=dict(default='present', choices=['absent', 'present']),
840        )
841        self.argument_spec = {}
842        self.argument_spec.update(f5_argument_spec)
843        self.argument_spec.update(argument_spec)
844
845
846def main():
847    spec = ArgumentSpec()
848
849    module = AnsibleModule(
850        argument_spec=spec.argument_spec,
851        supports_check_mode=spec.supports_check_mode,
852    )
853
854    try:
855        mm = ModuleManager(module=module)
856        results = mm.exec_module()
857        module.exit_json(**results)
858    except F5ModuleError as ex:
859        module.fail_json(msg=str(ex))
860
861
862if __name__ == '__main__':
863    main()
864