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