1#!/usr/local/bin/python3.8
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
10DOCUMENTATION = r'''
11---
12module: bigip_message_routing_route
13short_description: Manages static routes for routing message protocol messages
14description:
15  - Manages static routes for routing message protocol messages.
16version_added: "1.0.0"
17options:
18  name:
19    description:
20      - Specifies the name of the static route.
21    required: True
22    type: str
23  description:
24    description:
25      - The user-defined description of the static route.
26    type: str
27  type:
28    description:
29      - Parameter used to specify the type of the route to manage.
30      - Default setting is C(generic) with more options coming.
31    type: str
32    choices:
33      - generic
34    default: generic
35  src_address:
36    description:
37      - Specifies the source address of the route.
38      - Setting the attribute to an empty string will create a wildcard matching all message source-addresses, which is
39        the default when creating a new route.
40    type: str
41  dst_address:
42    description:
43      - Specifies the destination address of the route.
44      - Setting the attribute to an empty string will create a wildcard matching all message destination-addresses,
45        which is the default when creating a new route.
46    type: str
47  peer_selection_mode:
48    description:
49      - Specifies the method to use when selecting a peer from the provided list of C(peers).
50    type: str
51    choices:
52      - ratio
53      - sequential
54  peers:
55    description:
56      - Specifies a list of ltm messagerouting-peer objects.
57      - The specified peer must be on the same partition as the route.
58    type: list
59    elements: str
60  partition:
61    description:
62      - Device partition to create route object on.
63    type: str
64    default: Common
65  state:
66    description:
67      - When C(present), ensures the route exists.
68      - When C(absent), ensures the route is removed.
69    type: str
70    choices:
71      - present
72      - absent
73    default: present
74notes:
75  - Requires BIG-IP >= 14.0.0
76extends_documentation_fragment: f5networks.f5_modules.f5
77author:
78  - Wojciech Wypior (@wojtek0806)
79'''
80
81EXAMPLES = r'''
82- name: Create a simple generic route
83  bigip_message_routing_route:
84    name: foobar
85    provider:
86      password: secret
87      server: lb.mydomain.com
88      user: admin
89  delegate_to: localhost
90
91- name: Modify a generic route
92  bigip_message_routing_route:
93    name: foobar
94    peers:
95      - peer1
96      - peer2
97    peer_selection_mode: ratio
98    src_address: annoying_user
99    dst_address: blackhole
100    provider:
101      password: secret
102      server: lb.mydomain.com
103      user: admin
104  delegate_to: localhost
105
106- name: Remove a generic
107  bigip_message_routing_route:
108    name: foobar
109    state: absent
110    provider:
111      password: secret
112      server: lb.mydomain.com
113      user: admin
114  delegate_to: localhost
115'''
116
117RETURN = r'''
118description:
119  description: The user-defined description of the route.
120  returned: changed
121  type: str
122  sample: Some description
123src_address:
124  description: The source address of the route.
125  returned: changed
126  type: str
127  sample: annyoing_user
128dst_address:
129  description: The destination address of the route.
130  returned: changed
131  type: str
132  sample: blackhole
133peer_selection_mode:
134  description: The method to use when selecting a peer.
135  returned: changed
136  type: str
137  sample: ratio
138peers:
139  description: The list of ltm messagerouting-peer object.
140  returned: changed
141  type: list
142  sample: ['/Common/peer1', '/Common/peer2']
143'''
144from datetime import datetime
145from ansible.module_utils.basic import (
146    AnsibleModule, env_fallback
147)
148from distutils.version import LooseVersion
149
150from ..module_utils.bigip import F5RestClient
151from ..module_utils.common import (
152    F5ModuleError, AnsibleF5Parameters, transform_name, f5_argument_spec, is_empty_list, fq_name
153)
154from ..module_utils.compare import (
155    cmp_simple_list, cmp_str_with_none
156)
157from ..module_utils.icontrol import tmos_version
158from ..module_utils.teem import send_teem
159
160
161class Parameters(AnsibleF5Parameters):
162    api_map = {
163        'peerSelectionMode': 'peer_selection_mode',
164        'sourceAddress': 'src_address',
165        'destinationAddress': 'dst_address',
166    }
167
168    api_attributes = [
169        'description',
170        'peerSelectionMode',
171        'peers',
172        'sourceAddress',
173        'destinationAddress',
174    ]
175
176    returnables = [
177        'peer_selection_mode',
178        'peers',
179        'description',
180        'src_address',
181        'dst_address'
182    ]
183
184    updatables = [
185        'peer_selection_mode',
186        'peers',
187        'description',
188        'src_address',
189        'dst_address'
190    ]
191
192
193class ApiParameters(Parameters):
194    pass
195
196
197class ModuleParameters(Parameters):
198    @property
199    def peers(self):
200        if self._values['peers'] is None:
201            return None
202        if is_empty_list(self._values['peers']):
203            return ""
204        result = [fq_name(self.partition, peer) for peer in self._values['peers']]
205        return result
206
207
208class Changes(Parameters):
209    def to_return(self):
210        result = {}
211        try:
212            for returnable in self.returnables:
213                result[returnable] = getattr(self, returnable)
214            result = self._filter_params(result)
215        except Exception:
216            raise
217        return result
218
219
220class UsableChanges(Changes):
221    pass
222
223
224class ReportableChanges(Changes):
225    pass
226
227
228class Difference(object):
229    def __init__(self, want, have=None):
230        self.want = want
231        self.have = have
232
233    def compare(self, param):
234        try:
235            result = getattr(self, param)
236            return result
237        except AttributeError:
238            return self.__default(param)
239
240    def __default(self, param):
241        attr1 = getattr(self.want, param)
242        try:
243            attr2 = getattr(self.have, param)
244            if attr1 != attr2:
245                return attr1
246        except AttributeError:
247            return attr1
248
249    @property
250    def description(self):
251        result = cmp_str_with_none(self.want.description, self.have.description)
252        return result
253
254    @property
255    def dst_address(self):
256        result = cmp_str_with_none(self.want.dst_address, self.have.dst_address)
257        return result
258
259    @property
260    def src_address(self):
261        result = cmp_str_with_none(self.want.src_address, self.have.src_address)
262        return result
263
264    @property
265    def peers(self):
266        result = cmp_simple_list(self.want.peers, self.have.peers)
267        return result
268
269
270class BaseManager(object):
271    def __init__(self, *args, **kwargs):
272        self.module = kwargs.get('module', None)
273        self.client = F5RestClient(**self.module.params)
274        self.want = ModuleParameters(params=self.module.params)
275        self.have = ApiParameters()
276        self.changes = UsableChanges()
277
278    def _set_changed_options(self):
279        changed = {}
280        for key in Parameters.returnables:
281            if getattr(self.want, key) is not None:
282                changed[key] = getattr(self.want, key)
283        if changed:
284            self.changes = UsableChanges(params=changed)
285
286    def _update_changed_options(self):
287        diff = Difference(self.want, self.have)
288        updatables = Parameters.updatables
289        changed = dict()
290        for k in updatables:
291            change = diff.compare(k)
292            if change is None:
293                continue
294            else:
295                if isinstance(change, dict):
296                    changed.update(change)
297                else:
298                    changed[k] = change
299        if changed:
300            self.changes = UsableChanges(params=changed)
301            return True
302        return False
303
304    def _announce_deprecations(self, result):
305        warnings = result.pop('__warnings', [])
306        for warning in warnings:
307            self.client.module.deprecate(
308                msg=warning['msg'],
309                version=warning['version']
310            )
311
312    def exec_module(self):
313        start = datetime.now().isoformat()
314        version = tmos_version(self.client)
315        changed = False
316        result = dict()
317        state = self.want.state
318
319        if state == "present":
320            changed = self.present()
321        elif state == "absent":
322            changed = self.absent()
323
324        reportable = ReportableChanges(params=self.changes.to_return())
325        changes = reportable.to_return()
326        result.update(**changes)
327        result.update(dict(changed=changed))
328        self._announce_deprecations(result)
329        send_teem(start, self.client, self.module, version)
330        return result
331
332    def present(self):
333        if self.exists():
334            return self.update()
335        else:
336            return self.create()
337
338    def absent(self):
339        if self.exists():
340            return self.remove()
341        return False
342
343    def should_update(self):
344        result = self._update_changed_options()
345        if result:
346            return True
347        return False
348
349    def update(self):
350        self.have = self.read_current_from_device()
351        if not self.should_update():
352            return False
353        if self.module.check_mode:
354            return True
355        self.update_on_device()
356        return True
357
358    def remove(self):
359        if self.module.check_mode:
360            return True
361        self.remove_from_device()
362        if self.exists():
363            raise F5ModuleError("Failed to delete the resource.")
364        return True
365
366    def create(self):
367        self._set_changed_options()
368        if self.module.check_mode:
369            return True
370        self.create_on_device()
371        return True
372
373
374class GenericModuleManager(BaseManager):
375    def exists(self):
376        uri = "https://{0}:{1}/mgmt/tm/ltm/message-routing/generic/route/{2}".format(
377            self.client.provider['server'],
378            self.client.provider['server_port'],
379            transform_name(self.want.partition, self.want.name)
380        )
381        resp = self.client.api.get(uri)
382        try:
383            response = resp.json()
384        except ValueError as ex:
385            raise F5ModuleError(str(ex))
386
387        if resp.status == 404 or 'code' in response and response['code'] == 404:
388            return False
389        if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]:
390            return True
391
392        errors = [401, 403, 409, 500, 501, 502, 503, 504]
393
394        if resp.status in errors or 'code' in response and response['code'] in errors:
395            if 'message' in response:
396                raise F5ModuleError(response['message'])
397            else:
398                raise F5ModuleError(resp.content)
399
400    def create_on_device(self):
401        params = self.changes.api_params()
402        params['name'] = self.want.name
403        params['partition'] = self.want.partition
404        uri = "https://{0}:{1}/mgmt/tm/ltm/message-routing/generic/route/".format(
405            self.client.provider['server'],
406            self.client.provider['server_port'],
407        )
408        resp = self.client.api.post(uri, json=params)
409        try:
410            response = resp.json()
411        except ValueError as ex:
412            raise F5ModuleError(str(ex))
413
414        if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]:
415            return True
416        raise F5ModuleError(resp.content)
417
418    def update_on_device(self):
419        params = self.changes.api_params()
420        uri = "https://{0}:{1}/mgmt/tm/ltm/message-routing/generic/route/{2}".format(
421            self.client.provider['server'],
422            self.client.provider['server_port'],
423            transform_name(self.want.partition, self.want.name)
424        )
425        resp = self.client.api.patch(uri, json=params)
426        try:
427            response = resp.json()
428        except ValueError as ex:
429            raise F5ModuleError(str(ex))
430
431        if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]:
432            return True
433        raise F5ModuleError(resp.content)
434
435    def remove_from_device(self):
436        uri = "https://{0}:{1}/mgmt/tm/ltm/message-routing/generic/route/{2}".format(
437            self.client.provider['server'],
438            self.client.provider['server_port'],
439            transform_name(self.want.partition, self.want.name)
440        )
441        response = self.client.api.delete(uri)
442        if response.status == 200:
443            return True
444        raise F5ModuleError(response.content)
445
446    def read_current_from_device(self):
447        uri = "https://{0}:{1}/mgmt/tm/ltm/message-routing/generic/route/{2}".format(
448            self.client.provider['server'],
449            self.client.provider['server_port'],
450            transform_name(self.want.partition, self.want.name)
451        )
452        resp = self.client.api.get(uri)
453        try:
454            response = resp.json()
455        except ValueError as ex:
456            raise F5ModuleError(str(ex))
457
458        if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]:
459            return ApiParameters(params=response)
460        raise F5ModuleError(resp.content)
461
462
463class ModuleManager(object):
464    def __init__(self, *args, **kwargs):
465        self.module = kwargs.get('module', None)
466        self.client = F5RestClient(**self.module.params)
467        self.kwargs = kwargs
468
469    def version_less_than_14(self):
470        version = tmos_version(self.client)
471        if LooseVersion(version) < LooseVersion('14.0.0'):
472            return True
473        return False
474
475    def exec_module(self):
476        if self.version_less_than_14():
477            raise F5ModuleError('Message routing is not supported on TMOS version below 14.x')
478        if self.module.params['type'] == 'generic':
479            manager = self.get_manager('generic')
480        else:
481            raise F5ModuleError(
482                "Unknown type specified."
483            )
484        return manager.exec_module()
485
486    def get_manager(self, type):
487        if type == 'generic':
488            return GenericModuleManager(**self.kwargs)
489
490
491class ArgumentSpec(object):
492    def __init__(self):
493        self.supports_check_mode = True
494        argument_spec = dict(
495            name=dict(required=True),
496            description=dict(),
497            src_address=dict(),
498            dst_address=dict(),
499            peer_selection_mode=dict(
500                choices=['ratio', 'sequential']
501            ),
502            peers=dict(
503                type='list',
504                elements='str',
505            ),
506            type=dict(
507                choices=['generic'],
508                default='generic'
509            ),
510            partition=dict(
511                default='Common',
512                fallback=(env_fallback, ['F5_PARTITION'])
513            ),
514            state=dict(
515                default='present',
516                choices=['present', 'absent']
517            )
518
519        )
520        self.argument_spec = {}
521        self.argument_spec.update(f5_argument_spec)
522        self.argument_spec.update(argument_spec)
523
524
525def main():
526    spec = ArgumentSpec()
527
528    module = AnsibleModule(
529        argument_spec=spec.argument_spec,
530        supports_check_mode=spec.supports_check_mode,
531    )
532
533    try:
534        mm = ModuleManager(module=module)
535        results = mm.exec_module()
536        module.exit_json(**results)
537    except F5ModuleError as ex:
538        module.fail_json(msg=str(ex))
539
540
541if __name__ == '__main__':
542    main()
543