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:
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.
Now I install them into the switches by clicking on a flow and then ‘Install Flow’.
After the flows have all been installed, I can then try out my pings in mininet:
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.
You can find the complete script on Github: https://github.com/fredhsu/odl-scripts/tree/master/python/addflow
Is there any documentation on how we could use rest api’s to register for packet-in messages?
I believe that packet_in messages will need to be handled by using OSGI, and not REST.
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
Hi Fred
I was able to get it working finally!
Following were the 2 issues I found:
Issue 1:
The flowadd api should be:
http://localhost:8080/controller/nb/v2/flowprogrammer/default/node/OF/00:00:00:00:00:00:00:03/staticFlow/flowx
instead of:
http://localhost:8080/controller/nb/v2/flow/default/node/OF/00:00:00:00:00:00:00:03/staticFlow/flowx
Issue2:
Post api should use “PUT” instead of “POST” as method.
Thanks
Sreenivas
Thanks for the update! I’m sure many of the URLs in my examples have been changed since I first posted.
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
***************************************************************************************
– Info printed when I run the command “dump” in Mininet:
mininet> dump
– 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>
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.
When I try to ad flow i get the following repose. I have already logedin once then why is this happening.
The post url is
http://192.168.119.134:8080/controller/nb/v2/flow/default/OF/00:00:00:00:00:00:00:03/node3from
OpenDaylight – Login
less = {
env: “production”
};
Log In
i realized i did use the right flow URL.. Now i get an error . flow does not exist with the Url
http://192.168.119.134:8080/controller/nb/v2/flowprogrammer/default/node/OF/00:00:00:00:00:00:00:03/staticFlow/flowx
It is possible that the url/methods have changed since I posted this. You may want to double check with the ODL documentation to make sure the URL and method are correct. Are you doing a GET?
Hello,
I am have a same problem with ODL oxygen version, how can I change the URL? because the URL of this version is not same your version
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.
Hey did u get the code working with the host tracker. I too need it!
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.
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
How to do this in ODL-lithium?
Unfortunately I haven’t had time to play with ODL lately, so I’m not sure if/when I’ll update this to Lithium.