# Copyright (C) 2016 Nippon Telegraph and Telephone Corporation. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. """ This sample application performs as VTEP for EVPN VXLAN and constructs a Single Subnet per EVI corresponding to the VLAN Based service in [RFC7432]. .. NOTE:: This app will invoke OVSDB request to the switches. Please set the manager address before calling the API of this app. :: $ sudo ovs-vsctl set-manager ptcp:6640 $ sudo ovs-vsctl show ...(snip) Manager "ptcp:6640" ...(snip) Usage Example ============= Environment ----------- This example supposes the following environment:: Host A (172.17.0.1) Host B (172.17.0.2) +--------------------+ +--------------------+ | OSKen1 | --- BGP(EVPN) --- | OSKen2 | +--------------------+ +--------------------+ | | +--------------------+ +--------------------+ | s1 (OVS) | ===== vxlan ===== | s2 (OVS) | +--------------------+ +--------------------+ (s1-eth1) (s1-eth2) (s2-eth1) (s2-eth2) | | | | +--------+ +--------+ +--------+ +--------+ | s1h1 | | s1h2 | | s2h1 | | s2h2 | +--------+ +--------+ +--------+ +--------+ Configuration steps ------------------- 1. Creates a new BGPSpeaker instance on each host. On Host A:: (Host A)$ curl -X POST -d '{ "dpid": 1, "as_number": 65000, "router_id": "172.17.0.1" }' http://localhost:8080/vtep/speakers | python -m json.tool On Host B:: (Host B)$ curl -X POST -d '{ "dpid": 1, "as_number": 65000, "router_id": "172.17.0.2" }' http://localhost:8080/vtep/speakers | python -m json.tool 2. Registers the neighbor for the speakers on each host. On Host A:: (Host A)$ curl -X POST -d '{ "address": "172.17.0.2", "remote_as": 65000 }' http://localhost:8080/vtep/neighbors | python -m json.tool On Host B:: (Host B)$ curl -X POST -d '{ "address": "172.17.0.1", "remote_as": 65000 }' http://localhost:8080/vtep/neighbors | python -m json.tool 3. Defines a new VXLAN network(VNI=10) on the Host A/B. On Host A:: (Host A)$ curl -X POST -d '{ "vni": 10 }' http://localhost:8080/vtep/networks | python -m json.tool On Host B:: (Host B)$ curl -X POST -d '{ "vni": 10 }' http://localhost:8080/vtep/networks | python -m json.tool 4. Registers the clients to the VXLAN network. For "s1h1"(ip="10.0.0.11", mac="aa:bb:cc:00:00:11") on Host A:: (Host A)$ curl -X POST -d '{ "port": "s1-eth1", "mac": "aa:bb:cc:00:00:11", "ip": "10.0.0.11" } ' http://localhost:8080/vtep/networks/10/clients | python -m json.tool For "s2h1"(ip="10.0.0.21", mac="aa:bb:cc:00:00:21") on Host B:: (Host B)$ curl -X POST -d '{ "port": "s2-eth1", "mac": "aa:bb:cc:00:00:21", "ip": "10.0.0.21" } ' http://localhost:8080/vtep/networks/10/clients | python -m json.tool Testing ------- If BGP (EVPN) connection between OSKen1 and OSKen2 has been established, pings between the client s1h1 and s2h1 should work. :: (s1h1)$ ping 10.0.0.21 Troubleshooting --------------- If connectivity between s1h1 and s2h1 isn't working, please check the followings. 1. Make sure that Host A and Host B have full network connectivity. :: (Host A)$ ping 172.17.0.2 2. Make sure that BGP(EVPN) connection has been established. :: (Host A)$ curl -X GET http://localhost:8080/vtep/neighbors | python -m json.tool ... { "172.17.0.2": { "EvpnNeighbor": { "address": "172.17.0.2", "remote_as": 65000, "state": "up" # "up" shows the connection established } } } 3. Make sure that BGP(EVPN) routes have been advertised. :: (Host A)$ curl -X GET http://localhost:8080/vtep/networks | python -m json.tool ... { "10": { "EvpnNetwork": { "clients": { "aa:bb:cc:00:00:11": { "EvpnClient": { "ip": "10.0.0.11", "mac": "aa:bb:cc:00:00:11", "next_hop": "172.17.0.1", "port": 1 } }, "aa:bb:cc:00:00:21": { # route for "s2h1" on Host B "EvpnClient": { "ip": "10.0.0.21", "mac": "aa:bb:cc:00:00:21", "next_hop": "172.17.0.2", "port": 3 } } }, "ethernet_tag_id": 0, "route_dist": "65000:10", "vni": 10 } } } 4. Make sure that the IPv6 is enabled on your environment. Some OSKen BGP features require the IPv6 connectivity to bind sockets. Mininet seems to disable IPv6 on its installation. For example:: $ sysctl net.ipv6.conf.all.disable_ipv6 net.ipv6.conf.all.disable_ipv6 = 0 # should NOT be enabled $ grep GRUB_CMDLINE_LINUX_DEFAULT /etc/default/grub # please remove "ipv6.disable=1" and reboot GRUB_CMDLINE_LINUX_DEFAULT="ipv6.disable=1 quiet splash" 5. Make sure that your switch using the OpenFlow version 1.3. This application supports only the OpenFlow version 1.3. For example:: $ ovs-vsctl get Bridge s1 protocols ["OpenFlow13"] .. Note:: At the time of this writing, we use the the following version of OSKen, Open vSwitch and Mininet. :: $ os_ken --version os_ken 4.19 $ ovs-vsctl --version ovs-vsctl (Open vSwitch) 2.5.2 # APT packaged version of Ubuntu 16.04 Compiled Oct 17 2017 16:38:57 DB Schema 7.12.1 $ mn --version 2.2.1 # APT packaged version of Ubuntu 16.04 """ import json from os_ken.app.ofctl import api as ofctl_api from os_ken.app.wsgi import ControllerBase from os_ken.app.wsgi import Response from os_ken.app.wsgi import route from os_ken.app.wsgi import WSGIApplication from os_ken.base import app_manager from os_ken.exception import OSKenException from os_ken.lib.ovs import bridge as ovs_bridge from os_ken.lib.packet import arp from os_ken.lib.packet import ether_types from os_ken.lib.packet.bgp import _RouteDistinguisher from os_ken.lib.packet.bgp import EvpnNLRI from os_ken.lib.stringify import StringifyMixin from os_ken.ofproto import ofproto_v1_3 from os_ken.services.protocols.bgp.bgpspeaker import BGPSpeaker from os_ken.services.protocols.bgp.bgpspeaker import RF_L2_EVPN from os_ken.services.protocols.bgp.bgpspeaker import EVPN_MAC_IP_ADV_ROUTE from os_ken.services.protocols.bgp.bgpspeaker import EVPN_MULTICAST_ETAG_ROUTE from os_ken.services.protocols.bgp.info_base.evpn import EvpnPath API_NAME = 'restvtep' OVSDB_PORT = 6640 # The IANA registered port for OVSDB [RFC7047] PRIORITY_D_PLANE = 1 PRIORITY_ARP_REPLAY = 2 TABLE_ID_INGRESS = 0 TABLE_ID_EGRESS = 1 # Utility functions def to_int(i): return int(str(i), 0) def to_str_list(l): str_list = [] for s in l: str_list.append(str(s)) return str_list # Exception classes related to OpenFlow and OVSDB class RestApiException(OSKenException): def to_response(self, status): body = { "error": str(self), "status": status, } return Response(content_type='application/json', body=json.dumps(body), status=status) class DatapathNotFound(RestApiException): message = 'No such datapath: %(dpid)s' class OFPortNotFound(RestApiException): message = 'No such OFPort: %(port_name)s' # Exception classes related to BGP class BGPSpeakerNotFound(RestApiException): message = 'BGPSpeaker could not be found' class NeighborNotFound(RestApiException): message = 'No such neighbor: %(address)s' class VniNotFound(RestApiException): message = 'No such VNI: %(vni)s' class ClientNotFound(RestApiException): message = 'No such client: %(mac)s' class ClientNotLocal(RestApiException): message = 'Specified client is not local: %(mac)s' # Utility classes related to EVPN class EvpnSpeaker(BGPSpeaker, StringifyMixin): _TYPE = { 'ascii': [ 'router_id', ], } def __init__(self, dpid, as_number, router_id, best_path_change_handler, peer_down_handler, peer_up_handler, neighbors=None): super(EvpnSpeaker, self).__init__( as_number=as_number, router_id=router_id, best_path_change_handler=best_path_change_handler, peer_down_handler=peer_down_handler, peer_up_handler=peer_up_handler, ssh_console=True) self.dpid = dpid self.as_number = as_number self.router_id = router_id self.neighbors = neighbors or {} class EvpnNeighbor(StringifyMixin): _TYPE = { 'ascii': [ 'address', 'state', ], } def __init__(self, address, remote_as, state='down'): super(EvpnNeighbor, self).__init__() self.address = address self.remote_as = remote_as self.state = state class EvpnNetwork(StringifyMixin): _TYPE = { 'ascii': [ 'route_dist', ], } def __init__(self, vni, route_dist, ethernet_tag_id, clients=None): super(EvpnNetwork, self).__init__() self.vni = vni self.route_dist = route_dist self.ethernet_tag_id = ethernet_tag_id self.clients = clients or {} def get_clients(self, **kwargs): l = [] for _, c in self.clients.items(): for k, v in kwargs.items(): if getattr(c, k) != v: break else: l.append(c) return l class EvpnClient(StringifyMixin): _TYPE = { 'ascii': [ 'mac', 'ip', 'next_hop' ], } def __init__(self, port, mac, ip, next_hop): super(EvpnClient, self).__init__() self.port = port self.mac = mac self.ip = ip self.next_hop = next_hop class RestVtep(app_manager.OSKenApp): OFP_VERSIONS = [ofproto_v1_3.OFP_VERSION] _CONTEXTS = {'wsgi': WSGIApplication} def __init__(self, *args, **kwargs): super(RestVtep, self).__init__(*args, **kwargs) wsgi = kwargs['wsgi'] wsgi.register(RestVtepController, {RestVtep.__name__: self}) # EvpnSpeaker instance instantiated later self.speaker = None # OVSBridge instance instantiated later self.ovs = None # Dictionary for retrieving the EvpnNetwork instance by VNI # self.networks = { # : , # ... # } self.networks = {} # Utility methods related to OpenFlow def _get_datapath(self, dpid): return ofctl_api.get_datapath(self, dpid) @staticmethod def _add_flow(datapath, priority, match, instructions, table_id=TABLE_ID_INGRESS): parser = datapath.ofproto_parser mod = parser.OFPFlowMod( datapath=datapath, table_id=table_id, priority=priority, match=match, instructions=instructions) datapath.send_msg(mod) @staticmethod def _del_flow(datapath, priority, match, table_id=TABLE_ID_INGRESS): ofproto = datapath.ofproto parser = datapath.ofproto_parser mod = parser.OFPFlowMod( datapath=datapath, table_id=table_id, command=ofproto.OFPFC_DELETE, priority=priority, out_port=ofproto.OFPP_ANY, out_group=ofproto.OFPG_ANY, match=match) datapath.send_msg(mod) def _add_network_ingress_flow(self, datapath, tag, in_port, eth_src=None): parser = datapath.ofproto_parser if eth_src is None: match = parser.OFPMatch(in_port=in_port) else: match = parser.OFPMatch(in_port=in_port, eth_src=eth_src) instructions = [ parser.OFPInstructionWriteMetadata( metadata=tag, metadata_mask=parser.UINT64_MAX), parser.OFPInstructionGotoTable(1)] self._add_flow(datapath, PRIORITY_D_PLANE, match, instructions) def _del_network_ingress_flow(self, datapath, in_port, eth_src=None): parser = datapath.ofproto_parser if eth_src is None: match = parser.OFPMatch(in_port=in_port) else: match = parser.OFPMatch(in_port=in_port, eth_src=eth_src) self._del_flow(datapath, PRIORITY_D_PLANE, match) def _add_arp_reply_flow(self, datapath, tag, arp_tpa, arp_tha): ofproto = datapath.ofproto parser = datapath.ofproto_parser match = parser.OFPMatch( metadata=(tag, parser.UINT64_MAX), eth_type=ether_types.ETH_TYPE_ARP, arp_op=arp.ARP_REQUEST, arp_tpa=arp_tpa) actions = [ parser.NXActionRegMove( src_field="eth_src", dst_field="eth_dst", n_bits=48), parser.OFPActionSetField(eth_src=arp_tha), parser.OFPActionSetField(arp_op=arp.ARP_REPLY), parser.NXActionRegMove( src_field="arp_sha", dst_field="arp_tha", n_bits=48), parser.NXActionRegMove( src_field="arp_spa", dst_field="arp_tpa", n_bits=32), parser.OFPActionSetField(arp_sha=arp_tha), parser.OFPActionSetField(arp_spa=arp_tpa), parser.OFPActionOutput(ofproto.OFPP_IN_PORT)] instructions = [ parser.OFPInstructionActions( ofproto.OFPIT_APPLY_ACTIONS, actions)] self._add_flow(datapath, PRIORITY_ARP_REPLAY, match, instructions, table_id=TABLE_ID_EGRESS) def _del_arp_reply_flow(self, datapath, tag, arp_tpa): parser = datapath.ofproto_parser match = parser.OFPMatch( metadata=(tag, parser.UINT64_MAX), eth_type=ether_types.ETH_TYPE_ARP, arp_op=arp.ARP_REQUEST, arp_tpa=arp_tpa) self._del_flow(datapath, PRIORITY_ARP_REPLAY, match, table_id=TABLE_ID_EGRESS) def _add_l2_switching_flow(self, datapath, tag, eth_dst, out_port): ofproto = datapath.ofproto parser = datapath.ofproto_parser match = parser.OFPMatch(metadata=(tag, parser.UINT64_MAX), eth_dst=eth_dst) actions = [parser.OFPActionOutput(out_port)] instructions = [ parser.OFPInstructionActions( ofproto.OFPIT_APPLY_ACTIONS, actions)] self._add_flow(datapath, PRIORITY_D_PLANE, match, instructions, table_id=TABLE_ID_EGRESS) def _del_l2_switching_flow(self, datapath, tag, eth_dst): parser = datapath.ofproto_parser match = parser.OFPMatch(metadata=(tag, parser.UINT64_MAX), eth_dst=eth_dst) self._del_flow(datapath, PRIORITY_D_PLANE, match, table_id=TABLE_ID_EGRESS) def _del_network_egress_flow(self, datapath, tag): parser = datapath.ofproto_parser match = parser.OFPMatch(metadata=(tag, parser.UINT64_MAX)) self._del_flow(datapath, PRIORITY_D_PLANE, match, table_id=TABLE_ID_EGRESS) # Utility methods related to OVSDB def _get_ovs_bridge(self, dpid): datapath = self._get_datapath(dpid) if datapath is None: self.logger.debug('No such datapath: %s', dpid) return None ovsdb_addr = 'tcp:%s:%d' % (datapath.address[0], OVSDB_PORT) if (self.ovs is not None and self.ovs.datapath_id == dpid and self.ovs.vsctl.remote == ovsdb_addr): return self.ovs try: self.ovs = ovs_bridge.OVSBridge( CONF=self.CONF, datapath_id=datapath.id, ovsdb_addr=ovsdb_addr) self.ovs.init() except Exception as e: self.logger.exception('Cannot initiate OVSDB connection: %s', e) return None return self.ovs def _get_ofport(self, dpid, port_name): ovs = self._get_ovs_bridge(dpid) if ovs is None: return None try: return ovs.get_ofport(port_name) except Exception as e: self.logger.debug('Cannot get port number for %s: %s', port_name, e) return None def _get_vxlan_port(self, dpid, remote_ip, key): # Searches VXLAN port named 'vxlan__' return self._get_ofport(dpid, 'vxlan_%s_%s' % (remote_ip, key)) def _add_vxlan_port(self, dpid, remote_ip, key): # If VXLAN port already exists, returns OFPort number vxlan_port = self._get_vxlan_port(dpid, remote_ip, key) if vxlan_port is not None: return vxlan_port ovs = self._get_ovs_bridge(dpid) if ovs is None: return None # Adds VXLAN port named 'vxlan__' ovs.add_vxlan_port( name='vxlan_%s_%s' % (remote_ip, key), remote_ip=remote_ip, key=key) # Returns VXLAN port number return self._get_vxlan_port(dpid, remote_ip, key) def _del_vxlan_port(self, dpid, remote_ip, key): ovs = self._get_ovs_bridge(dpid) if ovs is None: return None # If VXLAN port does not exist, returns None vxlan_port = self._get_vxlan_port(dpid, remote_ip, key) if vxlan_port is None: return None # Adds VXLAN port named 'vxlan__' ovs.del_port('vxlan_%s_%s' % (remote_ip, key)) # Returns deleted VXLAN port number return vxlan_port # Event handlers for BGP def _evpn_mac_ip_adv_route_handler(self, ev): network = self.networks.get(ev.path.nlri.vni, None) if network is None: self.logger.debug('No such VNI registered: %s', ev.path.nlri) return datapath = self._get_datapath(self.speaker.dpid) if datapath is None: self.logger.debug('No such datapath: %s', self.speaker.dpid) return vxlan_port = self._add_vxlan_port( dpid=self.speaker.dpid, remote_ip=ev.nexthop, key=ev.path.nlri.vni) if vxlan_port is None: self.logger.debug('Cannot create a new VXLAN port: %s', 'vxlan_%s_%s' % (ev.nexthop, ev.path.nlri.vni)) return self._add_l2_switching_flow( datapath=datapath, tag=network.vni, eth_dst=ev.path.nlri.mac_addr, out_port=vxlan_port) self._add_arp_reply_flow( datapath=datapath, tag=network.vni, arp_tpa=ev.path.nlri.ip_addr, arp_tha=ev.path.nlri.mac_addr) network.clients[ev.path.nlri.mac_addr] = EvpnClient( port=vxlan_port, mac=ev.path.nlri.mac_addr, ip=ev.path.nlri.ip_addr, next_hop=ev.nexthop) def _evpn_incl_mcast_etag_route_handler(self, ev): # Note: For the VLAN Based service, we use RT(=RD) assigned # field as vid. vni = _RouteDistinguisher.from_str(ev.path.nlri.route_dist).assigned network = self.networks.get(vni, None) if network is None: self.logger.debug('No such VNI registered: %s', vni) return datapath = self._get_datapath(self.speaker.dpid) if datapath is None: self.logger.debug('No such datapath: %s', self.speaker.dpid) return vxlan_port = self._add_vxlan_port( dpid=self.speaker.dpid, remote_ip=ev.nexthop, key=vni) if vxlan_port is None: self.logger.debug('Cannot create a new VXLAN port: %s', 'vxlan_%s_%s' % (ev.nexthop, vni)) return self._add_network_ingress_flow( datapath=datapath, tag=vni, in_port=vxlan_port) def _evpn_route_handler(self, ev): if ev.path.nlri.type == EvpnNLRI.MAC_IP_ADVERTISEMENT: self._evpn_mac_ip_adv_route_handler(ev) elif ev.path.nlri.type == EvpnNLRI.INCLUSIVE_MULTICAST_ETHERNET_TAG: self._evpn_incl_mcast_etag_route_handler(ev) def _evpn_withdraw_mac_ip_adv_route_handler(self, ev): network = self.networks.get(ev.path.nlri.vni, None) if network is None: self.logger.debug('No such VNI registered: %s', ev.path.nlri) return datapath = self._get_datapath(self.speaker.dpid) if datapath is None: self.logger.debug('No such datapath: %s', self.speaker.dpid) return client = network.clients.get(ev.path.nlri.mac_addr, None) if client is None: self.logger.debug('No such client: %s', ev.path.nlri.mac_addr) return self._del_l2_switching_flow( datapath=datapath, tag=network.vni, eth_dst=ev.path.nlri.mac_addr) self._del_arp_reply_flow( datapath=datapath, tag=network.vni, arp_tpa=ev.path.nlri.ip_addr) network.clients.pop(ev.path.nlri.mac_addr) def _evpn_withdraw_incl_mcast_etag_route_handler(self, ev): # Note: For the VLAN Based service, we use RT(=RD) assigned # field as vid. vni = _RouteDistinguisher.from_str(ev.path.nlri.route_dist).assigned network = self.networks.get(vni, None) if network is None: self.logger.debug('No such VNI registered: %s', vni) return datapath = self._get_datapath(self.speaker.dpid) if datapath is None: self.logger.debug('No such datapath: %s', self.speaker.dpid) return vxlan_port = self._get_vxlan_port( dpid=self.speaker.dpid, remote_ip=ev.nexthop, key=vni) if vxlan_port is None: self.logger.debug('No such VXLAN port: %s', 'vxlan_%s_%s' % (ev.nexthop, vni)) return self._del_network_ingress_flow( datapath=datapath, in_port=vxlan_port) vxlan_port = self._del_vxlan_port( dpid=self.speaker.dpid, remote_ip=ev.nexthop, key=vni) if vxlan_port is None: self.logger.debug('Cannot delete VXLAN port: %s', 'vxlan_%s_%s' % (ev.nexthop, vni)) return def _evpn_withdraw_route_handler(self, ev): if ev.path.nlri.type == EvpnNLRI.MAC_IP_ADVERTISEMENT: self._evpn_withdraw_mac_ip_adv_route_handler(ev) elif ev.path.nlri.type == EvpnNLRI.INCLUSIVE_MULTICAST_ETHERNET_TAG: self._evpn_withdraw_incl_mcast_etag_route_handler(ev) def _best_path_change_handler(self, ev): if not isinstance(ev.path, EvpnPath): # Ignores non-EVPN routes return elif ev.nexthop == self.speaker.router_id: # Ignore local connected routes return elif ev.is_withdraw: self._evpn_withdraw_route_handler(ev) else: self._evpn_route_handler(ev) def _peer_down_handler(self, remote_ip, remote_as): neighbor = self.speaker.neighbors.get(remote_ip, None) if neighbor is None: self.logger.debug('No such neighbor: remote_ip=%s, remote_as=%s', remote_ip, remote_as) return neighbor.state = 'down' def _peer_up_handler(self, remote_ip, remote_as): neighbor = self.speaker.neighbors.get(remote_ip, None) if neighbor is None: self.logger.debug('No such neighbor: remote_ip=%s, remote_as=%s', remote_ip, remote_as) return neighbor.state = 'up' # API methods for REST controller def add_speaker(self, dpid, as_number, router_id): # Check if the datapath for the specified dpid exist or not datapath = self._get_datapath(dpid) if datapath is None: raise DatapathNotFound(dpid=dpid) self.speaker = EvpnSpeaker( dpid=dpid, as_number=as_number, router_id=router_id, best_path_change_handler=self._best_path_change_handler, peer_down_handler=self._peer_down_handler, peer_up_handler=self._peer_up_handler) return {self.speaker.router_id: self.speaker.to_jsondict()} def get_speaker(self): if self.speaker is None: return BGPSpeakerNotFound() return {self.speaker.router_id: self.speaker.to_jsondict()} def del_speaker(self): if self.speaker is None: return BGPSpeakerNotFound() for vni in list(self.networks.keys()): self.del_network(vni=vni) for address in list(self.speaker.neighbors.keys()): self.del_neighbor(address=address) self.speaker.shutdown() speaker = self.speaker self.speaker = None return {speaker.router_id: speaker.to_jsondict()} def add_neighbor(self, address, remote_as): if self.speaker is None: raise BGPSpeakerNotFound() self.speaker.neighbor_add( address=address, remote_as=remote_as, enable_evpn=True) neighbor = EvpnNeighbor( address=address, remote_as=remote_as) self.speaker.neighbors[address] = neighbor return {address: neighbor.to_jsondict()} def get_neighbors(self, address=None): if self.speaker is None: raise BGPSpeakerNotFound() if address is not None: neighbor = self.speaker.neighbors.get(address, None) if neighbor is None: raise NeighborNotFound(address=address) return {address: neighbor.to_jsondict()} neighbors = {} for address, neighbor in self.speaker.neighbors.items(): neighbors[address] = neighbor.to_jsondict() return neighbors def del_neighbor(self, address): if self.speaker is None: raise BGPSpeakerNotFound() neighbor = self.speaker.neighbors.get(address, None) if neighbor is None: raise NeighborNotFound(address=address) for network in self.networks.values(): for mac, client in list(network.clients.items()): if client.next_hop == address: network.clients.pop(mac) self.speaker.neighbor_del(address=address) neighbor = self.speaker.neighbors.pop(address) return {address: neighbor.to_jsondict()} def add_network(self, vni): if self.speaker is None: raise BGPSpeakerNotFound() # Constructs type 0 RD with as_number and vni route_dist = "%s:%d" % (self.speaker.as_number, vni) self.speaker.vrf_add( route_dist=route_dist, import_rts=[route_dist], export_rts=[route_dist], route_family=RF_L2_EVPN) # Note: For the VLAN Based service, ethernet_tag_id # must be set to zero. self.speaker.evpn_prefix_add( route_type=EVPN_MULTICAST_ETAG_ROUTE, route_dist=route_dist, ethernet_tag_id=vni, ip_addr=self.speaker.router_id, next_hop=self.speaker.router_id) network = EvpnNetwork( vni=vni, route_dist=route_dist, ethernet_tag_id=0) self.networks[vni] = network return {vni: network.to_jsondict()} def get_networks(self, vni=None): if self.speaker is None: raise BGPSpeakerNotFound() if vni is not None: network = self.networks.get(vni, None) if network is None: raise VniNotFound(vni=vni) return {vni: network.to_jsondict()} networks = {} for vni, network in self.networks.items(): networks[vni] = network.to_jsondict() return networks def del_network(self, vni): if self.speaker is None: raise BGPSpeakerNotFound() datapath = self._get_datapath(self.speaker.dpid) if datapath is None: raise DatapathNotFound(dpid=self.speaker.dpid) network = self.networks.get(vni, None) if network is None: raise VniNotFound(vni=vni) for client in network.get_clients(next_hop=self.speaker.router_id): self.del_client( vni=vni, mac=client.mac) self._del_network_egress_flow( datapath=datapath, tag=vni) for address in self.speaker.neighbors: self._del_vxlan_port( dpid=self.speaker.dpid, remote_ip=address, key=vni) self.speaker.evpn_prefix_del( route_type=EVPN_MULTICAST_ETAG_ROUTE, route_dist=network.route_dist, ethernet_tag_id=vni, ip_addr=self.speaker.router_id) self.speaker.vrf_del(route_dist=network.route_dist) network = self.networks.pop(vni) return {vni: network.to_jsondict()} def add_client(self, vni, port, mac, ip): if self.speaker is None: raise BGPSpeakerNotFound() datapath = self._get_datapath(self.speaker.dpid) if datapath is None: raise DatapathNotFound(dpid=self.speaker.dpid) network = self.networks.get(vni, None) if network is None: raise VniNotFound(vni=vni) port = self._get_ofport(self.speaker.dpid, port) if port is None: try: port = to_int(port) except ValueError: raise OFPortNotFound(port_name=port) self._add_network_ingress_flow( datapath=datapath, tag=network.vni, in_port=port, eth_src=mac) self._add_l2_switching_flow( datapath=datapath, tag=network.vni, eth_dst=mac, out_port=port) # Note: For the VLAN Based service, ethernet_tag_id # must be set to zero. self.speaker.evpn_prefix_add( route_type=EVPN_MAC_IP_ADV_ROUTE, route_dist=network.route_dist, esi=0, ethernet_tag_id=0, mac_addr=mac, ip_addr=ip, vni=vni, next_hop=self.speaker.router_id, tunnel_type='vxlan') # Stores local client info client = EvpnClient( port=port, mac=mac, ip=ip, next_hop=self.speaker.router_id) network.clients[mac] = client return {vni: client.to_jsondict()} def del_client(self, vni, mac): if self.speaker is None: raise BGPSpeakerNotFound() datapath = self._get_datapath(self.speaker.dpid) if datapath is None: raise DatapathNotFound(dpid=self.speaker.dpid) network = self.networks.get(vni, None) if network is None: raise VniNotFound(vni=vni) client = network.clients.get(mac, None) if client is None: raise ClientNotFound(mac=mac) elif client.next_hop != self.speaker.router_id: raise ClientNotLocal(mac=mac) self._del_network_ingress_flow( datapath=datapath, in_port=client.port, eth_src=mac) self._del_l2_switching_flow( datapath=datapath, tag=network.vni, eth_dst=mac) # Note: For the VLAN Based service, ethernet_tag_id # must be set to zero. self.speaker.evpn_prefix_del( route_type=EVPN_MAC_IP_ADV_ROUTE, route_dist=network.route_dist, esi=0, ethernet_tag_id=0, mac_addr=mac, ip_addr=client.ip) client = network.clients.pop(mac) return {vni: client.to_jsondict()} def post_method(keywords): def _wrapper(method): def __wrapper(self, req, **kwargs): try: try: body = req.json if req.body else {} except ValueError: raise ValueError('Invalid syntax %s', req.body) kwargs.update(body) for key, converter in keywords.items(): value = kwargs.get(key, None) if value is None: raise ValueError('%s not specified' % key) kwargs[key] = converter(value) except ValueError as e: return Response(content_type='application/json', body={"error": str(e)}, status=400) try: return method(self, **kwargs) except Exception as e: status = 500 body = { "error": str(e), "status": status, } return Response(content_type='application/json', body=json.dumps(body), status=status) __wrapper.__doc__ = method.__doc__ return __wrapper return _wrapper def get_method(keywords=None): keywords = keywords or {} def _wrapper(method): def __wrapper(self, _, **kwargs): try: for key, converter in keywords.items(): value = kwargs.get(key, None) if value is None: continue kwargs[key] = converter(value) except ValueError as e: return Response(content_type='application/json', body={"error": str(e)}, status=400) try: return method(self, **kwargs) except Exception as e: status = 500 body = { "error": str(e), "status": status, } return Response(content_type='application/json', body=json.dumps(body), status=status) __wrapper.__doc__ = method.__doc__ return __wrapper return _wrapper delete_method = get_method class RestVtepController(ControllerBase): def __init__(self, req, link, data, **config): super(RestVtepController, self).__init__(req, link, data, **config) self.vtep_app = data[RestVtep.__name__] self.logger = self.vtep_app.logger @route(API_NAME, '/vtep/speakers', methods=['POST']) @post_method( keywords={ "dpid": to_int, "as_number": to_int, "router_id": str, }) def add_speaker(self, **kwargs): """ Creates a new BGPSpeaker instance. Usage: ======= ================ Method URI ======= ================ POST /vtep/speakers ======= ================ Request parameters: ========== ============================================ Attribute Description ========== ============================================ dpid ID of Datapath binding to speaker. (e.g. 1) as_number AS number. (e.g. 65000) router_id Router ID. (e.g. "172.17.0.1") ========== ============================================ Example:: $ curl -X POST -d '{ "dpid": 1, "as_number": 65000, "router_id": "172.17.0.1" }' http://localhost:8080/vtep/speakers | python -m json.tool :: { "172.17.0.1": { "EvpnSpeaker": { "as_number": 65000, "dpid": 1, "neighbors": {}, "router_id": "172.17.0.1" } } } """ try: body = self.vtep_app.add_speaker(**kwargs) except DatapathNotFound as e: return e.to_response(status=404) return Response(content_type='application/json', body=json.dumps(body)) @route(API_NAME, '/vtep/speakers', methods=['GET']) @get_method() def get_speakers(self, **kwargs): """ Gets the info of BGPSpeaker instance. Usage: ======= ================ Method URI ======= ================ GET /vtep/speakers ======= ================ Example:: $ curl -X GET http://localhost:8080/vtep/speakers | python -m json.tool :: { "172.17.0.1": { "EvpnSpeaker": { "as_number": 65000, "dpid": 1, "neighbors": { "172.17.0.2": { "EvpnNeighbor": { "address": "172.17.0.2", "remote_as": 65000, "state": "up" } } }, "router_id": "172.17.0.1" } } } """ try: body = self.vtep_app.get_speaker() except BGPSpeakerNotFound as e: return e.to_response(status=404) return Response(content_type='application/json', body=json.dumps(body)) @route(API_NAME, '/vtep/speakers', methods=['DELETE']) @delete_method() def del_speaker(self, **kwargs): """ Shutdowns BGPSpeaker instance. Usage: ======= ================ Method URI ======= ================ DELETE /vtep/speakers ======= ================ Example:: $ curl -X DELETE http://localhost:8080/vtep/speakers | python -m json.tool :: { "172.17.0.1": { "EvpnSpeaker": { "as_number": 65000, "dpid": 1, "neighbors": {}, "router_id": "172.17.0.1" } } } """ try: body = self.vtep_app.del_speaker() except BGPSpeakerNotFound as e: return e.to_response(status=404) return Response(content_type='application/json', body=json.dumps(body)) @route(API_NAME, '/vtep/neighbors', methods=['POST']) @post_method( keywords={ "address": str, "remote_as": to_int, }) def add_neighbor(self, **kwargs): """ Registers a new neighbor to the speaker. Usage: ======= ======================== Method URI ======= ======================== POST /vtep/neighbors ======= ======================== Request parameters: ========== ================================================ Attribute Description ========== ================================================ address IP address of neighbor. (e.g. "172.17.0.2") remote_as AS number of neighbor. (e.g. 65000) ========== ================================================ Example:: $ curl -X POST -d '{ "address": "172.17.0.2", "remote_as": 65000 }' http://localhost:8080/vtep/neighbors | python -m json.tool :: { "172.17.0.2": { "EvpnNeighbor": { "address": "172.17.0.2", "remote_as": 65000, "state": "down" } } } """ try: body = self.vtep_app.add_neighbor(**kwargs) except BGPSpeakerNotFound as e: return e.to_response(status=400) return Response(content_type='application/json', body=json.dumps(body)) def _get_neighbors(self, **kwargs): try: body = self.vtep_app.get_neighbors(**kwargs) except (BGPSpeakerNotFound, NeighborNotFound) as e: return e.to_response(status=404) return Response(content_type='application/json', body=json.dumps(body)) @route(API_NAME, '/vtep/neighbors', methods=['GET']) @get_method() def get_neighbors(self, **kwargs): """ Gets a list of all neighbors. Usage: ======= ======================== Method URI ======= ======================== GET /vtep/neighbors ======= ======================== Example:: $ curl -X GET http://localhost:8080/vtep/neighbors | python -m json.tool :: { "172.17.0.2": { "EvpnNeighbor": { "address": "172.17.0.2", "remote_as": 65000, "state": "up" } } } """ return self._get_neighbors(**kwargs) @route(API_NAME, '/vtep/neighbors/{address}', methods=['GET']) @get_method( keywords={ "address": str, }) def get_neighbor(self, **kwargs): """ Gets the neighbor for the specified address. Usage: ======= ================================== Method URI ======= ================================== GET /vtep/neighbors/{address} ======= ================================== Request parameters: ========== ================================================ Attribute Description ========== ================================================ address IP address of neighbor. (e.g. "172.17.0.2") ========== ================================================ Example:: $ curl -X GET http://localhost:8080/vtep/neighbors/172.17.0.2 | python -m json.tool :: { "172.17.0.2": { "EvpnNeighbor": { "address": "172.17.0.2", "remote_as": 65000, "state": "up" } } } """ return self._get_neighbors(**kwargs) @route(API_NAME, '/vtep/neighbors/{address}', methods=['DELETE']) @delete_method( keywords={ "address": str, }) def del_neighbor(self, **kwargs): """ Unregister the specified neighbor from the speaker. Usage: ======= ================================== Method URI ======= ================================== DELETE /vtep/speaker/neighbors/{address} ======= ================================== Request parameters: ========== ================================================ Attribute Description ========== ================================================ address IP address of neighbor. (e.g. "172.17.0.2") ========== ================================================ Example:: $ curl -X DELETE http://localhost:8080/vtep/speaker/neighbors/172.17.0.2 | python -m json.tool :: { "172.17.0.2": { "EvpnNeighbor": { "address": "172.17.0.2", "remote_as": 65000, "state": "up" } } } """ try: body = self.vtep_app.del_neighbor(**kwargs) except (BGPSpeakerNotFound, NeighborNotFound) as e: return e.to_response(status=404) return Response(content_type='application/json', body=json.dumps(body)) @route(API_NAME, '/vtep/networks', methods=['POST']) @post_method( keywords={ "vni": to_int, }) def add_network(self, **kwargs): """ Defines a new network. Usage: ======= =============== Method URI ======= =============== POST /vtep/networks ======= =============== Request parameters: ================ ======================================== Attribute Description ================ ======================================== vni Virtual Network Identifier. (e.g. 10) ================ ======================================== Example:: $ curl -X POST -d '{ "vni": 10 }' http://localhost:8080/vtep/networks | python -m json.tool :: { "10": { "EvpnNetwork": { "clients": {}, "ethernet_tag_id": 0, "route_dist": "65000:10", "vni": 10 } } } """ try: body = self.vtep_app.add_network(**kwargs) except BGPSpeakerNotFound as e: return e.to_response(status=404) return Response(content_type='application/json', body=json.dumps(body)) def _get_networks(self, **kwargs): try: body = self.vtep_app.get_networks(**kwargs) except (BGPSpeakerNotFound, VniNotFound) as e: return e.to_response(status=404) return Response(content_type='application/json', body=json.dumps(body)) @route(API_NAME, '/vtep/networks', methods=['GET']) @get_method() def get_networks(self, **kwargs): """ Gets a list of all networks. Usage: ======= =============== Method URI ======= =============== GET /vtep/networks ======= =============== Example:: $ curl -X GET http://localhost:8080/vtep/networks | python -m json.tool :: { "10": { "EvpnNetwork": { "clients": { "aa:bb:cc:dd:ee:ff": { "EvpnClient": { "ip": "10.0.0.1", "mac": "aa:bb:cc:dd:ee:ff", "next_hop": "172.17.0.1", "port": 1 } } }, "ethernet_tag_id": 0, "route_dist": "65000:10", "vni": 10 } } } """ return self._get_networks(**kwargs) @route(API_NAME, '/vtep/networks/{vni}', methods=['GET']) @get_method( keywords={ "vni": to_int, }) def get_network(self, **kwargs): """ Gets the network for the specified VNI. Usage: ======= ===================== Method URI ======= ===================== GET /vtep/networks/{vni} ======= ===================== Request parameters: ================ ======================================== Attribute Description ================ ======================================== vni Virtual Network Identifier. (e.g. 10) ================ ======================================== Example:: $ curl -X GET http://localhost:8080/vtep/networks/10 | python -m json.tool :: { "10": { "EvpnNetwork": { "clients": { "aa:bb:cc:dd:ee:ff": { "EvpnClient": { "ip": "10.0.0.1", "mac": "aa:bb:cc:dd:ee:ff", "next_hop": "172.17.0.1", "port": 1 } } }, "ethernet_tag_id": 0, "route_dist": "65000:10", "vni": 10 } } } """ return self._get_networks(**kwargs) @route(API_NAME, '/vtep/networks/{vni}', methods=['DELETE']) @delete_method( keywords={ "vni": to_int, }) def del_network(self, **kwargs): """ Deletes the network for the specified VNI. Usage: ======= ===================== Method URI ======= ===================== DELETE /vtep/networks/{vni} ======= ===================== Request parameters: ================ ======================================== Attribute Description ================ ======================================== vni Virtual Network Identifier. (e.g. 10) ================ ======================================== Example:: $ curl -X DELETE http://localhost:8080/vtep/networks/10 | python -m json.tool :: { "10": { "EvpnNetwork": { "ethernet_tag_id": 10, "clients": [ { "EvpnClient": { "ip": "10.0.0.11", "mac": "e2:b1:0c:ba:42:ed", "port": 1 } } ], "route_dist": "65000:100", "vni": 10 } } } """ try: body = self.vtep_app.del_network(**kwargs) except (BGPSpeakerNotFound, DatapathNotFound, VniNotFound) as e: return e.to_response(status=404) return Response(content_type='application/json', body=json.dumps(body)) @route(API_NAME, '/vtep/networks/{vni}/clients', methods=['POST']) @post_method( keywords={ "vni": to_int, "port": str, "mac": str, "ip": str, }) def add_client(self, **kwargs): """ Registers a new client to the specified network. Usage: ======= ============================= Method URI ======= ============================= POST /vtep/networks/{vni}/clients ======= ============================= Request parameters: =========== =============================================== Attribute Description =========== =============================================== vni Virtual Network Identifier. (e.g. 10) port Port number to connect client. For convenience, port name can be specified and automatically translated to port number. (e.g. "s1-eth1" or 1) mac Client MAC address to register. (e.g. "aa:bb:cc:dd:ee:ff") ip Client IP address. (e.g. "10.0.0.1") =========== =============================================== Example:: $ curl -X POST -d '{ "port": "s1-eth1", "mac": "aa:bb:cc:dd:ee:ff", "ip": "10.0.0.1" }' http://localhost:8080/vtep/networks/10/clients | python -m json.tool :: { "10": { "EvpnClient": { "ip": "10.0.0.1", "mac": "aa:bb:cc:dd:ee:ff", "next_hop": "172.17.0.1", "port": 1 } } } """ try: body = self.vtep_app.add_client(**kwargs) except (BGPSpeakerNotFound, DatapathNotFound, VniNotFound, OFPortNotFound) as e: return e.to_response(status=404) return Response(content_type='application/json', body=json.dumps(body)) @route(API_NAME, '/vtep/networks/{vni}/clients/{mac}', methods=['DELETE']) @delete_method( keywords={ "vni": to_int, "mac": str, }) def del_client(self, **kwargs): """ Registers a new client to the specified network. Usage: ======= =================================== Method URI ======= =================================== DELETE /vtep/networks/{vni}/clients/{mac} ======= =================================== Request parameters: =========== =============================================== Attribute Description =========== =============================================== vni Virtual Network Identifier. (e.g. 10) mac Client MAC address to register. =========== =============================================== Example:: $ curl -X DELETE http://localhost:8080/vtep/networks/10/clients/aa:bb:cc:dd:ee:ff | python -m json.tool :: { "10": { "EvpnClient": { "ip": "10.0.0.1", "mac": "aa:bb:cc:dd:ee:ff", "next_hop": "172.17.0.1", "port": 1 } } } """ try: body = self.vtep_app.del_client(**kwargs) except (BGPSpeakerNotFound, DatapathNotFound, VniNotFound, ClientNotFound, ClientNotLocal) as e: return Response(body=str(e), status=500) return Response(content_type='application/json', body=json.dumps(body))