1f30ba7abfcce8422fcd8189622944049d53edcd
[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.debug("Creating 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.debug("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.debug("Creating 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.debug("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 LOG.info("Installed flow rule: ({}:{}) -> ({}:{}) with options: {}"
750 .format(vnf_src_name, vnf_src_interface, vnf_dst_name, vnf_dst_interface, flow_options))
751 return "success: {2} between {0} and {1} with options: {3}".format(
752 vnf_src_name, vnf_dst_name, cmd, flow_options_str)
753
754 def _set_flow_entry_ryu_rest(
755 self, node, switch_inport_nr, switch_outport_nr, **kwargs):
756 match = 'in_port=%s' % switch_inport_nr
757
758 cookie = kwargs.get('cookie')
759 match_input = kwargs.get('match')
760 cmd = kwargs.get('cmd')
761 path = kwargs.get('path')
762 index = kwargs.get('pathindex')
763
764 vlan = kwargs.get('vlan')
765 priority = kwargs.get('priority', DEFAULT_PRIORITY)
766 # flag to not set the ovs port vlan tag
767 skip_vlan_tag = kwargs.get('skip_vlan_tag')
768 # table id to put this flowentry
769 table_id = kwargs.get('table_id')
770 if not table_id:
771 table_id = 0
772
773 s = ','
774 if match_input:
775 match = s.join([match, match_input])
776
777 flow = {}
778 flow['dpid'] = int(node.dpid, 16)
779
780 if cookie:
781 flow['cookie'] = int(cookie)
782 if priority:
783 flow['priority'] = int(priority)
784
785 flow['table_id'] = table_id
786
787 flow['actions'] = []
788
789 # possible Ryu actions, match fields:
790 # http://ryu.readthedocs.io/en/latest/app/ofctl_rest.html#add-a-flow-entry
791 if cmd == 'add-flow':
792 prefix = 'stats/flowentry/add'
793 if vlan is not None:
794 if index == 0: # first node
795 # set vlan tag in ovs instance (to isolate E-LANs)
796 if not skip_vlan_tag:
797 in_port_name = kwargs.get('switch_inport_name')
798 self._set_vlan_tag(node, in_port_name, vlan)
799 # set vlan push action if more than 1 switch in the path
800 if len(path) > 1:
801 action = {}
802 # Push a new VLAN tag if a input frame is
803 # non-VLAN-tagged
804 action['type'] = 'PUSH_VLAN'
805 # Ethertype 0x8100(=33024): IEEE 802.1Q VLAN-tagged
806 # frame
807 action['ethertype'] = 33024
808 flow['actions'].append(action)
809 action = {}
810 action['type'] = 'SET_FIELD'
811 action['field'] = 'vlan_vid'
812 # ryu expects the field to be masked
813 action['value'] = vlan | 0x1000
814 flow['actions'].append(action)
815
816 elif index == len(path) - 1: # last node
817 # set vlan tag in ovs instance (to isolate E-LANs)
818 if not skip_vlan_tag:
819 out_port_name = kwargs.get('switch_outport_name')
820 self._set_vlan_tag(node, out_port_name, vlan)
821 # set vlan pop action if more than 1 switch in the path
822 if len(path) > 1:
823 match += ',dl_vlan=%s' % vlan
824 action = {}
825 action['type'] = 'POP_VLAN'
826 flow['actions'].append(action)
827
828 else: # middle nodes
829 match += ',dl_vlan=%s' % vlan
830
831 # output action must come last
832 action = {}
833 action['type'] = 'OUTPUT'
834 action['port'] = switch_outport_nr
835 flow['actions'].append(action)
836
837 elif cmd == 'del-flows':
838 prefix = 'stats/flowentry/delete'
839
840 if cookie:
841 # TODO: add cookie_mask as argument
842 # need full mask to match complete cookie
843 flow['cookie_mask'] = int('0xffffffffffffffff', 16)
844
845 action = {}
846 action['type'] = 'OUTPUT'
847 action['port'] = switch_outport_nr
848 flow['actions'].append(action)
849
850 flow['match'] = self._parse_match(match)
851 self.ryu_REST(prefix, data=flow)
852
853 def _set_vlan_tag(self, node, switch_port, tag):
854 node.vsctl('set', 'port {0} tag={1}'.format(switch_port, tag))
855 LOG.debug("set vlan in switch: {0} in_port: {1} vlan tag: {2}".format(
856 node.name, switch_port, tag))
857
858 def _set_flow_entry_dpctl(
859 self, node, switch_inport_nr, switch_outport_nr, **kwargs):
860
861 match = 'in_port=%s' % switch_inport_nr
862
863 cookie = kwargs.get('cookie')
864 match_input = kwargs.get('match')
865 cmd = kwargs.get('cmd')
866 path = kwargs.get('path')
867 index = kwargs.get('pathindex')
868 vlan = kwargs.get('vlan')
869
870 s = ','
871 if cookie:
872 cookie = 'cookie=%s' % cookie
873 match = s.join([cookie, match])
874 if match_input:
875 match = s.join([match, match_input])
876 if cmd == 'add-flow':
877 action = 'action=%s' % switch_outport_nr
878 if vlan is not None:
879 if index == 0: # first node
880 action = ('action=mod_vlan_vid:%s' % vlan) + \
881 (',output=%s' % switch_outport_nr)
882 match = '-O OpenFlow13 ' + match
883 elif index == len(path) - 1: # last node
884 match += ',dl_vlan=%s' % vlan
885 action = 'action=strip_vlan,output=%s' % switch_outport_nr
886 else: # middle nodes
887 match += ',dl_vlan=%s' % vlan
888 ofcmd = s.join([match, action])
889 elif cmd == 'del-flows':
890 ofcmd = match
891 else:
892 ofcmd = ''
893
894 node.dpctl(cmd, ofcmd)
895 LOG.info("{3} in switch: {0} in_port: {1} out_port: {2}".format(node.name, switch_inport_nr,
896 switch_outport_nr, cmd))
897
898 # start Ryu Openflow controller as Remote Controller for the DCNetwork
899 def startRyu(self, learning_switch=True):
900 # start Ryu controller with rest-API
901 python_install_path = site.getsitepackages()[0]
902 # ryu default learning switch
903 # ryu_path = python_install_path + '/ryu/app/simple_switch_13.py'
904 # custom learning switch that installs a default NORMAL action in the
905 # ovs switches
906 dir_path = os.path.dirname(os.path.realpath(__file__))
907 ryu_path = dir_path + '/son_emu_simple_switch_13.py'
908 ryu_path2 = python_install_path + '/ryu/app/ofctl_rest.py'
909 # change the default Openflow controller port to 6653 (official IANA-assigned port number), as used by Mininet
910 # Ryu still uses 6633 as default
911 ryu_option = '--ofp-tcp-listen-port'
912 ryu_of_port = '6653'
913 ryu_cmd = 'ryu-manager'
914 FNULL = open("/tmp/ryu.log", 'w')
915 if learning_switch:
916 self.ryu_process = Popen(
917 [ryu_cmd, ryu_path, ryu_path2, ryu_option, ryu_of_port], stdout=FNULL, stderr=FNULL)
918 LOG.debug('starting ryu-controller with {0}'.format(ryu_path))
919 LOG.debug('starting ryu-controller with {0}'.format(ryu_path2))
920 else:
921 # no learning switch, but with rest api
922 self.ryu_process = Popen(
923 [ryu_cmd, ryu_path2, ryu_option, ryu_of_port], stdout=FNULL, stderr=FNULL)
924 LOG.debug('starting ryu-controller with {0}'.format(ryu_path2))
925 time.sleep(1)
926
927 def killRyu(self):
928 """
929 Stop the Ryu controller that might be started by son-emu.
930 :return:
931 """
932 # try it nicely
933 if self.ryu_process is not None:
934 self.ryu_process.terminate()
935 self.ryu_process.kill()
936 # ensure its death ;-)
937 Popen(['pkill', '-f', 'ryu-manager'])
938
939 def ryu_REST(self, prefix, dpid=None, data=None):
940
941 if dpid:
942 url = self.ryu_REST_api + '/' + str(prefix) + '/' + str(dpid)
943 else:
944 url = self.ryu_REST_api + '/' + str(prefix)
945 if data:
946 req = self.RyuSession.post(url, json=data)
947 else:
948 req = self.RyuSession.get(url)
949
950 # do extra logging if status code is not 200 (OK)
951 if req.status_code is not requests.codes.ok:
952 logging.info(
953 'type {0} encoding: {1} text: {2} headers: {3} history: {4}'.format(req.headers['content-type'],
954 req.encoding, req.text,
955 req.headers, req.history))
956 LOG.info('url: {0}'.format(str(url)))
957 if data:
958 LOG.info('POST: {0}'.format(str(data)))
959 LOG.info('status: {0} reason: {1}'.format(
960 req.status_code, req.reason))
961
962 if 'json' in req.headers['content-type']:
963 ret = req.json()
964 return ret
965
966 ret = req.text.rstrip()
967 return ret
968
969 # need to respect that some match fields must be integers
970 # http://ryu.readthedocs.io/en/latest/app/ofctl_rest.html#description-of-match-and-actions
971
972 def _parse_match(self, match):
973 matches = match.split(',')
974 dict = {}
975 for m in matches:
976 match = m.split('=')
977 if len(match) == 2:
978 try:
979 m2 = int(match[1], 0)
980 except BaseException:
981 m2 = match[1]
982
983 dict.update({match[0]: m2})
984 return dict
985
986 def find_connected_dc_interface(
987 self, vnf_src_name, vnf_src_interface=None):
988
989 if vnf_src_interface is None:
990 # take first interface by default
991 connected_sw = self.DCNetwork_graph.neighbors(vnf_src_name)[0]
992 link_dict = self.DCNetwork_graph[vnf_src_name][connected_sw]
993 vnf_src_interface = link_dict[0]['src_port_id']
994
995 for connected_sw in self.DCNetwork_graph.neighbors(vnf_src_name):
996 link_dict = self.DCNetwork_graph[vnf_src_name][connected_sw]
997 for link in link_dict:
998 if (link_dict[link]['src_port_id'] == vnf_src_interface or
999 link_dict[link]['src_port_name'] == vnf_src_interface):
1000 # Fix: we might also get interface names, e.g, from a son-emu-cli call
1001 # found the right link and connected switch
1002 src_sw_inport_name = link_dict[link]['dst_port_name']
1003 return src_sw_inport_name