diff options
-rw-r--r-- | ryu/app/rest_vtep.py | 1803 |
1 files changed, 1803 insertions, 0 deletions
diff --git a/ryu/app/rest_vtep.py b/ryu/app/rest_vtep.py new file mode 100644 index 00000000..716786d1 --- /dev/null +++ b/ryu/app/rest_vtep.py @@ -0,0 +1,1803 @@ +# 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) + +--------------------+ +--------------------+ + | Ryu1 | --- BGP(EVPN) --- | Ryu2 | + +--------------------+ +--------------------+ + | | + +--------------------+ +--------------------+ + | 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 Ryu1 and Ryu2 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 + } + } + } +""" + +import json + +from ryu.app.ofctl import api as ofctl_api +from ryu.app.wsgi import ControllerBase +from ryu.app.wsgi import Response +from ryu.app.wsgi import route +from ryu.app.wsgi import WSGIApplication +from ryu.base import app_manager +from ryu.exception import RyuException +from ryu.lib.ovs import bridge as ovs_bridge +from ryu.lib.packet import arp +from ryu.lib.packet import ether_types +from ryu.lib.packet.bgp import _RouteDistinguisher +from ryu.lib.packet.bgp import EvpnNLRI +from ryu.lib.stringify import StringifyMixin +from ryu.ofproto import ofproto_v1_3 +from ryu.services.protocols.bgp.bgpspeaker import BGPSpeaker +from ryu.services.protocols.bgp.bgpspeaker import RF_L2_EVPN +from ryu.services.protocols.bgp.bgpspeaker import EVPN_MAC_IP_ADV_ROUTE +from ryu.services.protocols.bgp.bgpspeaker import EVPN_MULTICAST_ETAG_ROUTE +from ryu.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(RyuException): + + 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.RyuApp): + 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 = { + # <vni>: <instance 'EvpnNetwork'>, + # ... + # } + 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_<remote_ip>_<key>' + 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_<remote_ip>_<key>' + 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_<remote_ip>_<key>' + 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)) |