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