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 eos_ospfv3 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
20import re
21from ansible.module_utils.six import iteritems
22from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import (
23    dict_merge,
24)
25from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.resource_module import (
26    ResourceModule,
27)
28from ansible_collections.arista.eos.plugins.module_utils.network.eos.facts.facts import (
29    Facts,
30)
31from ansible_collections.arista.eos.plugins.module_utils.network.eos.rm_templates.ospfv3 import (
32    Ospfv3Template,
33)
34from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import (
35    get_from_dict,
36)
37
38
39class Ospfv3(ResourceModule):
40    """
41    The eos_ospfv3 config class
42    """
43
44    def __init__(self, module):
45        super(Ospfv3, self).__init__(
46            empty_fact_val={},
47            facts_module=Facts(module),
48            module=module,
49            resource="ospfv3",
50            tmplt=Ospfv3Template(module=module),
51        )
52        self.parsers = [
53            "vrf",
54            "address_family",
55            "adjacency",
56            "auto_cost",
57            "area.default_cost",
58            "area.authentication",
59            "area.encryption",
60            "area.nssa",
61            "area.ranges",
62            "area.stub",
63            "bfd",
64            "default_information",
65            "default_metric",
66            "distance",
67            "fips_restrictions",
68            "graceful_restart",
69            "graceful_restart_period",
70            "graceful_restart_helper",
71            "log_adjacency_changes",
72            "max_metric",
73            "maximum_paths",
74            "passive_interface",
75            "redistribute",
76            "router_id",
77            "shutdown",
78            "timers.lsa",
79            "timers.out_delay",
80            "timers.pacing",
81            "timers.throttle.lsa",
82            "timers.throttle.spf",
83        ]
84
85    def execute_module(self):
86        """Execute the module
87
88        :rtype: A dictionary
89        :returns: The result from module execution
90        """
91        if self.state not in ["parsed", "gathered"]:
92            self.generate_commands()
93            self.run_commands()
94        return self.result
95
96    def generate_commands(self):
97        """Generate configuration commands to send based on
98        want, have and desired state.
99        """
100        wantd = {}
101        haved = {}
102        for entry in self.want.get("processes", []):
103            wantd.update({entry["vrf"]: entry})
104        for entry in self.have.get("processes", []):
105            haved.update({entry["vrf"]: entry})
106
107        # turn all lists of dicts into dicts prior to merge
108        for entry in wantd, haved:
109            self._ospf_list_to_dict(entry)
110        # if state is merged, merge want onto have and then compare
111        if self.state == "merged":
112            wantd = dict_merge(haved, wantd)
113
114        # if state is deleted, empty out wantd and set haved to wantd
115        if self.state == "deleted":
116            h_del = {}
117            for k, v in iteritems(haved):
118                if k in wantd or not wantd:
119                    h_del.update({k: v})
120            wantd = {}
121            haved = h_del
122
123        # remove superfluous config for overridden and deleted
124        if self.state in ["overridden", "deleted"]:
125            for k, have in iteritems(haved):
126                if k not in wantd and have.get("vrf") == k:
127                    self.commands.append(self._tmplt.render(have, "vrf", True))
128
129        for k, want in iteritems(wantd):
130            self._compare(want=want, have=haved.pop(k, {}))
131
132    def _compare(self, want, have):
133        """Leverages the base class `compare()` method and
134        populates the list of commands to be run by comparing
135        the `want` and `have` data with the `parsers` defined
136        for the Ospfv3 network resource.
137        """
138        begin = len(self.commands)
139        self._af_compare(want=want, have=have)
140        self._global_compare(want=want, have=have)
141
142        if len(self.commands) != begin or (not have and want):
143            self.commands.insert(
144                begin, self._tmplt.render(want or have, "vrf", False)
145            )
146            self.commands.append("exit")
147
148    def _global_compare(self, want, have):
149        for name, entry in iteritems(want):
150            if name in ["vrf", "address_family"]:
151                continue
152            if not isinstance(entry, dict) and name != "areas":
153                self.compare(
154                    parsers=self.parsers,
155                    want={name: entry},
156                    have={name: have.pop(name, None)},
157                )
158            else:
159                if name == "areas" and entry:
160                    self._areas_compare(
161                        want={name: entry}, have={name: have.get(name, {})}
162                    )
163                else:
164                    # passing dict without vrf, inorder to avoid  no router ospfv3 command
165                    h = {}
166                    for i in have:
167                        if i != "vrf":
168                            h.update({i: have[i]})
169                    self.compare(
170                        parsers=self.parsers,
171                        want={name: entry},
172                        have={name: h.pop(name, {})},
173                    )
174        # remove remaining items in have for replaced
175        for name, entry in iteritems(have):
176            if name in ["vrf", "address_family"]:
177                continue
178            if not isinstance(entry, dict):
179                self.compare(
180                    parsers=self.parsers,
181                    want={name: want.pop(name, None)},
182                    have={name: entry},
183                )
184            else:
185                # passing dict without vrf, inorder to avoid  no router ospfv3 command
186                # w = {i: want[i] for i in want if i != "vrf"}
187                self.compare(
188                    parsers=self.parsers,
189                    want={name: want.pop(name, {})},
190                    have={name: entry},
191                )
192
193    def _af_compare(self, want, have):
194        wafs = want.get("address_family", {})
195        hafs = have.get("address_family", {})
196        for name, entry in iteritems(wafs):
197            begin = len(self.commands)
198            self._compare_lists(want=entry, have=hafs.get(name, {}))
199            self._areas_compare(want=entry, have=hafs.get(name, {}))
200            self.compare(
201                parsers=self.parsers, want=entry, have=hafs.pop(name, {})
202            )
203            if (
204                len(self.commands) != begin
205                and "afi" in entry
206                and entry["afi"] != "router"
207            ):
208                self._rotate_commands(begin=begin)
209                self.commands.insert(
210                    begin, self._tmplt.render(entry, "address_family", False)
211                )
212                self.commands.append("exit")
213        for name, entry in iteritems(hafs):
214            self.addcmd(entry, "address_family", True)
215
216    def _rotate_commands(self, begin=0):
217        # move negate commands to beginning
218        for cmd in self.commands[begin::]:
219            negate = re.match(r"^no .*", cmd)
220            if negate:
221                self.commands.insert(
222                    begin, self.commands.pop(self.commands.index(cmd))
223                )
224                begin += 1
225
226    def _areas_compare(self, want, have):
227        wareas = want.get("areas", {})
228        hareas = have.get("areas", {})
229        for name, entry in iteritems(wareas):
230            self._area_compare(want=entry, have=hareas.pop(name, {}))
231        for name, entry in iteritems(hareas):
232            self._area_compare(want={}, have=entry)
233
234    def _area_compare(self, want, have):
235        parsers = [
236            "area.default_cost",
237            "area.encryption",
238            "area.authentication",
239            "area.nssa",
240            "area.stub",
241        ]
242        self.compare(parsers=parsers, want=want, have=have)
243        self._area_compare_lists(want=want, have=have)
244
245    def _area_compare_lists(self, want, have):
246        for attrib in ["ranges"]:
247            wdict = want.get(attrib, {})
248            hdict = have.get(attrib, {})
249            for key, entry in iteritems(wdict):
250                if entry != hdict.pop(key, {}):
251                    entry["area_id"] = want["area_id"]
252                    self.addcmd(entry, "area.{0}".format(attrib), False)
253            # remove remaining items in have for replaced
254            for entry in hdict.values():
255                entry["area_id"] = have["area_id"]
256                self.addcmd(entry, "area.{0}".format(attrib), True)
257
258    def _compare_lists(self, want, have):
259        for attrib in ["redistribute"]:
260            wdict = get_from_dict(want, attrib) or {}
261            hdict = get_from_dict(have, attrib) or {}
262            for key, entry in iteritems(wdict):
263                if entry != hdict.pop(key, {}):
264                    self.addcmd(entry, attrib, False)
265            # remove remaining items in have for replaced
266            for entry in hdict.values():
267                self.addcmd(entry, attrib, True)
268
269    def _ospf_list_to_dict(self, entry):
270        for name, proc in iteritems(entry):
271            for area in proc.get("areas", []):
272                if "ranges" in area:
273                    range_dict = {}
274                    for entry in area.get("ranges", []):
275                        range_dict.update({entry["address"]: entry})
276                    area["ranges"] = range_dict
277            areas_dict = {}
278            for entry in proc.get("areas", []):
279                areas_dict.update({entry["area_id"]: entry})
280            proc["areas"] = areas_dict
281
282            redis_dict = {}
283            for entry in proc.get("redistribute", []):
284                redis_dict.update({entry["routes"]: entry})
285            proc["redistribute"] = redis_dict
286
287            if "address_family" in proc:
288                addr_dict = {}
289                for entry in proc.get("address_family", []):
290                    addr_dict.update({entry["afi"]: entry})
291                proc["address_family"] = addr_dict
292                self._ospf_list_to_dict(proc["address_family"])
293