1# 2# -*- coding: utf-8 -*- 3# Copyright 2019 Red Hat 4# GNU General Public License v3.0+ 5# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 6""" 7The vyos_static_routes class 8It is in this file where the current configuration (as dict) 9is compared to the provided configuration (as dict) and the command set 10necessary to bring the current configuration to it's desired end-state is 11created 12""" 13 14from __future__ import absolute_import, division, print_function 15 16__metaclass__ = type 17from copy import deepcopy 18from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.cfg.base import ( 19 ConfigBase, 20) 21from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import ( 22 to_list, 23 dict_diff, 24 remove_empties, 25) 26from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.facts import ( 27 Facts, 28) 29from ansible.module_utils.six import iteritems 30from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.utils.utils import ( 31 get_route_type, 32 get_lst_diff_for_dicts, 33 get_lst_same_for_dicts, 34 dict_delete, 35) 36 37 38class Static_routes(ConfigBase): 39 """ 40 The vyos_static_routes class 41 """ 42 43 gather_subset = ["!all", "!min"] 44 45 gather_network_resources = ["static_routes"] 46 47 def __init__(self, module): 48 super(Static_routes, self).__init__(module) 49 50 def get_static_routes_facts(self, data=None): 51 """Get the 'facts' (the current configuration) 52 53 :rtype: A dictionary 54 :returns: The current configuration as a dictionary 55 """ 56 facts, _warnings = Facts(self._module).get_facts( 57 self.gather_subset, self.gather_network_resources, data=data 58 ) 59 static_routes_facts = facts["ansible_network_resources"].get( 60 "static_routes" 61 ) 62 if not static_routes_facts: 63 return [] 64 return static_routes_facts 65 66 def execute_module(self): 67 """Execute the module 68 69 :rtype: A dictionary 70 :returns: The result from module execution 71 """ 72 result = {"changed": False} 73 warnings = list() 74 commands = list() 75 76 if self.state in self.ACTION_STATES: 77 existing_static_routes_facts = self.get_static_routes_facts() 78 else: 79 existing_static_routes_facts = [] 80 81 if self.state in self.ACTION_STATES or self.state == "rendered": 82 commands.extend(self.set_config(existing_static_routes_facts)) 83 84 if commands and self.state in self.ACTION_STATES: 85 if not self._module.check_mode: 86 self._connection.edit_config(commands) 87 result["changed"] = True 88 89 if self.state in self.ACTION_STATES: 90 result["commands"] = commands 91 92 if self.state in self.ACTION_STATES or self.state == "gathered": 93 changed_static_routes_facts = self.get_static_routes_facts() 94 elif self.state == "rendered": 95 result["rendered"] = commands 96 elif self.state == "parsed": 97 running_config = self._module.params["running_config"] 98 if not running_config: 99 self._module.fail_json( 100 msg="value of running_config parameter must not be empty for state parsed" 101 ) 102 result["parsed"] = self.get_static_routes_facts( 103 data=running_config 104 ) 105 else: 106 changed_static_routes_facts = [] 107 108 if self.state in self.ACTION_STATES: 109 result["before"] = existing_static_routes_facts 110 if result["changed"]: 111 result["after"] = changed_static_routes_facts 112 elif self.state == "gathered": 113 result["gathered"] = changed_static_routes_facts 114 115 result["warnings"] = warnings 116 return result 117 118 def set_config(self, existing_static_routes_facts): 119 """Collect the configuration from the args passed to the module, 120 collect the current configuration (as a dict from facts) 121 122 :rtype: A list 123 :returns: the commands necessary to migrate the current configuration 124 to the desired configuration 125 """ 126 want = self._module.params["config"] 127 have = existing_static_routes_facts 128 resp = self.set_state(want, have) 129 return to_list(resp) 130 131 def set_state(self, want, have): 132 """Select the appropriate function based on the state provided 133 134 :param want: the desired configuration as a dictionary 135 :param have: the current configuration as a dictionary 136 :rtype: A list 137 :returns: the commands necessary to migrate the current configuration 138 to the desired configuration 139 """ 140 commands = [] 141 if ( 142 self.state in ("merged", "replaced", "overridden", "rendered") 143 and not want 144 ): 145 self._module.fail_json( 146 msg="value of config parameter must not be empty for state {0}".format( 147 self.state 148 ) 149 ) 150 if self.state == "overridden": 151 commands.extend(self._state_overridden(want=want, have=have)) 152 elif self.state == "deleted": 153 commands.extend(self._state_deleted(want=want, have=have)) 154 elif want: 155 routes = self._get_routes(want) 156 for r in routes: 157 h_item = self.search_route_in_have(have, r["dest"]) 158 if self.state in ("merged", "rendered"): 159 commands.extend(self._state_merged(want=r, have=h_item)) 160 elif self.state == "replaced": 161 commands.extend(self._state_replaced(want=r, have=h_item)) 162 return commands 163 164 def search_route_in_have(self, have, want_dest): 165 """ 166 This function returns the route if its found in 167 have config. 168 :param have: 169 :param dest: 170 :return: the matched route 171 """ 172 routes = self._get_routes(have) 173 for r in routes: 174 if r["dest"] == want_dest: 175 return r 176 return None 177 178 def _state_replaced(self, want, have): 179 """The command generator when state is replaced 180 181 :rtype: A list 182 :returns: the commands necessary to migrate the current configuration 183 to the desired configuration 184 """ 185 commands = [] 186 if have: 187 for key, value in iteritems(want): 188 if value: 189 if key == "next_hops": 190 commands.extend(self._update_next_hop(want, have)) 191 elif key == "blackhole_config": 192 commands.extend( 193 self._update_blackhole(key, want, have) 194 ) 195 commands.extend(self._state_merged(want, have)) 196 return commands 197 198 def _state_overridden(self, want, have): 199 """The command generator when state is overridden 200 201 :rtype: A list 202 :returns: the commands necessary to migrate the current configuration 203 to the desired configuration 204 """ 205 commands = [] 206 routes = self._get_routes(have) 207 for r in routes: 208 route_in_want = self.search_route_in_have(want, r["dest"]) 209 if not route_in_want: 210 commands.append(self._compute_command(r["dest"], remove=True)) 211 routes = self._get_routes(want) 212 for r in routes: 213 route_in_have = self.search_route_in_have(have, r["dest"]) 214 commands.extend(self._state_replaced(r, route_in_have)) 215 return commands 216 217 def _state_merged(self, want, have, opr=True): 218 """The command generator when state is merged 219 220 :rtype: A list 221 :returns: the commands necessary to merge the provided into 222 the current configuration 223 """ 224 commands = [] 225 if have: 226 commands.extend(self._render_updates(want, have)) 227 else: 228 commands.extend(self._render_set_commands(want)) 229 return commands 230 231 def _state_deleted(self, want, have): 232 """The command generator when state is deleted 233 234 :rtype: A list 235 :returns: the commands necessary to remove the current configuration 236 of the provided objects 237 """ 238 commands = [] 239 if want: 240 routes = self._get_routes(want) 241 if not routes: 242 for w in want: 243 af = w["address_families"] 244 for item in af: 245 if self.afi_in_have(have, item): 246 commands.append( 247 self._compute_command( 248 afi=item["afi"], remove=True 249 ) 250 ) 251 else: 252 routes = self._get_routes(have) 253 if self._is_ip_route_exist(routes): 254 commands.append(self._compute_command(afi="ipv4", remove=True)) 255 if self._is_ip_route_exist(routes, "route6"): 256 commands.append(self._compute_command(afi="ipv6", remove=True)) 257 return commands 258 259 def _render_set_commands(self, want): 260 """ 261 This function returns the list of commands to add attributes which are 262 present in want 263 :param want: 264 :return: list of commands. 265 """ 266 commands = [] 267 have = {} 268 for key, value in iteritems(want): 269 if value: 270 if key == "dest": 271 commands.append(self._compute_command(dest=want["dest"])) 272 elif key == "blackhole_config": 273 commands.extend(self._add_blackhole(key, want, have)) 274 275 elif key == "next_hops": 276 commands.extend(self._add_next_hop(want, have)) 277 278 return commands 279 280 def _add_blackhole(self, key, want, have): 281 """ 282 This function gets the diff for blackhole config specific attributes 283 and form the commands for attributes which are present in want but not in have. 284 :param key: 285 :param want: 286 :param have: 287 :return: list of commands 288 """ 289 commands = [] 290 want_copy = deepcopy(remove_empties(want)) 291 have_copy = deepcopy(remove_empties(have)) 292 293 want_blackhole = want_copy.get(key) or {} 294 have_blackhole = have_copy.get(key) or {} 295 296 updates = dict_delete(want_blackhole, have_blackhole) 297 if updates: 298 for attrib, value in iteritems(updates): 299 if value: 300 if attrib == "distance": 301 commands.append( 302 self._compute_command( 303 dest=want["dest"], 304 key="blackhole", 305 attrib=attrib, 306 remove=False, 307 value=str(value), 308 ) 309 ) 310 elif attrib == "type": 311 commands.append( 312 self._compute_command( 313 dest=want["dest"], key="blackhole" 314 ) 315 ) 316 return commands 317 318 def _add_next_hop(self, want, have, opr=True): 319 """ 320 This function gets the diff for next hop specific attributes 321 and form the commands to add attributes which are present in want but not in have. 322 :param want: 323 :param have: 324 :return: list of commands. 325 """ 326 commands = [] 327 want_copy = deepcopy(remove_empties(want)) 328 have_copy = deepcopy(remove_empties(have)) 329 if not opr: 330 diff_next_hops = get_lst_same_for_dicts( 331 want_copy, have_copy, "next_hops" 332 ) 333 else: 334 diff_next_hops = get_lst_diff_for_dicts( 335 want_copy, have_copy, "next_hops" 336 ) 337 if diff_next_hops: 338 for hop in diff_next_hops: 339 for element in hop: 340 if element == "forward_router_address": 341 commands.append( 342 self._compute_command( 343 dest=want["dest"], 344 key="next-hop", 345 value=hop[element], 346 opr=opr, 347 ) 348 ) 349 elif element == "enabled" and not hop[element]: 350 commands.append( 351 self._compute_command( 352 dest=want["dest"], 353 key="next-hop", 354 attrib=hop["forward_router_address"], 355 value="disable", 356 opr=opr, 357 ) 358 ) 359 elif element == "admin_distance": 360 commands.append( 361 self._compute_command( 362 dest=want["dest"], 363 key="next-hop", 364 attrib=hop["forward_router_address"] 365 + " " 366 + "distance", 367 value=str(hop[element]), 368 opr=opr, 369 ) 370 ) 371 elif element == "interface": 372 commands.append( 373 self._compute_command( 374 dest=want["dest"], 375 key="next-hop", 376 attrib=hop["forward_router_address"] 377 + " " 378 + "next-hop-interface", 379 value=hop[element], 380 opr=opr, 381 ) 382 ) 383 return commands 384 385 def _update_blackhole(self, key, want, have): 386 """ 387 This function gets the difference for blackhole dict and 388 form the commands to delete the attributes which are present in have but not in want. 389 :param want: 390 :param have: 391 :return: list of commands 392 :param key: 393 :param want: 394 :param have: 395 :return: list of commands 396 """ 397 commands = [] 398 want_copy = deepcopy(remove_empties(want)) 399 have_copy = deepcopy(remove_empties(have)) 400 401 want_blackhole = want_copy.get(key) or {} 402 have_blackhole = have_copy.get(key) or {} 403 updates = dict_delete(have_blackhole, want_blackhole) 404 if updates: 405 for attrib, value in iteritems(updates): 406 if value: 407 if attrib == "distance": 408 commands.append( 409 self._compute_command( 410 dest=want["dest"], 411 key="blackhole", 412 attrib=attrib, 413 remove=True, 414 value=str(value), 415 ) 416 ) 417 elif ( 418 attrib == "type" 419 and "distance" not in want_blackhole.keys() 420 ): 421 commands.append( 422 self._compute_command( 423 dest=want["dest"], key="blackhole", remove=True 424 ) 425 ) 426 return commands 427 428 def _update_next_hop(self, want, have, opr=True): 429 """ 430 This function gets the difference for next_hops list and 431 form the commands to delete the attributes which are present in have but not in want. 432 :param want: 433 :param have: 434 :return: list of commands 435 """ 436 commands = [] 437 438 want_copy = deepcopy(remove_empties(want)) 439 have_copy = deepcopy(remove_empties(have)) 440 441 diff_next_hops = get_lst_diff_for_dicts( 442 have_copy, want_copy, "next_hops" 443 ) 444 if diff_next_hops: 445 for hop in diff_next_hops: 446 for element in hop: 447 if element == "forward_router_address": 448 commands.append( 449 self._compute_command( 450 dest=want["dest"], 451 key="next-hop", 452 value=hop[element], 453 remove=True, 454 ) 455 ) 456 elif element == "enabled": 457 commands.append( 458 self._compute_command( 459 dest=want["dest"], 460 key="next-hop", 461 attrib=hop["forward_router_address"], 462 value="disable", 463 remove=True, 464 ) 465 ) 466 elif element == "admin_distance": 467 commands.append( 468 self._compute_command( 469 dest=want["dest"], 470 key="next-hop", 471 attrib=hop["forward_router_address"] 472 + " " 473 + "distance", 474 value=str(hop[element]), 475 remove=True, 476 ) 477 ) 478 elif element == "interface": 479 commands.append( 480 self._compute_command( 481 dest=want["dest"], 482 key="next-hop", 483 attrib=hop["forward_router_address"] 484 + " " 485 + "next-hop-interface", 486 value=hop[element], 487 remove=True, 488 ) 489 ) 490 return commands 491 492 def _render_updates(self, want, have, opr=True): 493 """ 494 This function takes the diff between want and have and 495 invokes the appropriate functions to create the commands 496 to update the attributes. 497 :param want: 498 :param have: 499 :return: list of commands 500 """ 501 commands = [] 502 want_nh = want.get("next_hops") or [] 503 # delete static route operation per destination 504 if not opr and not want_nh: 505 commands.append( 506 self._compute_command(dest=want["dest"], remove=True) 507 ) 508 509 else: 510 temp_have_next_hops = have.pop("next_hops", None) 511 temp_want_next_hops = want.pop("next_hops", None) 512 updates = dict_diff(have, want) 513 if temp_have_next_hops: 514 have["next_hops"] = temp_have_next_hops 515 if temp_want_next_hops: 516 want["next_hops"] = temp_want_next_hops 517 commands.extend(self._add_next_hop(want, have, opr=opr)) 518 519 if opr and updates: 520 for key, value in iteritems(updates): 521 if value: 522 if key == "blackhole_config": 523 commands.extend( 524 self._add_blackhole(key, want, have) 525 ) 526 return commands 527 528 def _compute_command( 529 self, 530 dest=None, 531 key=None, 532 attrib=None, 533 value=None, 534 remove=False, 535 afi=None, 536 opr=True, 537 ): 538 """ 539 This functions construct the required command based on the passed arguments. 540 :param dest: 541 :param key: 542 :param attrib: 543 :param value: 544 :param remove: 545 :return: constructed command 546 """ 547 if remove or not opr: 548 cmd = "delete protocols static " + self.get_route_type(dest, afi) 549 else: 550 cmd = "set protocols static " + self.get_route_type(dest, afi) 551 if dest: 552 cmd += " " + dest 553 if key: 554 cmd += " " + key 555 if attrib: 556 cmd += " " + attrib 557 if value: 558 cmd += " '" + value + "'" 559 return cmd 560 561 def afi_in_have(self, have, w_item): 562 """ 563 This functions checks for the afi 564 list in have 565 :param have: 566 :param w_item: 567 :return: 568 """ 569 if have: 570 for h in have: 571 af = h.get("address_families") or [] 572 for item in af: 573 if w_item["afi"] == item["afi"]: 574 return True 575 return False 576 577 def get_route_type(self, dest=None, afi=None): 578 """ 579 This function returns the route type based on 580 destination ip address or afi 581 :param address: 582 :return: 583 """ 584 if dest: 585 return get_route_type(dest) 586 elif afi == "ipv4": 587 return "route" 588 elif afi == "ipv6": 589 return "route6" 590 591 def _is_ip_route_exist(self, routes, type="route"): 592 """ 593 This functions checks for the type of route. 594 :param routes: 595 :param type: 596 :return: True/False 597 """ 598 for r in routes: 599 if type == self.get_route_type(r["dest"]): 600 return True 601 return False 602 603 def _get_routes(self, lst): 604 """ 605 This function returns the list of routes 606 :param lst: list of address families 607 :return: list of routes 608 """ 609 r_list = [] 610 for item in lst: 611 af = item["address_families"] 612 for element in af: 613 routes = element.get("routes") or [] 614 for r in routes: 615 r_list.append(r) 616 return r_list 617