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