Adding flows in OpenDaylight using Python and REST API

Building on my last post I’ve put together a small script that will find the shortest path between two switches, then install some flows to create a path between them. I’m leveraging some tools from the NetworkX library in Python and the Northbound REST API on the OpenDaylight controller.

I’m using Mininet as my test network, and the built-in tree topology that was used previously. When loaded into the controller the topology looks like this:
Tree3 Topo
Mininet creates two hosts off each leaf node. The end goal here is to ping between H1 and H8, which are connected to S3 (port 1) and S7 (port 2). First I want to get the shortest path between S3 and S7. To do that I load the nodes(switches) and edges(links) into a NetworkX Graph object:

I wrote a helper function to make building the URLs for the REST calls a little easier. The URL path to get the edges is:/controller/nb/v2/topology/default/

This returns a list of ‘edgeProperties’. An edge has a headNodeConnector and a tailNodeConnector, which are the ports on either end of the edge (each edge is unidirectional, so there will be two entries for each link). These NodeConnectors have nodes associated with them that I use to create tuple to signify an edge. For example, the edge between S4 and S2 has a headNodeConnector of:

            "edge": {
                "tailNodeConnector": {
                    "@type": "OF",
                    "@id": "2",
                    "node": {
                        "@type": "OF",
                        "@id": "00:00:00:00:00:00:00:02"
                    }
                },
                "headNodeConnector": {
                    "@type": "OF",
                    "@id": "3",
                    "node": {
                        "@type": "OF",
                        "@id": "00:00:00:00:00:00:00:04"
                    }
                }

From which I extract the node id’s (i.e. OpenFlow IDs) to create a tuple:
(00:00:00:00:00:00:00:04, 00:00:00:00:00:00:00:02)
And then add that as an edge to my Graph object. You’ll also see the ID of the NodeConnector itself as one of the attributes, which we’ll use later when defining ingress and output ports for our flows. Here’s code:

# Get all the edges/links
resp, content = h.request(build_url(baseUrl, 'topology', containerName), "GET")
edgeProperties = json.loads(content)
odlEdges = edgeProperties['edgeProperties']

# Put nodes and edges into a graph
graph = nx.Graph()
for edge in odlEdges:
  e = (edge['edge']['headNodeConnector']['node']['@id'], edge['edge']['tailNodeConnector']['node']['@id'])
  graph.add_edge(*e)

I do something similar with the nodes, using the URL path: /controller/nb/v2/switch/default/nodes
That returns a list of all the switches in the network, and I add them as nodes to the graph.

# Get all the nodes/switches
resp, content = h.request(build_url(baseUrl, 'switch', containerName) + '/nodes/', "GET")
nodeProperties = json.loads(content)
odlNodes = nodeProperties['nodeProperties']
for node in odlNodes:
  graph.add_node(node['node']['@id'])

Now I run Djikstra’s algorithm provided by the shortest_path method. This returns a list of nodes along the shortest path.

shortest_path = nx.shortest_path(graph, "00:00:00:00:00:00:00:03", "00:00:00:00:00:00:00:07")

In this case the list is:
[00:00:00:00:00:00:00:03, 00:00:00:00:00:00:00:02, 00:00:00:00:00:00:00:01, 00:00:00:00:00:00:00:05, 00:00:00:00:00:00:00:07]

Now I can build a series of flow entries for each switch along the path, not counting the ones directly connected to the hosts. To do this I created a couple functions. find_edge will look for an edge that has a particular head node and tail node. push_path takes a path and the edges from the API call and pushes the appropriate flows to the switches. To create a flow on the switch I use an HTTP POST and send over a JSON object that describes the flow. An example JSON flow entry object:

{"installInHw":"false","name":"test2","node":{"@id":"00:00:00:00:00:00:00:07","@type":"OF"}, "ingressPort":"1","priority":"500","etherType":"0x800","nwSrc":"10.0.0.7","nwDst":"10.0.0.3", "actions":"OUTPUT=2"}

And the code:

def find_edge(edges, headNode, tailNode):
  for edge in odlEdges:
    if edge['edge']['headNodeConnector']['node']['@id'] == headNode and edge['edge']['tailNodeConnector']['node']['@id'] == tailNode:
      return edge
  return None

def push_path(path, odlEdges, srcIP, dstIP, baseUrl):
  for i, node in enumerate(path[1:-1]):
    flowName = "fromIP" + srcIP[-1:] + "Po" + str(i)
    ingressEdge = find_edge(odlEdges, shortest_path[i], node)
    egressEdge = find_edge(odlEdges, node, shortest_path[i+2])
    newFlow = build_flow_entry(flowName, ingressEdge, egressEdge, node, srcIP, dstIP)
    switchType = newFlow['node']['@type']
    postUrl = build_flow_url(baseUrl, 'default', switchType, node, flowName)
    # post the flow to the controller
    resp, content = post_dict(h, postUrl, newFlow)

def build_flow_entry(flowName, ingressEdge, egressEdge, node, srcIP, dstIP):
  # Since I don't specify the EtherType, it looks like the IP field is ignored
  # Alternatively I could add a second flow with 0x806 for ARP then 0x800 for IP
  defaultPriority = "500"
  newFlow = {"installInHw":"false"}
  ingressPort = ingressEdge['edge']['tailNodeConnector']['@id']
  egressPort = egressEdge['edge']['headNodeConnector']['@id']
  switchType = egressEdge['edge']['headNodeConnector']['node']['@type']
  newFlow.update({"name":flowName})
  newFlow.update({"node":ingressEdge['edge']['tailNodeConnector']['node']})
  newFlow.update({"ingressPort":ingressPort, "priority":defaultPriority})
  newFlow.update({"nwSrc":srcIP, "nwDst":dstIP})  # This can probably be ignored for this example
  newFlow.update({"actions":"OUTPUT=" + egressPort})
  return newFlow

def post_dict(h, url, d):
  resp, content = h.request(
      uri = url,
      method = 'POST',
      headers={'Content-Type' : 'application/json'},
      body=json.dumps(d),
      )
  return resp, content

A few things to note:

  • I didn’t specify an EtherType so that it will send any packet over the links. I needed to do this so that ARP messages would make it across. But in doing so, the IP addresses are ignored. This works fine for my simple example, but I think I may revise this later to handle both ARP and IP separately so that there are not such broad flow entries installed. Then I could specify IP match criteria.
  • I chose to not install the flow to the switch immediately so that I can take a look at them before actually putting them to work (InstallInHw : false)
  • I’m not yet error checking on the response, but you should get an HTTP 201 for successful flow entries.

After pushing these flows, I reverse the path, and push the reverse path onto the switches:

shortest_path.reverse()
push_path(shortest_path, odlEdges, dstIP, srcIP, baseUrl)

Finally I add the entries for the leaf nodes connected to the hosts. There is probably a way to do this with host tracker to make this dynamic, but I just hardcoded it for now:

node3FlowFromHost = {"installInHw":"false","name":"node3from","node":{"@id":"00:00:00:00:00:00:00:03","@type":"OF"},"ingressPort":"1","priority":"500","nwSrc":"10.0.0.1","actions":"OUTPUT=3"}
node7FlowFromHost = {"installInHw":"false","name":"node7from","node":{"@id":"00:00:00:00:00:00:00:07","@type":"OF"},"ingressPort":"2","priority":"500","nwSrc":"10.0.0.8","actions":"OUTPUT=3"}
node3FlowToHost = {"installInHw":"false","name":"node3to","node":{"@id":"00:00:00:00:00:00:00:03","@type":"OF"},"ingressPort":"3","priority":"500","nwDst":"10.0.0.1","actions":"OUTPUT=1"}
node7FlowToHost = {"installInHw":"false","name":"node7to","node":{"@id":"00:00:00:00:00:00:00:07","@type":"OF"},"ingressPort":"3","priority":"500","nwDst":"10.0.0.8","actions":"OUTPUT=2"}
postUrl = build_flow_url(baseUrl, 'default', "OF", "00:00:00:00:00:00:00:03", "node3from")
resp, content = post_dict(h, postUrl, node3FlowFromHost)
postUrl = build_flow_url(baseUrl, 'default', "OF", "00:00:00:00:00:00:00:07", "node7from")
resp, content = post_dict(h, postUrl, node7FlowFromHost)
postUrl = build_flow_url(baseUrl, 'default', "OF", "00:00:00:00:00:00:00:03", "node3to")
resp, content = post_dict(h, postUrl, node3FlowToHost)
postUrl = build_flow_url(baseUrl, 'default', "OF", "00:00:00:00:00:00:00:07", "node7to")
resp, content = post_dict(h, postUrl, node7FlowToHost)

Ok, after all that, I can run my script and then go to the controller to see all the flows added.
2013-06-14-080757_418x428_scrot
Now I install them into the switches by clicking on a flow and then ‘Install Flow’.
2013-06-14-081202_988x333_scrot
After the flows have all been installed, I can then try out my pings in mininet:
Mininet Ping 1
And just for good measure I can go over to the Troubleshooting tab on the controller to see the stats of these flows, and see how packets are matching on my flow entries.
2013-06-14-080824_984x180_scrot
You can find the complete script on Github: https://github.com/fredhsu/odl-scripts/tree/master/python/addflow

20 thoughts on “Adding flows in OpenDaylight using Python and REST API

  1. Sreenivas Makam says:

    Hi Fred
    Nice blog!

    I tried adding flows using REST api using your example and for some reason, I am not successful in adding flows.

    Iam successful in fetching flows.

    Following is the test flow I was trying to add:
    req_str = ‘http://localhost:8080/controller/nb/v2/flow/default/node/OF/00:00:00:00:00:00:00:01/staticFlow/flow1’

    flowtest = {
    “installInHw”:”true”,
    “name”:”flow1″,
    “node”:{
    “id”:”00:00:00:00:00:00:00:03″,
    “type”:”OF”
    },
    “ingressPort”:”1″,
    “priority”:”500″,
    “etherType”:”0x800″,
    “nwSrc”:”9.9.1.1″,
    “actions”:[
    “OUTPUT=2”
    ]
    }

    Following is the response I see:
    resp {‘status’: ‘200’, ‘content-length’: ‘1261’, ‘set-cookie’: ‘JSESSIONID=D615AE3C0A6C0794CBEC5DAA5C012491; Path=/’, ‘server’: ‘Apache-Coyote/1.1’, ‘date’: ‘Thu, 28 Nov 2013 16:24:22 GMT’, ‘content-type’: ‘text/html;charset=UTF-8’}

    Based on the response code, flow seems to be added successfully. When I try to read flows, I dont see the flows. Also, when I use opendaylight gui, I dont see the flows(both in flow tab as well as troubleshooting tab)

    I tried your program as it is and the program works except flows getting added.

    Any ideas?
    As a next step, I am trying to turn on TRACE messages within ODL to see whats happening. No luck so far.

    Thanks
    Sreenivas

  2. Pauline MARTIAL says:

    Hi,
    I’m new to OpenDaylight and something seems to be wrong with my Subnet Gateway Configuration: I cannot ping any 2 hosts even when the gateway is configured. However, if I manually add 2 flow entries linking 2 hosts connected to the same switch, then those 2 hosts are able to ping one another…

    Does anyone know what is wrong with my tests? Is it possible that it has something to do with Maven (I didn’t configure anything about Maven yet…)

    Thank you very much in advance,
    Pauline

    ***************************************************************************************

    – My controller is running on Windows 7 (IP address 10.10.255.52)
    – I’m using Mininet on a Linux VM
    – Command used to create topology: sudo mn –controller=remote,ip=10.10.255.52 –topo tree,3
    – I used the following Gateway IP Address/Mask : 10.0.0.254/24
    – Info printed when I run the command “dump” in Mininet:
    mininet> dump

    ***************************************************************************************

      • Pauline MARTIAL says:

        – Info printed when I run the command “dump” in Mininet:
        mininet> dump
        RemoteController c0: 10.10.255.52:6633 pid=2101>
        OVSSwitch s1: lo:127.0.0.1,s1-eth1:None,s1-eth2:None pid=2118>
        OVSSwitch s2: lo:127.0.0.1,s2-eth1:None,s2-eth2:None,s2-eth3:None pid=2123>
        OVSSwitch s3: lo:127.0.0.1,s3-eth1:None,s3-eth2:None,s3-eth3:None pid=2128>
        OVSSwitch s4: lo:127.0.0.1,s4-eth1:None,s4-eth2:None,s4-eth3:None pid=2133>
        OVSSwitch s5: lo:127.0.0.1,s5-eth1:None,s5-eth2:None,s5-eth3:None pid=2138>
        OVSSwitch s6: lo:127.0.0.1,s6-eth1:None,s6-eth2:None,s6-eth3:None pid=2143>
        OVSSwitch s7: lo:127.0.0.1,s7-eth1:None,s7-eth2:None,s7-eth3:None pid=2148>
        Host h1: h1-eth0:10.0.0.1 pid=2108>
        Host h2: h2-eth0:10.0.0.2 pid=2109>
        Host h3: h3-eth0:10.0.0.3 pid=2110>
        Host h4: h4-eth0:10.0.0.4 pid=2111>
        Host h5: h5-eth0:10.0.0.5 pid=2112>
        Host h6: h6-eth0:10.0.0.6 pid=2113>
        Host h7: h7-eth0:10.0.0.7 pid=2114>
        Host h8: h8-eth0:10.0.0.8 pid=2115>

  3. Ankita says:

    Hi Fred…. I m trying to run your code odl-addflow.py on Mininet and we are getting the following error… could you please help me

    raise ValueError(“No JSON object could be decoded”)
    ValueError: No JSON object could be decoded
    Traceback (most recent call last):
    File “odl-addflow.py”, line 84, in
    nodeProperties = json.loads(content)
    File “/usr/lib/python2.7/json/__init__.py”, line 328, in loads
    return _default_decoder.decode(s)
    File “/usr/lib/python2.7/json/decoder.py”, line 365, in decode
    obj, end = self.raw_decode(s, idx=_w(s, 0).end())
    File “/usr/lib/python2.7/json/decoder.py”, line 383, in raw_decode
    raise ValueError(“No JSON object could be decoded”)
    ValueError: No JSON object could be decoded

    • You may need to modify some of the URLs, as the code has changed since I posted this. Have a look at one of the earlier comments about one of the changes in URL and method.

  4. Abhijeet says:

    Hi Fred. Thanks for sharing your code. It helped me a lot:)
    Were you able to add flows for leaves using hosttracker? I tried but couldnt make it work.

  5. Developer says:

    Does someone have a working version of this code? I’ve made URL changes as mentioned before, but I’m still not able to create any flows.

  6. Dev says:

    I’m getting a socket.error: Connection refused. How can I change the code to get it to work? I haven’t changed the username or password for the controller. It’s the same admin admin.

    THanks

Leave a reply to Abhijeet Cancel reply