1#!/usr/local/bin/python3.8 2# 3# This file is part of Ansible 4# 5# Ansible is free software: you can redistribute it and/or modify 6# it under the terms of the GNU General Public License as published by 7# the Free Software Foundation, either version 3 of the License, or 8# (at your option) any later version. 9# 10# Ansible is distributed in the hope that it will be useful, 11# but WITHOUT ANY WARRANTY; without even the implied warranty of 12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13# GNU General Public License for more details. 14# 15# You should have received a copy of the GNU General Public License 16# along with Ansible. If not, see <http://www.gnu.org/licenses/>. 17# 18from __future__ import absolute_import, division, print_function 19 20__metaclass__ = type 21DOCUMENTATION = """ 22module: ios_vrf 23author: Peter Sprygada (@privateip) 24short_description: Manage the collection of VRF definitions on Cisco IOS devices 25description: 26- This module provides declarative management of VRF definitions on Cisco IOS devices. It 27 allows playbooks to manage individual or the entire VRF collection. It also supports 28 purging VRF definitions from the configuration that are not explicitly defined. 29version_added: 1.0.0 30extends_documentation_fragment: 31- cisco.ios.ios 32notes: 33- Tested against IOS 15.6 34options: 35 vrfs: 36 description: 37 - The set of VRF definition objects to be configured on the remote IOS device. Ths 38 list entries can either be the VRF name or a hash of VRF definitions and attributes. This 39 argument is mutually exclusive with the C(name) argument. 40 type: list 41 elements: raw 42 name: 43 description: 44 - The name of the VRF definition to be managed on the remote IOS device. The 45 VRF definition name is an ASCII string name used to uniquely identify the VRF. This 46 argument is mutually exclusive with the C(vrfs) argument 47 type: str 48 description: 49 description: 50 - Provides a short description of the VRF definition in the current active configuration. The 51 VRF definition value accepts alphanumeric characters used to provide additional 52 information about the VRF. 53 type: str 54 rd: 55 description: 56 - The router-distinguisher value uniquely identifies the VRF to routing processes 57 on the remote IOS system. The RD value takes the form of C(A:B) where C(A) 58 and C(B) are both numeric values. 59 type: str 60 interfaces: 61 description: 62 - Identifies the set of interfaces that should be configured in the VRF. Interfaces 63 must be routed interfaces in order to be placed into a VRF. 64 type: list 65 elements: str 66 associated_interfaces: 67 description: 68 - This is a intent option and checks the operational state of the for given vrf 69 C(name) for associated interfaces. If the value in the C(associated_interfaces) 70 does not match with the operational state of vrf interfaces on device it will 71 result in failure. 72 type: list 73 elements: str 74 delay: 75 description: 76 - Time in seconds to wait before checking for the operational state on remote 77 device. 78 default: 10 79 type: int 80 purge: 81 description: 82 - Instructs the module to consider the VRF definition absolute. It will remove 83 any previously configured VRFs on the device. 84 default: false 85 type: bool 86 state: 87 description: 88 - Configures the state of the VRF definition as it relates to the device operational 89 configuration. When set to I(present), the VRF should be configured in the 90 device active configuration and when set to I(absent) the VRF should not be 91 in the device active configuration 92 default: present 93 choices: 94 - present 95 - absent 96 type: str 97 route_both: 98 description: 99 - Adds an export and import list of extended route target communities to the VRF. 100 type: list 101 elements: str 102 route_export: 103 description: 104 - Adds an export list of extended route target communities to the VRF. 105 type: list 106 elements: str 107 route_import: 108 description: 109 - Adds an import list of extended route target communities to the VRF. 110 type: list 111 elements: str 112 route_both_ipv4: 113 description: 114 - Adds an export and import list of extended route target communities in address-family 115 configuration submode to the VRF. 116 type: list 117 elements: str 118 route_export_ipv4: 119 description: 120 - Adds an export list of extended route target communities in address-family configuration 121 submode to the VRF. 122 type: list 123 elements: str 124 route_import_ipv4: 125 description: 126 - Adds an import list of extended route target communities in address-family configuration 127 submode to the VRF. 128 type: list 129 elements: str 130 route_both_ipv6: 131 description: 132 - Adds an export and import list of extended route target communities in address-family 133 configuration submode to the VRF. 134 type: list 135 elements: str 136 route_export_ipv6: 137 description: 138 - Adds an export list of extended route target communities in address-family configuration 139 submode to the VRF. 140 type: list 141 elements: str 142 route_import_ipv6: 143 description: 144 - Adds an import list of extended route target communities in address-family configuration 145 submode to the VRF. 146 type: list 147 elements: str 148""" 149EXAMPLES = """ 150- name: configure a vrf named management 151 cisco.ios.ios_vrf: 152 name: management 153 description: oob mgmt vrf 154 interfaces: 155 - Management1 156 157- name: remove a vrf named test 158 cisco.ios.ios_vrf: 159 name: test 160 state: absent 161 162- name: configure set of VRFs and purge any others 163 cisco.ios.ios_vrf: 164 vrfs: 165 - red 166 - blue 167 - green 168 purge: yes 169 170- name: Creates a list of import RTs for the VRF with the same parameters 171 cisco.ios.ios_vrf: 172 name: test_import 173 rd: 1:100 174 route_import: 175 - 1:100 176 - 3:100 177 178- name: Creates a list of import RTs in address-family configuration submode for the 179 VRF with the same parameters 180 cisco.ios.ios_vrf: 181 name: test_import_ipv4 182 rd: 1:100 183 route_import_ipv4: 184 - 1:100 185 - 3:100 186 187- name: Creates a list of import RTs in address-family configuration submode for the 188 VRF with the same parameters 189 cisco.ios.ios_vrf: 190 name: test_import_ipv6 191 rd: 1:100 192 route_import_ipv6: 193 - 1:100 194 - 3:100 195 196- name: Creates a list of export RTs for the VRF with the same parameters 197 cisco.ios.ios_vrf: 198 name: test_export 199 rd: 1:100 200 route_export: 201 - 1:100 202 - 3:100 203 204- name: Creates a list of export RTs in address-family configuration submode for the 205 VRF with the same parameters 206 cisco.ios.ios_vrf: 207 name: test_export_ipv4 208 rd: 1:100 209 route_export_ipv4: 210 - 1:100 211 - 3:100 212 213- name: Creates a list of export RTs in address-family configuration submode for the 214 VRF with the same parameters 215 cisco.ios.ios_vrf: 216 name: test_export_ipv6 217 rd: 1:100 218 route_export_ipv6: 219 - 1:100 220 - 3:100 221 222- name: Creates a list of import and export route targets for the VRF with the same 223 parameters 224 cisco.ios.ios_vrf: 225 name: test_both 226 rd: 1:100 227 route_both: 228 - 1:100 229 - 3:100 230 231- name: Creates a list of import and export route targets in address-family configuration 232 submode for the VRF with the same parameters 233 cisco.ios.ios_vrf: 234 name: test_both_ipv4 235 rd: 1:100 236 route_both_ipv4: 237 - 1:100 238 - 3:100 239 240- name: Creates a list of import and export route targets in address-family configuration 241 submode for the VRF with the same parameters 242 cisco.ios.ios_vrf: 243 name: test_both_ipv6 244 rd: 1:100 245 route_both_ipv6: 246 - 1:100 247 - 3:100 248""" 249RETURN = """ 250commands: 251 description: The list of configuration mode commands to send to the device 252 returned: always 253 type: list 254 sample: 255 - vrf definition ansible 256 - description management vrf 257 - rd: 1:100 258start: 259 description: The time the job started 260 returned: always 261 type: str 262 sample: "2016-11-16 10:38:15.126146" 263end: 264 description: The time the job ended 265 returned: always 266 type: str 267 sample: "2016-11-16 10:38:25.595612" 268delta: 269 description: The time elapsed to perform all operations 270 returned: always 271 type: str 272 sample: "0:00:10.469466\" 273""" 274import re 275import time 276from functools import partial 277from ansible.module_utils.basic import AnsibleModule 278from ansible.module_utils.connection import exec_command 279from ansible_collections.cisco.ios.plugins.module_utils.network.ios.ios import ( 280 load_config, 281 get_config, 282) 283from ansible_collections.cisco.ios.plugins.module_utils.network.ios.ios import ( 284 ios_argument_spec, 285) 286from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.config import ( 287 NetworkConfig, 288) 289from ansible.module_utils.six import iteritems 290 291 292def get_interface_type(interface): 293 if interface.upper().startswith("ET"): 294 return "ethernet" 295 elif interface.upper().startswith("VL"): 296 return "svi" 297 elif interface.upper().startswith("LO"): 298 return "loopback" 299 elif interface.upper().startswith("MG"): 300 return "management" 301 elif interface.upper().startswith("MA"): 302 return "management" 303 elif interface.upper().startswith("PO"): 304 return "portchannel" 305 elif interface.upper().startswith("NV"): 306 return "nve" 307 else: 308 return "unknown" 309 310 311def add_command_to_vrf(name, cmd, commands): 312 if "vrf definition %s" % name not in commands: 313 commands.extend(["vrf definition %s" % name]) 314 commands.append(cmd) 315 316 317def map_obj_to_commands(updates, module): 318 commands = list() 319 for update in updates: 320 want, have = update 321 322 def needs_update(want, have, x): 323 if isinstance(want.get(x), list) and isinstance(have.get(x), list): 324 return ( 325 want.get(x) 326 and want.get(x) != have.get(x) 327 and not all(elem in have.get(x) for elem in want.get(x)) 328 ) 329 return want.get(x) and want.get(x) != have.get(x) 330 331 if want["state"] == "absent": 332 commands.append("no vrf definition %s" % want["name"]) 333 continue 334 if not have.get("state"): 335 commands.extend(["vrf definition %s" % want["name"]]) 336 ipv6 = ( 337 len( 338 [ 339 k 340 for k, v in module.params.items() 341 if (k.endswith("_ipv6") or k.endswith("_both")) and v 342 ] 343 ) 344 != 0 345 ) 346 ipv4 = ( 347 len( 348 [ 349 k 350 for k, v in module.params.items() 351 if (k.endswith("_ipv4") or k.endswith("_both")) and v 352 ] 353 ) 354 != 0 355 ) 356 if ipv4: 357 commands.extend(["address-family ipv4", "exit"]) 358 if ipv6: 359 commands.extend(["address-family ipv6", "exit"]) 360 if needs_update(want, have, "description"): 361 cmd = "description %s" % want["description"] 362 add_command_to_vrf(want["name"], cmd, commands) 363 if needs_update(want, have, "rd"): 364 cmd = "rd %s" % want["rd"] 365 add_command_to_vrf(want["name"], cmd, commands) 366 if needs_update(want, have, "route_import"): 367 for route in want["route_import"]: 368 cmd = "route-target import %s" % route 369 add_command_to_vrf(want["name"], cmd, commands) 370 if needs_update(want, have, "route_export"): 371 for route in want["route_export"]: 372 cmd = "route-target export %s" % route 373 add_command_to_vrf(want["name"], cmd, commands) 374 if needs_update(want, have, "route_import_ipv4"): 375 cmd = "address-family ipv4" 376 add_command_to_vrf(want["name"], cmd, commands) 377 for route in want["route_import_ipv4"]: 378 cmd = "route-target import %s" % route 379 add_command_to_vrf(want["name"], cmd, commands) 380 cmd = "exit-address-family" 381 add_command_to_vrf(want["name"], cmd, commands) 382 if needs_update(want, have, "route_export_ipv4"): 383 cmd = "address-family ipv4" 384 add_command_to_vrf(want["name"], cmd, commands) 385 for route in want["route_export_ipv4"]: 386 cmd = "route-target export %s" % route 387 add_command_to_vrf(want["name"], cmd, commands) 388 cmd = "exit-address-family" 389 add_command_to_vrf(want["name"], cmd, commands) 390 if needs_update(want, have, "route_import_ipv6"): 391 cmd = "address-family ipv6" 392 add_command_to_vrf(want["name"], cmd, commands) 393 for route in want["route_import_ipv6"]: 394 cmd = "route-target import %s" % route 395 add_command_to_vrf(want["name"], cmd, commands) 396 cmd = "exit-address-family" 397 add_command_to_vrf(want["name"], cmd, commands) 398 if needs_update(want, have, "route_export_ipv6"): 399 cmd = "address-family ipv6" 400 add_command_to_vrf(want["name"], cmd, commands) 401 for route in want["route_export_ipv6"]: 402 cmd = "route-target export %s" % route 403 add_command_to_vrf(want["name"], cmd, commands) 404 cmd = "exit-address-family" 405 add_command_to_vrf(want["name"], cmd, commands) 406 if want["interfaces"] is not None: 407 for intf in set(have.get("interfaces", [])).difference( 408 want["interfaces"] 409 ): 410 commands.extend( 411 [ 412 "interface %s" % intf, 413 "no vrf forwarding %s" % want["name"], 414 ] 415 ) 416 for intf in set(want["interfaces"]).difference( 417 have.get("interfaces", []) 418 ): 419 cfg = get_config(module) 420 configobj = NetworkConfig(indent=1, contents=cfg) 421 children = configobj["interface %s" % intf].children 422 intf_config = "\n".join(children) 423 commands.extend( 424 ["interface %s" % intf, "vrf forwarding %s" % want["name"]] 425 ) 426 match = re.search("ip address .+", intf_config, re.M) 427 if match: 428 commands.append(match.group()) 429 return commands 430 431 432def parse_description(configobj, name): 433 cfg = configobj["vrf definition %s" % name] 434 cfg = "\n".join(cfg.children) 435 match = re.search("description (.+)$", cfg, re.M) 436 if match: 437 return match.group(1) 438 439 440def parse_rd(configobj, name): 441 cfg = configobj["vrf definition %s" % name] 442 cfg = "\n".join(cfg.children) 443 match = re.search("rd (.+)$", cfg, re.M) 444 if match: 445 return match.group(1) 446 447 448def parse_interfaces(configobj): 449 vrf_cfg = "vrf forwarding" 450 interfaces = dict() 451 for intf in set(re.findall("^interface .+", str(configobj), re.M)): 452 for line in configobj[intf].children: 453 if vrf_cfg in line: 454 try: 455 interfaces[line.split()[-1]].append(intf.split(" ")[1]) 456 except KeyError: 457 interfaces[line.split()[-1]] = [intf.split(" ")[1]] 458 return interfaces 459 460 461def parse_import(configobj, name): 462 cfg = configobj["vrf definition %s" % name] 463 cfg = "\n".join(cfg.children) 464 matches = re.findall("route-target\\s+import\\s+(.+)", cfg, re.M) 465 return matches 466 467 468def parse_export(configobj, name): 469 cfg = configobj["vrf definition %s" % name] 470 cfg = "\n".join(cfg.children) 471 matches = re.findall("route-target\\s+export\\s+(.+)", cfg, re.M) 472 return matches 473 474 475def parse_both(configobj, name, address_family="global"): 476 rd_pattern = re.compile("(?P<rd>.+:.+)") 477 matches = list() 478 export_match = None 479 import_match = None 480 if address_family == "global": 481 export_match = parse_export(configobj, name) 482 import_match = parse_import(configobj, name) 483 elif address_family == "ipv4": 484 export_match = parse_export_ipv4(configobj, name) 485 import_match = parse_import_ipv4(configobj, name) 486 elif address_family == "ipv6": 487 export_match = parse_export_ipv6(configobj, name) 488 import_match = parse_import_ipv6(configobj, name) 489 if import_match and export_match: 490 for ex in export_match: 491 exrd = rd_pattern.search(ex) 492 exrd = exrd.groupdict().get("rd") 493 for im in import_match: 494 imrd = rd_pattern.search(im) 495 imrd = imrd.groupdict().get("rd") 496 if exrd == imrd: 497 matches.extend([exrd]) if exrd not in matches else None 498 matches.extend([imrd]) if imrd not in matches else None 499 return matches 500 501 502def parse_import_ipv4(configobj, name): 503 cfg = configobj["vrf definition %s" % name] 504 try: 505 subcfg = cfg["address-family ipv4"] 506 subcfg = "\n".join(subcfg.children) 507 matches = re.findall("route-target\\s+import\\s+(.+)", subcfg, re.M) 508 return matches 509 except KeyError: 510 pass 511 512 513def parse_export_ipv4(configobj, name): 514 cfg = configobj["vrf definition %s" % name] 515 try: 516 subcfg = cfg["address-family ipv4"] 517 subcfg = "\n".join(subcfg.children) 518 matches = re.findall("route-target\\s+export\\s+(.+)", subcfg, re.M) 519 return matches 520 except KeyError: 521 pass 522 523 524def parse_import_ipv6(configobj, name): 525 cfg = configobj["vrf definition %s" % name] 526 try: 527 subcfg = cfg["address-family ipv6"] 528 subcfg = "\n".join(subcfg.children) 529 matches = re.findall("route-target\\s+import\\s+(.+)", subcfg, re.M) 530 return matches 531 except KeyError: 532 pass 533 534 535def parse_export_ipv6(configobj, name): 536 cfg = configobj["vrf definition %s" % name] 537 try: 538 subcfg = cfg["address-family ipv6"] 539 subcfg = "\n".join(subcfg.children) 540 matches = re.findall("route-target\\s+export\\s+(.+)", subcfg, re.M) 541 return matches 542 except KeyError: 543 pass 544 545 546def map_config_to_obj(module): 547 config = get_config(module) 548 configobj = NetworkConfig(indent=1, contents=config) 549 match = re.findall("^vrf definition (\\S+)", config, re.M) 550 if not match: 551 return list() 552 instances = list() 553 interfaces = parse_interfaces(configobj) 554 for item in set(match): 555 obj = { 556 "name": item, 557 "state": "present", 558 "description": parse_description(configobj, item), 559 "rd": parse_rd(configobj, item), 560 "interfaces": interfaces.get(item), 561 "route_import": parse_import(configobj, item), 562 "route_export": parse_export(configobj, item), 563 "route_both": parse_both(configobj, item), 564 "route_import_ipv4": parse_import_ipv4(configobj, item), 565 "route_export_ipv4": parse_export_ipv4(configobj, item), 566 "route_both_ipv4": parse_both( 567 configobj, item, address_family="ipv4" 568 ), 569 "route_import_ipv6": parse_import_ipv6(configobj, item), 570 "route_export_ipv6": parse_export_ipv6(configobj, item), 571 "route_both_ipv6": parse_both( 572 configobj, item, address_family="ipv6" 573 ), 574 } 575 instances.append(obj) 576 return instances 577 578 579def get_param_value(key, item, module): 580 # if key doesn't exist in the item, get it from module.params 581 if not item.get(key): 582 value = module.params[key] 583 # if key does exist, do a type check on it to validate it 584 else: 585 value_type = module.argument_spec[key].get("type", "str") 586 type_checker = module._CHECK_ARGUMENT_TYPES_DISPATCHER[value_type] 587 type_checker(item[key]) 588 value = item[key] 589 # validate the param value (if validator func exists) 590 validator = globals().get("validate_%s" % key) 591 if validator: 592 validator(value, module) 593 return value 594 595 596def map_params_to_obj(module): 597 vrfs = module.params.get("vrfs") 598 if not vrfs: 599 if not module.params["name"] and module.params["purge"]: 600 return list() 601 elif not module.params["name"]: 602 module.fail_json(msg="name is required") 603 collection = [{"name": module.params["name"]}] 604 else: 605 collection = list() 606 for item in vrfs: 607 if not isinstance(item, dict): 608 collection.append({"name": item}) 609 elif "name" not in item: 610 module.fail_json(msg="name is required") 611 else: 612 collection.append(item) 613 objects = list() 614 for item in collection: 615 get_value = partial(get_param_value, item=item, module=module) 616 item["description"] = get_value("description") 617 item["rd"] = get_value("rd") 618 item["interfaces"] = get_value("interfaces") 619 item["state"] = get_value("state") 620 item["route_import"] = get_value("route_import") 621 item["route_export"] = get_value("route_export") 622 item["route_both"] = get_value("route_both") 623 item["route_import_ipv4"] = get_value("route_import_ipv4") 624 item["route_export_ipv4"] = get_value("route_export_ipv4") 625 item["route_both_ipv4"] = get_value("route_both_ipv4") 626 item["route_import_ipv6"] = get_value("route_import_ipv6") 627 item["route_export_ipv6"] = get_value("route_export_ipv6") 628 item["route_both_ipv6"] = get_value("route_both_ipv6") 629 both_addresses_family = ["", "_ipv6", "_ipv4"] 630 for address_family in both_addresses_family: 631 if item["route_both%s" % address_family]: 632 if not item["route_export%s" % address_family]: 633 item["route_export%s" % address_family] = list() 634 if not item["route_import%s" % address_family]: 635 item["route_import%s" % address_family] = list() 636 item["route_export%s" % address_family].extend( 637 get_value("route_both%s" % address_family) 638 ) 639 item["route_import%s" % address_family].extend( 640 get_value("route_both%s" % address_family) 641 ) 642 item["associated_interfaces"] = get_value("associated_interfaces") 643 objects.append(item) 644 return objects 645 646 647def update_objects(want, have): 648 updates = list() 649 for entry in want: 650 item = next((i for i in have if i["name"] == entry["name"]), None) 651 if all((item is None, entry["state"] == "present")): 652 updates.append((entry, {})) 653 else: 654 for key, value in iteritems(entry): 655 if value: 656 try: 657 if isinstance(value, list): 658 if sorted(value) != sorted(item[key]): 659 if (entry, item) not in updates: 660 updates.append((entry, item)) 661 elif value != item[key]: 662 if (entry, item) not in updates: 663 updates.append((entry, item)) 664 except TypeError: 665 pass 666 return updates 667 668 669def check_declarative_intent_params(want, module, result): 670 if module.params["associated_interfaces"]: 671 if result["changed"]: 672 time.sleep(module.params["delay"]) 673 name = module.params["name"] 674 rc, out, err = exec_command( 675 module, "show vrf | include {0}".format(name) 676 ) 677 if rc == 0: 678 data = out.strip().split() 679 if not data: 680 return 681 vrf = data[0] 682 interface = data[-1] 683 for w in want: 684 if w["name"] == vrf: 685 if w.get("associated_interfaces") is None: 686 continue 687 for i in w["associated_interfaces"]: 688 if get_interface_type(i) is not get_interface_type( 689 interface 690 ): 691 module.fail_json( 692 msg="Interface %s not configured on vrf %s" 693 % (interface, name) 694 ) 695 696 697def main(): 698 """ main entry point for module execution 699 """ 700 argument_spec = dict( 701 vrfs=dict(type="list", elements="raw"), 702 name=dict(), 703 description=dict(), 704 rd=dict(), 705 route_export=dict(type="list", elements="str"), 706 route_import=dict(type="list", elements="str"), 707 route_both=dict(type="list", elements="str"), 708 route_export_ipv4=dict(type="list", elements="str"), 709 route_import_ipv4=dict(type="list", elements="str"), 710 route_both_ipv4=dict(type="list", elements="str"), 711 route_export_ipv6=dict(type="list", elements="str"), 712 route_import_ipv6=dict(type="list", elements="str"), 713 route_both_ipv6=dict(type="list", elements="str"), 714 interfaces=dict(type="list", elements="str"), 715 associated_interfaces=dict(type="list", elements="str"), 716 delay=dict(default=10, type="int"), 717 purge=dict(type="bool", default=False), 718 state=dict(default="present", choices=["present", "absent"]), 719 ) 720 argument_spec.update(ios_argument_spec) 721 mutually_exclusive = [("name", "vrfs")] 722 module = AnsibleModule( 723 argument_spec=argument_spec, 724 mutually_exclusive=mutually_exclusive, 725 supports_check_mode=True, 726 ) 727 result = {"changed": False} 728 warnings = list() 729 result["warnings"] = warnings 730 want = map_params_to_obj(module) 731 have = map_config_to_obj(module) 732 commands = map_obj_to_commands(update_objects(want, have), module) 733 if module.params["purge"]: 734 want_vrfs = [x["name"] for x in want] 735 have_vrfs = [x["name"] for x in have] 736 for item in set(have_vrfs).difference(want_vrfs): 737 cmd = "no vrf definition %s" % item 738 if cmd not in commands: 739 commands.append(cmd) 740 result["commands"] = commands 741 if commands: 742 if not module.check_mode: 743 load_config(module, commands) 744 result["changed"] = True 745 check_declarative_intent_params(want, module, result) 746 module.exit_json(**result) 747 748 749if __name__ == "__main__": 750 main() 751