-
Notifications
You must be signed in to change notification settings - Fork 11
/
controller.py
381 lines (342 loc) · 14.9 KB
/
controller.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
# coding=utf-8
""" Implements functions necessary to operate the OpenDaylight controller
using their RESTful APIs
"""
from collections import defaultdict
import copy
import json
import re
import itertools
import networkx
import netaddr
import requests
from requests.auth import HTTPBasicAuth
from sol.utils.exceptions import ControllerException
class OpenDaylightInterface(object):
"""
Manages OpenDaylight controller using its REST interface.
This is a prototype implementation
"""
def __init__(self, daylightURL='http://localhost:8080/controller/nb/v2',
daylightUser='admin', daylightPass='admin'):
"""
Create a new controller
:param daylightURL: URL to the daylight controller/namespace
:param daylightUser: username to use when connecting. 'admin' by default
:param daylightPass: password to use when connecting. 'admin' by default
"""
self._baseURL = daylightURL
self._auth = HTTPBasicAuth(daylightUser, daylightPass)
self._session = requests.session()
self._session.auth = self._auth
self._defheaders = {'content-type': 'application/json'}
self._pathmap = {}
def _buildURL(self, service, container='default'):
return self._baseURL + '/{}/{}'.format(service, container)
def getTopology(self):
"""
Returns the Daylight topology by querying the OpenDaylight controller
:rtype: :py:class:`networkx.DiGraph`
"""
G = networkx.DiGraph()
r = self._session.get(self._buildURL('topology'))
checkErr(r)
d = r.json()
links = []
props = []
for edge in d[u'edgeProperties']:
u = edge[u'edge'][u'headNodeConnector'][u'node'][u'id']
v = edge[u'edge'][u'tailNodeConnector'][u'node'][u'id']
tailport = edge[u'edge'][u'tailNodeConnector'][u'id']
headport = edge[u'edge'][u'headNodeConnector'][u'id']
name = edge[u'properties'][u'name'][u'value']
# print u, v, name, 'head=', headport, 'tail=', tailport
links.append((u, v))
props.append(
{'tailport': tailport, 'headport': headport, 'name': name})
# Now add the other direction
links.append((v, u))
props.append(
{'tailport': headport, 'headport': tailport, 'name': name}
)
G.add_edges_from(links)
for ind, (u, v) in enumerate(links):
G.edge[u][v].update(props[ind])
r = self._session.get(self._buildURL('switchmanager') + '/nodes')
checkErr(r)
d = r.json()
for node in d['nodeProperties']:
nodeid = node['node']['id']
# print nodeid
typ = node['node']['type']
try:
G.node[nodeid]['type'] = typ
except KeyError:
G.add_node(nodeid)
G.node[nodeid]['type'] = typ
assert networkx.is_connected(G.to_undirected())
return G
def getRoutes(self):
"""
Returns currently installed routes in the network by querying OpenDaylight
"""
r = self._session.get(self._buildURL('flowprogrammer'))
checkErr(r)
return r.json()
def pushPath(self, daylightPath, daylightGraph, srcPrefix, dstPrefix,
protocol=None, srcPort=None, dstPort=None, installHw=True,
priority=500):
"""
Pushes a single route to the network using the OD controller
:param daylightPath: the route, using OpenDaylight node IDs
:param daylightGraph: the topology, obtained from OpenDaylight (see :py:func:`getTopology`)
:param srcPrefix: IP src prefix
:param dstPrefix: IP dst prefix
:param protocol: tcp/udp etc.
:param srcPort: source ports, if any
:param dstPort: destination ports, if any
:param installHw: Something OpenDaylight wants. Set it to true
:param priority: rule priority
:raises ControllerException:
In case OpenDaylight returns any of the non-success codes
"""
props = {'installInHW': installHw,
'priority': priority}
# print daylightPath
for ind in xrange(1, len(daylightPath) - 1):
node = daylightPath[ind]
# print ind, node
# print daylightGraph.edge[daylightPath[ind - 1]]
ingressPort = daylightGraph.edge[daylightPath[ind - 1]][node][
'tailport']
egressPort = daylightGraph.edge[node][daylightPath[ind + 1]][
'headport']
flow_repr = 'flow_{}_{}_{}_{}_{}_{}_{}'.format(
str(daylightPath[0]).replace(':', '').lstrip('0'),
str(daylightPath[-1]).replace(':', '').lstrip('0'),
ingressPort, egressPort,
str(srcPrefix).replace('/', '.'),
str(dstPrefix).replace('/', '.'),
str(node).replace(':', '').lstrip('0'))
# print flow_repr
# print ingressPort, egressPort
nodeType = daylightGraph.node[node]['type']
newFlow = {'name': flow_repr, 'ingressPort': str(ingressPort),
'actions': ['OUTPUT=' + str(egressPort)],
'node': {'id': node,
'type': nodeType}, 'nwSrc': srcPrefix,
'nwDst': dstPrefix, 'etherType': '0x800'}
if protocol is not None:
newFlow['protocol'] = str(protocol)
if srcPort is not None:
newFlow['tpSrc'] = str(srcPort)
if dstPort is not None:
newFlow['tpDst'] = str(dstPort)
newFlow.update(props)
# print newFlow
url = self._buildURL('flowprogrammer') + \
'/node/{}/{}/staticFlow/{}'.format(nodeType, node, flow_repr)
# print url
r = self._session.put(url, json.dumps(newFlow),
headers=self._defheaders)
checkErr(r)
self._pathmap[daylightPath] = (srcPrefix, dstPrefix)
def _computeSplit(self, k, paths, blockbits, mindiff):
srcnet = netaddr.IPNetwork(k.srcprefix)
dstnet = netaddr.IPNetwork(k.dstprefix)
# Diffenrent length of the IP address based on the version
ipbits = 32
if srcnet.version == 6:
ipbits = 128
# Set up our blocks. Block is a pair of src-dst prefixes
assert blockbits <= ipbits - srcnet.prefixlen
assert blockbits <= ipbits - dstnet.prefixlen
numblocks = len(srcnet) * len(dstnet) / (2 ** (2 * blockbits))
newmask1 = srcnet.prefixlen + blockbits
newmask2 = srcnet.prefixlen + blockbits
blockweight = 1.0 / numblocks # This is not volume-aware
assigned = defaultdict(lambda: [])
if not mindiff:
# This is the basic version, no min-diff required.
assweight = 0
index = 0
path = paths[index]
# Iterate over the blocks and pack then into paths
for block in itertools.product(srcnet.subnet(newmask1),
dstnet.subnet(newmask2)):
if index >= len(paths):
raise Exception('no bueno')
assigned[path].append(block)
assweight += blockweight
if assweight >= path.getNumFlows():
# print path.getNumFlows(), assweight
assweight = 0
index += 1
if index < len(paths):
path = paths[index]
else:
leftovers = []
# iteration one, remove any exess blocks and put them into leftover
# array
for p in paths:
oldsrc, olddst = self._pathmap[p]
oldweight = len(oldsrc) * len(olddst) / (2 ** blockbits)
if p.getNumFlows() < oldweight:
assweight = 0
for block in itertools.product(oldsrc.subnet(newmask1),
olddst.subnet(newmask2)):
assigned[p].append(block)
assweight += blockweight
if assweight >= p.getNumFlows():
leftovers.append(block)
# iteration two, use the leftovers to pad paths where fractions
# are too low
for p in paths:
oldsrc, olddst = self._pathmap[p]
oldweight = len(oldsrc) * len(olddst) / (2 ** blockbits)
if p.getNumFlows() > oldweight:
assweight = oldweight
while leftovers:
block = leftovers.pop(0)
assigned[p].append(block)
assweight += blockweight
if assweight >= p.getNumFlows():
break
assert len(leftovers) == 0
return assigned
# TODO: implement more sophisticated volume-aware splitting
# TODO: corner case: splitting if just one IP per src/dst?
def generateRoutes(self, pptc, daylightGraph, blockbits=5, convertoffset=0):
"""
Take the output of the optimization and generate OpenDaylight routes.
:param pptc: path per traffic class obtained from the optimization.
.. warning::
Assumes that that all non-flow-carrying paths have been filtered
out.
:param daylightGraph: the OpenDaylight topology
:param blockbits: How many bits define a block of IP adresses.
The smaller the number, the more fine-grained splitting is.
Default is 5 bits.
:param convertoffset: offset to use when converting paths.
If you have topology that uses with nodeID of 0, set this to 1.
:return: a list of routes to be installed on the switches.
"""
routeList = []
for k in pptc:
numpaths = len(pptc[k])
if numpaths > 1:
# The complex case, need to compute a split between paths
assigned = self._computeSplit(k, pptc[k], blockbits, False)
for path in assigned:
sources, dests = zip(*assigned[path])
subsrcprefix = netaddr.cidr_merge(sources)
subdstprefix = netaddr.cidr_merge(dests)
# print path, subsrcprefix, subdstprefix
#TODO: test the correctness of this better
assert len(subsrcprefix) == len(subdstprefix)
for s, d in itertools.izip(subsrcprefix, subdstprefix):
routeList.append((convertPath(path, offset=convertoffset),
daylightGraph, str(s), str(d)))
else:
# Easy case, only one flow-carrying path
routeList.append((convertPath(pptc[k][0], offset=convertoffset),
daylightGraph,
k.srcprefix, k.dstprefix))
return routeList
def pushRoutes(self, routeList):
"""
Push a list of routes using REST API. (See :py:func:`generateRoutes` for route generation)
:param routeList: list of routes/paths
"""
for route in routeList:
self.pushPath(*route)
# def generateUpdatedRoutes(self, ppk, daylightGraph, blockbits=5,
# convertoffset=0):
# """
#
# :param ppk:
# :param daylightGraph:
# :param blockbits:
# :return:
# """
# routeList = []
# for k in ppk:
# numpaths = len(ppk[k])
# if numpaths > 1:
# assigned = self._computeSplit(k, ppk[k], blockbits, True)
# for path in assigned:
# sources, dests = zip(*assigned[path])
# subsrcprefix = netaddr.cidr_merge(sources)
# subdstprefix = netaddr.cidr_merge(dests)
# assert len(subsrcprefix) == 1
# assert len(subdstprefix) == 1
# routeList.append((convertPath(path, offset=convertoffset),
# daylightGraph,
# str(subsrcprefix[0]),
# str(subdstprefix[0])))
# else:
# routeList.append((convertPath(ppk[k][0], offset=convertoffset),
# daylightGraph,
# k.srcprefix, k.dstprefix))
# return routeList
# def updateRoutes(self, routeList):
# self.deleteAllFlows()
# self.pushRoutes(routeList)
def getAllFlows(self):
"""
Get all installed flows from OpenDaylight
:return: the JSON object with all flows
"""
r = self._session.get(self._buildURL('flowprogrammer'))
checkErr(r)
return r.json()
def deleteAllFlows(self):
"""
Delete all installed flows in OpenDaylight
"""
flows = self.getAllFlows()
for f in flows['flowConfig']:
typ = f['node']['type']
node = f['node']['id']
name = f['name']
r = self._session.delete(
self._buildURL('flowprogrammer') + '/node/{}/{}/staticFlow/{}'
.format(typ, node, name))
checkErr(r)
def convertPath(path, offset=0):
"""
Convert node IDs in the path to the daylight IDs
:param path: the path obtained from the optimization
:param offset: An offset when converting node numbers. Default is 0,
but some topologies start at 0 and opendaylight panics. So
offset of 1 is required.
"""
_path = copy.copy(path)
for ind, node in enumerate(_path):
if type(node) == int:
node = hex(node + offset).lstrip('0x').zfill(16)
_path[ind] = ':'.join(
s.encode('hex') for s in node.decode('hex'))
elif type(node) == str \
and re.match(r'^([0-9A-Fa-f]{2}[:-]){5,8}([0-9A-Fa-f]{2})$',
node) is None:
node = int(node) + 1
# print node
node = hex(node).lstrip('0x').zfill(16)
_path[ind] = ':'.join(
s.encode('hex') for s in node.decode('hex'))
# print _path
return _path
def checkErr(r):
"""
Check the response from OpenDaylight for any error codes
:param r: the response recieved form the requests library
:return: True if status code is within 200s
:raises: :py:class:`~panacea.util.exceptions.ControllerException`
If the status code is not OK (not within the 200s)
"""
if not (200 <= r.status_code < 300):
print r.text
raise ControllerException("REST API error, code {}".format(
r.status_code))
return True