340f340db957024edad9fc4b09a73d22b60b8765
[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 mod_dl_dst = kwargs.get('mod_dl_dst')
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 if mod_dl_dst:
831 action = {}
832 action['type'] = 'SET_FIELD'
833 action['field'] = 'eth_dst'
834 action['value'] = mod_dl_dst
835 flow['actions'].append(action)
836
837 # output action must come last
838 action = {}
839 action['type'] = 'OUTPUT'
840 action['port'] = switch_outport_nr
841 flow['actions'].append(action)
842
843 elif cmd == 'del-flows':
844 prefix = 'stats/flowentry/delete'
845
846 if cookie:
847 # TODO: add cookie_mask as argument
848 # need full mask to match complete cookie
849 flow['cookie_mask'] = int('0xffffffffffffffff', 16)
850
851 action = {}
852 action['type'] = 'OUTPUT'
853 action['port'] = switch_outport_nr
854 flow['actions'].append(action)
855
856 flow['match'] = self._parse_match(match)
857 self.ryu_REST(prefix, data=flow)
858
859 def _set_vlan_tag(self, node, switch_port, tag):
860 node.vsctl('set', 'port {0} tag={1}'.format(switch_port, tag))
861 LOG.debug("set vlan in switch: {0} in_port: {1} vlan tag: {2}".format(
862 node.name, switch_port, tag))
863
864 def _set_flow_entry_dpctl(
865 self, node, switch_inport_nr, switch_outport_nr, **kwargs):
866
867 match = 'in_port=%s' % switch_inport_nr
868
869 cookie = kwargs.get('cookie')
870 match_input = kwargs.get('match')
871 cmd = kwargs.get('cmd')
872 path = kwargs.get('path')
873 index = kwargs.get('pathindex')
874 vlan = kwargs.get('vlan')
875
876 s = ','
877 if cookie:
878 cookie = 'cookie=%s' % cookie
879 match = s.join([cookie, match])
880 if match_input:
881 match = s.join([match, match_input])
882 if cmd == 'add-flow':
883 action = 'action=%s' % switch_outport_nr
884 if vlan is not None:
885 if index == 0: # first node
886 action = ('action=mod_vlan_vid:%s' % vlan) + \
887 (',output=%s' % switch_outport_nr)
888 match = '-O OpenFlow13 ' + match
889 elif index == len(path) - 1: # last node
890 match += ',dl_vlan=%s' % vlan
891 action = 'action=strip_vlan,output=%s' % switch_outport_nr
892 else: # middle nodes
893 match += ',dl_vlan=%s' % vlan
894 ofcmd = s.join([match, action])
895 elif cmd == 'del-flows':
896 ofcmd = match
897 else:
898 ofcmd = ''
899
900 node.dpctl(cmd, ofcmd)
901 LOG.info("{3} in switch: {0} in_port: {1} out_port: {2}".format(node.name, switch_inport_nr,
902 switch_outport_nr, cmd))
903
904 # start Ryu Openflow controller as Remote Controller for the DCNetwork
905 def startRyu(self, learning_switch=True):
906 # start Ryu controller with rest-API
907
908 # ryu default learning switch
909 # ryu_learning_app = python_install_path + '/ryu/app/simple_switch_13.py'
910 # custom learning switch that installs a default NORMAL action in the
911 # ovs switches
912 dir_path = os.path.dirname(os.path.realpath(__file__))
913 ryu_learning_app = dir_path + '/son_emu_simple_switch_13.py'
914 ryu_rest_app = 'ryu.app.ofctl_rest'
915 # change the default Openflow controller port to 6653 (official IANA-assigned port number), as used by Mininet
916 # Ryu still uses 6633 as default
917 ryu_option = '--ofp-tcp-listen-port'
918 ryu_of_port = '6653'
919 ryu_cmd = 'ryu-manager'
920 FNULL = open("/tmp/ryu.log", 'w')
921 if learning_switch:
922 # learning and rest api
923 args = [ryu_cmd, ryu_learning_app, ryu_rest_app, ryu_option, ryu_of_port]
924 else:
925 # no learning switch, but with rest api
926 args = [ryu_cmd, ryu_rest_app, ryu_option, ryu_of_port]
927 self.ryu_process = Popen(args, stdout=FNULL, stderr=FNULL)
928 LOG.debug('starting ryu-controller with %s' % args)
929 time.sleep(1)
930
931 def killRyu(self):
932 """
933 Stop the Ryu controller that might be started by son-emu.
934 :return:
935 """
936 # try it nicely
937 if self.ryu_process is not None:
938 self.ryu_process.terminate()
939 self.ryu_process.kill()
940 # ensure its death ;-)
941 Popen(['pkill', '-f', 'ryu-manager'])
942
943 def ryu_REST(self, prefix, dpid=None, data=None):
944
945 if dpid:
946 url = self.ryu_REST_api + '/' + str(prefix) + '/' + str(dpid)
947 else:
948 url = self.ryu_REST_api + '/' + str(prefix)
949 if data:
950 req = self.RyuSession.post(url, json=data)
951 else:
952 req = self.RyuSession.get(url)
953
954 # do extra logging if status code is not 200 (OK)
955 if req.status_code is not requests.codes.ok:
956 logging.info(
957 'type {0} encoding: {1} text: {2} headers: {3} history: {4}'.format(req.headers['content-type'],
958 req.encoding, req.text,
959 req.headers, req.history))
960 LOG.info('url: {0}'.format(str(url)))
961 if data:
962 LOG.info('POST: {0}'.format(str(data)))
963 LOG.info('status: {0} reason: {1}'.format(
964 req.status_code, req.reason))
965
966 if 'json' in req.headers['content-type']:
967 ret = req.json()
968 return ret
969
970 ret = req.text.rstrip()
971 return ret
972
973 # need to respect that some match fields must be integers
974 # http://ryu.readthedocs.io/en/latest/app/ofctl_rest.html#description-of-match-and-actions
975
976 def _parse_match(self, match):
977 matches = match.split(',')
978 dict = {}
979 for m in matches:
980 match = m.split('=')
981 if len(match) == 2:
982 try:
983 m2 = int(match[1], 0)
984 except BaseException:
985 m2 = match[1]
986
987 dict.update({match[0]: m2})
988 return dict
989
990 def find_connected_dc_interface(
991 self, vnf_src_name, vnf_src_interface=None):
992
993 if vnf_src_interface is None:
994 # take first interface by default
995 connected_sw = self.DCNetwork_graph.neighbors(vnf_src_name)[0]
996 link_dict = self.DCNetwork_graph[vnf_src_name][connected_sw]
997 vnf_src_interface = link_dict[0]['src_port_id']
998
999 for connected_sw in self.DCNetwork_graph.neighbors(vnf_src_name):
1000 link_dict = self.DCNetwork_graph[vnf_src_name][connected_sw]
1001 for link in link_dict:
1002 if (link_dict[link]['src_port_id'] == vnf_src_interface or
1003 link_dict[link]['src_port_name'] == vnf_src_interface):
1004 # Fix: we might also get interface names, e.g, from a son-emu-cli call
1005 # found the right link and connected switch
1006 src_sw_inport_name = link_dict[link]['dst_port_name']
1007 return src_sw_inport_name