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