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_firewall_global_rules
13short_description: Manage AFM global rule settings on BIG-IP
14description:
15  - Configures the global network firewall rules on AFM (Advanced Firewall Manager).
16    These firewall rules are applied to all packets except those going through
17    the management interface. They are applied first, before any firewall rules
18    for the packet's virtual server, route domain, and/or self IP address.
19version_added: "1.0.0"
20options:
21  enforced_policy:
22    description:
23      - Specifies an enforced firewall policy.
24      - C(enforced_policy) rules are enforced globally.
25    type: str
26  service_policy:
27    description:
28      - Specifies a service policy that would apply to traffic globally.
29      - The service policy is applied to all flows, provided there are
30        no other context specific service policy configurations that
31        override the global service policy. For example, when a service
32        policy is configured both at a global level and on a
33        firewall rule, and a flow matches the rule, the more specific
34        service policy configuration in the rule will override the service
35        policy setting at the global level.
36      - The service policy associated here can be created using the
37        C(bigip_service_policy) module.
38    type: str
39  staged_policy:
40    description:
41      - Specifies a staged firewall policy.
42      - C(staged_policy) rules are not enforced while all the visibility
43        aspects (statistics, reporting, and logging) function as if
44        the staged-policy rules were enforced globally.
45    type: str
46  description:
47    description:
48      - Description for the global list of firewall rules.
49    type: str
50extends_documentation_fragment: f5networks.f5_modules.f5
51author:
52  - Tim Rupp (@caphrim007)
53'''
54
55EXAMPLES = r'''
56- name: Change enforced policy in AFM global rules
57  bigip_firewall_global_rules:
58    enforced_policy: enforcing1
59    provider:
60      password: secret
61      server: lb.mydomain.com
62      user: admin
63  delegate_to: localhost
64'''
65
66RETURN = r'''
67enforced_policy:
68  description: The new global Enforced Policy.
69  returned: changed
70  type: str
71  sample: /Common/enforced1
72service_policy:
73  description: The new global Service Policy.
74  returned: changed
75  type: str
76  sample: /Common/service1
77staged_policy:
78  description: The new global Staged Policy.
79  returned: changed
80  type: str
81  sample: /Common/staged1
82description:
83  description: The new description.
84  returned: changed
85  type: str
86  sample: My description
87'''
88from datetime import datetime
89
90from ansible.module_utils.basic import AnsibleModule
91
92from ..module_utils.bigip import F5RestClient
93from ..module_utils.common import (
94    F5ModuleError, AnsibleF5Parameters, f5_argument_spec, fq_name
95)
96from ..module_utils.compare import cmp_str_with_none
97from ..module_utils.icontrol import tmos_version
98from ..module_utils.teem import send_teem
99
100
101class Parameters(AnsibleF5Parameters):
102    api_map = {
103        'enforcedPolicy': 'enforced_policy',
104        'servicePolicy': 'service_policy',
105        'stagedPolicy': 'staged_policy',
106    }
107
108    api_attributes = [
109        'enforcedPolicy',
110        'servicePolicy',
111        'stagedPolicy',
112        'description',
113    ]
114
115    returnables = [
116        'enforced_policy',
117        'service_policy',
118        'staged_policy',
119        'description',
120    ]
121
122    updatables = [
123        'enforced_policy',
124        'service_policy',
125        'staged_policy',
126        'description',
127    ]
128
129
130class ApiParameters(Parameters):
131    @property
132    def description(self):
133        if self._values['description'] in [None, 'none']:
134            return None
135        return self._values['description']
136
137
138class ModuleParameters(Parameters):
139    @property
140    def enforced_policy(self):
141        if self._values['enforced_policy'] is None:
142            return None
143        if self._values['enforced_policy'] in ['', 'none']:
144            return ''
145        return fq_name(self.partition, self._values['enforced_policy'])
146
147    @property
148    def service_policy(self):
149        if self._values['service_policy'] is None:
150            return None
151        if self._values['service_policy'] in ['', 'none']:
152            return ''
153        return fq_name(self.partition, self._values['service_policy'])
154
155    @property
156    def staged_policy(self):
157        if self._values['staged_policy'] is None:
158            return None
159        if self._values['staged_policy'] in ['', 'none']:
160            return ''
161        return fq_name(self.partition, self._values['staged_policy'])
162
163    @property
164    def description(self):
165        if self._values['description'] is None:
166            return None
167        elif self._values['description'] in ['none', '']:
168            return ''
169        return self._values['description']
170
171
172class Changes(Parameters):
173    def to_return(self):
174        result = {}
175        try:
176            for returnable in self.returnables:
177                result[returnable] = getattr(self, returnable)
178            result = self._filter_params(result)
179        except Exception:
180            raise
181        return result
182
183
184class UsableChanges(Changes):
185    pass
186
187
188class ReportableChanges(Changes):
189    pass
190
191
192class Difference(object):
193    def __init__(self, want, have=None):
194        self.want = want
195        self.have = have
196
197    def compare(self, param):
198        try:
199            result = getattr(self, param)
200            return result
201        except AttributeError:
202            return self.__default(param)
203
204    def __default(self, param):
205        attr1 = getattr(self.want, param)
206        try:
207            attr2 = getattr(self.have, param)
208            if attr1 != attr2:
209                return attr1
210        except AttributeError:
211            return attr1
212
213    @property
214    def description(self):
215        return cmp_str_with_none(self.want.description, self.have.description)
216
217    @property
218    def enforced_policy(self):
219        return cmp_str_with_none(self.want.enforced_policy, self.have.enforced_policy)
220
221    @property
222    def staged_policy(self):
223        return cmp_str_with_none(self.want.staged_policy, self.have.staged_policy)
224
225    @property
226    def service_policy(self):
227        return cmp_str_with_none(self.want.service_policy, self.have.service_policy)
228
229
230class ModuleManager(object):
231    def __init__(self, *args, **kwargs):
232        self.module = kwargs.get('module', None)
233        self.client = F5RestClient(**self.module.params)
234        self.want = ModuleParameters(params=self.module.params)
235        self.have = ApiParameters()
236        self.changes = UsableChanges()
237
238    def _set_changed_options(self):
239        changed = {}
240        for key in Parameters.returnables:
241            if getattr(self.want, key) is not None:
242                changed[key] = getattr(self.want, key)
243        if changed:
244            self.changes = UsableChanges(params=changed)
245
246    def _update_changed_options(self):
247        diff = Difference(self.want, self.have)
248        updatables = Parameters.updatables
249        changed = dict()
250        for k in updatables:
251            change = diff.compare(k)
252            if change is None:
253                continue
254            else:
255                if isinstance(change, dict):
256                    changed.update(change)
257                else:
258                    changed[k] = change
259        if changed:
260            self.changes = UsableChanges(params=changed)
261            return True
262        return False
263
264    def should_update(self):
265        result = self._update_changed_options()
266        if result:
267            return True
268        return False
269
270    def exec_module(self):
271        start = datetime.now().isoformat()
272        version = tmos_version(self.client)
273        result = dict()
274
275        changed = self.present()
276
277        reportable = ReportableChanges(params=self.changes.to_return())
278        changes = reportable.to_return()
279        result.update(**changes)
280        result.update(dict(changed=changed))
281        self._announce_deprecations(result)
282        send_teem(start, self.client, self.module, version)
283        return result
284
285    def _announce_deprecations(self, result):
286        warnings = result.pop('__warnings', [])
287        for warning in warnings:
288            self.client.module.deprecate(
289                msg=warning['msg'],
290                version=warning['version']
291            )
292
293    def present(self):
294        return self.update()
295
296    def update(self):
297        self.have = self.read_current_from_device()
298        if not self.should_update():
299            return False
300        if self.module.check_mode:
301            return True
302        self.update_on_device()
303        return True
304
305    def update_on_device(self):
306        params = self.changes.api_params()
307        uri = "https://{0}:{1}/mgmt/tm/security/firewall/global-rules".format(
308            self.client.provider['server'],
309            self.client.provider['server_port']
310        )
311        resp = self.client.api.patch(uri, json=params)
312        try:
313            response = resp.json()
314        except ValueError as ex:
315            raise F5ModuleError(str(ex))
316
317        if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]:
318            return True
319        raise F5ModuleError(resp.content)
320
321    def read_current_from_device(self):
322        uri = "https://{0}:{1}/mgmt/tm/security/firewall/global-rules".format(
323            self.client.provider['server'],
324            self.client.provider['server_port']
325        )
326        resp = self.client.api.get(uri)
327        try:
328            response = resp.json()
329        except ValueError as ex:
330            raise F5ModuleError(str(ex))
331
332        if resp.status in [200, 201] or 'code' in response and response['code'] in [200, 201]:
333            return ApiParameters(params=response)
334        raise F5ModuleError(resp.content)
335
336
337class ArgumentSpec(object):
338    def __init__(self):
339        self.supports_check_mode = True
340        argument_spec = dict(
341            enforced_policy=dict(),
342            service_policy=dict(),
343            staged_policy=dict(),
344            description=dict(),
345        )
346        self.argument_spec = {}
347        self.argument_spec.update(f5_argument_spec)
348        self.argument_spec.update(argument_spec)
349
350
351def main():
352    spec = ArgumentSpec()
353
354    module = AnsibleModule(
355        argument_spec=spec.argument_spec,
356        supports_check_mode=spec.supports_check_mode,
357    )
358
359    try:
360        mm = ModuleManager(module=module)
361        results = mm.exec_module()
362        module.exit_json(**results)
363    except F5ModuleError as ex:
364        module.fail_json(msg=str(ex))
365
366
367if __name__ == '__main__':
368    main()
369