1#
2# -*- coding: utf-8 -*-
3# Copyright 2020 Red Hat
4# GNU General Public License v3.0+
5# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
6#
7
8from __future__ import absolute_import, division, print_function
9
10__metaclass__ = type
11
12"""
13The cisco.ios_bgp_address_family config file.
14It is in this file where the current configuration (as dict)
15is compared to the provided configuration (as dict) and the command set
16necessary to bring the current configuration to its desired end-state is
17created.
18"""
19
20from ansible.module_utils.six import iteritems
21from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import (
22    dict_merge,
23)
24from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.resource_module import (
25    ResourceModule,
26)
27from ansible_collections.cisco.ios.plugins.module_utils.network.ios.facts.facts import (
28    Facts,
29)
30from ansible_collections.cisco.ios.plugins.module_utils.network.ios.rm_templates.bgp_address_family import (
31    Bgp_address_familyTemplate,
32)
33
34
35class Bgp_address_family(ResourceModule):
36    """
37    The cisco.ios_bgp_address_family config class
38    """
39
40    gather_subset = ["!all", "!min"]
41
42    parsers = [
43        "as_number",
44        "afi",
45        "default",
46        "default_metric",
47        "distance",
48        "table_map",
49    ]
50
51    def __init__(self, module):
52        super(Bgp_address_family, self).__init__(
53            empty_fact_val={},
54            facts_module=Facts(module),
55            module=module,
56            resource="bgp_address_family",
57            tmplt=Bgp_address_familyTemplate(),
58        )
59
60    def execute_module(self):
61        """ Execute the module
62
63        :rtype: A dictionary
64        :returns: The result from module execution
65        """
66        if self.state not in ["parsed", "gathered"]:
67            self.generate_commands()
68            self.run_commands()
69        return self.result
70
71    def generate_commands(self):
72        """ Generate configuration commands to send based on
73            want, have and desired state.
74        """
75        if self.want:
76            wantd = {self.want["as_number"]: self.want}
77        else:
78            wantd = dict()
79        if self.have:
80            haved = {self.have["as_number"]: self.have}
81        else:
82            haved = dict()
83
84        for each in wantd, haved:
85            self.list_to_dict(each)
86
87        # if state is merged, merge want onto have and then compare
88        if self.state == "merged":
89            wantd = dict_merge(haved, wantd)
90            if len(wantd) > 1:
91                self._module.fail_json(
92                    msg="BGP is already running. Only one BGP instance is allowed per device."
93                )
94        # if state is deleted, empty out wantd and set haved to wantd
95        if self.state == "deleted":
96            as_number = None
97            if wantd:
98                temp = dict()
99                for key, val in iteritems(haved):
100                    as_number = key
101                    if wantd.get(key):
102                        for k, v in iteritems(val.get("address_family")):
103                            w = wantd[key].get("address_family")
104                            if k in w:
105                                temp.update({k: v})
106                    val["address_family"] = temp
107            elif haved:
108                as_number = list(haved)[0]
109                temp = {}
110                for k, v in iteritems(haved):
111                    if not wantd:
112                        temp.update({k: v})
113                haved = temp
114            wantd = dict()
115            for k, have in iteritems(haved):
116                if k not in wantd:
117                    self._compare(want=wantd, have=have, as_number=as_number)
118
119        # remove superfluous config for overridden
120        if self.state == "overridden":
121            for key, have in iteritems(haved):
122                if wantd.get(key):
123                    if have.get("address_family"):
124                        for k, v in iteritems(have.get("address_family")):
125                            w = wantd[key].get("address_family")
126                            if k not in w:
127                                self._compare(
128                                    want=dict(),
129                                    have={"address_family": {k: v}},
130                                    as_number=key,
131                                )
132
133        for k, want in iteritems(wantd):
134            self._compare(want=want, have=haved.pop(k, dict()), as_number=k)
135
136    def _compare(self, want, have, as_number):
137        """Leverages the base class `compare()` method and
138           populates the list of commands to be run by comparing
139           the `want` and `have` data with the `parsers` defined
140           for the Bgp_address_family network resource.
141        """
142        if want != have and self.state != "deleted":
143            self._compare_af(want, have)
144        elif self.state == "deleted":
145            self._delete_af(have)
146
147        if self.commands and "router bgp" not in self.commands[0]:
148            self.commands.insert(0, "router bgp {0}".format(as_number))
149
150    def _compare_af(self, want, have):
151        w = want.get("address_family", dict())
152        h = have.get("address_family", dict())
153
154        for key, val in iteritems(w):
155            cmd_len = len(self.commands)
156            if h.get(key):
157                h_key = h.get(key)
158            else:
159                h_key = dict()
160            if val != h_key:
161                self._aggregate_address_af_config_compare(val, have=h_key)
162                self._bgp_af_config_compare(val, have=h_key)
163                self._compare_neighbor(val, have=h_key)
164                self._compare_network(val, have=h_key)
165                self._compare_snmp(val, have=h_key)
166                self.compare(
167                    parsers=self.parsers, want=val, have=h.pop(key, dict())
168                )
169            if cmd_len != len(self.commands):
170                af_cmd = "address-family {afi}".format(**val)
171                if val.get("safi"):
172                    af_cmd += " {safi}".format(**val)
173                if val.get("vrf"):
174                    af_cmd += " vrf {vrf}".format(**val)
175                self.commands.insert(cmd_len, af_cmd)
176        if not w and self.state == "overridden":
177            self._delete_af(have)
178
179    def _aggregate_address_af_config_compare(self, want, have):
180        parsers = ["aggregate_address"]
181        w_aggregate_address = want.get("aggregate_address", {})
182        if have:
183            h_aggregate_address = have.get("aggregate_address", {})
184        else:
185            h_aggregate_address = {}
186        for key, val in iteritems(w_aggregate_address):
187            if h_aggregate_address and h_aggregate_address.get(key):
188                self.compare(
189                    parsers=parsers,
190                    want={"aggregate_address": val},
191                    have={"aggregate_address": h_aggregate_address.pop(key)},
192                )
193            else:
194                self.compare(
195                    parsers=parsers,
196                    want={"aggregate_address": val},
197                    have=dict(),
198                )
199        if self.state == "replaced" or self.state == "overridden":
200            if h_aggregate_address:
201                for key, val in iteritems(h_aggregate_address):
202                    self.compare(
203                        parsers=parsers,
204                        want=dict(),
205                        have={"aggregate_address": val},
206                    )
207            elif have.get("aggregate_address"):
208                for key, val in iteritems(have.pop("aggregate_address")):
209                    self.compare(
210                        parsers=parsers,
211                        want=dict(),
212                        have={"aggregate_address": val},
213                    )
214
215    def _bgp_af_config_compare(self, want, have):
216        parsers = [
217            "bgp.additional_paths",
218            "bgp.dampening",
219            "bgp.config",
220            "bgp.slow_peer",
221        ]
222        w_bgp = want.get("bgp", dict())
223        if have:
224            h_bgp = have.get("bgp", dict())
225        else:
226            h_bgp = dict()
227        for key, val in iteritems(w_bgp):
228            if h_bgp and h_bgp.get(key):
229                self.compare(
230                    parsers=parsers,
231                    want={"bgp": {key: val}},
232                    have={"bgp": {key: h_bgp.pop(key)}},
233                )
234            else:
235                self.compare(
236                    parsers=parsers, want={"bgp": {key: val}}, have=dict()
237                )
238        if self.state == "replaced" or self.state == "overridden":
239            if h_bgp:
240                for key, val in iteritems(h_bgp):
241                    self.compare(
242                        parsers=parsers, want=dict(), have={"bgp": {key: val}}
243                    )
244            elif have.get("bgp"):
245                for key, val in iteritems(have.pop("bgp")):
246                    self.compare(
247                        parsers=parsers, want=dict(), have={"bgp": {key: val}}
248                    )
249
250    def _compare_neighbor(self, want, have):
251        parsers = [
252            "neighbor",
253            "neighbor.prefix_lists",
254            "neighbor.route_maps",
255            "neighbor.slow_peer",
256        ]
257        neighbor_key = ["address", "ipv6_address", "tag"]
258        deprecated = False
259        w = want.get("neighbor", {}) if want else {}
260        if have:
261            h = have.get("neighbor", {})
262        else:
263            h = {}
264
265        def _handle_neighbor_deprecated(w_key, h_key, want, have):
266            # function to handle idempotency, when deprecated params are present
267            # in want and their equivalent supported param are present inside have
268            keys = w_key + "s"
269            if have[h_key].get(keys):
270                temp_have = have[h_key][keys]
271                if want["name"] in temp_have:
272                    param_name = want["name"]
273                    if have[h_key][keys][param_name] == want:
274                        k = keys
275                        v = {param_name: want}
276                        deprecated = True
277                    return k, v, deprecated
278
279        for key, val in iteritems(w):
280            val = self.handle_deprecated(val)
281            if h and h.get(key):
282                neighbor_tag = [each for each in neighbor_key if each in val][
283                    0
284                ]
285                for k, v in iteritems(val):
286                    if k == "route_map" or k == "prefix_list":
287                        k, v, deprecated = _handle_neighbor_deprecated(
288                            k, key, v, h
289                        )
290                    if h[key].get(k) and k not in neighbor_key:
291                        if k not in ["prefix_lists", "route_maps"]:
292                            self.compare(
293                                parsers=parsers,
294                                want={
295                                    "neighbor": {
296                                        neighbor_tag: val[neighbor_tag],
297                                        k: v,
298                                    }
299                                },
300                                have={
301                                    "neighbor": {
302                                        neighbor_tag: val[neighbor_tag],
303                                        k: h[key].pop(k, {}),
304                                    }
305                                },
306                            )
307                        if k in ["prefix_lists", "route_maps"]:
308                            for k_param, v_param in iteritems(val[k]):
309                                self.compare(
310                                    parsers=parsers,
311                                    want={
312                                        "neighbor": {
313                                            neighbor_tag: val[neighbor_tag],
314                                            k: v_param,
315                                        }
316                                    },
317                                    have={
318                                        "neighbor": {
319                                            neighbor_tag: val[neighbor_tag],
320                                            k: h[key][k].pop(k_param, {}),
321                                        }
322                                    },
323                                )
324                    elif k not in neighbor_key:
325                        if k not in ["prefix_lists", "route_maps"]:
326                            self.compare(
327                                parsers=parsers,
328                                want={
329                                    "neighbor": {
330                                        neighbor_tag: val[neighbor_tag],
331                                        k: v,
332                                    }
333                                },
334                                have=dict(),
335                            )
336                        elif (
337                            k in ["prefix_lists", "route_maps"]
338                            and not deprecated
339                        ):
340                            for k_param, v_param in iteritems(val[k]):
341                                self.compare(
342                                    parsers=parsers,
343                                    want={
344                                        "neighbor": {
345                                            neighbor_tag: val[neighbor_tag],
346                                            k: v_param,
347                                        }
348                                    },
349                                    have=dict(),
350                                )
351            else:
352                self.compare(
353                    parsers=parsers, want={"neighbor": val}, have=dict()
354                )
355                for param in ["prefix_lists", "route_maps"]:
356                    if param in val:
357                        for k_param, v_param in iteritems(val[param]):
358                            self.compare(
359                                parsers=parsers,
360                                want={
361                                    "neighbor": {
362                                        "address": val["address"],
363                                        param: v_param,
364                                    }
365                                },
366                                have=dict(),
367                            )
368                        val.pop(param)
369        if self.state == "replaced" or self.state == "overridden":
370            for key, val in iteritems(h):
371                self.compare(
372                    parsers=parsers, want=dict(), have={"neighbor": val}
373                )
374            count = 0
375            remote = None
376            activate = None
377            for each in self.commands:
378                if "no" in each and "remote-as" in each:
379                    remote = count
380                if "no" in each and "activate" in each:
381                    activate = count
382                count += 1
383            if activate and activate > remote:
384                if count > 0 or "activate" in self.commands[activate]:
385                    self.commands.append(self.commands.pop(activate))
386            if remote and activate > remote:
387                if count > 0 or "remote-as" in self.commands[remote]:
388                    self.commands.append(self.commands.pop(remote))
389
390    def _compare_network(self, want, have):
391        parsers = ["network"]
392        w = want.get("network", dict())
393        h = have.get("network", dict())
394        for key, val in iteritems(w):
395            if h and h.get(key):
396                h_network = h.pop(key)
397                if h_network != val:
398                    self.compare(
399                        parsers=parsers,
400                        want={"network": val},
401                        have={"network": val},
402                    )
403            else:
404                self.compare(
405                    parsers=parsers, want={"network": val}, have=dict()
406                )
407        if self.state == "replaced" or self.state == "overridden":
408            for key, val in iteritems(h):
409                self.compare(
410                    parsers=parsers, want=dict(), have={"network": val}
411                )
412
413    def _compare_snmp(self, want, have):
414        parsers = ["snmp"]
415        w = want.get("snmp", dict())
416        h = have.get("snmp", dict())
417        if w:
418            for key, val in iteritems(w["context"]):
419                if h:
420                    h_snmp_param = h["context"].pop(key)
421                    if h_snmp_param and key != "name":
422                        self.compare(
423                            parsers=parsers,
424                            want={
425                                "snmp": {
426                                    key: val,
427                                    "name": w["context"]["name"],
428                                }
429                            },
430                            have={
431                                "snmp": {
432                                    key: h_snmp_param,
433                                    "name": w["context"]["name"],
434                                }
435                            },
436                        )
437                elif key == "community" or key == "user":
438                    self.compare(
439                        parsers=parsers,
440                        want={
441                            "snmp": {key: val, "name": w["context"]["name"]}
442                        },
443                        have=dict(),
444                    )
445        elif h and self.state == "replaced" or self.state == "overridden":
446            for key, val in iteritems(h):
447                self.compare(
448                    parsers=parsers,
449                    want=dict(),
450                    have={"snmp": {key: val, "name": h["context"]["name"]}},
451                )
452
453    def _delete_af(self, have):
454        h = have.get("address_family", dict())
455        for key, val in iteritems(h):
456            if "safi" in val and "vrf" in val:
457                cmd = "no address-family {afi} {safi} vrf {vrf}".format(**val)
458            elif "safi" in val:
459                cmd = "no address-family {afi} {safi}".format(**val)
460            else:
461                cmd = "no address-family {afi}".format(**val)
462            self.commands.append(cmd)
463
464    def list_to_dict(self, param):
465        if param:
466            for key, val in iteritems(param):
467                if val.get("address_family"):
468                    temp = {}
469                    for each in val.get("address_family", []):
470                        temp.update(
471                            {
472                                each["afi"]
473                                + "_"
474                                + each.get("safi", "")
475                                + "_"
476                                + each.get("vrf", ""): each
477                            }
478                        )
479                    val["address_family"] = temp
480                    self.list_to_dict(val["address_family"])
481                if "aggregate_address" in val:
482                    temp = {}
483                    for each in val["aggregate_address"]:
484                        temp.update({each["address"]: each})
485                    val["aggregate_address"] = temp
486                if "bgp" in val and "slow_peer" in val["bgp"]:
487                    temp = {}
488                    for each in val["bgp"]["slow_peer"]:
489                        temp.update({list(each)[0]: each[list(each)[0]]})
490                    val["bgp"]["slow_peer"] = temp
491                if "neighbor" in val:
492                    for each in val["neighbor"]:
493                        if each.get("prefix_lists"):
494                            temp = {}
495                            for every in each["prefix_lists"]:
496                                temp.update({every["name"]: every})
497                            each["prefix_lists"] = temp
498                        if each.get("route_maps"):
499                            temp = {}
500                            for every in each["route_maps"]:
501                                temp.update({every["name"]: every})
502                            each["route_maps"] = temp
503                        if each.get("slow_peer"):
504                            each["slow_peer"] = {
505                                list(every)[0]: every[list(every)[0]]
506                                for every in each["slow_peer"]
507                            }
508                    val["neighbor"] = {
509                        each.get("address")
510                        or each.get("ipv6_address")
511                        or each.get("tag"): each
512                        for each in val.get("neighbor", [])
513                    }
514                if "network" in val:
515                    temp = {}
516                    for each in val.get("network", []):
517                        temp.update({each["address"]: each})
518                    val["network"] = temp
519
520    def handle_deprecated(self, want_to_validate):
521        if want_to_validate.get("next_hop_self") and want_to_validate.get(
522            "nexthop_self"
523        ):
524            del want_to_validate["next_hop_self"]
525        elif want_to_validate.get("next_hop_self"):
526            del want_to_validate["next_hop_self"]
527            want_to_validate["nexthop_self"] = {"all": True}
528        return want_to_validate
529