building a rest API for ExBGP

The last couple of years there is a trend to extend layer three to the top of rack switch (TOR). This gives a more stable and scalable design compared to the classic layer two network design. On major disadvantage of the layer 3 to the TOR switch is IP mobility. In the classic L2 design it was a simple live migration of a vm to a  different compute host in a different rack. When L3 is extended to the TOR IP mobility isn’t that simple anymore. A solution for this might be to let the VM Host advertise a unique service IP for a particular VM when it becomes active on that VM host. A great tool for this use case is ExaBGP.

ExaBGP does not modify the route table on the host itself it only announces routes to its neighbours. After ExaBGP starts the routes it advertises can be influenced by sending messages to STDIN
Below is the config used by the ExaBGP daemon

group ebgp {
router-id 172.16.2.11;
neighbor 172.16.2.252 {
local-address 172.16.2.11;
local-as 65001;
peer-as 65000;
group-updates;
}
process add-routes {
run /etc/exabgp/exabgp_rest3.py;
}
}

Most of this is pretty self explanatory the important stuff happens on line 9-11. These lines start a script and all output of this script is parsed by ExaBGP.

The script exabgp_rest3.py provides a rest API which outputs on STDOUT the announce and withdraw commands for ExaBGP.

For testing purposes I created a simple setup within KVM and two hosts, docker1 which runs ExaBGP and firewall-1 which runs the birdc bgp daemon. There is a L2 segment between those clients over which BGP peering is created

The python script is only 75 lines long.

#!/usr/bin/env python
import web
from sys import stdout
from netaddr import *
from pprint import pprint
urls = (
    '/announce/(.*)', 'announce',
    '/withdraw/(.*)', 'withdraw',
)
class MyOutputStream(object):
    def write(self, data):
        pass   # Ignore output
	
web.httpserver.sys.stderr = MyOutputStream()
class bgpPrefix:
    def __init__(self,prefix,action="announce",next_hop="self",attributes={}):
        self.prefix=prefix
        self.action=action
        self.next_hop=next_hop
        self.attributes=attributes
        print self.attributes
    def get_exabgp_message(self):
        if (self.action=='withdraw'):
            exabgp_message="{0} route {1} next-hop {2}".format(self.action,self.prefix,self.next_hop)
        else:
            attribute_string=""
            for attribute in self.attributes:
                 if attribute == "local-preference":
                     attribute_string+=" local-preference {0}".format(self.attributes[attribute])
                 elif attribute == "med":
                     attribute_string+=" med {0}".format(self.attributes[attribute])
                 elif attribute == "community":
                     print self.attributes[attribute]
                     if len(self.attributes[attribute])>0:
			 attribute_string+=" community [ "
			 for comm in self.attributes[attribute]:
			     attribute_string+=" {0} ".format(comm)
			 attribute_string+=" ]"

                     
            exabgp_message="{0} route {1} next-hop {2}{3}".format(self.action,self.prefix,self.next_hop,attribute_string)
	return exabgp_message
     
def verifyIp(ip):
    if not '/' in ip:
        ip="{0}/32".format(ip)
    try:
        ip_object=IPNetwork(ip)
    except:
        raise web.badrequest("invalid IP")
    return(ip_object)

class announce:
    def GET(self, prefix):
        ip_object=verifyIp(prefix)
       # bgp_prefix=bgpPrefix(str(ip_object),action="announce",attributes={'local-preference': 300})
        bgp_prefix=bgpPrefix(str(ip_object),action="announce",attributes=web.input(community=[]))
        stdout.write( bgp_prefix.get_exabgp_message() + '\n')
        stdout.flush()
        return "OK"


class withdraw:
    def GET(self, prefix):
        ip_object=verifyIp(prefix)
        bgp_prefix=bgpPrefix(str(ip_object),action="withdraw")
        stdout.write( bgp_prefix.get_exabgp_message() + '\n')
        stdout.flush()
        return "OK"

app = web.application(urls, globals())

if __name__ == "__main__":
app.run()

The heavy lifting of the web service is handled by web.py this is a powerfull library to create a webserver in Python. I am a network engineer with very limitted experience with Python but creating the script only took me a couple of hours.

The script in action

We start with starting the ExaBGP Daemo

.
.
.
Mon, 29 Aug 2016 21:01:14 | INFO     | 15213  | reactor       | New peer setup: neighbor 172.16.2.252 local-ip 172.16.2.11 local-as 65001 peer-as 65000 router-id 172.16.2.11 family-allowed in-open
Mon, 29 Aug 2016 21:01:14 | WARNING  | 15213  | configuration | Loaded new configuration successfully
Mon, 29 Aug 2016 21:01:14 | INFO     | 15213  | processes     | Forked process add-routes


Mon, 29 Aug 2016 21:01:16 | INFO     | 15213  | network       | Connected to peer neighbor 172.16.2.252 local-ip 172.16.2.11 local-as 65001 peer-as 65000 router-id 172.16.2.11 family-allowed in-open (out)

By default the service is started at port 8080

root@docker-1:/home/eelcon# netstat -anp | grep 8080
tcp        0      0 0.0.0.0:8080            0.0.0.0:*               LISTEN      15183/python
root@docker-1:/home/eelcon#

The BGP neighbor is also shown as established by bird

bird> show protocols all bgp3
name     proto    table    state  since       info
bgp3     BGP      master   up     15:01:33    Established
  Preference:     100
  Input filter:   ACCEPT
  Output filter:  REJECT
  Routes:         0 imported, 0 exported, 0 preferred
  Route change stats:     received   rejected   filtered    ignored   accepted
    Import updates:              0          0          0          0          0
    Import withdraws:            0          0        ---          0          0
    Export updates:              0          0          0        ---          0
    Export withdraws:            0        ---        ---        ---          0
  BGP state:          Established
    Neighbor address: 172.16.2.11
    Neighbor AS:      65001
    Neighbor ID:      172.16.2.11
    Neighbor caps:    AS4
    Session:          external AS4
    Source address:   172.16.2.252
    Hold timer:       155/180
    Keepalive timer:  51/60

bird>

adding a route is as simple as doing a simple curl on the host on which the ExaBGP is running

nettinkerer@docker-1:~$ curl http://127.0.0.1:8080/announce/1.2.3.0/25
OK
nettinkerer@docker-1:~$

ExaBGP gets the announce message

Mon, 29 Aug 2016 21:08:18 | INFO     | 15231  | processes     | Command from process add-routes : announce route 1.2.3.0/25 next-hop self
Mon, 29 Aug 2016 21:08:18 | INFO     | 15231  | reactor       | Route added to neighbor 172.16.2.252 local-ip 172.16.2.11 local-as 65001 peer-as 65000 router-id 172.16.2.11 family-allowed in-open : 1.2.3.0/25 next-hop 172.16.2.11
Mon, 29 Aug 2016 21:08:18 | INFO     | 15231  | reactor       | Performing dynamic route update
Mon, 29 Aug 2016 21:08:19 | INFO     | 15231  | reactor       | Updated peers dynamic routes successfully

the bgp daemon on the firewall also knows the route

bird> show route 1.2.3.0/25 all
1.2.3.0/25         via 172.16.2.11 on ens9 [bgp3 15:08:36] * (100) [AS65001i]
        Type: BGP unicast univ
        BGP.origin: IGP
        BGP.as_path: 65001
        BGP.next_hop: 172.16.2.11
        BGP.local_pref: 100
bird>

the REST API also accepts communities and meds

curl "http://127.0.0.1:8080/announce/1.2.3.0/25?med=200&comnity=100:400&community=300:600"

which is shown by the bird daemon as well

bird> show route 1.2.3.0/25 all
1.2.3.0/25         via 172.16.2.11 on ens9 [bgp3 15:14:01] * (100) [AS65001i]
        Type: BGP unicast univ
        BGP.origin: IGP
        BGP.as_path: 65001
        BGP.next_hop: 172.16.2.11
        BGP.med: 200
        BGP.local_pref: 100
        BGP.community: (100,400) (300,600)
bird>

Withdrawing routes can also be done easily with a curl statement

 curl "http://127.0.0.1:8080/withdraw/1.2.3.0/25"

And the route is gone

bird> show route 1.2.3.0/25 all
Network not in table
bird>

At the moment there is only limitted input validation. The REST API does check if the ip address entered is valid but no other checks are implemented at this moment. I might add this if need arises.

The script and configs used in this blog can be found on my Github

 

Hi There

Hi for the past 15 years I have been working as a network engineer/designer and in 2008 I obtained my CCIE certification and recently I passen my VMWare NSX as well.

I have been working at large ISP’s, Oil en Gas and goverment with a focus on datacenters projects.

Since the start I have been using various methods to make my work easier. I started with using Excel and Word and mailmerges to create Cisco configs. Later I switched to Perl and SNMP to create create configs and push them to the network devices. Sometimes it saved me a lot of time but often it took me more time to create some kind of Perl script than do the work by hard labour.

The last couple of years there is a shift  by vendors towards programmabillity based on REST API. Many vendors provide an Python SDK to ease the use of the REST API.

This blog will focus on automating network provisioning but you never know what’s the next big thing.