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