add monitoring optimizations
[osm/vim-emu.git] / src / emuvim / dcemulator / net.py
1 """
2 Copyright (c) 2015 SONATA-NFV and Paderborn University
3 ALL RIGHTS RESERVED.
4
5 Licensed under the Apache License, Version 2.0 (the "License");
6 you may not use this file except in compliance with the License.
7 You may obtain a copy of the License at
8
9 http://www.apache.org/licenses/LICENSE-2.0
10
11 Unless required by applicable law or agreed to in writing, software
12 distributed under the License is distributed on an "AS IS" BASIS,
13 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 See the License for the specific language governing permissions and
15 limitations under the License.
16
17 Neither the name of the SONATA-NFV [, ANY ADDITIONAL AFFILIATION]
18 nor the names of its contributors may be used to endorse or promote
19 products derived from this software without specific prior written
20 permission.
21
22 This work has been performed in the framework of the SONATA project,
23 funded by the European Commission under Grant number 671517 through
24 the Horizon 2020 and 5G-PPP programmes. The authors would like to
25 acknowledge the contributions of their colleagues of the SONATA
26 partner consortium (www.sonata-nfv.eu).
27 """
28 import logging
29
30 import site
31 import time
32 from subprocess import Popen
33 import re
34 import requests
35 import os
36
37 from mininet.net import Containernet
38 from mininet.node import Controller, DefaultController, OVSSwitch, OVSKernelSwitch, Docker, RemoteController
39 from mininet.cli import CLI
40 from mininet.link import TCLink
41 from mininet.clean import cleanup
42 import networkx as nx
43 from emuvim.dcemulator.monitoring import DCNetworkMonitor
44 from emuvim.dcemulator.node import Datacenter, EmulatorCompute
45 from emuvim.dcemulator.resourcemodel import ResourceModelRegistrar
46
47 LOG = logging.getLogger("dcemulator.net")
48 LOG.setLevel(logging.DEBUG)
49
50 class DCNetwork(Containernet):
51 """
52 Wraps the original Mininet/Containernet class and provides
53 methods to add data centers, switches, etc.
54
55 This class is used by topology definition scripts.
56 """
57
58 def __init__(self, controller=RemoteController, monitor=False,
59 enable_learning=False, # learning switch behavior of the default ovs switches icw Ryu controller can be turned off/on, neede for E-LAN functionality
60 dc_emulation_max_cpu=1.0, # fraction of overall CPU time for emulation
61 dc_emulation_max_mem=512, # emulation max mem in MB
62 **kwargs):
63 """
64 Create an extended version of a Containernet network
65 :param dc_emulation_max_cpu: max. CPU time used by containers in data centers
66 :param kwargs: path through for Mininet parameters
67 :return:
68 """
69 # members
70 self.dcs = {}
71 self.ryu_process = None
72 #list of deployed nsds.E_Lines and E_LANs (uploaded from the dummy gatekeeper)
73 self.deployed_nsds = []
74 self.deployed_elines = []
75 self.deployed_elans = []
76 self.installed_chains = []
77
78
79 # always cleanup environment before we start the emulator
80 self.killRyu()
81 cleanup()
82
83 # call original Docker.__init__ and setup default controller
84 Containernet.__init__(
85 self, switch=OVSKernelSwitch, controller=controller, **kwargs)
86
87 # default switch configuration
88 enable_ryu_learning = False
89 if enable_learning :
90 self.failMode = 'standalone'
91 enable_ryu_learning = True
92 else:
93 self.failMode = 'secure'
94
95 # Ryu management
96 if controller == RemoteController:
97 # start Ryu controller
98 self.startRyu(learning_switch=enable_ryu_learning)
99
100 # add the specified controller
101 self.addController('c0', controller=controller)
102
103 # graph of the complete DC network
104 self.DCNetwork_graph = nx.MultiDiGraph()
105
106 # initialize pool of vlan tags to setup the SDN paths
107 self.vlans = range(4096)[::-1]
108
109 # link to Ryu REST_API
110 ryu_ip = 'localhost'
111 ryu_port = '8080'
112 self.ryu_REST_api = 'http://{0}:{1}'.format(ryu_ip, ryu_port)
113 self.RyuSession = requests.Session()
114
115 # monitoring agent
116 if monitor:
117 self.monitor_agent = DCNetworkMonitor(self)
118 else:
119 self.monitor_agent = None
120
121 # initialize resource model registrar
122 self.rm_registrar = ResourceModelRegistrar(
123 dc_emulation_max_cpu, dc_emulation_max_mem)
124
125 def addDatacenter(self, label, metadata={}, resource_log_path=None):
126 """
127 Create and add a logical cloud data center to the network.
128 """
129 if label in self.dcs:
130 raise Exception("Data center label already exists: %s" % label)
131 dc = Datacenter(label, metadata=metadata, resource_log_path=resource_log_path)
132 dc.net = self # set reference to network
133 self.dcs[label] = dc
134 dc.create() # finally create the data center in our Mininet instance
135 LOG.info("added data center: %s" % label)
136 return dc
137
138 def addLink(self, node1, node2, **params):
139 """
140 Able to handle Datacenter objects as link
141 end points.
142 """
143 assert node1 is not None
144 assert node2 is not None
145
146 # ensure type of node1
147 if isinstance( node1, basestring ):
148 if node1 in self.dcs:
149 node1 = self.dcs[node1].switch
150 if isinstance( node1, Datacenter ):
151 node1 = node1.switch
152 # ensure type of node2
153 if isinstance( node2, basestring ):
154 if node2 in self.dcs:
155 node2 = self.dcs[node2].switch
156 if isinstance( node2, Datacenter ):
157 node2 = node2.switch
158 # try to give containers a default IP
159 if isinstance( node1, Docker ):
160 if "params1" not in params:
161 params["params1"] = {}
162 if "ip" not in params["params1"]:
163 params["params1"]["ip"] = self.getNextIp()
164 if isinstance( node2, Docker ):
165 if "params2" not in params:
166 params["params2"] = {}
167 if "ip" not in params["params2"]:
168 params["params2"]["ip"] = self.getNextIp()
169 # ensure that we allow TCLinks between data centers
170 # TODO this is not optimal, we use cls=Link for containers and TCLink for data centers
171 # see Containernet issue: https://github.com/mpeuster/containernet/issues/3
172 if "cls" not in params:
173 params["cls"] = TCLink
174
175 link = Containernet.addLink(self, node1, node2, **params)
176
177 # try to give container interfaces a default id
178 node1_port_id = node1.ports[link.intf1]
179 if isinstance(node1, Docker):
180 if "id" in params["params1"]:
181 node1_port_id = params["params1"]["id"]
182 node1_port_name = link.intf1.name
183
184 node2_port_id = node2.ports[link.intf2]
185 if isinstance(node2, Docker):
186 if "id" in params["params2"]:
187 node2_port_id = params["params2"]["id"]
188 node2_port_name = link.intf2.name
189
190
191 # add edge and assigned port number to graph in both directions between node1 and node2
192 # port_id: id given in descriptor (if available, otherwise same as port)
193 # port: portnumber assigned by Containernet
194
195 attr_dict = {}
196 # possible weight metrics allowed by TClink class:
197 weight_metrics = ['bw', 'delay', 'jitter', 'loss']
198 edge_attributes = [p for p in params if p in weight_metrics]
199 for attr in edge_attributes:
200 # if delay: strip ms (need number as weight in graph)
201 match = re.search('([0-9]*\.?[0-9]+)', params[attr])
202 if match:
203 attr_number = match.group(1)
204 else:
205 attr_number = None
206 attr_dict[attr] = attr_number
207
208
209 attr_dict2 = {'src_port_id': node1_port_id, 'src_port_nr': node1.ports[link.intf1],
210 'src_port_name': node1_port_name,
211 'dst_port_id': node2_port_id, 'dst_port_nr': node2.ports[link.intf2],
212 'dst_port_name': node2_port_name}
213 attr_dict2.update(attr_dict)
214 self.DCNetwork_graph.add_edge(node1.name, node2.name, attr_dict=attr_dict2)
215
216 attr_dict2 = {'src_port_id': node2_port_id, 'src_port_nr': node2.ports[link.intf2],
217 'src_port_name': node2_port_name,
218 'dst_port_id': node1_port_id, 'dst_port_nr': node1.ports[link.intf1],
219 'dst_port_name': node1_port_name}
220 attr_dict2.update(attr_dict)
221 self.DCNetwork_graph.add_edge(node2.name, node1.name, attr_dict=attr_dict2)
222
223 LOG.debug("addLink: n1={0} intf1={1} -- n2={2} intf2={3}".format(
224 str(node1),node1_port_name, str(node2), node2_port_name))
225
226 return link
227
228 def addDocker( self, label, **params ):
229 """
230 Wrapper for addDocker method to use custom container class.
231 """
232 self.DCNetwork_graph.add_node(label)
233 return Containernet.addDocker(self, label, cls=EmulatorCompute, **params)
234
235 def removeDocker( self, label, **params ):
236 """
237 Wrapper for removeDocker method to update graph.
238 """
239 self.DCNetwork_graph.remove_node(label)
240 return Containernet.removeDocker(self, label, **params)
241
242 def addSwitch( self, name, add_to_graph=True, **params ):
243 """
244 Wrapper for addSwitch method to store switch also in graph.
245 """
246
247 # add this switch to the global topology overview
248 if add_to_graph:
249 self.DCNetwork_graph.add_node(name)
250
251 # set the learning switch behavior
252 if 'failMode' in params :
253 failMode = params['failMode']
254 else :
255 failMode = self.failMode
256
257 s = Containernet.addSwitch(self, name, protocols='OpenFlow10,OpenFlow12,OpenFlow13', failMode=failMode, **params)
258
259 # set flow entry that enables learning switch behavior (needed to enable E-LAN functionality)
260 #LOG.info('failmode {0}'.format(failMode))
261 #if failMode == 'standalone' :
262 # LOG.info('add NORMAL')
263 # s.dpctl('add-flow', 'actions=NORMAL')
264
265 return s
266
267 def getAllContainers(self):
268 """
269 Returns a list with all containers within all data centers.
270 """
271 all_containers = []
272 for dc in self.dcs.itervalues():
273 all_containers += dc.listCompute()
274 return all_containers
275
276 def start(self):
277 # start
278 for dc in self.dcs.itervalues():
279 dc.start()
280 Containernet.start(self)
281
282 def stop(self):
283
284 # stop the monitor agent
285 if self.monitor_agent is not None:
286 self.monitor_agent.stop()
287
288 # stop emulator net
289 Containernet.stop(self)
290
291 # stop Ryu controller
292 self.killRyu()
293
294
295 def CLI(self):
296 CLI(self)
297
298 def setLAN(self, vnf_list):
299 """
300 setup an E-LAN network by assigning the same VLAN tag to each DC interface of the VNFs in the E-LAN
301
302 :param vnf_list: names of the VNFs in this E-LAN [{name:,interface:},...]
303 :return:
304 """
305 src_sw = None
306 src_sw_inport_nr = 0
307 src_sw_inport_name = None
308
309 # get a vlan tag for this E-LAN
310 vlan = self.vlans.pop()
311
312 for vnf in vnf_list:
313 vnf_src_name = vnf['name']
314 vnf_src_interface = vnf['interface']
315
316 # check if port is specified (vnf:port)
317 if vnf_src_interface is None:
318 # take first interface by default
319 connected_sw = self.DCNetwork_graph.neighbors(vnf_src_name)[0]
320 link_dict = self.DCNetwork_graph[vnf_src_name][connected_sw]
321 vnf_src_interface = link_dict[0]['src_port_id']
322
323 for connected_sw in self.DCNetwork_graph.neighbors(vnf_src_name):
324 link_dict = self.DCNetwork_graph[vnf_src_name][connected_sw]
325 for link in link_dict:
326 if (link_dict[link]['src_port_id'] == vnf_src_interface or
327 link_dict[link]['src_port_name'] == vnf_src_interface): # Fix: we might also get interface names, e.g, from a son-emu-cli call
328 # found the right link and connected switch
329 src_sw = connected_sw
330 src_sw_inport_nr = link_dict[link]['dst_port_nr']
331 src_sw_inport_name = link_dict[link]['dst_port_name']
332 break
333
334 # set the tag on the dc switch interface
335 LOG.debug('set E-LAN: vnf name: {0} interface: {1} tag: {2}'.format(vnf_src_name, vnf_src_interface,vlan))
336 switch_node = self.getNodeByName(src_sw)
337 self._set_vlan_tag(switch_node, src_sw_inport_name, vlan)
338
339 def _addMonitorFlow(self, vnf_src_name, vnf_dst_name, vnf_src_interface=None, vnf_dst_interface=None,
340 tag=None, **kwargs):
341
342 src_sw = None
343 src_sw_inport_nr = 0
344 src_sw_inport_name = None
345 dst_sw = None
346 dst_sw_outport_nr = 0
347 dst_sw_outport_name = None
348
349 LOG.debug("call AddMonitorFlow vnf_src_name=%r, vnf_src_interface=%r, vnf_dst_name=%r, vnf_dst_interface=%r",
350 vnf_src_name, vnf_src_interface, vnf_dst_name, vnf_dst_interface)
351
352 #check if port is specified (vnf:port)
353 if vnf_src_interface is None:
354 # take first interface by default
355 connected_sw = self.DCNetwork_graph.neighbors(vnf_src_name)[0]
356 link_dict = self.DCNetwork_graph[vnf_src_name][connected_sw]
357 vnf_src_interface = link_dict[0]['src_port_id']
358
359 for connected_sw in self.DCNetwork_graph.neighbors(vnf_src_name):
360 link_dict = self.DCNetwork_graph[vnf_src_name][connected_sw]
361 for link in link_dict:
362 if (link_dict[link]['src_port_id'] == vnf_src_interface or
363 link_dict[link]['src_port_name'] == vnf_src_interface): # Fix: we might also get interface names, e.g, from a son-emu-cli call
364 # found the right link and connected switch
365 src_sw = connected_sw
366 src_sw_inport_nr = link_dict[link]['dst_port_nr']
367 src_sw_inport_name = link_dict[link]['dst_port_name']
368 break
369
370 if vnf_dst_interface is None:
371 # take first interface by default
372 connected_sw = self.DCNetwork_graph.neighbors(vnf_dst_name)[0]
373 link_dict = self.DCNetwork_graph[connected_sw][vnf_dst_name]
374 vnf_dst_interface = link_dict[0]['dst_port_id']
375
376 vnf_dst_name = vnf_dst_name.split(':')[0]
377 for connected_sw in self.DCNetwork_graph.neighbors(vnf_dst_name):
378 link_dict = self.DCNetwork_graph[connected_sw][vnf_dst_name]
379 for link in link_dict:
380 if link_dict[link]['dst_port_id'] == vnf_dst_interface or \
381 link_dict[link]['dst_port_name'] == vnf_dst_interface: # Fix: we might also get interface names, e.g, from a son-emu-cli call
382 # found the right link and connected switch
383 dst_sw = connected_sw
384 dst_sw_outport_nr = link_dict[link]['src_port_nr']
385 dst_sw_outport_name = link_dict[link]['src_port_name']
386 break
387
388 if not tag >= 0:
389 LOG.exception('tag not valid: {0}'.format(tag))
390
391 # get shortest path
392 try:
393 # returns the first found shortest path
394 # if all shortest paths are wanted, use: all_shortest_paths
395 path = nx.shortest_path(self.DCNetwork_graph, src_sw, dst_sw, weight=kwargs.get('weight'))
396 except:
397 LOG.exception("No path could be found between {0} and {1} using src_sw={2} and dst_sw={3}".format(
398 vnf_src_name, vnf_dst_name, src_sw, dst_sw))
399 LOG.debug("Graph nodes: %r" % self.DCNetwork_graph.nodes())
400 LOG.debug("Graph edges: %r" % self.DCNetwork_graph.edges())
401 for e, v in self.DCNetwork_graph.edges():
402 LOG.debug("%r" % self.DCNetwork_graph[e][v])
403 return "No path could be found between {0} and {1}".format(vnf_src_name, vnf_dst_name)
404
405 LOG.info("Path between {0} and {1}: {2}".format(vnf_src_name, vnf_dst_name, path))
406
407 current_hop = src_sw
408 switch_inport_nr = src_sw_inport_nr
409
410 cmd = kwargs.get('cmd')
411
412 #iterate through the path to install the flow-entries
413 for i in range(0,len(path)):
414 current_node = self.getNodeByName(current_hop)
415
416 if path.index(current_hop) < len(path)-1:
417 next_hop = path[path.index(current_hop)+1]
418 else:
419 #last switch reached
420 next_hop = vnf_dst_name
421
422 next_node = self.getNodeByName(next_hop)
423
424 if next_hop == vnf_dst_name:
425 switch_outport_nr = dst_sw_outport_nr
426 LOG.info("end node reached: {0}".format(vnf_dst_name))
427 elif not isinstance( next_node, OVSSwitch ):
428 LOG.info("Next node: {0} is not a switch".format(next_hop))
429 return "Next node: {0} is not a switch".format(next_hop)
430 else:
431 # take first link between switches by default
432 index_edge_out = 0
433 switch_outport_nr = self.DCNetwork_graph[current_hop][next_hop][index_edge_out]['src_port_nr']
434
435
436 # set of entry via ovs-ofctl
437 if isinstance( current_node, OVSSwitch ):
438 kwargs['vlan'] = tag
439 kwargs['path'] = path
440 kwargs['current_hop'] = current_hop
441 kwargs['switch_inport_name'] = src_sw_inport_name
442 kwargs['switch_outport_name'] = dst_sw_outport_name
443 kwargs['skip_vlan_tag'] = True
444
445 monitor_placement = kwargs.get('monitor_placement')
446 # put monitor flow at the dst switch
447 insert_flow = False
448 if monitor_placement == 'tx' and path.index(current_hop) == 0: # first node:
449 insert_flow = True
450 # put monitoring flow at the src switch
451 elif monitor_placement == 'rx' and path.index(current_hop) == len(path) - 1: # last node:
452 insert_flow = True
453 else:
454 LOG.exception('invalid monitor command: {0}'.format(monitor_placement))
455
456
457 if self.controller == RemoteController and insert_flow:
458 ## set flow entry via ryu rest api
459 self._set_flow_entry_ryu_rest(current_node, switch_inport_nr, switch_outport_nr, **kwargs)
460 break
461 elif insert_flow:
462 ## set flow entry via ovs-ofctl
463 self._set_flow_entry_dpctl(current_node, switch_inport_nr, switch_outport_nr, **kwargs)
464 break
465
466 # take first link between switches by default
467 if isinstance( next_node, OVSSwitch ):
468 switch_inport_nr = self.DCNetwork_graph[current_hop][next_hop][0]['dst_port_nr']
469 current_hop = next_hop
470
471 return "path {2} between {0} and {1}".format(vnf_src_name, vnf_dst_name, cmd)
472
473
474 def setChain(self, vnf_src_name, vnf_dst_name, vnf_src_interface=None, vnf_dst_interface=None, **kwargs):
475 """
476 Chain 2 vnf interfaces together by installing the flowrules in the switches along their path.
477 Currently the path is found using the default networkx shortest path function.
478 Each chain gets a unique vlan id , so different chains wil not interfere.
479
480 :param vnf_src_name: vnf name (string)
481 :param vnf_dst_name: vnf name (string)
482 :param vnf_src_interface: source interface name (string)
483 :param vnf_dst_interface: destination interface name (string)
484 :param cmd: 'add-flow' (default) to add a chain, 'del-flows' to remove a chain
485 :param cookie: cookie for the installed flowrules (can be used later as identifier for a set of installed chains)
486 :param match: custom match entry to be added to the flowrules (default: only in_port and vlan tag)
487 :param priority: custom flowrule priority
488 :return: output log string
489 """
490
491 # special procedure for monitoring flows
492 if kwargs.get('monitor'):
493
494 # check if chain already exists
495 found_chains = [chain_dict for chain_dict in self.installed_chains if
496 (chain_dict['vnf_src_name'] == vnf_src_name and chain_dict['vnf_src_interface'] == vnf_src_interface
497 and chain_dict['vnf_dst_name'] == vnf_dst_name and chain_dict['vnf_dst_interface'] == vnf_dst_interface)]
498
499 if len(found_chains) > 0:
500 # this chain exists, so need an extra monitoring flow
501 # assume only 1 chain per vnf/interface pair
502 LOG.debug('*** installing monitoring chain on top of pre-defined chain from {0}:{1} -> {2}:{3}'.
503 format(vnf_src_name, vnf_src_interface, vnf_dst_name, vnf_dst_interface))
504 tag = found_chains[0]['tag']
505 ret = self._addMonitorFlow(vnf_src_name, vnf_dst_name, vnf_src_interface, vnf_dst_interface,
506 tag=tag, table_id=0, **kwargs)
507 return ret
508 else:
509 # no chain existing (or E-LAN) -> install normal chain
510 LOG.warning('*** installing monitoring chain without pre-defined chain from {0}:{1} -> {2}:{3}'.
511 format(vnf_src_name, vnf_src_interface, vnf_dst_name, vnf_dst_interface))
512 pass
513
514
515 cmd = kwargs.get('cmd')
516 if cmd == 'add-flow' or cmd == 'del-flows':
517 ret = self._chainAddFlow(vnf_src_name, vnf_dst_name, vnf_src_interface, vnf_dst_interface, **kwargs)
518 if kwargs.get('bidirectional'):
519 ret = ret +'\n' + self._chainAddFlow(vnf_dst_name, vnf_src_name, vnf_dst_interface, vnf_src_interface, **kwargs)
520
521 else:
522 ret = "Command unknown"
523
524 return ret
525
526
527 def _chainAddFlow(self, vnf_src_name, vnf_dst_name, vnf_src_interface=None, vnf_dst_interface=None, **kwargs):
528
529 src_sw = None
530 src_sw_inport_nr = 0
531 src_sw_inport_name = None
532 dst_sw = None
533 dst_sw_outport_nr = 0
534 dst_sw_outport_name = None
535
536 LOG.debug("call chainAddFlow vnf_src_name=%r, vnf_src_interface=%r, vnf_dst_name=%r, vnf_dst_interface=%r",
537 vnf_src_name, vnf_src_interface, vnf_dst_name, vnf_dst_interface)
538
539 #check if port is specified (vnf:port)
540 if vnf_src_interface is None:
541 # take first interface by default
542 connected_sw = self.DCNetwork_graph.neighbors(vnf_src_name)[0]
543 link_dict = self.DCNetwork_graph[vnf_src_name][connected_sw]
544 vnf_src_interface = link_dict[0]['src_port_id']
545
546 for connected_sw in self.DCNetwork_graph.neighbors(vnf_src_name):
547 link_dict = self.DCNetwork_graph[vnf_src_name][connected_sw]
548 for link in link_dict:
549 if (link_dict[link]['src_port_id'] == vnf_src_interface or
550 link_dict[link]['src_port_name'] == vnf_src_interface): # Fix: we might also get interface names, e.g, from a son-emu-cli call
551 # found the right link and connected switch
552 src_sw = connected_sw
553 src_sw_inport_nr = link_dict[link]['dst_port_nr']
554 src_sw_inport_name = link_dict[link]['dst_port_name']
555 break
556
557 if vnf_dst_interface is None:
558 # take first interface by default
559 connected_sw = self.DCNetwork_graph.neighbors(vnf_dst_name)[0]
560 link_dict = self.DCNetwork_graph[connected_sw][vnf_dst_name]
561 vnf_dst_interface = link_dict[0]['dst_port_id']
562
563 vnf_dst_name = vnf_dst_name.split(':')[0]
564 for connected_sw in self.DCNetwork_graph.neighbors(vnf_dst_name):
565 link_dict = self.DCNetwork_graph[connected_sw][vnf_dst_name]
566 for link in link_dict:
567 if link_dict[link]['dst_port_id'] == vnf_dst_interface or \
568 link_dict[link]['dst_port_name'] == vnf_dst_interface: # Fix: we might also get interface names, e.g, from a son-emu-cli call
569 # found the right link and connected switch
570 dst_sw = connected_sw
571 dst_sw_outport_nr = link_dict[link]['src_port_nr']
572 dst_sw_outport_name = link_dict[link]['src_port_name']
573 break
574
575
576 # get shortest path
577 try:
578 # returns the first found shortest path
579 # if all shortest paths are wanted, use: all_shortest_paths
580 path = nx.shortest_path(self.DCNetwork_graph, src_sw, dst_sw, weight=kwargs.get('weight'))
581 except:
582 LOG.exception("No path could be found between {0} and {1} using src_sw={2} and dst_sw={3}".format(
583 vnf_src_name, vnf_dst_name, src_sw, dst_sw))
584 LOG.debug("Graph nodes: %r" % self.DCNetwork_graph.nodes())
585 LOG.debug("Graph edges: %r" % self.DCNetwork_graph.edges())
586 for e, v in self.DCNetwork_graph.edges():
587 LOG.debug("%r" % self.DCNetwork_graph[e][v])
588 return "No path could be found between {0} and {1}".format(vnf_src_name, vnf_dst_name)
589
590 LOG.info("Path between {0} and {1}: {2}".format(vnf_src_name, vnf_dst_name, path))
591
592 current_hop = src_sw
593 switch_inport_nr = src_sw_inport_nr
594
595 # choose free vlan
596 ## if path contains more than 1 switch
597 cmd = kwargs.get('cmd')
598 vlan = None
599 if cmd == 'add-flow':
600 if kwargs.get('tag'):
601 # use pre-defined tag
602 vlan = kwargs.get('tag')
603 else:
604 vlan = self.vlans.pop()
605
606 # store the used vlan tag to identify this chain
607 if not kwargs.get('monitor'):
608 chain_dict = {}
609 chain_dict['vnf_src_name'] = vnf_src_name
610 chain_dict['vnf_dst_name'] = vnf_dst_name
611 chain_dict['vnf_src_interface'] = vnf_src_interface
612 chain_dict['vnf_dst_interface'] = vnf_dst_interface
613 chain_dict['tag'] = vlan
614 self.installed_chains.append(chain_dict)
615
616 #iterate through the path to install the flow-entries
617 for i in range(0,len(path)):
618 current_node = self.getNodeByName(current_hop)
619
620 if path.index(current_hop) < len(path)-1:
621 next_hop = path[path.index(current_hop)+1]
622 else:
623 #last switch reached
624 next_hop = vnf_dst_name
625
626 next_node = self.getNodeByName(next_hop)
627
628 if next_hop == vnf_dst_name:
629 switch_outport_nr = dst_sw_outport_nr
630 LOG.info("end node reached: {0}".format(vnf_dst_name))
631 elif not isinstance( next_node, OVSSwitch ):
632 LOG.info("Next node: {0} is not a switch".format(next_hop))
633 return "Next node: {0} is not a switch".format(next_hop)
634 else:
635 # take first link between switches by default
636 index_edge_out = 0
637 switch_outport_nr = self.DCNetwork_graph[current_hop][next_hop][index_edge_out]['src_port_nr']
638
639
640 # set of entry via ovs-ofctl
641 if isinstance( current_node, OVSSwitch ):
642 kwargs['vlan'] = vlan
643 kwargs['path'] = path
644 kwargs['current_hop'] = current_hop
645 kwargs['switch_inport_name'] = src_sw_inport_name
646 kwargs['switch_outport_name'] = dst_sw_outport_name
647
648 if self.controller == RemoteController:
649 ## set flow entry via ryu rest api
650 self._set_flow_entry_ryu_rest(current_node, switch_inport_nr, switch_outport_nr, **kwargs)
651 else:
652 ## set flow entry via ovs-ofctl
653 self._set_flow_entry_dpctl(current_node, switch_inport_nr, switch_outport_nr, **kwargs)
654
655 # take first link between switches by default
656 if isinstance( next_node, OVSSwitch ):
657 switch_inport_nr = self.DCNetwork_graph[current_hop][next_hop][0]['dst_port_nr']
658 current_hop = next_hop
659
660 return "path {2} between {0} and {1}".format(vnf_src_name, vnf_dst_name, cmd)
661
662 def _set_flow_entry_ryu_rest(self, node, switch_inport_nr, switch_outport_nr, **kwargs):
663 match = 'in_port=%s' % switch_inport_nr
664
665 cookie = kwargs.get('cookie')
666 match_input = kwargs.get('match')
667 cmd = kwargs.get('cmd')
668 path = kwargs.get('path')
669 current_hop = kwargs.get('current_hop')
670 vlan = kwargs.get('vlan')
671 priority = kwargs.get('priority')
672 # flag to not set the ovs port vlan tag
673 skip_vlan_tag = kwargs.get('skip_vlan_tag')
674 # table id to put this flowentry
675 table_id = kwargs.get('table_id')
676 if not table_id:
677 table_id = 0
678
679 s = ','
680 if match_input:
681 match = s.join([match, match_input])
682
683 flow = {}
684 flow['dpid'] = int(node.dpid, 16)
685
686 if cookie:
687 flow['cookie'] = int(cookie)
688 if priority:
689 flow['priority'] = int(priority)
690
691 flow['table_id'] = table_id
692
693 flow['actions'] = []
694
695 # possible Ryu actions, match fields:
696 # http://ryu.readthedocs.io/en/latest/app/ofctl_rest.html#add-a-flow-entry
697 if cmd == 'add-flow':
698 prefix = 'stats/flowentry/add'
699 if vlan != None:
700 if path.index(current_hop) == 0: # first node
701 # set vlan tag in ovs instance (to isolate E-LANs)
702 if not skip_vlan_tag:
703 in_port_name = kwargs.get('switch_inport_name')
704 self._set_vlan_tag(node, in_port_name, vlan)
705 # set vlan push action if more than 1 switch in the path
706 if len(path) > 1:
707 action = {}
708 action['type'] = 'PUSH_VLAN' # Push a new VLAN tag if a input frame is non-VLAN-tagged
709 action['ethertype'] = 33024 # Ethertype 0x8100(=33024): IEEE 802.1Q VLAN-tagged frame
710 flow['actions'].append(action)
711 action = {}
712 action['type'] = 'SET_FIELD'
713 action['field'] = 'vlan_vid'
714 action['value'] = vlan
715 flow['actions'].append(action)
716
717 if path.index(current_hop) == len(path) - 1: # last node
718 # set vlan tag in ovs instance (to isolate E-LANs)
719 if not skip_vlan_tag:
720 out_port_name = kwargs.get('switch_outport_name')
721 self._set_vlan_tag(node, out_port_name, vlan)
722 # set vlan pop action if more than 1 switch in the path
723 if len(path) > 1:
724 match += ',dl_vlan=%s' % vlan
725 action = {}
726 action['type'] = 'POP_VLAN'
727 flow['actions'].append(action)
728
729 if 0 < path.index(current_hop) < (len(path) - 1): # middle nodes
730 match += ',dl_vlan=%s' % vlan
731
732 # output action must come last
733 action = {}
734 action['type'] = 'OUTPUT'
735 action['port'] = switch_outport_nr
736 flow['actions'].append(action)
737
738 elif cmd == 'del-flows':
739 prefix = 'stats/flowentry/delete'
740
741 if cookie:
742 # TODO: add cookie_mask as argument
743 flow['cookie_mask'] = int('0xffffffffffffffff', 16) # need full mask to match complete cookie
744
745 action = {}
746 action['type'] = 'OUTPUT'
747 action['port'] = switch_outport_nr
748 flow['actions'].append(action)
749
750 flow['match'] = self._parse_match(match)
751 self.ryu_REST(prefix, data=flow)
752
753 def _set_vlan_tag(self, node, switch_port, tag):
754 node.vsctl('set', 'port {0} tag={1}'.format(switch_port,tag))
755 LOG.debug("set vlan in switch: {0} in_port: {1} vlan tag: {2}".format(node.name, switch_port, tag))
756
757 def _set_flow_entry_dpctl(self, node, switch_inport_nr, switch_outport_nr, **kwargs):
758
759 match = 'in_port=%s' % switch_inport_nr
760
761 cookie = kwargs.get('cookie')
762 match_input = kwargs.get('match')
763 cmd = kwargs.get('cmd')
764 path = kwargs.get('path')
765 current_hop = kwargs.get('current_hop')
766 vlan = kwargs.get('vlan')
767
768 s = ','
769 if cookie:
770 cookie = 'cookie=%s' % cookie
771 match = s.join([cookie, match])
772 if match_input:
773 match = s.join([match, match_input])
774 if cmd == 'add-flow':
775 action = 'action=%s' % switch_outport_nr
776 if vlan != None:
777 if path.index(current_hop) == 0: # first node
778 action = ('action=mod_vlan_vid:%s' % vlan) + (',output=%s' % switch_outport_nr)
779 match = '-O OpenFlow13 ' + match
780 elif path.index(current_hop) == len(path) - 1: # last node
781 match += ',dl_vlan=%s' % vlan
782 action = 'action=strip_vlan,output=%s' % switch_outport_nr
783 else: # middle nodes
784 match += ',dl_vlan=%s' % vlan
785 ofcmd = s.join([match, action])
786 elif cmd == 'del-flows':
787 ofcmd = match
788 else:
789 ofcmd = ''
790
791 node.dpctl(cmd, ofcmd)
792 LOG.info("{3} in switch: {0} in_port: {1} out_port: {2}".format(node.name, switch_inport_nr,
793 switch_outport_nr, cmd))
794
795 # start Ryu Openflow controller as Remote Controller for the DCNetwork
796 def startRyu(self, learning_switch=True):
797 # start Ryu controller with rest-API
798 python_install_path = site.getsitepackages()[0]
799 # ryu default learning switch
800 #ryu_path = python_install_path + '/ryu/app/simple_switch_13.py'
801 #custom learning switch that installs a default NORMAL action in the ovs switches
802 dir_path = os.path.dirname(os.path.realpath(__file__))
803 ryu_path = dir_path + '/son_emu_simple_switch_13.py'
804 ryu_path2 = python_install_path + '/ryu/app/ofctl_rest.py'
805 # change the default Openflow controller port to 6653 (official IANA-assigned port number), as used by Mininet
806 # Ryu still uses 6633 as default
807 ryu_option = '--ofp-tcp-listen-port'
808 ryu_of_port = '6653'
809 ryu_cmd = 'ryu-manager'
810 FNULL = open("/tmp/ryu.log", 'w')
811 if learning_switch:
812 self.ryu_process = Popen([ryu_cmd, ryu_path, ryu_path2, ryu_option, ryu_of_port], stdout=FNULL, stderr=FNULL)
813 LOG.debug('starting ryu-controller with {0}'.format(ryu_path))
814 LOG.debug('starting ryu-controller with {0}'.format(ryu_path2))
815 else:
816 # no learning switch, but with rest api
817 self.ryu_process = Popen([ryu_cmd, ryu_path2, ryu_option, ryu_of_port], stdout=FNULL, stderr=FNULL)
818 LOG.debug('starting ryu-controller with {0}'.format(ryu_path2))
819 time.sleep(1)
820
821 def killRyu(self):
822 """
823 Stop the Ryu controller that might be started by son-emu.
824 :return:
825 """
826 # try it nicely
827 if self.ryu_process is not None:
828 self.ryu_process.terminate()
829 self.ryu_process.kill()
830 # ensure its death ;-)
831 Popen(['pkill', '-f', 'ryu-manager'])
832
833 def ryu_REST(self, prefix, dpid=None, data=None):
834
835 if dpid:
836 url = self.ryu_REST_api + '/' + str(prefix) + '/' + str(dpid)
837 else:
838 url = self.ryu_REST_api + '/' + str(prefix)
839 if data:
840 req = self.RyuSession.post(url, json=data)
841 else:
842 req = self.RyuSession.get(url)
843
844
845 # do extra logging if status code is not 200 (OK)
846 if req.status_code is not requests.codes.ok:
847 logging.info(
848 'type {0} encoding: {1} text: {2} headers: {3} history: {4}'.format(req.headers['content-type'],
849 req.encoding, req.text,
850 req.headers, req.history))
851 LOG.info('url: {0}'.format(str(url)))
852 if data: LOG.info('POST: {0}'.format(str(data)))
853 LOG.info('status: {0} reason: {1}'.format(req.status_code, req.reason))
854
855
856 if 'json' in req.headers['content-type']:
857 ret = req.json()
858 return ret
859
860 ret = req.text.rstrip()
861 return ret
862
863
864 # need to respect that some match fields must be integers
865 # http://ryu.readthedocs.io/en/latest/app/ofctl_rest.html#description-of-match-and-actions
866 def _parse_match(self, match):
867 matches = match.split(',')
868 dict = {}
869 for m in matches:
870 match = m.split('=')
871 if len(match) == 2:
872 try:
873 m2 = int(match[1], 0)
874 except:
875 m2 = match[1]
876
877 dict.update({match[0]:m2})
878 return dict
879