Updated to latest docker-py version.
[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 = list(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, str):
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, str):
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_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_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.values():
324 all_containers += dc.listCompute()
325 return all_containers
326
327 def start(self):
328 # start
329 for dc in self.dcs.values():
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 getNodeByName(self, name):
389 """
390 Wraps Containernet's getNodeByName method to avoid
391 key not found exceptions.
392 """
393 try:
394 return super(DCNetwork, self).getNodeByName(name)
395 except BaseException as ex:
396 LOG.warning("Node not found: {}".format(name))
397 LOG.debug("Node not found: {}".format(ex))
398 return None
399
400 def _addMonitorFlow(self, vnf_src_name, vnf_dst_name, vnf_src_interface=None, vnf_dst_interface=None,
401 tag=None, **kwargs):
402 """
403 Add a monitoring flow entry that adds a special flowentry/counter at the begin or end of a chain.
404 So this monitoring flowrule exists on top of a previously defined chain rule and uses the same vlan tag/routing.
405 :param vnf_src_name:
406 :param vnf_dst_name:
407 :param vnf_src_interface:
408 :param vnf_dst_interface:
409 :param tag: vlan tag to be used for this chain (same tag as existing chain)
410 :param monitor_placement: 'tx' or 'rx' indicating to place the extra flowentry resp. at the beginning or end of the chain
411 :return:
412 """
413
414 src_sw = None
415 src_sw_inport_nr = 0
416 src_sw_inport_name = None
417 dst_sw = None
418 dst_sw_outport_nr = 0
419 dst_sw_outport_name = None
420
421 LOG.debug("call AddMonitorFlow vnf_src_name=%r, vnf_src_interface=%r, vnf_dst_name=%r, vnf_dst_interface=%r",
422 vnf_src_name, vnf_src_interface, vnf_dst_name, vnf_dst_interface)
423
424 # check if port is specified (vnf:port)
425 if vnf_src_interface is None:
426 # take first interface by default
427 connected_sw = self.DCNetwork_graph.neighbors(vnf_src_name)[0]
428 link_dict = self.DCNetwork_graph[vnf_src_name][connected_sw]
429 vnf_src_interface = link_dict[0]['src_port_id']
430
431 for connected_sw in self.DCNetwork_graph.neighbors(vnf_src_name):
432 link_dict = self.DCNetwork_graph[vnf_src_name][connected_sw]
433 for link in link_dict:
434 if (link_dict[link]['src_port_id'] == vnf_src_interface or
435 link_dict[link]['src_port_name'] == vnf_src_interface): # Fix: we might also get interface names, e.g, from a son-emu-cli call
436 # found the right link and connected switch
437 src_sw = connected_sw
438 src_sw_inport_nr = link_dict[link]['dst_port_nr']
439 src_sw_inport_name = link_dict[link]['dst_port_name']
440 break
441
442 if vnf_dst_interface is None:
443 # take first interface by default
444 connected_sw = self.DCNetwork_graph.neighbors(vnf_dst_name)[0]
445 link_dict = self.DCNetwork_graph[connected_sw][vnf_dst_name]
446 vnf_dst_interface = link_dict[0]['dst_port_id']
447
448 vnf_dst_name = vnf_dst_name.split(':')[0]
449 for connected_sw in self.DCNetwork_graph.neighbors(vnf_dst_name):
450 link_dict = self.DCNetwork_graph[connected_sw][vnf_dst_name]
451 for link in link_dict:
452 if link_dict[link]['dst_port_id'] == vnf_dst_interface or \
453 link_dict[link]['dst_port_name'] == vnf_dst_interface: # Fix: we might also get interface names, e.g, from a son-emu-cli call
454 # found the right link and connected switch
455 dst_sw = connected_sw
456 dst_sw_outport_nr = link_dict[link]['src_port_nr']
457 dst_sw_outport_name = link_dict[link]['src_port_name']
458 break
459
460 if not tag >= 0:
461 LOG.exception('tag not valid: {0}'.format(tag))
462
463 # get shortest path
464 try:
465 # returns the first found shortest path
466 # if all shortest paths are wanted, use: all_shortest_paths
467 path = nx.shortest_path(
468 self.DCNetwork_graph, src_sw, dst_sw, weight=kwargs.get('weight'))
469 except BaseException:
470 LOG.exception("No path could be found between {0} and {1} using src_sw={2} and dst_sw={3}".format(
471 vnf_src_name, vnf_dst_name, src_sw, dst_sw))
472 LOG.debug("Graph nodes: %r" % self.DCNetwork_graph.nodes())
473 LOG.debug("Graph edges: %r" % self.DCNetwork_graph.edges())
474 for e, v in self.DCNetwork_graph.edges():
475 LOG.debug("%r" % self.DCNetwork_graph[e][v])
476 return "No path could be found between {0} and {1}".format(
477 vnf_src_name, vnf_dst_name)
478
479 LOG.debug("Creating path between {0} and {1}: {2}".format(
480 vnf_src_name, vnf_dst_name, path))
481
482 current_hop = src_sw
483 switch_inport_nr = src_sw_inport_nr
484
485 cmd = kwargs.get('cmd')
486
487 # iterate through the path to install the flow-entries
488 for i in range(0, len(path)):
489 current_node = self.getNodeByName(current_hop)
490
491 if path.index(current_hop) < len(path) - 1:
492 next_hop = path[path.index(current_hop) + 1]
493 else:
494 # last switch reached
495 next_hop = vnf_dst_name
496
497 next_node = self.getNodeByName(next_hop)
498
499 if next_hop == vnf_dst_name:
500 switch_outport_nr = dst_sw_outport_nr
501 LOG.debug("end node reached: {0}".format(vnf_dst_name))
502 elif not isinstance(next_node, OVSSwitch):
503 LOG.info("Next node: {0} is not a switch".format(next_hop))
504 return "Next node: {0} is not a switch".format(next_hop)
505 else:
506 # take first link between switches by default
507 index_edge_out = 0
508 switch_outport_nr = self.DCNetwork_graph[current_hop][next_hop][index_edge_out]['src_port_nr']
509
510 # set of entry via ovs-ofctl
511 if isinstance(current_node, OVSSwitch):
512 kwargs['vlan'] = tag
513 kwargs['path'] = path
514 kwargs['current_hop'] = current_hop
515 kwargs['switch_inport_name'] = src_sw_inport_name
516 kwargs['switch_outport_name'] = dst_sw_outport_name
517 kwargs['skip_vlan_tag'] = True
518 kwargs['pathindex'] = i
519
520 monitor_placement = kwargs.get('monitor_placement').strip()
521 # put monitor flow at the dst switch
522 insert_flow = False
523 # first node:
524 if monitor_placement == 'tx' and path.index(current_hop) == 0:
525 insert_flow = True
526 # put monitoring flow at the src switch
527 # last node:
528 elif monitor_placement == 'rx' and path.index(current_hop) == len(path) - 1:
529 insert_flow = True
530 elif monitor_placement not in ['rx', 'tx']:
531 LOG.exception(
532 'invalid monitor command: {0}'.format(monitor_placement))
533
534 if self.controller == RemoteController and insert_flow:
535 # set flow entry via ryu rest api
536 self._set_flow_entry_ryu_rest(
537 current_node, switch_inport_nr, switch_outport_nr, **kwargs)
538 break
539 elif insert_flow:
540 # set flow entry via ovs-ofctl
541 self._set_flow_entry_dpctl(
542 current_node, switch_inport_nr, switch_outport_nr, **kwargs)
543 break
544
545 # take first link between switches by default
546 if isinstance(next_node, OVSSwitch):
547 switch_inport_nr = self.DCNetwork_graph[current_hop][next_hop][0]['dst_port_nr']
548 current_hop = next_hop
549
550 return "path {2} between {0} and {1}".format(
551 vnf_src_name, vnf_dst_name, cmd)
552
553 def setChain(self, vnf_src_name, vnf_dst_name,
554 vnf_src_interface=None, vnf_dst_interface=None, **kwargs):
555 """
556 Chain 2 vnf interfaces together by installing the flowrules in the switches along their path.
557 Currently the path is found using the default networkx shortest path function.
558 Each chain gets a unique vlan id , so different chains wil not interfere.
559
560 :param vnf_src_name: vnf name (string)
561 :param vnf_dst_name: vnf name (string)
562 :param vnf_src_interface: source interface name (string)
563 :param vnf_dst_interface: destination interface name (string)
564 :param cmd: 'add-flow' (default) to add a chain, 'del-flows' to remove a chain
565 :param cookie: cookie for the installed flowrules (can be used later as identifier for a set of installed chains)
566 :param match: custom match entry to be added to the flowrules (default: only in_port and vlan tag)
567 :param priority: custom flowrule priority
568 :param monitor: boolean to indicate whether this chain is a monitoring chain
569 :param tag: vlan tag to be used for this chain (pre-defined or new one if none is specified)
570 :param skip_vlan_tag: boolean to indicate if a vlan tag should be appointed to this flow or not
571 :param path: custom path between the two VNFs (list of switches)
572 :return: output log string
573 """
574
575 # special procedure for monitoring flows
576 if kwargs.get('monitor'):
577
578 # check if chain already exists
579 found_chains = [chain_dict for chain_dict in self.installed_chains if
580 (chain_dict['vnf_src_name'] == vnf_src_name and
581 chain_dict['vnf_src_interface'] == vnf_src_interface and
582 chain_dict['vnf_dst_name'] == vnf_dst_name and
583 chain_dict['vnf_dst_interface'] == vnf_dst_interface)]
584
585 if len(found_chains) > 0:
586 # this chain exists, so need an extra monitoring flow
587 # assume only 1 chain per vnf/interface pair
588 LOG.debug('*** installing monitoring chain on top of pre-defined chain from {0}:{1} -> {2}:{3}'.
589 format(vnf_src_name, vnf_src_interface, vnf_dst_name, vnf_dst_interface))
590 tag = found_chains[0]['tag']
591 ret = self._addMonitorFlow(vnf_src_name, vnf_dst_name, vnf_src_interface, vnf_dst_interface,
592 tag=tag, table_id=0, **kwargs)
593 return ret
594 else:
595 # no chain existing (or E-LAN) -> install normal chain
596 LOG.warning('*** installing monitoring chain without pre-defined NSD chain from {0}:{1} -> {2}:{3}'.
597 format(vnf_src_name, vnf_src_interface, vnf_dst_name, vnf_dst_interface))
598 pass
599
600 cmd = kwargs.get('cmd', 'add-flow')
601 if cmd == 'add-flow' or cmd == 'del-flows':
602 ret = self._chainAddFlow(
603 vnf_src_name, vnf_dst_name, vnf_src_interface, vnf_dst_interface, **kwargs)
604 if kwargs.get('bidirectional'):
605 if kwargs.get('path') is not None:
606 kwargs['path'] = list(reversed(kwargs.get('path')))
607 ret = ret + '\n' + \
608 self._chainAddFlow(
609 vnf_dst_name, vnf_src_name, vnf_dst_interface, vnf_src_interface, **kwargs)
610
611 else:
612 ret = "Command unknown"
613
614 return ret
615
616 def _chainAddFlow(self, vnf_src_name, vnf_dst_name,
617 vnf_src_interface=None, vnf_dst_interface=None, **kwargs):
618
619 src_sw = None
620 src_sw_inport_nr = 0
621 src_sw_inport_name = None
622 dst_sw = None
623 dst_sw_outport_nr = 0
624 dst_sw_outport_name = None
625
626 LOG.debug("call chainAddFlow vnf_src_name=%r, vnf_src_interface=%r, vnf_dst_name=%r, vnf_dst_interface=%r",
627 vnf_src_name, vnf_src_interface, vnf_dst_name, vnf_dst_interface)
628
629 # check if port is specified (vnf:port)
630 if vnf_src_interface is None:
631 # take first interface by default
632 connected_sw = list(self.DCNetwork_graph.neighbors(vnf_src_name))[0]
633 link_dict = self.DCNetwork_graph[vnf_src_name][connected_sw]
634 vnf_src_interface = link_dict[0]['src_port_id']
635
636 for connected_sw in self.DCNetwork_graph.neighbors(vnf_src_name):
637 link_dict = self.DCNetwork_graph[vnf_src_name][connected_sw]
638 for link in link_dict:
639 if (link_dict[link]['src_port_id'] == vnf_src_interface or
640 link_dict[link]['src_port_name'] == vnf_src_interface): # Fix: we might also get interface names, e.g, from a son-emu-cli call
641 # found the right link and connected switch
642 src_sw = connected_sw
643 src_sw_inport_nr = link_dict[link]['dst_port_nr']
644 src_sw_inport_name = link_dict[link]['dst_port_name']
645 break
646
647 if vnf_dst_interface is None:
648 # take first interface by default
649 connected_sw = list(self.DCNetwork_graph.neighbors(vnf_dst_name))[0]
650 link_dict = self.DCNetwork_graph[connected_sw][vnf_dst_name]
651 vnf_dst_interface = link_dict[0]['dst_port_id']
652
653 vnf_dst_name = vnf_dst_name.split(':')[0]
654 for connected_sw in self.DCNetwork_graph.neighbors(vnf_dst_name):
655 link_dict = self.DCNetwork_graph[connected_sw][vnf_dst_name]
656 for link in link_dict:
657 if link_dict[link]['dst_port_id'] == vnf_dst_interface or \
658 link_dict[link]['dst_port_name'] == vnf_dst_interface: # Fix: we might also get interface names, e.g, from a son-emu-cli call
659 # found the right link and connected switch
660 dst_sw = connected_sw
661 dst_sw_outport_nr = link_dict[link]['src_port_nr']
662 dst_sw_outport_name = link_dict[link]['src_port_name']
663 break
664
665 path = kwargs.get('path')
666 if path is None:
667 # get shortest path
668 try:
669 # returns the first found shortest path
670 # if all shortest paths are wanted, use: all_shortest_paths
671 path = nx.shortest_path(
672 self.DCNetwork_graph, src_sw, dst_sw, weight=kwargs.get('weight'))
673 except BaseException:
674 LOG.exception("No path could be found between {0} and {1} using src_sw={2} and dst_sw={3}".format(
675 vnf_src_name, vnf_dst_name, src_sw, dst_sw))
676 LOG.debug("Graph nodes: %r" % self.DCNetwork_graph.nodes())
677 LOG.debug("Graph edges: %r" % self.DCNetwork_graph.edges())
678 for e, v in self.DCNetwork_graph.edges():
679 LOG.debug("%r" % self.DCNetwork_graph[e][v])
680 return "No path could be found between {0} and {1}".format(
681 vnf_src_name, vnf_dst_name)
682
683 LOG.debug("Creating path between {0} and {1}: {2}".format(
684 vnf_src_name, vnf_dst_name, path))
685
686 current_hop = src_sw
687 switch_inport_nr = src_sw_inport_nr
688
689 # choose free vlan
690 cmd = kwargs.get('cmd')
691 vlan = None
692 if cmd == 'add-flow':
693 if kwargs.get('tag'):
694 # use pre-defined tag
695 vlan = kwargs.get('tag')
696 else:
697 vlan = self.vlans.pop()
698
699 # store the used vlan tag to identify this chain
700 if not kwargs.get('monitor'):
701 chain_dict = {}
702 chain_dict['vnf_src_name'] = vnf_src_name
703 chain_dict['vnf_dst_name'] = vnf_dst_name
704 chain_dict['vnf_src_interface'] = vnf_src_interface
705 chain_dict['vnf_dst_interface'] = vnf_dst_interface
706 chain_dict['tag'] = vlan
707 self.installed_chains.append(chain_dict)
708
709 # iterate through the path to install the flow-entries
710 for i in range(0, len(path)):
711 current_node = self.getNodeByName(current_hop)
712
713 if i < len(path) - 1:
714 next_hop = path[i + 1]
715 else:
716 # last switch reached
717 next_hop = vnf_dst_name
718
719 next_node = self.getNodeByName(next_hop)
720
721 if next_hop == vnf_dst_name:
722 switch_outport_nr = dst_sw_outport_nr
723 LOG.debug("end node reached: {0}".format(vnf_dst_name))
724 elif not isinstance(next_node, OVSSwitch):
725 LOG.info("Next node: {0} is not a switch".format(next_hop))
726 return "Next node: {0} is not a switch".format(next_hop)
727 else:
728 # take first link between switches by default
729 index_edge_out = 0
730 switch_outport_nr = self.DCNetwork_graph[current_hop][next_hop][index_edge_out]['src_port_nr']
731
732 # set OpenFlow entry
733 if isinstance(current_node, OVSSwitch):
734 kwargs['vlan'] = vlan
735 kwargs['path'] = path
736 kwargs['current_hop'] = current_hop
737 kwargs['switch_inport_name'] = src_sw_inport_name
738 kwargs['switch_outport_name'] = dst_sw_outport_name
739 kwargs['pathindex'] = i
740
741 if self.controller == RemoteController:
742 # set flow entry via ryu rest api
743 self._set_flow_entry_ryu_rest(
744 current_node, switch_inport_nr, switch_outport_nr, **kwargs)
745 else:
746 # set flow entry via ovs-ofctl
747 self._set_flow_entry_dpctl(
748 current_node, switch_inport_nr, switch_outport_nr, **kwargs)
749
750 # take first link between switches by default
751 if isinstance(next_node, OVSSwitch):
752 switch_inport_nr = self.DCNetwork_graph[current_hop][next_hop][0]['dst_port_nr']
753 current_hop = next_hop
754
755 flow_options = {
756 'priority': kwargs.get('priority', DEFAULT_PRIORITY),
757 'cookie': kwargs.get('cookie', DEFAULT_COOKIE),
758 'vlan': kwargs['vlan'],
759 'path': kwargs['path'],
760 'match_input': kwargs.get('match')
761 }
762 flow_options_str = json.dumps(flow_options, indent=1)
763 LOG.info("Installed flow rule: ({}:{}) -> ({}:{}) with options: {}"
764 .format(vnf_src_name, vnf_src_interface, vnf_dst_name, vnf_dst_interface, flow_options))
765 return "success: {2} between {0} and {1} with options: {3}".format(
766 vnf_src_name, vnf_dst_name, cmd, flow_options_str)
767
768 def _set_flow_entry_ryu_rest(
769 self, node, switch_inport_nr, switch_outport_nr, **kwargs):
770 match = 'in_port=%s' % switch_inport_nr
771
772 cookie = kwargs.get('cookie')
773 match_input = kwargs.get('match')
774 cmd = kwargs.get('cmd')
775 path = kwargs.get('path')
776 index = kwargs.get('pathindex')
777 mod_dl_dst = kwargs.get('mod_dl_dst')
778
779 vlan = kwargs.get('vlan')
780 priority = kwargs.get('priority', DEFAULT_PRIORITY)
781 # flag to not set the ovs port vlan tag
782 skip_vlan_tag = kwargs.get('skip_vlan_tag')
783 # table id to put this flowentry
784 table_id = kwargs.get('table_id')
785 if not table_id:
786 table_id = 0
787
788 s = ','
789 if match_input:
790 match = s.join([match, match_input])
791
792 flow = {}
793 flow['dpid'] = int(node.dpid, 16)
794
795 if cookie:
796 flow['cookie'] = int(cookie)
797 if priority:
798 flow['priority'] = int(priority)
799
800 flow['table_id'] = table_id
801
802 flow['actions'] = []
803
804 # possible Ryu actions, match fields:
805 # http://ryu.readthedocs.io/en/latest/app/ofctl_rest.html#add-a-flow-entry
806 if cmd == 'add-flow':
807 prefix = 'stats/flowentry/add'
808 if vlan is not None:
809 if index == 0: # first node
810 # set vlan tag in ovs instance (to isolate E-LANs)
811 if not skip_vlan_tag:
812 in_port_name = kwargs.get('switch_inport_name')
813 self._set_vlan_tag(node, in_port_name, vlan)
814 # set vlan push action if more than 1 switch in the path
815 if len(path) > 1:
816 action = {}
817 # Push a new VLAN tag if a input frame is
818 # non-VLAN-tagged
819 action['type'] = 'PUSH_VLAN'
820 # Ethertype 0x8100(=33024): IEEE 802.1Q VLAN-tagged
821 # frame
822 action['ethertype'] = 33024
823 flow['actions'].append(action)
824 action = {}
825 action['type'] = 'SET_FIELD'
826 action['field'] = 'vlan_vid'
827 # ryu expects the field to be masked
828 action['value'] = vlan | 0x1000
829 flow['actions'].append(action)
830
831 elif index == len(path) - 1: # last node
832 # set vlan tag in ovs instance (to isolate E-LANs)
833 if not skip_vlan_tag:
834 out_port_name = kwargs.get('switch_outport_name')
835 self._set_vlan_tag(node, out_port_name, vlan)
836 # set vlan pop action if more than 1 switch in the path
837 if len(path) > 1:
838 match += ',dl_vlan=%s' % vlan
839 action = {}
840 action['type'] = 'POP_VLAN'
841 flow['actions'].append(action)
842
843 else: # middle nodes
844 match += ',dl_vlan=%s' % vlan
845 if mod_dl_dst:
846 action = {}
847 action['type'] = 'SET_FIELD'
848 action['field'] = 'eth_dst'
849 action['value'] = mod_dl_dst
850 flow['actions'].append(action)
851
852 # output action must come last
853 action = {}
854 action['type'] = 'OUTPUT'
855 action['port'] = switch_outport_nr
856 flow['actions'].append(action)
857
858 elif cmd == 'del-flows':
859 prefix = 'stats/flowentry/delete'
860
861 if cookie:
862 # TODO: add cookie_mask as argument
863 # need full mask to match complete cookie
864 flow['cookie_mask'] = int('0xffffffffffffffff', 16)
865
866 action = {}
867 action['type'] = 'OUTPUT'
868 action['port'] = switch_outport_nr
869 flow['actions'].append(action)
870
871 flow['match'] = self._parse_match(match)
872 self.ryu_REST(prefix, data=flow)
873
874 def _set_vlan_tag(self, node, switch_port, tag):
875 node.vsctl('set', 'port {0} tag={1}'.format(switch_port, tag))
876 LOG.debug("set vlan in switch: {0} in_port: {1} vlan tag: {2}".format(
877 node.name, switch_port, tag))
878
879 def _set_flow_entry_dpctl(
880 self, node, switch_inport_nr, switch_outport_nr, **kwargs):
881
882 match = 'in_port=%s' % switch_inport_nr
883
884 cookie = kwargs.get('cookie')
885 match_input = kwargs.get('match')
886 cmd = kwargs.get('cmd')
887 path = kwargs.get('path')
888 index = kwargs.get('pathindex')
889 vlan = kwargs.get('vlan')
890
891 s = ','
892 if cookie:
893 cookie = 'cookie=%s' % cookie
894 match = s.join([cookie, match])
895 if match_input:
896 match = s.join([match, match_input])
897 if cmd == 'add-flow':
898 action = 'action=%s' % switch_outport_nr
899 if vlan is not None:
900 if index == 0: # first node
901 action = ('action=mod_vlan_vid:%s' % vlan) + \
902 (',output=%s' % switch_outport_nr)
903 match = '-O OpenFlow13 ' + match
904 elif index == len(path) - 1: # last node
905 match += ',dl_vlan=%s' % vlan
906 action = 'action=strip_vlan,output=%s' % switch_outport_nr
907 else: # middle nodes
908 match += ',dl_vlan=%s' % vlan
909 ofcmd = s.join([match, action])
910 elif cmd == 'del-flows':
911 ofcmd = match
912 else:
913 ofcmd = ''
914
915 node.dpctl(cmd, ofcmd)
916 LOG.info("{3} in switch: {0} in_port: {1} out_port: {2}".format(node.name, switch_inport_nr,
917 switch_outport_nr, cmd))
918
919 # start Ryu Openflow controller as Remote Controller for the DCNetwork
920 def startRyu(self, learning_switch=True):
921 # start Ryu controller with rest-API
922
923 # ryu default learning switch
924 # ryu_learning_app = python_install_path + '/ryu/app/simple_switch_13.py'
925 # custom learning switch that installs a default NORMAL action in the
926 # ovs switches
927 dir_path = os.path.dirname(os.path.realpath(__file__))
928 ryu_learning_app = dir_path + '/son_emu_simple_switch_13.py'
929 ryu_rest_app = 'ryu.app.ofctl_rest'
930 # change the default Openflow controller port to 6653 (official IANA-assigned port number), as used by Mininet
931 # Ryu still uses 6633 as default
932 ryu_option = '--ofp-tcp-listen-port'
933 ryu_of_port = '6653'
934 ryu_cmd = 'ryu-manager'
935 FNULL = open("/tmp/ryu.log", 'w')
936 if learning_switch:
937 # learning and rest api
938 args = [ryu_cmd, ryu_learning_app, ryu_rest_app, ryu_option, ryu_of_port]
939 else:
940 # no learning switch, but with rest api
941 args = [ryu_cmd, ryu_rest_app, ryu_option, ryu_of_port]
942 self.ryu_process = Popen(args, stdout=FNULL, stderr=FNULL)
943 LOG.debug('starting ryu-controller with %s' % args)
944 time.sleep(1)
945
946 def killRyu(self):
947 """
948 Stop the Ryu controller that might be started by son-emu.
949 :return:
950 """
951 # try it nicely
952 try:
953 if self.ryu_process is not None:
954 self.ryu_process.terminate()
955 self.ryu_process.kill()
956 except BaseException as ex:
957 LOG.warning("Error during Ryu stop: {}".format(ex))
958 # ensure its death ;-)
959 Popen(['pkill', '-f', 'ryu-manager'])
960
961 def ryu_REST(self, prefix, dpid=None, data=None):
962
963 if dpid:
964 url = self.ryu_REST_api + '/' + str(prefix) + '/' + str(dpid)
965 else:
966 url = self.ryu_REST_api + '/' + str(prefix)
967
968 LOG.debug('sending RYU command: %s, payload: %s', url, data)
969 if data:
970 req = self.RyuSession.post(url, json=data)
971 else:
972 req = self.RyuSession.get(url)
973
974 # do extra logging if status code is not 200 (OK)
975 if req.status_code is not requests.codes.ok:
976 LOG.info(
977 'type {0} encoding: {1} text: {2} headers: {3} history: {4}'.format(req.headers['content-type'],
978 req.encoding, req.text,
979 req.headers, req.history))
980 LOG.info('url: {0}'.format(str(url)))
981 if data:
982 LOG.info('POST: {0}'.format(str(data)))
983 LOG.info('status: {0} reason: {1}'.format(
984 req.status_code, req.reason))
985
986 if 'json' in req.headers['content-type']:
987 ret = req.json()
988 return ret
989
990 ret = req.text.rstrip()
991 return ret
992
993 # need to respect that some match fields must be integers
994 # http://ryu.readthedocs.io/en/latest/app/ofctl_rest.html#description-of-match-and-actions
995
996 def _parse_match(self, match):
997 matches = match.split(',')
998 dict = {}
999 for m in matches:
1000 match = m.split('=')
1001 if len(match) == 2:
1002 try:
1003 m2 = int(match[1], 0)
1004 except BaseException:
1005 m2 = match[1]
1006
1007 dict.update({match[0]: m2})
1008 return dict
1009
1010 def find_connected_dc_interface(
1011 self, vnf_src_name, vnf_src_interface=None):
1012
1013 if vnf_src_interface is None:
1014 # take first interface by default
1015 connected_sw = self.DCNetwork_graph.neighbors(vnf_src_name)[0]
1016 link_dict = self.DCNetwork_graph[vnf_src_name][connected_sw]
1017 vnf_src_interface = link_dict[0]['src_port_id']
1018
1019 for connected_sw in self.DCNetwork_graph.neighbors(vnf_src_name):
1020 link_dict = self.DCNetwork_graph[vnf_src_name][connected_sw]
1021 for link in link_dict:
1022 if (link_dict[link]['src_port_id'] == vnf_src_interface or
1023 link_dict[link]['src_port_name'] == vnf_src_interface):
1024 # Fix: we might also get interface names, e.g, from a son-emu-cli call
1025 # found the right link and connected switch
1026 src_sw_inport_name = link_dict[link]['dst_port_name']
1027 return src_sw_inport_name