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 nxos_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
20from copy import deepcopy
21
22from ansible.module_utils.six import iteritems
23from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import (
24    dict_merge,
25    get_from_dict,
26)
27from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.resource_module import (
28    ResourceModule,
29)
30from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.facts.facts import (
31    Facts,
32)
33from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.rm_templates.ospfv3 import (
34    Ospfv3Template,
35)
36
37
38class Ospfv3(ResourceModule):
39    """
40    The nxos_ospfv3 config class
41    """
42
43    def __init__(self, module):
44        super(Ospfv3, self).__init__(
45            empty_fact_val={},
46            facts_module=Facts(module),
47            module=module,
48            resource="ospfv3",
49            tmplt=Ospfv3Template(),
50        )
51        self.parsers = [
52            "auto_cost",
53            "flush_routes",
54            "graceful_restart.set",
55            "graceful_restart.helper_disable",
56            "graceful_restart.grace_period",
57            "graceful_restart.planned_only",
58            "isolate",
59            "log_adjacency_changes",
60            "max_lsa",
61            "max_metric",
62            "name_lookup",
63            "passive_interface.default",
64            "router_id",
65            "shutdown",
66            "timers.lsa_arrival",
67            "timers.lsa_group_pacing",
68            "timers.throttle.lsa",
69        ]
70
71    def execute_module(self):
72        """ Execute the module
73
74        :rtype: A dictionary
75        :returns: The result from module execution
76        """
77        if self.state not in ["parsed", "gathered"]:
78            self.generate_commands()
79            self.run_commands()
80        return self.result
81
82    def generate_commands(self):
83        """ Generate configuration commands to send based on
84            want, have and desired state.
85        """
86        wantd = {
87            (entry["process_id"]): entry
88            for entry in self.want.get("processes", [])
89        }
90        haved = {
91            (entry["process_id"]): entry
92            for entry in self.have.get("processes", [])
93        }
94
95        # turn all lists of dicts into dicts prior to merge
96        for entry in wantd, haved:
97            self._ospfv3_list_to_dict(entry)
98
99        # if state is merged, merge want onto have and then compare
100        if self.state == "merged":
101            wantd = dict_merge(haved, wantd)
102
103        # if state is deleted, empty out wantd and set haved to wantd
104        if self.state == "deleted":
105            haved = {
106                k: v for k, v in iteritems(haved) if k in wantd or not wantd
107            }
108            wantd = {}
109
110        # if state is overridden, first remove processes that are in have but not in want
111        if self.state in ["overridden", "deleted"]:
112            for k, have in iteritems(haved):
113                if k not in wantd:
114                    self.addcmd(have, "process_id", True)
115
116        for k, want in iteritems(wantd):
117            self._compare(want=want, have=haved.pop(k, {}))
118
119    def _compare(self, want, have):
120        """Leverages the base class `compare()` method and
121           populates the list of commands to be run by comparing
122           the `want` and `have` data with the `parsers` defined
123           for the Ospfv3 network resource.
124        """
125        begin = len(self.commands)
126        self.compare(parsers=self.parsers, want=want, have=have)
127        self._areas_compare(want=want, have=have)
128        self._vrfs_compare(want=want, have=have)
129        self._af_compare(want=want, have=have)
130
131        if len(self.commands) != begin or (not have and want):
132            self.commands.insert(
133                begin,
134                self._tmplt.render(
135                    want or have,
136                    "vrf"
137                    if "vrf" in (want.keys() or have.keys())
138                    else "process_id",
139                    False,
140                ),
141            )
142
143    def _areas_compare(self, want, have):
144        wareas = want.get("areas", {})
145        hareas = have.get("areas", {})
146        for name, entry in iteritems(wareas):
147            self._area_compare(want=entry, have=hareas.pop(name, {}))
148        for name, entry in iteritems(hareas):
149            self._area_compare(want={}, have=entry)
150
151    def _area_compare(self, want, have):
152        parsers = ["area.nssa", "area.nssa.translate", "area.stub"]
153        self.compare(parsers=parsers, want=want, have=have)
154
155    def _vrfs_compare(self, want, have):
156        wvrfs = want.get("vrfs", {})
157        hvrfs = have.get("vrfs", {})
158        for name, entry in iteritems(wvrfs):
159            self._compare(want=entry, have=hvrfs.pop(name, {}))
160        # remove remaining items in have for replaced
161        for name, entry in iteritems(hvrfs):
162            self.addcmd(entry, "vrf", True)
163
164    def _af_compare(self, want, have):
165        parsers = [
166            "default_information.originate",
167            "distance",
168            "maximum_paths",
169            "table_map",
170            "timers.throttle.spf",
171        ]
172        waf = want.get("address_family", {})
173        haf = have.get("address_family", {})
174
175        cmd_ptr = len(self.commands)
176
177        self._af_areas_compare(want=waf, have=haf)
178        self._af_compare_lists(want=waf, have=haf)
179        self.compare(parsers=parsers, want=waf, have=haf)
180
181        cmd_ptr_nxt = len(self.commands)
182        if cmd_ptr < cmd_ptr_nxt:
183            self.commands.insert(cmd_ptr, "address-family ipv6 unicast")
184
185    def _af_areas_compare(self, want, have):
186        wareas = want.get("areas", {})
187        hareas = have.get("areas", {})
188        for name, entry in iteritems(wareas):
189            self._af_area_compare(want=entry, have=hareas.pop(name, {}))
190        for name, entry in iteritems(hareas):
191            self._af_area_compare(want={}, have=entry)
192
193    def _af_area_compare(self, want, have):
194        self.compare(parsers=["area.default_cost"], want=want, have=have)
195        self._af_area_compare_lists(want=want, have=have)
196
197    def _af_area_compare_lists(self, want, have):
198        for attrib in ["filter_list", "ranges"]:
199            wdict = want.get(attrib, {})
200            hdict = have.get(attrib, {})
201            for key, entry in iteritems(wdict):
202                if entry != hdict.pop(key, {}):
203                    entry["area_id"] = want["area_id"]
204                    self.addcmd(entry, "area.{0}".format(attrib), False)
205            # remove remaining items in have for replaced
206            for entry in hdict.values():
207                entry["area_id"] = have["area_id"]
208                self.addcmd(entry, "area.{0}".format(attrib), True)
209
210    def _af_compare_lists(self, want, have):
211        for attrib in ["summary_address", "redistribute"]:
212            wdict = get_from_dict(want, attrib) or {}
213            hdict = get_from_dict(have, attrib) or {}
214
215            for key, entry in iteritems(wdict):
216                if entry != hdict.pop(key, {}):
217                    self.addcmd(entry, attrib, False)
218            # remove remaining items in have for replaced
219            for entry in hdict.values():
220                self.addcmd(entry, attrib, True)
221
222    def _ospfv3_list_to_dict(self, entry):
223        for _pid, proc in iteritems(entry):
224            proc["areas"] = {
225                entry["area_id"]: entry for entry in proc.get("areas", [])
226            }
227            af = proc.get("address_family")
228            if af:
229                for area in af.get("areas", []):
230                    area["ranges"] = {
231                        entry["prefix"]: entry
232                        for entry in area.get("ranges", [])
233                    }
234                    area["filter_list"] = {
235                        entry["direction"]: entry
236                        for entry in area.get("filter_list", [])
237                    }
238                af["areas"] = {
239                    entry["area_id"]: entry for entry in af.get("areas", [])
240                }
241                af["summary_address"] = {
242                    entry["prefix"]: entry
243                    for entry in af.get("summary_address", [])
244                }
245                af["redistribute"] = {
246                    (entry.get("id"), entry["protocol"]): entry
247                    for entry in af.get("redistribute", [])
248                }
249            if "vrfs" in proc:
250                proc["vrfs"] = {
251                    entry["vrf"]: entry for entry in proc.get("vrfs", [])
252                }
253                self._ospfv3_list_to_dict(proc["vrfs"])
254