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