os-ken/os_ken/app/rest_vtep.py

1843 lines
56 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)
+--------------------+ +--------------------+
| 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 = {
# <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))