1# Copyright (C) 2016 Nippon Telegraph and Telephone Corporation. 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12# implied. 13# See the License for the specific language governing permissions and 14# limitations under the License. 15 16""" 17This sample application performs as VTEP for EVPN VXLAN and constructs 18a Single Subnet per EVI corresponding to the VLAN Based service in [RFC7432]. 19 20.. NOTE:: 21 22 This app will invoke OVSDB request to the switches. 23 Please set the manager address before calling the API of this app. 24 25 :: 26 27 $ sudo ovs-vsctl set-manager ptcp:6640 28 $ sudo ovs-vsctl show 29 ...(snip) 30 Manager "ptcp:6640" 31 ...(snip) 32 33 34Usage Example 35============= 36 37Environment 38----------- 39 40This example supposes the following environment:: 41 42 Host A (172.17.0.1) Host B (172.17.0.2) 43 +--------------------+ +--------------------+ 44 | Ryu1 | --- BGP(EVPN) --- | Ryu2 | 45 +--------------------+ +--------------------+ 46 | | 47 +--------------------+ +--------------------+ 48 | s1 (OVS) | ===== vxlan ===== | s2 (OVS) | 49 +--------------------+ +--------------------+ 50 (s1-eth1) (s1-eth2) (s2-eth1) (s2-eth2) 51 | | | | 52 +--------+ +--------+ +--------+ +--------+ 53 | s1h1 | | s1h2 | | s2h1 | | s2h2 | 54 +--------+ +--------+ +--------+ +--------+ 55 56Configuration steps 57------------------- 58 591. Creates a new BGPSpeaker instance on each host. 60 61 On Host A:: 62 63 (Host A)$ curl -X POST -d '{ 64 "dpid": 1, 65 "as_number": 65000, 66 "router_id": "172.17.0.1" 67 }' http://localhost:8080/vtep/speakers | python -m json.tool 68 69 On Host B:: 70 71 (Host B)$ curl -X POST -d '{ 72 "dpid": 1, 73 "as_number": 65000, 74 "router_id": "172.17.0.2" 75 }' http://localhost:8080/vtep/speakers | python -m json.tool 76 772. Registers the neighbor for the speakers on each host. 78 79 On Host A:: 80 81 (Host A)$ curl -X POST -d '{ 82 "address": "172.17.0.2", 83 "remote_as": 65000 84 }' http://localhost:8080/vtep/neighbors | 85 python -m json.tool 86 87 On Host B:: 88 89 (Host B)$ curl -X POST -d '{ 90 "address": "172.17.0.1", 91 "remote_as": 65000 92 }' http://localhost:8080/vtep/neighbors | 93 python -m json.tool 94 953. Defines a new VXLAN network(VNI=10) on the Host A/B. 96 97 On Host A:: 98 99 (Host A)$ curl -X POST -d '{ 100 "vni": 10 101 }' http://localhost:8080/vtep/networks | python -m json.tool 102 103 On Host B:: 104 105 (Host B)$ curl -X POST -d '{ 106 "vni": 10 107 }' http://localhost:8080/vtep/networks | python -m json.tool 108 1094. Registers the clients to the VXLAN network. 110 111 For "s1h1"(ip="10.0.0.11", mac="aa:bb:cc:00:00:11") on Host A:: 112 113 (Host A)$ curl -X POST -d '{ 114 "port": "s1-eth1", 115 "mac": "aa:bb:cc:00:00:11", 116 "ip": "10.0.0.11" 117 } ' http://localhost:8080/vtep/networks/10/clients | 118 python -m json.tool 119 120 For "s2h1"(ip="10.0.0.21", mac="aa:bb:cc:00:00:21") on Host B:: 121 122 (Host B)$ curl -X POST -d '{ 123 "port": "s2-eth1", 124 "mac": "aa:bb:cc:00:00:21", 125 "ip": "10.0.0.21" 126 } ' http://localhost:8080/vtep/networks/10/clients | 127 python -m json.tool 128 129Testing 130------- 131 132If BGP (EVPN) connection between Ryu1 and Ryu2 has been established, 133pings between the client s1h1 and s2h1 should work. 134 135:: 136 137 (s1h1)$ ping 10.0.0.21 138 139 140Troubleshooting 141--------------- 142 143If connectivity between s1h1 and s2h1 isn't working, 144please check the followings. 145 1461. Make sure that Host A and Host B have full network connectivity. 147 148 :: 149 150 (Host A)$ ping 172.17.0.2 151 1522. Make sure that BGP(EVPN) connection has been established. 153 154 :: 155 156 (Host A)$ curl -X GET http://localhost:8080/vtep/neighbors | 157 python -m json.tool 158 159 ... 160 { 161 "172.17.0.2": { 162 "EvpnNeighbor": { 163 "address": "172.17.0.2", 164 "remote_as": 65000, 165 "state": "up" # "up" shows the connection established 166 } 167 } 168 } 169 1703. Make sure that BGP(EVPN) routes have been advertised. 171 172 :: 173 174 (Host A)$ curl -X GET http://localhost:8080/vtep/networks | 175 python -m json.tool 176 177 ... 178 { 179 "10": { 180 "EvpnNetwork": { 181 "clients": { 182 "aa:bb:cc:00:00:11": { 183 "EvpnClient": { 184 "ip": "10.0.0.11", 185 "mac": "aa:bb:cc:00:00:11", 186 "next_hop": "172.17.0.1", 187 "port": 1 188 } 189 }, 190 "aa:bb:cc:00:00:21": { # route for "s2h1" on Host B 191 "EvpnClient": { 192 "ip": "10.0.0.21", 193 "mac": "aa:bb:cc:00:00:21", 194 "next_hop": "172.17.0.2", 195 "port": 3 196 } 197 } 198 }, 199 "ethernet_tag_id": 0, 200 "route_dist": "65000:10", 201 "vni": 10 202 } 203 } 204 } 205 2064. Make sure that the IPv6 is enabled on your environment. Some Ryu BGP 207features require the IPv6 connectivity to bind sockets. Mininet seems to 208disable IPv6 on its installation. 209 210 For example:: 211 212 $ sysctl net.ipv6.conf.all.disable_ipv6 213 net.ipv6.conf.all.disable_ipv6 = 0 # should NOT be enabled 214 215 $ grep GRUB_CMDLINE_LINUX_DEFAULT /etc/default/grub 216 # please remove "ipv6.disable=1" and reboot 217 GRUB_CMDLINE_LINUX_DEFAULT="ipv6.disable=1 quiet splash" 218 2195. Make sure that your switch using the OpenFlow version 1.3. This application 220supports only the OpenFlow version 1.3. 221 222 For example:: 223 224 $ ovs-vsctl get Bridge s1 protocols 225 ["OpenFlow13"] 226 227.. Note:: 228 229 At the time of this writing, we use the the following version of Ryu, 230 Open vSwitch and Mininet. 231 232 :: 233 234 $ ryu --version 235 ryu 4.19 236 237 $ ovs-vsctl --version 238 ovs-vsctl (Open vSwitch) 2.5.2 # APT packaged version of Ubuntu 16.04 239 Compiled Oct 17 2017 16:38:57 240 DB Schema 7.12.1 241 242 $ mn --version 243 2.2.1 # APT packaged version of Ubuntu 16.04 244""" 245 246import json 247 248from ryu.app.ofctl import api as ofctl_api 249from ryu.app.wsgi import ControllerBase 250from ryu.app.wsgi import Response 251from ryu.app.wsgi import route 252from ryu.app.wsgi import WSGIApplication 253from ryu.base import app_manager 254from ryu.exception import RyuException 255from ryu.lib.ovs import bridge as ovs_bridge 256from ryu.lib.packet import arp 257from ryu.lib.packet import ether_types 258from ryu.lib.packet.bgp import _RouteDistinguisher 259from ryu.lib.packet.bgp import EvpnNLRI 260from ryu.lib.stringify import StringifyMixin 261from ryu.ofproto import ofproto_v1_3 262from ryu.services.protocols.bgp.bgpspeaker import BGPSpeaker 263from ryu.services.protocols.bgp.bgpspeaker import RF_L2_EVPN 264from ryu.services.protocols.bgp.bgpspeaker import EVPN_MAC_IP_ADV_ROUTE 265from ryu.services.protocols.bgp.bgpspeaker import EVPN_MULTICAST_ETAG_ROUTE 266from ryu.services.protocols.bgp.info_base.evpn import EvpnPath 267 268 269API_NAME = 'restvtep' 270 271OVSDB_PORT = 6640 # The IANA registered port for OVSDB [RFC7047] 272 273PRIORITY_D_PLANE = 1 274PRIORITY_ARP_REPLAY = 2 275 276TABLE_ID_INGRESS = 0 277TABLE_ID_EGRESS = 1 278 279 280# Utility functions 281 282def to_int(i): 283 return int(str(i), 0) 284 285 286def to_str_list(l): 287 str_list = [] 288 for s in l: 289 str_list.append(str(s)) 290 return str_list 291 292 293# Exception classes related to OpenFlow and OVSDB 294 295class RestApiException(RyuException): 296 297 def to_response(self, status): 298 body = { 299 "error": str(self), 300 "status": status, 301 } 302 return Response(content_type='application/json', 303 body=json.dumps(body), status=status) 304 305 306class DatapathNotFound(RestApiException): 307 message = 'No such datapath: %(dpid)s' 308 309 310class OFPortNotFound(RestApiException): 311 message = 'No such OFPort: %(port_name)s' 312 313 314# Exception classes related to BGP 315 316class BGPSpeakerNotFound(RestApiException): 317 message = 'BGPSpeaker could not be found' 318 319 320class NeighborNotFound(RestApiException): 321 message = 'No such neighbor: %(address)s' 322 323 324class VniNotFound(RestApiException): 325 message = 'No such VNI: %(vni)s' 326 327 328class ClientNotFound(RestApiException): 329 message = 'No such client: %(mac)s' 330 331 332class ClientNotLocal(RestApiException): 333 message = 'Specified client is not local: %(mac)s' 334 335 336# Utility classes related to EVPN 337 338class EvpnSpeaker(BGPSpeaker, StringifyMixin): 339 _TYPE = { 340 'ascii': [ 341 'router_id', 342 ], 343 } 344 345 def __init__(self, dpid, as_number, router_id, 346 best_path_change_handler, 347 peer_down_handler, peer_up_handler, 348 neighbors=None): 349 super(EvpnSpeaker, self).__init__( 350 as_number=as_number, 351 router_id=router_id, 352 best_path_change_handler=best_path_change_handler, 353 peer_down_handler=peer_down_handler, 354 peer_up_handler=peer_up_handler, 355 ssh_console=True) 356 357 self.dpid = dpid 358 self.as_number = as_number 359 self.router_id = router_id 360 self.neighbors = neighbors or {} 361 362 363class EvpnNeighbor(StringifyMixin): 364 _TYPE = { 365 'ascii': [ 366 'address', 367 'state', 368 ], 369 } 370 371 def __init__(self, address, remote_as, state='down'): 372 super(EvpnNeighbor, self).__init__() 373 self.address = address 374 self.remote_as = remote_as 375 self.state = state 376 377 378class EvpnNetwork(StringifyMixin): 379 _TYPE = { 380 'ascii': [ 381 'route_dist', 382 ], 383 } 384 385 def __init__(self, vni, route_dist, ethernet_tag_id, clients=None): 386 super(EvpnNetwork, self).__init__() 387 self.vni = vni 388 self.route_dist = route_dist 389 self.ethernet_tag_id = ethernet_tag_id 390 self.clients = clients or {} 391 392 def get_clients(self, **kwargs): 393 l = [] 394 for _, c in self.clients.items(): 395 for k, v in kwargs.items(): 396 if getattr(c, k) != v: 397 break 398 else: 399 l.append(c) 400 return l 401 402 403class EvpnClient(StringifyMixin): 404 _TYPE = { 405 'ascii': [ 406 'mac', 407 'ip', 408 'next_hop' 409 ], 410 } 411 412 def __init__(self, port, mac, ip, next_hop): 413 super(EvpnClient, self).__init__() 414 self.port = port 415 self.mac = mac 416 self.ip = ip 417 self.next_hop = next_hop 418 419 420class RestVtep(app_manager.RyuApp): 421 OFP_VERSIONS = [ofproto_v1_3.OFP_VERSION] 422 _CONTEXTS = {'wsgi': WSGIApplication} 423 424 def __init__(self, *args, **kwargs): 425 super(RestVtep, self).__init__(*args, **kwargs) 426 wsgi = kwargs['wsgi'] 427 wsgi.register(RestVtepController, {RestVtep.__name__: self}) 428 429 # EvpnSpeaker instance instantiated later 430 self.speaker = None 431 432 # OVSBridge instance instantiated later 433 self.ovs = None 434 435 # Dictionary for retrieving the EvpnNetwork instance by VNI 436 # self.networks = { 437 # <vni>: <instance 'EvpnNetwork'>, 438 # ... 439 # } 440 self.networks = {} 441 442 # Utility methods related to OpenFlow 443 444 def _get_datapath(self, dpid): 445 return ofctl_api.get_datapath(self, dpid) 446 447 @staticmethod 448 def _add_flow(datapath, priority, match, instructions, 449 table_id=TABLE_ID_INGRESS): 450 parser = datapath.ofproto_parser 451 452 mod = parser.OFPFlowMod( 453 datapath=datapath, 454 table_id=table_id, 455 priority=priority, 456 match=match, 457 instructions=instructions) 458 459 datapath.send_msg(mod) 460 461 @staticmethod 462 def _del_flow(datapath, priority, match, table_id=TABLE_ID_INGRESS): 463 ofproto = datapath.ofproto 464 parser = datapath.ofproto_parser 465 466 mod = parser.OFPFlowMod( 467 datapath=datapath, 468 table_id=table_id, 469 command=ofproto.OFPFC_DELETE, 470 priority=priority, 471 out_port=ofproto.OFPP_ANY, 472 out_group=ofproto.OFPG_ANY, 473 match=match) 474 475 datapath.send_msg(mod) 476 477 def _add_network_ingress_flow(self, datapath, tag, in_port, eth_src=None): 478 parser = datapath.ofproto_parser 479 480 if eth_src is None: 481 match = parser.OFPMatch(in_port=in_port) 482 else: 483 match = parser.OFPMatch(in_port=in_port, eth_src=eth_src) 484 instructions = [ 485 parser.OFPInstructionWriteMetadata( 486 metadata=tag, metadata_mask=parser.UINT64_MAX), 487 parser.OFPInstructionGotoTable(1)] 488 489 self._add_flow(datapath, PRIORITY_D_PLANE, match, instructions) 490 491 def _del_network_ingress_flow(self, datapath, in_port, eth_src=None): 492 parser = datapath.ofproto_parser 493 494 if eth_src is None: 495 match = parser.OFPMatch(in_port=in_port) 496 else: 497 match = parser.OFPMatch(in_port=in_port, eth_src=eth_src) 498 499 self._del_flow(datapath, PRIORITY_D_PLANE, match) 500 501 def _add_arp_reply_flow(self, datapath, tag, arp_tpa, arp_tha): 502 ofproto = datapath.ofproto 503 parser = datapath.ofproto_parser 504 505 match = parser.OFPMatch( 506 metadata=(tag, parser.UINT64_MAX), 507 eth_type=ether_types.ETH_TYPE_ARP, 508 arp_op=arp.ARP_REQUEST, 509 arp_tpa=arp_tpa) 510 511 actions = [ 512 parser.NXActionRegMove( 513 src_field="eth_src", dst_field="eth_dst", n_bits=48), 514 parser.OFPActionSetField(eth_src=arp_tha), 515 parser.OFPActionSetField(arp_op=arp.ARP_REPLY), 516 parser.NXActionRegMove( 517 src_field="arp_sha", dst_field="arp_tha", n_bits=48), 518 parser.NXActionRegMove( 519 src_field="arp_spa", dst_field="arp_tpa", n_bits=32), 520 parser.OFPActionSetField(arp_sha=arp_tha), 521 parser.OFPActionSetField(arp_spa=arp_tpa), 522 parser.OFPActionOutput(ofproto.OFPP_IN_PORT)] 523 instructions = [ 524 parser.OFPInstructionActions( 525 ofproto.OFPIT_APPLY_ACTIONS, actions)] 526 527 self._add_flow(datapath, PRIORITY_ARP_REPLAY, match, instructions, 528 table_id=TABLE_ID_EGRESS) 529 530 def _del_arp_reply_flow(self, datapath, tag, arp_tpa): 531 parser = datapath.ofproto_parser 532 533 match = parser.OFPMatch( 534 metadata=(tag, parser.UINT64_MAX), 535 eth_type=ether_types.ETH_TYPE_ARP, 536 arp_op=arp.ARP_REQUEST, 537 arp_tpa=arp_tpa) 538 539 self._del_flow(datapath, PRIORITY_ARP_REPLAY, match, 540 table_id=TABLE_ID_EGRESS) 541 542 def _add_l2_switching_flow(self, datapath, tag, eth_dst, out_port): 543 ofproto = datapath.ofproto 544 parser = datapath.ofproto_parser 545 546 match = parser.OFPMatch(metadata=(tag, parser.UINT64_MAX), 547 eth_dst=eth_dst) 548 actions = [parser.OFPActionOutput(out_port)] 549 instructions = [ 550 parser.OFPInstructionActions( 551 ofproto.OFPIT_APPLY_ACTIONS, actions)] 552 553 self._add_flow(datapath, PRIORITY_D_PLANE, match, instructions, 554 table_id=TABLE_ID_EGRESS) 555 556 def _del_l2_switching_flow(self, datapath, tag, eth_dst): 557 parser = datapath.ofproto_parser 558 559 match = parser.OFPMatch(metadata=(tag, parser.UINT64_MAX), 560 eth_dst=eth_dst) 561 562 self._del_flow(datapath, PRIORITY_D_PLANE, match, 563 table_id=TABLE_ID_EGRESS) 564 565 def _del_network_egress_flow(self, datapath, tag): 566 parser = datapath.ofproto_parser 567 568 match = parser.OFPMatch(metadata=(tag, parser.UINT64_MAX)) 569 570 self._del_flow(datapath, PRIORITY_D_PLANE, match, 571 table_id=TABLE_ID_EGRESS) 572 573 # Utility methods related to OVSDB 574 575 def _get_ovs_bridge(self, dpid): 576 datapath = self._get_datapath(dpid) 577 if datapath is None: 578 self.logger.debug('No such datapath: %s', dpid) 579 return None 580 581 ovsdb_addr = 'tcp:%s:%d' % (datapath.address[0], OVSDB_PORT) 582 if (self.ovs is not None 583 and self.ovs.datapath_id == dpid 584 and self.ovs.vsctl.remote == ovsdb_addr): 585 return self.ovs 586 587 try: 588 self.ovs = ovs_bridge.OVSBridge( 589 CONF=self.CONF, 590 datapath_id=datapath.id, 591 ovsdb_addr=ovsdb_addr) 592 self.ovs.init() 593 except Exception as e: 594 self.logger.exception('Cannot initiate OVSDB connection: %s', e) 595 return None 596 597 return self.ovs 598 599 def _get_ofport(self, dpid, port_name): 600 ovs = self._get_ovs_bridge(dpid) 601 if ovs is None: 602 return None 603 604 try: 605 return ovs.get_ofport(port_name) 606 except Exception as e: 607 self.logger.debug('Cannot get port number for %s: %s', 608 port_name, e) 609 return None 610 611 def _get_vxlan_port(self, dpid, remote_ip, key): 612 # Searches VXLAN port named 'vxlan_<remote_ip>_<key>' 613 return self._get_ofport(dpid, 'vxlan_%s_%s' % (remote_ip, key)) 614 615 def _add_vxlan_port(self, dpid, remote_ip, key): 616 # If VXLAN port already exists, returns OFPort number 617 vxlan_port = self._get_vxlan_port(dpid, remote_ip, key) 618 if vxlan_port is not None: 619 return vxlan_port 620 621 ovs = self._get_ovs_bridge(dpid) 622 if ovs is None: 623 return None 624 625 # Adds VXLAN port named 'vxlan_<remote_ip>_<key>' 626 ovs.add_vxlan_port( 627 name='vxlan_%s_%s' % (remote_ip, key), 628 remote_ip=remote_ip, 629 key=key) 630 631 # Returns VXLAN port number 632 return self._get_vxlan_port(dpid, remote_ip, key) 633 634 def _del_vxlan_port(self, dpid, remote_ip, key): 635 ovs = self._get_ovs_bridge(dpid) 636 if ovs is None: 637 return None 638 639 # If VXLAN port does not exist, returns None 640 vxlan_port = self._get_vxlan_port(dpid, remote_ip, key) 641 if vxlan_port is None: 642 return None 643 644 # Adds VXLAN port named 'vxlan_<remote_ip>_<key>' 645 ovs.del_port('vxlan_%s_%s' % (remote_ip, key)) 646 647 # Returns deleted VXLAN port number 648 return vxlan_port 649 650 # Event handlers for BGP 651 652 def _evpn_mac_ip_adv_route_handler(self, ev): 653 network = self.networks.get(ev.path.nlri.vni, None) 654 if network is None: 655 self.logger.debug('No such VNI registered: %s', ev.path.nlri) 656 return 657 658 datapath = self._get_datapath(self.speaker.dpid) 659 if datapath is None: 660 self.logger.debug('No such datapath: %s', self.speaker.dpid) 661 return 662 663 vxlan_port = self._add_vxlan_port( 664 dpid=self.speaker.dpid, 665 remote_ip=ev.nexthop, 666 key=ev.path.nlri.vni) 667 if vxlan_port is None: 668 self.logger.debug('Cannot create a new VXLAN port: %s', 669 'vxlan_%s_%s' % (ev.nexthop, ev.path.nlri.vni)) 670 return 671 672 self._add_l2_switching_flow( 673 datapath=datapath, 674 tag=network.vni, 675 eth_dst=ev.path.nlri.mac_addr, 676 out_port=vxlan_port) 677 678 self._add_arp_reply_flow( 679 datapath=datapath, 680 tag=network.vni, 681 arp_tpa=ev.path.nlri.ip_addr, 682 arp_tha=ev.path.nlri.mac_addr) 683 684 network.clients[ev.path.nlri.mac_addr] = EvpnClient( 685 port=vxlan_port, 686 mac=ev.path.nlri.mac_addr, 687 ip=ev.path.nlri.ip_addr, 688 next_hop=ev.nexthop) 689 690 def _evpn_incl_mcast_etag_route_handler(self, ev): 691 # Note: For the VLAN Based service, we use RT(=RD) assigned 692 # field as vid. 693 vni = _RouteDistinguisher.from_str(ev.path.nlri.route_dist).assigned 694 695 network = self.networks.get(vni, None) 696 if network is None: 697 self.logger.debug('No such VNI registered: %s', vni) 698 return 699 700 datapath = self._get_datapath(self.speaker.dpid) 701 if datapath is None: 702 self.logger.debug('No such datapath: %s', self.speaker.dpid) 703 return 704 705 vxlan_port = self._add_vxlan_port( 706 dpid=self.speaker.dpid, 707 remote_ip=ev.nexthop, 708 key=vni) 709 if vxlan_port is None: 710 self.logger.debug('Cannot create a new VXLAN port: %s', 711 'vxlan_%s_%s' % (ev.nexthop, vni)) 712 return 713 714 self._add_network_ingress_flow( 715 datapath=datapath, 716 tag=vni, 717 in_port=vxlan_port) 718 719 def _evpn_route_handler(self, ev): 720 if ev.path.nlri.type == EvpnNLRI.MAC_IP_ADVERTISEMENT: 721 self._evpn_mac_ip_adv_route_handler(ev) 722 elif ev.path.nlri.type == EvpnNLRI.INCLUSIVE_MULTICAST_ETHERNET_TAG: 723 self._evpn_incl_mcast_etag_route_handler(ev) 724 725 def _evpn_withdraw_mac_ip_adv_route_handler(self, ev): 726 network = self.networks.get(ev.path.nlri.vni, None) 727 if network is None: 728 self.logger.debug('No such VNI registered: %s', ev.path.nlri) 729 return 730 731 datapath = self._get_datapath(self.speaker.dpid) 732 if datapath is None: 733 self.logger.debug('No such datapath: %s', self.speaker.dpid) 734 return 735 736 client = network.clients.get(ev.path.nlri.mac_addr, None) 737 if client is None: 738 self.logger.debug('No such client: %s', ev.path.nlri.mac_addr) 739 return 740 741 self._del_l2_switching_flow( 742 datapath=datapath, 743 tag=network.vni, 744 eth_dst=ev.path.nlri.mac_addr) 745 746 self._del_arp_reply_flow( 747 datapath=datapath, 748 tag=network.vni, 749 arp_tpa=ev.path.nlri.ip_addr) 750 751 network.clients.pop(ev.path.nlri.mac_addr) 752 753 def _evpn_withdraw_incl_mcast_etag_route_handler(self, ev): 754 # Note: For the VLAN Based service, we use RT(=RD) assigned 755 # field as vid. 756 vni = _RouteDistinguisher.from_str(ev.path.nlri.route_dist).assigned 757 758 network = self.networks.get(vni, None) 759 if network is None: 760 self.logger.debug('No such VNI registered: %s', vni) 761 return 762 763 datapath = self._get_datapath(self.speaker.dpid) 764 if datapath is None: 765 self.logger.debug('No such datapath: %s', self.speaker.dpid) 766 return 767 768 vxlan_port = self._get_vxlan_port( 769 dpid=self.speaker.dpid, 770 remote_ip=ev.nexthop, 771 key=vni) 772 if vxlan_port is None: 773 self.logger.debug('No such VXLAN port: %s', 774 'vxlan_%s_%s' % (ev.nexthop, vni)) 775 return 776 777 self._del_network_ingress_flow( 778 datapath=datapath, 779 in_port=vxlan_port) 780 781 vxlan_port = self._del_vxlan_port( 782 dpid=self.speaker.dpid, 783 remote_ip=ev.nexthop, 784 key=vni) 785 if vxlan_port is None: 786 self.logger.debug('Cannot delete VXLAN port: %s', 787 'vxlan_%s_%s' % (ev.nexthop, vni)) 788 return 789 790 def _evpn_withdraw_route_handler(self, ev): 791 if ev.path.nlri.type == EvpnNLRI.MAC_IP_ADVERTISEMENT: 792 self._evpn_withdraw_mac_ip_adv_route_handler(ev) 793 elif ev.path.nlri.type == EvpnNLRI.INCLUSIVE_MULTICAST_ETHERNET_TAG: 794 self._evpn_withdraw_incl_mcast_etag_route_handler(ev) 795 796 def _best_path_change_handler(self, ev): 797 if not isinstance(ev.path, EvpnPath): 798 # Ignores non-EVPN routes 799 return 800 elif ev.nexthop == self.speaker.router_id: 801 # Ignore local connected routes 802 return 803 elif ev.is_withdraw: 804 self._evpn_withdraw_route_handler(ev) 805 else: 806 self._evpn_route_handler(ev) 807 808 def _peer_down_handler(self, remote_ip, remote_as): 809 neighbor = self.speaker.neighbors.get(remote_ip, None) 810 if neighbor is None: 811 self.logger.debug('No such neighbor: remote_ip=%s, remote_as=%s', 812 remote_ip, remote_as) 813 return 814 815 neighbor.state = 'down' 816 817 def _peer_up_handler(self, remote_ip, remote_as): 818 neighbor = self.speaker.neighbors.get(remote_ip, None) 819 if neighbor is None: 820 self.logger.debug('No such neighbor: remote_ip=%s, remote_as=%s', 821 remote_ip, remote_as) 822 return 823 824 neighbor.state = 'up' 825 826 # API methods for REST controller 827 828 def add_speaker(self, dpid, as_number, router_id): 829 # Check if the datapath for the specified dpid exist or not 830 datapath = self._get_datapath(dpid) 831 if datapath is None: 832 raise DatapathNotFound(dpid=dpid) 833 834 self.speaker = EvpnSpeaker( 835 dpid=dpid, 836 as_number=as_number, 837 router_id=router_id, 838 best_path_change_handler=self._best_path_change_handler, 839 peer_down_handler=self._peer_down_handler, 840 peer_up_handler=self._peer_up_handler) 841 842 return {self.speaker.router_id: self.speaker.to_jsondict()} 843 844 def get_speaker(self): 845 if self.speaker is None: 846 return BGPSpeakerNotFound() 847 848 return {self.speaker.router_id: self.speaker.to_jsondict()} 849 850 def del_speaker(self): 851 if self.speaker is None: 852 return BGPSpeakerNotFound() 853 854 for vni in list(self.networks.keys()): 855 self.del_network(vni=vni) 856 857 for address in list(self.speaker.neighbors.keys()): 858 self.del_neighbor(address=address) 859 860 self.speaker.shutdown() 861 speaker = self.speaker 862 self.speaker = None 863 864 return {speaker.router_id: speaker.to_jsondict()} 865 866 def add_neighbor(self, address, remote_as): 867 if self.speaker is None: 868 raise BGPSpeakerNotFound() 869 870 self.speaker.neighbor_add( 871 address=address, 872 remote_as=remote_as, 873 enable_evpn=True) 874 875 neighbor = EvpnNeighbor( 876 address=address, 877 remote_as=remote_as) 878 self.speaker.neighbors[address] = neighbor 879 880 return {address: neighbor.to_jsondict()} 881 882 def get_neighbors(self, address=None): 883 if self.speaker is None: 884 raise BGPSpeakerNotFound() 885 886 if address is not None: 887 neighbor = self.speaker.neighbors.get(address, None) 888 if neighbor is None: 889 raise NeighborNotFound(address=address) 890 return {address: neighbor.to_jsondict()} 891 892 neighbors = {} 893 for address, neighbor in self.speaker.neighbors.items(): 894 neighbors[address] = neighbor.to_jsondict() 895 896 return neighbors 897 898 def del_neighbor(self, address): 899 if self.speaker is None: 900 raise BGPSpeakerNotFound() 901 902 neighbor = self.speaker.neighbors.get(address, None) 903 if neighbor is None: 904 raise NeighborNotFound(address=address) 905 906 for network in self.networks.values(): 907 for mac, client in list(network.clients.items()): 908 if client.next_hop == address: 909 network.clients.pop(mac) 910 911 self.speaker.neighbor_del(address=address) 912 913 neighbor = self.speaker.neighbors.pop(address) 914 915 return {address: neighbor.to_jsondict()} 916 917 def add_network(self, vni): 918 if self.speaker is None: 919 raise BGPSpeakerNotFound() 920 921 # Constructs type 0 RD with as_number and vni 922 route_dist = "%s:%d" % (self.speaker.as_number, vni) 923 924 self.speaker.vrf_add( 925 route_dist=route_dist, 926 import_rts=[route_dist], 927 export_rts=[route_dist], 928 route_family=RF_L2_EVPN) 929 930 # Note: For the VLAN Based service, ethernet_tag_id 931 # must be set to zero. 932 self.speaker.evpn_prefix_add( 933 route_type=EVPN_MULTICAST_ETAG_ROUTE, 934 route_dist=route_dist, 935 ethernet_tag_id=vni, 936 ip_addr=self.speaker.router_id, 937 next_hop=self.speaker.router_id) 938 939 network = EvpnNetwork( 940 vni=vni, 941 route_dist=route_dist, 942 ethernet_tag_id=0) 943 self.networks[vni] = network 944 945 return {vni: network.to_jsondict()} 946 947 def get_networks(self, vni=None): 948 if self.speaker is None: 949 raise BGPSpeakerNotFound() 950 951 if vni is not None: 952 network = self.networks.get(vni, None) 953 if network is None: 954 raise VniNotFound(vni=vni) 955 return {vni: network.to_jsondict()} 956 957 networks = {} 958 for vni, network in self.networks.items(): 959 networks[vni] = network.to_jsondict() 960 961 return networks 962 963 def del_network(self, vni): 964 if self.speaker is None: 965 raise BGPSpeakerNotFound() 966 967 datapath = self._get_datapath(self.speaker.dpid) 968 if datapath is None: 969 raise DatapathNotFound(dpid=self.speaker.dpid) 970 971 network = self.networks.get(vni, None) 972 if network is None: 973 raise VniNotFound(vni=vni) 974 975 for client in network.get_clients(next_hop=self.speaker.router_id): 976 self.del_client( 977 vni=vni, 978 mac=client.mac) 979 980 self._del_network_egress_flow( 981 datapath=datapath, 982 tag=vni) 983 984 for address in self.speaker.neighbors: 985 self._del_vxlan_port( 986 dpid=self.speaker.dpid, 987 remote_ip=address, 988 key=vni) 989 990 self.speaker.evpn_prefix_del( 991 route_type=EVPN_MULTICAST_ETAG_ROUTE, 992 route_dist=network.route_dist, 993 ethernet_tag_id=vni, 994 ip_addr=self.speaker.router_id) 995 996 self.speaker.vrf_del(route_dist=network.route_dist) 997 998 network = self.networks.pop(vni) 999 1000 return {vni: network.to_jsondict()} 1001 1002 def add_client(self, vni, port, mac, ip): 1003 if self.speaker is None: 1004 raise BGPSpeakerNotFound() 1005 1006 datapath = self._get_datapath(self.speaker.dpid) 1007 if datapath is None: 1008 raise DatapathNotFound(dpid=self.speaker.dpid) 1009 1010 network = self.networks.get(vni, None) 1011 if network is None: 1012 raise VniNotFound(vni=vni) 1013 1014 port = self._get_ofport(self.speaker.dpid, port) 1015 if port is None: 1016 try: 1017 port = to_int(port) 1018 except ValueError: 1019 raise OFPortNotFound(port_name=port) 1020 1021 self._add_network_ingress_flow( 1022 datapath=datapath, 1023 tag=network.vni, 1024 in_port=port, 1025 eth_src=mac) 1026 1027 self._add_l2_switching_flow( 1028 datapath=datapath, 1029 tag=network.vni, 1030 eth_dst=mac, 1031 out_port=port) 1032 1033 # Note: For the VLAN Based service, ethernet_tag_id 1034 # must be set to zero. 1035 self.speaker.evpn_prefix_add( 1036 route_type=EVPN_MAC_IP_ADV_ROUTE, 1037 route_dist=network.route_dist, 1038 esi=0, 1039 ethernet_tag_id=0, 1040 mac_addr=mac, 1041 ip_addr=ip, 1042 vni=vni, 1043 next_hop=self.speaker.router_id, 1044 tunnel_type='vxlan') 1045 1046 # Stores local client info 1047 client = EvpnClient( 1048 port=port, 1049 mac=mac, 1050 ip=ip, 1051 next_hop=self.speaker.router_id) 1052 network.clients[mac] = client 1053 1054 return {vni: client.to_jsondict()} 1055 1056 def del_client(self, vni, mac): 1057 if self.speaker is None: 1058 raise BGPSpeakerNotFound() 1059 1060 datapath = self._get_datapath(self.speaker.dpid) 1061 if datapath is None: 1062 raise DatapathNotFound(dpid=self.speaker.dpid) 1063 1064 network = self.networks.get(vni, None) 1065 if network is None: 1066 raise VniNotFound(vni=vni) 1067 1068 client = network.clients.get(mac, None) 1069 if client is None: 1070 raise ClientNotFound(mac=mac) 1071 elif client.next_hop != self.speaker.router_id: 1072 raise ClientNotLocal(mac=mac) 1073 1074 self._del_network_ingress_flow( 1075 datapath=datapath, 1076 in_port=client.port, 1077 eth_src=mac) 1078 1079 self._del_l2_switching_flow( 1080 datapath=datapath, 1081 tag=network.vni, 1082 eth_dst=mac) 1083 1084 # Note: For the VLAN Based service, ethernet_tag_id 1085 # must be set to zero. 1086 self.speaker.evpn_prefix_del( 1087 route_type=EVPN_MAC_IP_ADV_ROUTE, 1088 route_dist=network.route_dist, 1089 esi=0, 1090 ethernet_tag_id=0, 1091 mac_addr=mac, 1092 ip_addr=client.ip) 1093 1094 client = network.clients.pop(mac) 1095 1096 return {vni: client.to_jsondict()} 1097 1098 1099def post_method(keywords): 1100 def _wrapper(method): 1101 def __wrapper(self, req, **kwargs): 1102 try: 1103 try: 1104 body = req.json if req.body else {} 1105 except ValueError: 1106 raise ValueError('Invalid syntax %s', req.body) 1107 kwargs.update(body) 1108 for key, converter in keywords.items(): 1109 value = kwargs.get(key, None) 1110 if value is None: 1111 raise ValueError('%s not specified' % key) 1112 kwargs[key] = converter(value) 1113 except ValueError as e: 1114 return Response(content_type='application/json', 1115 body={"error": str(e)}, status=400) 1116 try: 1117 return method(self, **kwargs) 1118 except Exception as e: 1119 status = 500 1120 body = { 1121 "error": str(e), 1122 "status": status, 1123 } 1124 return Response(content_type='application/json', 1125 body=json.dumps(body), status=status) 1126 __wrapper.__doc__ = method.__doc__ 1127 return __wrapper 1128 return _wrapper 1129 1130 1131def get_method(keywords=None): 1132 keywords = keywords or {} 1133 1134 def _wrapper(method): 1135 def __wrapper(self, _, **kwargs): 1136 try: 1137 for key, converter in keywords.items(): 1138 value = kwargs.get(key, None) 1139 if value is None: 1140 continue 1141 kwargs[key] = converter(value) 1142 except ValueError as e: 1143 return Response(content_type='application/json', 1144 body={"error": str(e)}, status=400) 1145 try: 1146 return method(self, **kwargs) 1147 except Exception as e: 1148 status = 500 1149 body = { 1150 "error": str(e), 1151 "status": status, 1152 } 1153 return Response(content_type='application/json', 1154 body=json.dumps(body), status=status) 1155 __wrapper.__doc__ = method.__doc__ 1156 return __wrapper 1157 return _wrapper 1158 1159 1160delete_method = get_method 1161 1162 1163class RestVtepController(ControllerBase): 1164 1165 def __init__(self, req, link, data, **config): 1166 super(RestVtepController, self).__init__(req, link, data, **config) 1167 self.vtep_app = data[RestVtep.__name__] 1168 self.logger = self.vtep_app.logger 1169 1170 @route(API_NAME, '/vtep/speakers', methods=['POST']) 1171 @post_method( 1172 keywords={ 1173 "dpid": to_int, 1174 "as_number": to_int, 1175 "router_id": str, 1176 }) 1177 def add_speaker(self, **kwargs): 1178 """ 1179 Creates a new BGPSpeaker instance. 1180 1181 Usage: 1182 1183 ======= ================ 1184 Method URI 1185 ======= ================ 1186 POST /vtep/speakers 1187 ======= ================ 1188 1189 Request parameters: 1190 1191 ========== ============================================ 1192 Attribute Description 1193 ========== ============================================ 1194 dpid ID of Datapath binding to speaker. (e.g. 1) 1195 as_number AS number. (e.g. 65000) 1196 router_id Router ID. (e.g. "172.17.0.1") 1197 ========== ============================================ 1198 1199 Example:: 1200 1201 $ curl -X POST -d '{ 1202 "dpid": 1, 1203 "as_number": 65000, 1204 "router_id": "172.17.0.1" 1205 }' http://localhost:8080/vtep/speakers | python -m json.tool 1206 1207 :: 1208 1209 { 1210 "172.17.0.1": { 1211 "EvpnSpeaker": { 1212 "as_number": 65000, 1213 "dpid": 1, 1214 "neighbors": {}, 1215 "router_id": "172.17.0.1" 1216 } 1217 } 1218 } 1219 """ 1220 try: 1221 body = self.vtep_app.add_speaker(**kwargs) 1222 except DatapathNotFound as e: 1223 return e.to_response(status=404) 1224 1225 return Response(content_type='application/json', 1226 body=json.dumps(body)) 1227 1228 @route(API_NAME, '/vtep/speakers', methods=['GET']) 1229 @get_method() 1230 def get_speakers(self, **kwargs): 1231 """ 1232 Gets the info of BGPSpeaker instance. 1233 1234 Usage: 1235 1236 ======= ================ 1237 Method URI 1238 ======= ================ 1239 GET /vtep/speakers 1240 ======= ================ 1241 1242 Example:: 1243 1244 $ curl -X GET http://localhost:8080/vtep/speakers | 1245 python -m json.tool 1246 1247 :: 1248 1249 { 1250 "172.17.0.1": { 1251 "EvpnSpeaker": { 1252 "as_number": 65000, 1253 "dpid": 1, 1254 "neighbors": { 1255 "172.17.0.2": { 1256 "EvpnNeighbor": { 1257 "address": "172.17.0.2", 1258 "remote_as": 65000, 1259 "state": "up" 1260 } 1261 } 1262 }, 1263 "router_id": "172.17.0.1" 1264 } 1265 } 1266 } 1267 """ 1268 try: 1269 body = self.vtep_app.get_speaker() 1270 except BGPSpeakerNotFound as e: 1271 return e.to_response(status=404) 1272 1273 return Response(content_type='application/json', 1274 body=json.dumps(body)) 1275 1276 @route(API_NAME, '/vtep/speakers', methods=['DELETE']) 1277 @delete_method() 1278 def del_speaker(self, **kwargs): 1279 """ 1280 Shutdowns BGPSpeaker instance. 1281 1282 Usage: 1283 1284 ======= ================ 1285 Method URI 1286 ======= ================ 1287 DELETE /vtep/speakers 1288 ======= ================ 1289 1290 Example:: 1291 1292 $ curl -X DELETE http://localhost:8080/vtep/speakers | 1293 python -m json.tool 1294 1295 :: 1296 1297 { 1298 "172.17.0.1": { 1299 "EvpnSpeaker": { 1300 "as_number": 65000, 1301 "dpid": 1, 1302 "neighbors": {}, 1303 "router_id": "172.17.0.1" 1304 } 1305 } 1306 } 1307 """ 1308 try: 1309 body = self.vtep_app.del_speaker() 1310 except BGPSpeakerNotFound as e: 1311 return e.to_response(status=404) 1312 1313 return Response(content_type='application/json', 1314 body=json.dumps(body)) 1315 1316 @route(API_NAME, '/vtep/neighbors', methods=['POST']) 1317 @post_method( 1318 keywords={ 1319 "address": str, 1320 "remote_as": to_int, 1321 }) 1322 def add_neighbor(self, **kwargs): 1323 """ 1324 Registers a new neighbor to the speaker. 1325 1326 Usage: 1327 1328 ======= ======================== 1329 Method URI 1330 ======= ======================== 1331 POST /vtep/neighbors 1332 ======= ======================== 1333 1334 Request parameters: 1335 1336 ========== ================================================ 1337 Attribute Description 1338 ========== ================================================ 1339 address IP address of neighbor. (e.g. "172.17.0.2") 1340 remote_as AS number of neighbor. (e.g. 65000) 1341 ========== ================================================ 1342 1343 Example:: 1344 1345 $ curl -X POST -d '{ 1346 "address": "172.17.0.2", 1347 "remote_as": 65000 1348 }' http://localhost:8080/vtep/neighbors | 1349 python -m json.tool 1350 1351 :: 1352 1353 { 1354 "172.17.0.2": { 1355 "EvpnNeighbor": { 1356 "address": "172.17.0.2", 1357 "remote_as": 65000, 1358 "state": "down" 1359 } 1360 } 1361 } 1362 """ 1363 try: 1364 body = self.vtep_app.add_neighbor(**kwargs) 1365 except BGPSpeakerNotFound as e: 1366 return e.to_response(status=400) 1367 1368 return Response(content_type='application/json', 1369 body=json.dumps(body)) 1370 1371 def _get_neighbors(self, **kwargs): 1372 try: 1373 body = self.vtep_app.get_neighbors(**kwargs) 1374 except (BGPSpeakerNotFound, NeighborNotFound) as e: 1375 return e.to_response(status=404) 1376 1377 return Response(content_type='application/json', 1378 body=json.dumps(body)) 1379 1380 @route(API_NAME, '/vtep/neighbors', methods=['GET']) 1381 @get_method() 1382 def get_neighbors(self, **kwargs): 1383 """ 1384 Gets a list of all neighbors. 1385 1386 Usage: 1387 1388 ======= ======================== 1389 Method URI 1390 ======= ======================== 1391 GET /vtep/neighbors 1392 ======= ======================== 1393 1394 Example:: 1395 1396 $ curl -X GET http://localhost:8080/vtep/neighbors | 1397 python -m json.tool 1398 1399 :: 1400 1401 { 1402 "172.17.0.2": { 1403 "EvpnNeighbor": { 1404 "address": "172.17.0.2", 1405 "remote_as": 65000, 1406 "state": "up" 1407 } 1408 } 1409 } 1410 """ 1411 return self._get_neighbors(**kwargs) 1412 1413 @route(API_NAME, '/vtep/neighbors/{address}', methods=['GET']) 1414 @get_method( 1415 keywords={ 1416 "address": str, 1417 }) 1418 def get_neighbor(self, **kwargs): 1419 """ 1420 Gets the neighbor for the specified address. 1421 1422 Usage: 1423 1424 ======= ================================== 1425 Method URI 1426 ======= ================================== 1427 GET /vtep/neighbors/{address} 1428 ======= ================================== 1429 1430 Request parameters: 1431 1432 ========== ================================================ 1433 Attribute Description 1434 ========== ================================================ 1435 address IP address of neighbor. (e.g. "172.17.0.2") 1436 ========== ================================================ 1437 1438 Example:: 1439 1440 $ curl -X GET http://localhost:8080/vtep/neighbors/172.17.0.2 | 1441 python -m json.tool 1442 1443 :: 1444 1445 { 1446 "172.17.0.2": { 1447 "EvpnNeighbor": { 1448 "address": "172.17.0.2", 1449 "remote_as": 65000, 1450 "state": "up" 1451 } 1452 } 1453 } 1454 """ 1455 return self._get_neighbors(**kwargs) 1456 1457 @route(API_NAME, '/vtep/neighbors/{address}', methods=['DELETE']) 1458 @delete_method( 1459 keywords={ 1460 "address": str, 1461 }) 1462 def del_neighbor(self, **kwargs): 1463 """ 1464 Unregister the specified neighbor from the speaker. 1465 1466 Usage: 1467 1468 ======= ================================== 1469 Method URI 1470 ======= ================================== 1471 DELETE /vtep/speaker/neighbors/{address} 1472 ======= ================================== 1473 1474 Request parameters: 1475 1476 ========== ================================================ 1477 Attribute Description 1478 ========== ================================================ 1479 address IP address of neighbor. (e.g. "172.17.0.2") 1480 ========== ================================================ 1481 1482 Example:: 1483 1484 $ curl -X DELETE http://localhost:8080/vtep/speaker/neighbors/172.17.0.2 | 1485 python -m json.tool 1486 1487 :: 1488 1489 { 1490 "172.17.0.2": { 1491 "EvpnNeighbor": { 1492 "address": "172.17.0.2", 1493 "remote_as": 65000, 1494 "state": "up" 1495 } 1496 } 1497 } 1498 """ 1499 try: 1500 body = self.vtep_app.del_neighbor(**kwargs) 1501 except (BGPSpeakerNotFound, NeighborNotFound) as e: 1502 return e.to_response(status=404) 1503 1504 return Response(content_type='application/json', 1505 body=json.dumps(body)) 1506 1507 @route(API_NAME, '/vtep/networks', methods=['POST']) 1508 @post_method( 1509 keywords={ 1510 "vni": to_int, 1511 }) 1512 def add_network(self, **kwargs): 1513 """ 1514 Defines a new network. 1515 1516 Usage: 1517 1518 ======= =============== 1519 Method URI 1520 ======= =============== 1521 POST /vtep/networks 1522 ======= =============== 1523 1524 Request parameters: 1525 1526 ================ ======================================== 1527 Attribute Description 1528 ================ ======================================== 1529 vni Virtual Network Identifier. (e.g. 10) 1530 ================ ======================================== 1531 1532 Example:: 1533 1534 $ curl -X POST -d '{ 1535 "vni": 10 1536 }' http://localhost:8080/vtep/networks | python -m json.tool 1537 1538 :: 1539 1540 { 1541 "10": { 1542 "EvpnNetwork": { 1543 "clients": {}, 1544 "ethernet_tag_id": 0, 1545 "route_dist": "65000:10", 1546 "vni": 10 1547 } 1548 } 1549 } 1550 """ 1551 try: 1552 body = self.vtep_app.add_network(**kwargs) 1553 except BGPSpeakerNotFound as e: 1554 return e.to_response(status=404) 1555 1556 return Response(content_type='application/json', 1557 body=json.dumps(body)) 1558 1559 def _get_networks(self, **kwargs): 1560 try: 1561 body = self.vtep_app.get_networks(**kwargs) 1562 except (BGPSpeakerNotFound, VniNotFound) as e: 1563 return e.to_response(status=404) 1564 1565 return Response(content_type='application/json', 1566 body=json.dumps(body)) 1567 1568 @route(API_NAME, '/vtep/networks', methods=['GET']) 1569 @get_method() 1570 def get_networks(self, **kwargs): 1571 """ 1572 Gets a list of all networks. 1573 1574 Usage: 1575 1576 ======= =============== 1577 Method URI 1578 ======= =============== 1579 GET /vtep/networks 1580 ======= =============== 1581 1582 Example:: 1583 1584 $ curl -X GET http://localhost:8080/vtep/networks | 1585 python -m json.tool 1586 1587 :: 1588 1589 { 1590 "10": { 1591 "EvpnNetwork": { 1592 "clients": { 1593 "aa:bb:cc:dd:ee:ff": { 1594 "EvpnClient": { 1595 "ip": "10.0.0.1", 1596 "mac": "aa:bb:cc:dd:ee:ff", 1597 "next_hop": "172.17.0.1", 1598 "port": 1 1599 } 1600 } 1601 }, 1602 "ethernet_tag_id": 0, 1603 "route_dist": "65000:10", 1604 "vni": 10 1605 } 1606 } 1607 } 1608 """ 1609 return self._get_networks(**kwargs) 1610 1611 @route(API_NAME, '/vtep/networks/{vni}', methods=['GET']) 1612 @get_method( 1613 keywords={ 1614 "vni": to_int, 1615 }) 1616 def get_network(self, **kwargs): 1617 """ 1618 Gets the network for the specified VNI. 1619 1620 Usage: 1621 1622 ======= ===================== 1623 Method URI 1624 ======= ===================== 1625 GET /vtep/networks/{vni} 1626 ======= ===================== 1627 1628 Request parameters: 1629 1630 ================ ======================================== 1631 Attribute Description 1632 ================ ======================================== 1633 vni Virtual Network Identifier. (e.g. 10) 1634 ================ ======================================== 1635 1636 Example:: 1637 1638 $ curl -X GET http://localhost:8080/vtep/networks/10 | 1639 python -m json.tool 1640 1641 :: 1642 1643 { 1644 "10": { 1645 "EvpnNetwork": { 1646 "clients": { 1647 "aa:bb:cc:dd:ee:ff": { 1648 "EvpnClient": { 1649 "ip": "10.0.0.1", 1650 "mac": "aa:bb:cc:dd:ee:ff", 1651 "next_hop": "172.17.0.1", 1652 "port": 1 1653 } 1654 } 1655 }, 1656 "ethernet_tag_id": 0, 1657 "route_dist": "65000:10", 1658 "vni": 10 1659 } 1660 } 1661 } 1662 """ 1663 return self._get_networks(**kwargs) 1664 1665 @route(API_NAME, '/vtep/networks/{vni}', methods=['DELETE']) 1666 @delete_method( 1667 keywords={ 1668 "vni": to_int, 1669 }) 1670 def del_network(self, **kwargs): 1671 """ 1672 Deletes the network for the specified VNI. 1673 1674 Usage: 1675 1676 ======= ===================== 1677 Method URI 1678 ======= ===================== 1679 DELETE /vtep/networks/{vni} 1680 ======= ===================== 1681 1682 Request parameters: 1683 1684 ================ ======================================== 1685 Attribute Description 1686 ================ ======================================== 1687 vni Virtual Network Identifier. (e.g. 10) 1688 ================ ======================================== 1689 1690 Example:: 1691 1692 $ curl -X DELETE http://localhost:8080/vtep/networks/10 | 1693 python -m json.tool 1694 1695 :: 1696 1697 { 1698 "10": { 1699 "EvpnNetwork": { 1700 "ethernet_tag_id": 10, 1701 "clients": [ 1702 { 1703 "EvpnClient": { 1704 "ip": "10.0.0.11", 1705 "mac": "e2:b1:0c:ba:42:ed", 1706 "port": 1 1707 } 1708 } 1709 ], 1710 "route_dist": "65000:100", 1711 "vni": 10 1712 } 1713 } 1714 } 1715 """ 1716 try: 1717 body = self.vtep_app.del_network(**kwargs) 1718 except (BGPSpeakerNotFound, DatapathNotFound, VniNotFound) as e: 1719 return e.to_response(status=404) 1720 1721 return Response(content_type='application/json', 1722 body=json.dumps(body)) 1723 1724 @route(API_NAME, '/vtep/networks/{vni}/clients', methods=['POST']) 1725 @post_method( 1726 keywords={ 1727 "vni": to_int, 1728 "port": str, 1729 "mac": str, 1730 "ip": str, 1731 }) 1732 def add_client(self, **kwargs): 1733 """ 1734 Registers a new client to the specified network. 1735 1736 Usage: 1737 1738 ======= ============================= 1739 Method URI 1740 ======= ============================= 1741 POST /vtep/networks/{vni}/clients 1742 ======= ============================= 1743 1744 Request parameters: 1745 1746 =========== =============================================== 1747 Attribute Description 1748 =========== =============================================== 1749 vni Virtual Network Identifier. (e.g. 10) 1750 port Port number to connect client. 1751 For convenience, port name can be specified 1752 and automatically translated to port number. 1753 (e.g. "s1-eth1" or 1) 1754 mac Client MAC address to register. 1755 (e.g. "aa:bb:cc:dd:ee:ff") 1756 ip Client IP address. (e.g. "10.0.0.1") 1757 =========== =============================================== 1758 1759 Example:: 1760 1761 $ curl -X POST -d '{ 1762 "port": "s1-eth1", 1763 "mac": "aa:bb:cc:dd:ee:ff", 1764 "ip": "10.0.0.1" 1765 }' http://localhost:8080/vtep/networks/10/clients | 1766 python -m json.tool 1767 1768 :: 1769 1770 { 1771 "10": { 1772 "EvpnClient": { 1773 "ip": "10.0.0.1", 1774 "mac": "aa:bb:cc:dd:ee:ff", 1775 "next_hop": "172.17.0.1", 1776 "port": 1 1777 } 1778 } 1779 } 1780 """ 1781 try: 1782 body = self.vtep_app.add_client(**kwargs) 1783 except (BGPSpeakerNotFound, DatapathNotFound, 1784 VniNotFound, OFPortNotFound) as e: 1785 return e.to_response(status=404) 1786 1787 return Response(content_type='application/json', 1788 body=json.dumps(body)) 1789 1790 @route(API_NAME, '/vtep/networks/{vni}/clients/{mac}', methods=['DELETE']) 1791 @delete_method( 1792 keywords={ 1793 "vni": to_int, 1794 "mac": str, 1795 }) 1796 def del_client(self, **kwargs): 1797 """ 1798 Registers a new client to the specified network. 1799 1800 Usage: 1801 1802 ======= =================================== 1803 Method URI 1804 ======= =================================== 1805 DELETE /vtep/networks/{vni}/clients/{mac} 1806 ======= =================================== 1807 1808 Request parameters: 1809 1810 =========== =============================================== 1811 Attribute Description 1812 =========== =============================================== 1813 vni Virtual Network Identifier. (e.g. 10) 1814 mac Client MAC address to register. 1815 =========== =============================================== 1816 1817 Example:: 1818 1819 $ curl -X DELETE http://localhost:8080/vtep/networks/10/clients/aa:bb:cc:dd:ee:ff | 1820 python -m json.tool 1821 1822 :: 1823 1824 { 1825 "10": { 1826 "EvpnClient": { 1827 "ip": "10.0.0.1", 1828 "mac": "aa:bb:cc:dd:ee:ff", 1829 "next_hop": "172.17.0.1", 1830 "port": 1 1831 } 1832 } 1833 } 1834 """ 1835 try: 1836 body = self.vtep_app.del_client(**kwargs) 1837 except (BGPSpeakerNotFound, DatapathNotFound, 1838 VniNotFound, ClientNotFound, ClientNotLocal) as e: 1839 return Response(body=str(e), status=500) 1840 1841 return Response(content_type='application/json', 1842 body=json.dumps(body)) 1843