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# 18 19from __future__ import (absolute_import, division, print_function) 20__metaclass__ = type 21 22DOCUMENTATION = ''' 23--- 24module: ce_evpn_bd_vni 25short_description: Manages EVPN VXLAN Network Identifier (VNI) on HUAWEI CloudEngine switches. 26description: 27 - Manages Ethernet Virtual Private Network (EVPN) VXLAN Network 28 Identifier (VNI) configurations on HUAWEI CloudEngine switches. 29author: Zhijin Zhou (@QijunPan) 30notes: 31 - Ensure that EVPN has been configured to serve as the VXLAN control plane when state is present. 32 - Ensure that a bridge domain (BD) has existed when state is present. 33 - Ensure that a VNI has been created and associated with a broadcast domain (BD) when state is present. 34 - If you configure evpn:false to delete an EVPN instance, all configurations in the EVPN instance are deleted. 35 - After an EVPN instance has been created in the BD view, you can configure an RD using route_distinguisher 36 parameter in BD-EVPN instance view. 37 - Before configuring VPN targets for a BD EVPN instance, ensure that an RD has been configured 38 for the BD EVPN instance 39 - If you unconfigure route_distinguisher, all VPN target attributes for the BD EVPN instance will be removed at the same time. 40 - When using state:absent, evpn is not supported and it will be ignored. 41 - When using state:absent to delete VPN target attributes, ensure the configuration of VPN target attributes has 42 existed and otherwise it will report an error. 43 - This module requires the netconf system service be enabled on the remote device being managed. 44 - Recommended connection is C(netconf). 45 - This module also works with C(local) connections for legacy playbooks. 46options: 47 bridge_domain_id: 48 description: 49 - Specify an existed bridge domain (BD).The value is an integer ranging from 1 to 16777215. 50 required: true 51 evpn: 52 description: 53 - Create or delete an EVPN instance for a VXLAN in BD view. 54 choices: ['enable','disable'] 55 default: 'enable' 56 route_distinguisher: 57 description: 58 - Configures a route distinguisher (RD) for a BD EVPN instance. 59 The format of an RD can be as follows 60 - 1) 2-byte AS number:4-byte user-defined number, for example, 1:3. An AS number is an integer ranging from 61 0 to 65535, and a user-defined number is an integer ranging from 0 to 4294967295. The AS and user-defined 62 numbers cannot be both 0s. This means that an RD cannot be 0:0. 63 - 2) Integral 4-byte AS number:2-byte user-defined number, for example, 65537:3. An AS number is an integer 64 ranging from 65536 to 4294967295, and a user-defined number is an integer ranging from 0 to 65535. 65 - 3) 4-byte AS number in dotted notation:2-byte user-defined number, for example, 0.0:3 or 0.1:0. A 4-byte 66 AS number in dotted notation is in the format of x.y, where x and y are integers ranging from 0 to 65535. 67 - 4) A user-defined number is an integer ranging from 0 to 65535. The AS and user-defined numbers cannot be 68 both 0s. This means that an RD cannot be 0.0:0. 69 - 5) 32-bit IP address:2-byte user-defined number. For example, 192.168.122.15:1. An IP address ranges from 70 0.0.0.0 to 255.255.255.255, and a user-defined number is an integer ranging from 0 to 65535. 71 - 6) 'auto' specifies the RD that is automatically generated. 72 vpn_target_both: 73 description: 74 - Add VPN targets to both the import and export VPN target lists of a BD EVPN instance. 75 The format is the same as route_distinguisher. 76 vpn_target_import: 77 description: 78 - Add VPN targets to the import VPN target list of a BD EVPN instance. 79 The format is the same as route_distinguisher. 80 required: true 81 vpn_target_export: 82 description: 83 - Add VPN targets to the export VPN target list of a BD EVPN instance. 84 The format is the same as route_distinguisher. 85 state: 86 description: 87 - Manage the state of the resource. 88 choices: ['present','absent'] 89 default: 'present' 90''' 91 92EXAMPLES = ''' 93- name: EVPN BD VNI test 94 hosts: cloudengine 95 connection: local 96 gather_facts: no 97 vars: 98 cli: 99 host: "{{ inventory_hostname }}" 100 port: "{{ ansible_ssh_port }}" 101 username: "{{ username }}" 102 password: "{{ password }}" 103 transport: cli 104 105 tasks: 106 107 - name: "Configure an EVPN instance for a VXLAN in BD view" 108 community.network.ce_evpn_bd_vni: 109 bridge_domain_id: 20 110 evpn: enable 111 provider: "{{ cli }}" 112 113 - name: "Configure a route distinguisher (RD) for a BD EVPN instance" 114 community.network.ce_evpn_bd_vni: 115 bridge_domain_id: 20 116 route_distinguisher: '22:22' 117 provider: "{{ cli }}" 118 119 - name: "Configure VPN targets to both the import and export VPN target lists of a BD EVPN instance" 120 community.network.ce_evpn_bd_vni: 121 bridge_domain_id: 20 122 vpn_target_both: 22:100,22:101 123 provider: "{{ cli }}" 124 125 - name: "Configure VPN targets to the import VPN target list of a BD EVPN instance" 126 community.network.ce_evpn_bd_vni: 127 bridge_domain_id: 20 128 vpn_target_import: 22:22,22:23 129 provider: "{{ cli }}" 130 131 - name: "Configure VPN targets to the export VPN target list of a BD EVPN instance" 132 community.network.ce_evpn_bd_vni: 133 bridge_domain_id: 20 134 vpn_target_export: 22:38,22:39 135 provider: "{{ cli }}" 136 137 - name: "Unconfigure VPN targets to both the import and export VPN target lists of a BD EVPN instance" 138 community.network.ce_evpn_bd_vni: 139 bridge_domain_id: 20 140 vpn_target_both: '22:100' 141 state: absent 142 provider: "{{ cli }}" 143 144 - name: "Unconfigure VPN targets to the import VPN target list of a BD EVPN instance" 145 community.network.ce_evpn_bd_vni: 146 bridge_domain_id: 20 147 vpn_target_import: '22:22' 148 state: absent 149 provider: "{{ cli }}" 150 151 - name: "Unconfigure VPN targets to the export VPN target list of a BD EVPN instance" 152 community.network.ce_evpn_bd_vni: 153 bridge_domain_id: 20 154 vpn_target_export: '22:38' 155 state: absent 156 provider: "{{ cli }}" 157 158 - name: "Unconfigure a route distinguisher (RD) of a BD EVPN instance" 159 community.network.ce_evpn_bd_vni: 160 bridge_domain_id: 20 161 route_distinguisher: '22:22' 162 state: absent 163 provider: "{{ cli }}" 164 165 - name: "Unconfigure an EVPN instance for a VXLAN in BD view" 166 community.network.ce_evpn_bd_vni: 167 bridge_domain_id: 20 168 evpn: disable 169 provider: "{{ cli }}" 170''' 171 172RETURN = ''' 173proposed: 174 description: k/v pairs of parameters passed into module 175 returned: always 176 type: dict 177 sample: { 178 "bridge_domain_id": "2", 179 "evpn": "enable", 180 "route_distinguisher": "22:22", 181 "state": "present", 182 "vpn_target_both": [ 183 "22:100", 184 "22:101" 185 ], 186 "vpn_target_export": [ 187 "22:38", 188 "22:39" 189 ], 190 "vpn_target_import": [ 191 "22:22", 192 "22:23" 193 ] 194 } 195existing: 196 description: k/v pairs of existing attributes on the device 197 returned: always 198 type: dict 199 sample: { 200 "bridge_domain_id": "2", 201 "evpn": "disable", 202 "route_distinguisher": null, 203 "vpn_target_both": [], 204 "vpn_target_export": [], 205 "vpn_target_import": [] 206 } 207end_state: 208 description: k/v pairs of end attributes on the device 209 returned: always 210 type: dict 211 sample: { 212 "bridge_domain_id": "2", 213 "evpn": "enable", 214 "route_distinguisher": "22:22", 215 "vpn_target_both": [ 216 "22:100", 217 "22:101" 218 ], 219 "vpn_target_export": [ 220 "22:38", 221 "22:39" 222 ], 223 "vpn_target_import": [ 224 "22:22", 225 "22:23" 226 ] 227 } 228updates: 229 description: command list sent to the device 230 returned: always 231 type: list 232 sample: [ 233 "bridge-domain 2", 234 " evpn", 235 " route-distinguisher 22:22", 236 " vpn-target 22:38 export-extcommunity", 237 " vpn-target 22:39 export-extcommunity", 238 " vpn-target 22:100 export-extcommunity", 239 " vpn-target 22:101 export-extcommunity", 240 " vpn-target 22:22 import-extcommunity", 241 " vpn-target 22:23 import-extcommunity", 242 " vpn-target 22:100 import-extcommunity", 243 " vpn-target 22:101 import-extcommunity" 244 ] 245changed: 246 description: check to see if a change was made on the device 247 returned: always 248 type: bool 249 sample: true 250''' 251 252import re 253import copy 254from xml.etree import ElementTree 255from ansible.module_utils.basic import AnsibleModule 256from ansible_collections.community.network.plugins.module_utils.network.cloudengine.ce import get_nc_config, set_nc_config, ce_argument_spec 257 258 259CE_NC_GET_VNI_BD = """ 260<filter type="subtree"> 261 <nvo3 xmlns="http://www.huawei.com/netconf/vrp" content-version="1.0" format-version="1.0"> 262 <nvo3Vni2Bds> 263 <nvo3Vni2Bd> 264 <vniId></vniId> 265 <bdId></bdId> 266 </nvo3Vni2Bd> 267 </nvo3Vni2Bds> 268 </nvo3> 269</filter> 270""" 271 272CE_NC_GET_EVPN_CONFIG = """ 273<filter type="subtree"> 274 <evpn xmlns="http://www.huawei.com/netconf/vrp" content-version="1.0" format-version="1.0"> 275 <evpnInstances> 276 <evpnInstance> 277 <evpnName>%s</evpnName> 278 <bdId>%s</bdId> 279 <evpnAutoRD></evpnAutoRD> 280 <evpnRD></evpnRD> 281 <evpnRTs> 282 <evpnRT> 283 <vrfRTType></vrfRTType> 284 <vrfRTValue></vrfRTValue> 285 </evpnRT> 286 </evpnRTs> 287 <evpnAutoRTs> 288 <evpnAutoRT> 289 <vrfRTType></vrfRTType> 290 </evpnAutoRT> 291 </evpnAutoRTs> 292 </evpnInstance> 293 </evpnInstances> 294 </evpn> 295</filter> 296""" 297 298CE_NC_DELETE_EVPN_CONFIG = """ 299<config> 300 <evpn xmlns="http://www.huawei.com/netconf/vrp" content-version="1.0" format-version="1.0"> 301 <evpnInstances> 302 <evpnInstance operation="delete"> 303 <evpnName>%s</evpnName> 304 <bdId>%s</bdId> 305 </evpnInstance> 306 </evpnInstances> 307 </evpn> 308</config> 309""" 310 311CE_NC_DELETE_EVPN_CONFIG_HEAD = """ 312<config> 313 <evpn xmlns="http://www.huawei.com/netconf/vrp" content-version="1.0" format-version="1.0"> 314 <evpnInstances> 315 <evpnInstance operation="delete"> 316 <evpnName>%s</evpnName> 317 <bdId>%s</bdId> 318""" 319 320CE_NC_MERGE_EVPN_CONFIG_HEAD = """ 321<config> 322 <evpn xmlns="http://www.huawei.com/netconf/vrp" content-version="1.0" format-version="1.0"> 323 <evpnInstances> 324 <evpnInstance operation="merge"> 325 <evpnName>%s</evpnName> 326 <bdId>%s</bdId> 327""" 328 329CE_NC_MERGE_EVPN_AUTORTS_HEAD = """ 330<evpnAutoRTs> 331""" 332 333CE_NC_MERGE_EVPN_AUTORTS_TAIL = """ 334</evpnAutoRTs> 335""" 336 337CE_NC_DELETE_EVPN_AUTORTS_CONTEXT = """ 338 <evpnAutoRT operation="delete"> 339 <vrfRTType>%s</vrfRTType> 340 </evpnAutoRT> 341""" 342 343CE_NC_MERGE_EVPN_AUTORTS_CONTEXT = """ 344<evpnAutoRT> 345 <vrfRTType>%s</vrfRTType> 346</evpnAutoRT> 347""" 348 349CE_NC_MERGE_EVPN_RTS_HEAD = """ 350<evpnRTs> 351""" 352 353CE_NC_MERGE_EVPN_RTS_TAIL = """ 354</evpnRTs> 355""" 356 357CE_NC_DELETE_EVPN_RTS_CONTEXT = """ 358 <evpnRT operation="delete"> 359 <vrfRTType>%s</vrfRTType> 360 <vrfRTValue>%s</vrfRTValue> 361 </evpnRT> 362""" 363 364CE_NC_MERGE_EVPN_RTS_CONTEXT = """ 365<evpnRT> 366 <vrfRTType>%s</vrfRTType> 367 <vrfRTValue>%s</vrfRTValue> 368</evpnRT> 369""" 370 371CE_NC_MERGE_EVPN_CONFIG_TAIL = """ 372 </evpnInstance> 373 </evpnInstances> 374 </evpn> 375</config> 376""" 377 378 379def is_valid_value(vrf_targe_value): 380 """check whether VPN target value is valid""" 381 382 each_num = None 383 if len(vrf_targe_value) > 21 or len(vrf_targe_value) < 3: 384 return False 385 if vrf_targe_value.find(':') == -1: 386 return False 387 elif vrf_targe_value == '0:0': 388 return False 389 elif vrf_targe_value == '0.0:0': 390 return False 391 else: 392 value_list = vrf_targe_value.split(':') 393 if value_list[0].find('.') != -1: 394 if not value_list[1].isdigit(): 395 return False 396 if int(value_list[1]) > 65535: 397 return False 398 value = value_list[0].split('.') 399 if len(value) == 4: 400 for each_num in value: 401 if not each_num.isdigit(): 402 return False 403 if int(each_num) > 255: 404 return False 405 return True 406 elif len(value) == 2: 407 for each_num in value: 408 if not each_num.isdigit(): 409 return False 410 if int(each_num) > 65535: 411 return False 412 return True 413 else: 414 return False 415 elif not value_list[0].isdigit(): 416 return False 417 elif not value_list[1].isdigit(): 418 return False 419 elif int(value_list[0]) < 65536 and int(value_list[1]) < 4294967296: 420 return True 421 elif int(value_list[0]) > 65535 and int(value_list[0]) < 4294967296: 422 return bool(int(value_list[1]) < 65536) 423 else: 424 return False 425 426 427class EvpnBd(object): 428 """Manage evpn instance in BD view""" 429 430 def __init__(self, argument_spec, ): 431 self.spec = argument_spec 432 self.module = None 433 self.__init_module__() 434 435 # EVPN instance info 436 self.bridge_domain_id = self.module.params['bridge_domain_id'] 437 self.evpn = self.module.params['evpn'] 438 self.route_distinguisher = self.module.params['route_distinguisher'] 439 self.vpn_target_both = self.module.params['vpn_target_both'] or list() 440 self.vpn_target_import = self.module.params[ 441 'vpn_target_import'] or list() 442 self.vpn_target_export = self.module.params[ 443 'vpn_target_export'] or list() 444 self.state = self.module.params['state'] 445 self.__string_to_lowercase__() 446 447 self.commands = list() 448 self.evpn_info = dict() 449 self.conf_exist = False 450 451 # state 452 self.changed = False 453 self.updates_cmd = list() 454 self.results = dict() 455 self.proposed = dict() 456 self.existing = dict() 457 self.end_state = dict() 458 459 def __init_module__(self): 460 """Init module""" 461 462 self.module = AnsibleModule( 463 argument_spec=self.spec, supports_check_mode=True) 464 465 def __check_response__(self, xml_str, xml_name): 466 """Check if response message is already succeed""" 467 if "<ok/>" not in xml_str: 468 self.module.fail_json(msg='Error: %s failed.' % xml_name) 469 470 def __string_to_lowercase__(self): 471 """Convert string to lowercase""" 472 473 if self.route_distinguisher: 474 self.route_distinguisher = self.route_distinguisher.lower() 475 476 if self.vpn_target_export: 477 for index, ele in enumerate(self.vpn_target_export): 478 self.vpn_target_export[index] = ele.lower() 479 480 if self.vpn_target_import: 481 for index, ele in enumerate(self.vpn_target_import): 482 self.vpn_target_import[index] = ele.lower() 483 484 if self.vpn_target_both: 485 for index, ele in enumerate(self.vpn_target_both): 486 self.vpn_target_both[index] = ele.lower() 487 488 def get_all_evpn_rts(self, evpn_rts): 489 """Get all EVPN RTS""" 490 491 rts = evpn_rts.findall("evpnRT") 492 if not rts: 493 return 494 495 for ele in rts: 496 vrf_rttype = ele.find('vrfRTType') 497 vrf_rtvalue = ele.find('vrfRTValue') 498 499 if vrf_rttype.text == 'export_extcommunity': 500 self.evpn_info['vpn_target_export'].append(vrf_rtvalue.text) 501 elif vrf_rttype.text == 'import_extcommunity': 502 self.evpn_info['vpn_target_import'].append(vrf_rtvalue.text) 503 504 def get_all_evpn_autorts(self, evpn_autorts): 505 """"Get all EVPN AUTORTS""" 506 507 autorts = evpn_autorts.findall("evpnAutoRT") 508 if not autorts: 509 return 510 511 for autort in autorts: 512 vrf_rttype = autort.find('vrfRTType') 513 514 if vrf_rttype.text == 'export_extcommunity': 515 self.evpn_info['vpn_target_export'].append('auto') 516 elif vrf_rttype.text == 'import_extcommunity': 517 self.evpn_info['vpn_target_import'].append('auto') 518 519 def process_rts_info(self): 520 """Process RTS information""" 521 522 if not self.evpn_info['vpn_target_export'] or\ 523 not self.evpn_info['vpn_target_import']: 524 return 525 526 vpn_target_export = copy.deepcopy(self.evpn_info['vpn_target_export']) 527 for ele in vpn_target_export: 528 if ele in self.evpn_info['vpn_target_import']: 529 self.evpn_info['vpn_target_both'].append(ele) 530 self.evpn_info['vpn_target_export'].remove(ele) 531 self.evpn_info['vpn_target_import'].remove(ele) 532 533 def get_evpn_instance_info(self): 534 """Get current EVPN instance information""" 535 536 if not self.bridge_domain_id: 537 self.module.fail_json(msg='Error: The value of bridge_domain_id cannot be empty.') 538 539 self.evpn_info['route_distinguisher'] = None 540 self.evpn_info['vpn_target_import'] = list() 541 self.evpn_info['vpn_target_export'] = list() 542 self.evpn_info['vpn_target_both'] = list() 543 self.evpn_info['evpn_inst'] = 'enable' 544 545 xml_str = CE_NC_GET_EVPN_CONFIG % ( 546 self.bridge_domain_id, self.bridge_domain_id) 547 xml_str = get_nc_config(self.module, xml_str) 548 if "<data/>" in xml_str: 549 self.evpn_info['evpn_inst'] = 'disable' 550 return 551 552 xml_str = xml_str.replace('\r', '').replace('\n', '').\ 553 replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "").\ 554 replace('xmlns="http://www.huawei.com/netconf/vrp"', "") 555 556 root = ElementTree.fromstring(xml_str) 557 evpn_inst = root.find("evpn/evpnInstances/evpnInstance") 558 if evpn_inst: 559 for eles in evpn_inst: 560 if eles.tag in ["evpnAutoRD", "evpnRD", "evpnRTs", "evpnAutoRTs"]: 561 if eles.tag == 'evpnAutoRD' and eles.text == 'true': 562 self.evpn_info['route_distinguisher'] = 'auto' 563 elif eles.tag == 'evpnRD' and self.evpn_info['route_distinguisher'] != 'auto': 564 self.evpn_info['route_distinguisher'] = eles.text 565 elif eles.tag == 'evpnRTs': 566 self.get_all_evpn_rts(eles) 567 elif eles.tag == 'evpnAutoRTs': 568 self.get_all_evpn_autorts(eles) 569 self.process_rts_info() 570 571 def get_existing(self): 572 """Get existing config""" 573 574 self.existing = dict(bridge_domain_id=self.bridge_domain_id, 575 evpn=self.evpn_info['evpn_inst'], 576 route_distinguisher=self.evpn_info[ 577 'route_distinguisher'], 578 vpn_target_both=self.evpn_info['vpn_target_both'], 579 vpn_target_import=self.evpn_info[ 580 'vpn_target_import'], 581 vpn_target_export=self.evpn_info['vpn_target_export']) 582 583 def get_proposed(self): 584 """Get proposed config""" 585 586 self.proposed = dict(bridge_domain_id=self.bridge_domain_id, 587 evpn=self.evpn, 588 route_distinguisher=self.route_distinguisher, 589 vpn_target_both=self.vpn_target_both, 590 vpn_target_import=self.vpn_target_import, 591 vpn_target_export=self.vpn_target_export, 592 state=self.state) 593 594 def get_end_state(self): 595 """Get end config""" 596 597 self.get_evpn_instance_info() 598 self.end_state = dict(bridge_domain_id=self.bridge_domain_id, 599 evpn=self.evpn_info['evpn_inst'], 600 route_distinguisher=self.evpn_info[ 601 'route_distinguisher'], 602 vpn_target_both=self.evpn_info[ 603 'vpn_target_both'], 604 vpn_target_import=self.evpn_info[ 605 'vpn_target_import'], 606 vpn_target_export=self.evpn_info['vpn_target_export']) 607 608 def show_result(self): 609 """Show result""" 610 611 self.results['changed'] = self.changed 612 self.results['proposed'] = self.proposed 613 self.results['existing'] = self.existing 614 self.results['end_state'] = self.end_state 615 if self.changed: 616 self.results['updates'] = self.updates_cmd 617 else: 618 self.results['updates'] = list() 619 620 self.module.exit_json(**self.results) 621 622 def judge_if_vpn_target_exist(self, vpn_target_type): 623 """Judge whether proposed vpn target has existed""" 624 625 vpn_target = list() 626 if vpn_target_type == 'vpn_target_import': 627 vpn_target.extend(self.existing['vpn_target_both']) 628 vpn_target.extend(self.existing['vpn_target_import']) 629 return set(self.proposed['vpn_target_import']).issubset(vpn_target) 630 elif vpn_target_type == 'vpn_target_export': 631 vpn_target.extend(self.existing['vpn_target_both']) 632 vpn_target.extend(self.existing['vpn_target_export']) 633 return set(self.proposed['vpn_target_export']).issubset(vpn_target) 634 635 return False 636 637 def judge_if_config_exist(self): 638 """Judge whether configuration has existed""" 639 640 if self.state == 'absent': 641 if self.route_distinguisher or self.vpn_target_import or self.vpn_target_export or self.vpn_target_both: 642 return False 643 else: 644 return True 645 646 if self.evpn_info['evpn_inst'] != self.evpn: 647 return False 648 649 if self.evpn == 'disable' and self.evpn_info['evpn_inst'] == 'disable': 650 return True 651 652 if self.proposed['bridge_domain_id'] != self.existing['bridge_domain_id']: 653 return False 654 655 if self.proposed['route_distinguisher']: 656 if self.proposed['route_distinguisher'] != self.existing['route_distinguisher']: 657 return False 658 659 if self.proposed['vpn_target_both']: 660 if not self.existing['vpn_target_both']: 661 return False 662 if not set(self.proposed['vpn_target_both']).issubset(self.existing['vpn_target_both']): 663 return False 664 665 if self.proposed['vpn_target_import']: 666 if not self.judge_if_vpn_target_exist('vpn_target_import'): 667 return False 668 669 if self.proposed['vpn_target_export']: 670 if not self.judge_if_vpn_target_exist('vpn_target_export'): 671 return False 672 673 return True 674 675 def check_response(self, xml_str, xml_name): 676 """Check if response message is already succeed.""" 677 678 if "<ok/>" not in xml_str: 679 self.module.fail_json(msg='Error: %s failed.' % xml_name) 680 681 def unconfig_evpn_instance(self): 682 """Unconfigure EVPN instance""" 683 684 self.updates_cmd.append("bridge-domain %s" % self.bridge_domain_id) 685 xml_str = CE_NC_MERGE_EVPN_CONFIG_HEAD % ( 686 self.bridge_domain_id, self.bridge_domain_id) 687 self.updates_cmd.append(" evpn") 688 689 # unconfigure RD 690 if self.route_distinguisher: 691 if self.route_distinguisher.lower() == 'auto': 692 xml_str += '<evpnAutoRD>false</evpnAutoRD>' 693 self.updates_cmd.append(" undo route-distinguisher auto") 694 else: 695 xml_str += '<evpnRD></evpnRD>' 696 self.updates_cmd.append( 697 " undo route-distinguisher %s" % self.route_distinguisher) 698 xml_str += CE_NC_MERGE_EVPN_CONFIG_TAIL 699 recv_xml = set_nc_config(self.module, xml_str) 700 self.check_response(recv_xml, "UNDO_EVPN_BD_RD") 701 self.changed = True 702 return 703 704 # process VPN target list 705 vpn_target_export = copy.deepcopy(self.vpn_target_export) 706 vpn_target_import = copy.deepcopy(self.vpn_target_import) 707 if self.vpn_target_both: 708 for ele in self.vpn_target_both: 709 if ele not in vpn_target_export: 710 vpn_target_export.append(ele) 711 if ele not in vpn_target_import: 712 vpn_target_import.append(ele) 713 714 # unconfig EVPN auto RTS 715 head_flag = False 716 if vpn_target_export: 717 for ele in vpn_target_export: 718 if ele.lower() == 'auto': 719 if not head_flag: 720 xml_str += CE_NC_MERGE_EVPN_AUTORTS_HEAD 721 head_flag = True 722 xml_str += CE_NC_DELETE_EVPN_AUTORTS_CONTEXT % ( 723 'export_extcommunity') 724 self.updates_cmd.append( 725 " undo vpn-target auto export-extcommunity") 726 if vpn_target_import: 727 for ele in vpn_target_import: 728 if ele.lower() == 'auto': 729 if not head_flag: 730 xml_str += CE_NC_MERGE_EVPN_AUTORTS_HEAD 731 head_flag = True 732 xml_str += CE_NC_DELETE_EVPN_AUTORTS_CONTEXT % ( 733 'import_extcommunity') 734 self.updates_cmd.append( 735 " undo vpn-target auto import-extcommunity") 736 737 if head_flag: 738 xml_str += CE_NC_MERGE_EVPN_AUTORTS_TAIL 739 740 # unconfig EVPN RTS 741 head_flag = False 742 if vpn_target_export: 743 for ele in vpn_target_export: 744 if ele.lower() != 'auto': 745 if not head_flag: 746 xml_str += CE_NC_MERGE_EVPN_RTS_HEAD 747 head_flag = True 748 xml_str += CE_NC_DELETE_EVPN_RTS_CONTEXT % ( 749 'export_extcommunity', ele) 750 self.updates_cmd.append( 751 " undo vpn-target %s export-extcommunity" % ele) 752 753 if vpn_target_import: 754 for ele in vpn_target_import: 755 if ele.lower() != 'auto': 756 if not head_flag: 757 xml_str += CE_NC_MERGE_EVPN_RTS_HEAD 758 head_flag = True 759 xml_str += CE_NC_DELETE_EVPN_RTS_CONTEXT % ( 760 'import_extcommunity', ele) 761 self.updates_cmd.append( 762 " undo vpn-target %s import-extcommunity" % ele) 763 764 if head_flag: 765 xml_str += CE_NC_MERGE_EVPN_RTS_TAIL 766 767 xml_str += CE_NC_MERGE_EVPN_CONFIG_TAIL 768 recv_xml = set_nc_config(self.module, xml_str) 769 self.check_response(recv_xml, "MERGE_EVPN_BD_VPN_TARGET_CONFIG") 770 self.changed = True 771 772 def config_evpn_instance(self): 773 """Configure EVPN instance""" 774 775 self.updates_cmd.append("bridge-domain %s" % self.bridge_domain_id) 776 777 if self.evpn == 'disable': 778 xml_str = CE_NC_DELETE_EVPN_CONFIG % ( 779 self.bridge_domain_id, self.bridge_domain_id) 780 recv_xml = set_nc_config(self.module, xml_str) 781 self.check_response(recv_xml, "MERGE_EVPN_BD_CONFIG") 782 self.updates_cmd.append(" undo evpn") 783 self.changed = True 784 return 785 786 xml_str = CE_NC_MERGE_EVPN_CONFIG_HEAD % ( 787 self.bridge_domain_id, self.bridge_domain_id) 788 self.updates_cmd.append(" evpn") 789 790 # configure RD 791 if self.route_distinguisher: 792 if not self.existing['route_distinguisher']: 793 if self.route_distinguisher.lower() == 'auto': 794 xml_str += '<evpnAutoRD>true</evpnAutoRD>' 795 self.updates_cmd.append(" route-distinguisher auto") 796 else: 797 xml_str += '<evpnRD>%s</evpnRD>' % self.route_distinguisher 798 self.updates_cmd.append( 799 " route-distinguisher %s" % self.route_distinguisher) 800 801 # process VPN target list 802 vpn_target_export = copy.deepcopy(self.vpn_target_export) 803 vpn_target_import = copy.deepcopy(self.vpn_target_import) 804 if self.vpn_target_both: 805 for ele in self.vpn_target_both: 806 if ele not in vpn_target_export: 807 vpn_target_export.append(ele) 808 if ele not in vpn_target_import: 809 vpn_target_import.append(ele) 810 811 # config EVPN auto RTS 812 head_flag = False 813 if vpn_target_export: 814 for ele in vpn_target_export: 815 if ele.lower() == 'auto' and \ 816 (not self.is_vpn_target_exist('export_extcommunity', ele.lower())): 817 if not head_flag: 818 xml_str += CE_NC_MERGE_EVPN_AUTORTS_HEAD 819 head_flag = True 820 xml_str += CE_NC_MERGE_EVPN_AUTORTS_CONTEXT % ( 821 'export_extcommunity') 822 self.updates_cmd.append( 823 " vpn-target auto export-extcommunity") 824 if vpn_target_import: 825 for ele in vpn_target_import: 826 if ele.lower() == 'auto' and \ 827 (not self.is_vpn_target_exist('import_extcommunity', ele.lower())): 828 if not head_flag: 829 xml_str += CE_NC_MERGE_EVPN_AUTORTS_HEAD 830 head_flag = True 831 xml_str += CE_NC_MERGE_EVPN_AUTORTS_CONTEXT % ( 832 'import_extcommunity') 833 self.updates_cmd.append( 834 " vpn-target auto import-extcommunity") 835 836 if head_flag: 837 xml_str += CE_NC_MERGE_EVPN_AUTORTS_TAIL 838 839 # config EVPN RTS 840 head_flag = False 841 if vpn_target_export: 842 for ele in vpn_target_export: 843 if ele.lower() != 'auto' and \ 844 (not self.is_vpn_target_exist('export_extcommunity', ele.lower())): 845 if not head_flag: 846 xml_str += CE_NC_MERGE_EVPN_RTS_HEAD 847 head_flag = True 848 xml_str += CE_NC_MERGE_EVPN_RTS_CONTEXT % ( 849 'export_extcommunity', ele) 850 self.updates_cmd.append( 851 " vpn-target %s export-extcommunity" % ele) 852 853 if vpn_target_import: 854 for ele in vpn_target_import: 855 if ele.lower() != 'auto' and \ 856 (not self.is_vpn_target_exist('import_extcommunity', ele.lower())): 857 if not head_flag: 858 xml_str += CE_NC_MERGE_EVPN_RTS_HEAD 859 head_flag = True 860 xml_str += CE_NC_MERGE_EVPN_RTS_CONTEXT % ( 861 'import_extcommunity', ele) 862 self.updates_cmd.append( 863 " vpn-target %s import-extcommunity" % ele) 864 865 if head_flag: 866 xml_str += CE_NC_MERGE_EVPN_RTS_TAIL 867 868 xml_str += CE_NC_MERGE_EVPN_CONFIG_TAIL 869 recv_xml = set_nc_config(self.module, xml_str) 870 self.check_response(recv_xml, "MERGE_EVPN_BD_CONFIG") 871 self.changed = True 872 873 def is_vpn_target_exist(self, target_type, value): 874 """Judge whether VPN target has existed""" 875 876 if target_type == 'export_extcommunity': 877 if (value not in self.existing['vpn_target_export']) and\ 878 (value not in self.existing['vpn_target_both']): 879 return False 880 return True 881 882 if target_type == 'import_extcommunity': 883 if (value not in self.existing['vpn_target_import']) and\ 884 (value not in self.existing['vpn_target_both']): 885 return False 886 return True 887 888 return False 889 890 def config_evnp_bd(self): 891 """Configure EVPN in BD view""" 892 893 if not self.conf_exist: 894 if self.state == 'present': 895 self.config_evpn_instance() 896 else: 897 self.unconfig_evpn_instance() 898 899 def process_input_params(self): 900 """Process input parameters""" 901 902 if self.state == 'absent': 903 self.evpn = None 904 else: 905 if self.evpn == 'disable': 906 return 907 908 if self.vpn_target_both: 909 for ele in self.vpn_target_both: 910 if ele in self.vpn_target_export: 911 self.vpn_target_export.remove(ele) 912 if ele in self.vpn_target_import: 913 self.vpn_target_import.remove(ele) 914 915 if self.vpn_target_export and self.vpn_target_import: 916 vpn_target_export = copy.deepcopy(self.vpn_target_export) 917 for ele in vpn_target_export: 918 if ele in self.vpn_target_import: 919 self.vpn_target_both.append(ele) 920 self.vpn_target_import.remove(ele) 921 self.vpn_target_export.remove(ele) 922 923 def check_vpn_target_para(self): 924 """Check whether VPN target value is valid""" 925 926 if self.route_distinguisher: 927 if self.route_distinguisher.lower() != 'auto' and\ 928 not is_valid_value(self.route_distinguisher): 929 self.module.fail_json( 930 msg='Error: Route distinguisher has invalid value %s.' % self.route_distinguisher) 931 932 if self.vpn_target_export: 933 for ele in self.vpn_target_export: 934 if ele.lower() != 'auto' and not is_valid_value(ele): 935 self.module.fail_json( 936 msg='Error: VPN target extended community attribute has invalid value %s.' % ele) 937 938 if self.vpn_target_import: 939 for ele in self.vpn_target_import: 940 if ele.lower() != 'auto' and not is_valid_value(ele): 941 self.module.fail_json( 942 msg='Error: VPN target extended community attribute has invalid value %s.' % ele) 943 944 if self.vpn_target_both: 945 for ele in self.vpn_target_both: 946 if ele.lower() != 'auto' and not is_valid_value(ele): 947 self.module.fail_json( 948 msg='Error: VPN target extended community attribute has invalid value %s.' % ele) 949 950 def check_undo_params_if_exist(self): 951 """Check whether all undo parameters is existed""" 952 953 if self.vpn_target_import: 954 for ele in self.vpn_target_import: 955 if ele not in self.evpn_info['vpn_target_import'] and ele not in self.evpn_info['vpn_target_both']: 956 self.module.fail_json( 957 msg='Error: VPN target import attribute value %s does not exist.' % ele) 958 959 if self.vpn_target_export: 960 for ele in self.vpn_target_export: 961 if ele not in self.evpn_info['vpn_target_export'] and ele not in self.evpn_info['vpn_target_both']: 962 self.module.fail_json( 963 msg='Error: VPN target export attribute value %s does not exist.' % ele) 964 965 if self.vpn_target_both: 966 for ele in self.vpn_target_both: 967 if ele not in self.evpn_info['vpn_target_both']: 968 self.module.fail_json( 969 msg='Error: VPN target export and import attribute value %s does not exist.' % ele) 970 971 def check_params(self): 972 """Check all input params""" 973 974 # bridge_domain_id check 975 if self.bridge_domain_id: 976 if not self.bridge_domain_id.isdigit(): 977 self.module.fail_json( 978 msg='Error: The parameter of bridge domain id is invalid.') 979 if int(self.bridge_domain_id) > 16777215 or int(self.bridge_domain_id) < 1: 980 self.module.fail_json( 981 msg='Error: The bridge domain id must be an integer between 1 and 16777215.') 982 983 if self.state == 'absent': 984 self.check_undo_params_if_exist() 985 986 # check bd whether binding the vxlan vni 987 self.check_vni_bd() 988 self.check_vpn_target_para() 989 990 if self.state == 'absent': 991 if self.route_distinguisher: 992 if not self.evpn_info['route_distinguisher']: 993 self.module.fail_json( 994 msg='Error: Route distinguisher has not been configured.') 995 else: 996 if self.route_distinguisher != self.evpn_info['route_distinguisher']: 997 self.module.fail_json( 998 msg='Error: Current route distinguisher value is %s.' % 999 self.evpn_info['route_distinguisher']) 1000 1001 if self.state == 'present': 1002 if self.route_distinguisher: 1003 if self.evpn_info['route_distinguisher'] and\ 1004 self.route_distinguisher != self.evpn_info['route_distinguisher']: 1005 self.module.fail_json( 1006 msg='Error: Route distinguisher has already been configured.') 1007 1008 def check_vni_bd(self): 1009 """Check whether vxlan vni is configured in BD view""" 1010 1011 xml_str = CE_NC_GET_VNI_BD 1012 xml_str = get_nc_config(self.module, xml_str) 1013 if "<data/>" in xml_str or not re.findall(r'<vniId>\S+</vniId>\s+<bdId>%s</bdId>' % self.bridge_domain_id, xml_str): 1014 self.module.fail_json( 1015 msg='Error: The vxlan vni is not configured or the bridge domain id is invalid.') 1016 1017 def work(self): 1018 """Execute task""" 1019 1020 self.get_evpn_instance_info() 1021 self.process_input_params() 1022 self.check_params() 1023 self.get_existing() 1024 self.get_proposed() 1025 self.conf_exist = self.judge_if_config_exist() 1026 1027 self.config_evnp_bd() 1028 1029 self.get_end_state() 1030 self.show_result() 1031 1032 1033def main(): 1034 """Main function entry""" 1035 1036 argument_spec = dict( 1037 bridge_domain_id=dict(required=True, type='str'), 1038 evpn=dict(required=False, type='str', 1039 default='enable', choices=['enable', 'disable']), 1040 route_distinguisher=dict(required=False, type='str'), 1041 vpn_target_both=dict(required=False, type='list'), 1042 vpn_target_import=dict(required=False, type='list'), 1043 vpn_target_export=dict(required=False, type='list'), 1044 state=dict(required=False, default='present', 1045 choices=['present', 'absent']) 1046 ) 1047 argument_spec.update(ce_argument_spec) 1048 evpn_bd = EvpnBd(argument_spec) 1049 evpn_bd.work() 1050 1051 1052if __name__ == '__main__': 1053 main() 1054