os-ken/ryu/app/rest_vtep.py
IWASE Yusuke ab92cc8529 app: Add sample application for REST EVPN switch
Signed-off-by: IWASE Yusuke <iwase.yusuke0@gmail.com>
Signed-off-by: FUJITA Tomonori <fujita.tomonori@lab.ntt.co.jp>
2016-10-01 21:28:52 +09:00

1804 lines
54 KiB
Python

# 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))