2 Copyright (c) 2015 SONATA-NFV and Paderborn University
5 Licensed under the Apache License, Version 2.0 (the "License");
6 you may not use this file except in compliance with the License.
7 You may obtain a copy of the License at
9 http://www.apache.org/licenses/LICENSE-2.0
11 Unless required by applicable law or agreed to in writing, software
12 distributed under the License is distributed on an "AS IS" BASIS,
13 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 See the License for the specific language governing permissions and
15 limitations under the License.
17 Neither the name of the SONATA-NFV [, ANY ADDITIONAL AFFILIATION]
18 nor the names of its contributors may be used to endorse or promote
19 products derived from this software without specific prior written
22 This work has been performed in the framework of the SONATA project,
23 funded by the European Commission under Grant number 671517 through
24 the Horizon 2020 and 5G-PPP programmes. The authors would like to
25 acknowledge the contributions of their colleagues of the SONATA
26 partner consortium (www.sonata-nfv.eu).
32 from subprocess
import Popen
37 from mininet
.net
import Containernet
38 from mininet
.node
import Controller
, DefaultController
, OVSSwitch
, OVSKernelSwitch
, Docker
, RemoteController
39 from mininet
.cli
import CLI
40 from mininet
.link
import TCLink
41 from mininet
.clean
import cleanup
43 from emuvim
.dcemulator
.monitoring
import DCNetworkMonitor
44 from emuvim
.dcemulator
.node
import Datacenter
, EmulatorCompute
45 from emuvim
.dcemulator
.resourcemodel
import ResourceModelRegistrar
47 LOG
= logging
.getLogger("dcemulator.net")
48 LOG
.setLevel(logging
.DEBUG
)
50 # default CPU period used for cpu percentage-based cfs values (microseconds)
53 class DCNetwork(Containernet
):
55 Wraps the original Mininet/Containernet class and provides
56 methods to add data centers, switches, etc.
58 This class is used by topology definition scripts.
61 def __init__(self
, controller
=RemoteController
, monitor
=False,
62 enable_learning
=False, # learning switch behavior of the default ovs switches icw Ryu controller can be turned off/on, needed for E-LAN functionality
63 dc_emulation_max_cpu
=1.0, # fraction of overall CPU time for emulation
64 dc_emulation_max_mem
=512, # emulation max mem in MB
67 Create an extended version of a Containernet network
68 :param dc_emulation_max_cpu: max. CPU time used by containers in data centers
69 :param kwargs: path through for Mininet parameters
74 self
.ryu_process
= None
75 #list of deployed nsds.E_Lines and E_LANs (uploaded from the dummy gatekeeper)
76 self
.deployed_nsds
= []
77 self
.deployed_elines
= []
78 self
.deployed_elans
= []
79 self
.installed_chains
= []
82 # always cleanup environment before we start the emulator
86 # call original Docker.__init__ and setup default controller
87 Containernet
.__init
__(
88 self
, switch
=OVSKernelSwitch
, controller
=controller
, **kwargs
)
90 # default switch configuration
91 enable_ryu_learning
= False
93 self
.failMode
= 'standalone'
94 enable_ryu_learning
= True
96 self
.failMode
= 'secure'
99 if controller
== RemoteController
:
100 # start Ryu controller
101 self
.startRyu(learning_switch
=enable_ryu_learning
)
103 # add the specified controller
104 self
.addController('c0', controller
=controller
)
106 # graph of the complete DC network
107 self
.DCNetwork_graph
= nx
.MultiDiGraph()
109 # initialize pool of vlan tags to setup the SDN paths
110 self
.vlans
= range(4096)[::-1]
112 # link to Ryu REST_API
115 self
.ryu_REST_api
= 'http://{0}:{1}'.format(ryu_ip
, ryu_port
)
116 self
.RyuSession
= requests
.Session()
120 self
.monitor_agent
= DCNetworkMonitor(self
)
122 self
.monitor_agent
= None
124 # initialize resource model registrar
125 self
.rm_registrar
= ResourceModelRegistrar(
126 dc_emulation_max_cpu
, dc_emulation_max_mem
)
127 self
.cpu_period
= CPU_PERIOD
129 def addDatacenter(self
, label
, metadata
={}, resource_log_path
=None):
131 Create and add a logical cloud data center to the network.
133 if label
in self
.dcs
:
134 raise Exception("Data center label already exists: %s" % label
)
135 dc
= Datacenter(label
, metadata
=metadata
, resource_log_path
=resource_log_path
)
136 dc
.net
= self
# set reference to network
138 dc
.create() # finally create the data center in our Mininet instance
139 LOG
.info("added data center: %s" % label
)
142 def addLink(self
, node1
, node2
, **params
):
144 Able to handle Datacenter objects as link
147 assert node1
is not None
148 assert node2
is not None
150 # ensure type of node1
151 if isinstance( node1
, basestring
):
152 if node1
in self
.dcs
:
153 node1
= self
.dcs
[node1
].switch
154 if isinstance( node1
, Datacenter
):
156 # ensure type of node2
157 if isinstance( node2
, basestring
):
158 if node2
in self
.dcs
:
159 node2
= self
.dcs
[node2
].switch
160 if isinstance( node2
, Datacenter
):
162 # try to give containers a default IP
163 if isinstance( node1
, Docker
):
164 if "params1" not in params
:
165 params
["params1"] = {}
166 if "ip" not in params
["params1"]:
167 params
["params1"]["ip"] = self
.getNextIp()
168 if isinstance( node2
, Docker
):
169 if "params2" not in params
:
170 params
["params2"] = {}
171 if "ip" not in params
["params2"]:
172 params
["params2"]["ip"] = self
.getNextIp()
173 # ensure that we allow TCLinks between data centers
174 # TODO this is not optimal, we use cls=Link for containers and TCLink for data centers
175 # see Containernet issue: https://github.com/mpeuster/containernet/issues/3
176 if "cls" not in params
:
177 params
["cls"] = TCLink
179 link
= Containernet
.addLink(self
, node1
, node2
, **params
)
181 # try to give container interfaces a default id
182 node1_port_id
= node1
.ports
[link
.intf1
]
183 if isinstance(node1
, Docker
):
184 if "id" in params
["params1"]:
185 node1_port_id
= params
["params1"]["id"]
186 node1_port_name
= link
.intf1
.name
188 node2_port_id
= node2
.ports
[link
.intf2
]
189 if isinstance(node2
, Docker
):
190 if "id" in params
["params2"]:
191 node2_port_id
= params
["params2"]["id"]
192 node2_port_name
= link
.intf2
.name
195 # add edge and assigned port number to graph in both directions between node1 and node2
196 # port_id: id given in descriptor (if available, otherwise same as port)
197 # port: portnumber assigned by Containernet
200 # possible weight metrics allowed by TClink class:
201 weight_metrics
= ['bw', 'delay', 'jitter', 'loss']
202 edge_attributes
= [p
for p
in params
if p
in weight_metrics
]
203 for attr
in edge_attributes
:
204 # if delay: strip ms (need number as weight in graph)
205 match
= re
.search('([0-9]*\.?[0-9]+)', str(params
[attr
]))
207 attr_number
= match
.group(1)
210 attr_dict
[attr
] = attr_number
213 attr_dict2
= {'src_port_id': node1_port_id
, 'src_port_nr': node1
.ports
[link
.intf1
],
214 'src_port_name': node1_port_name
,
215 'dst_port_id': node2_port_id
, 'dst_port_nr': node2
.ports
[link
.intf2
],
216 'dst_port_name': node2_port_name
}
217 attr_dict2
.update(attr_dict
)
218 self
.DCNetwork_graph
.add_edge(node1
.name
, node2
.name
, attr_dict
=attr_dict2
)
220 attr_dict2
= {'src_port_id': node2_port_id
, 'src_port_nr': node2
.ports
[link
.intf2
],
221 'src_port_name': node2_port_name
,
222 'dst_port_id': node1_port_id
, 'dst_port_nr': node1
.ports
[link
.intf1
],
223 'dst_port_name': node1_port_name
}
224 attr_dict2
.update(attr_dict
)
225 self
.DCNetwork_graph
.add_edge(node2
.name
, node1
.name
, attr_dict
=attr_dict2
)
227 LOG
.debug("addLink: n1={0} intf1={1} -- n2={2} intf2={3}".format(
228 str(node1
),node1_port_name
, str(node2
), node2_port_name
))
232 def removeLink(self
, link
=None, node1
=None, node2
=None):
234 Remove the link from the Containernet and the networkx graph
236 Containernet
.removeLink(self
, link
=link
, node1
=node1
, node2
=node2
)
237 self
.DCNetwork_graph
.remove_edge(node2
.name
, node1
.name
)
239 def addDocker( self
, label
, **params
):
241 Wrapper for addDocker method to use custom container class.
243 self
.DCNetwork_graph
.add_node(label
, type=params
.get('type', 'docker'))
244 return Containernet
.addDocker(self
, label
, cls
=EmulatorCompute
, **params
)
246 def removeDocker( self
, label
, **params
):
248 Wrapper for removeDocker method to update graph.
250 self
.DCNetwork_graph
.remove_node(label
)
251 return Containernet
.removeDocker(self
, label
, **params
)
253 def addExtSAP(self
, sap_name
, sap_ip
, **params
):
255 Wrapper for addExtSAP method to store SAP also in graph.
257 # make sure that 'type' is set
258 params
['type'] = params
.get('type','sap_ext')
259 self
.DCNetwork_graph
.add_node(sap_name
, type=params
['type'])
260 LOG
.info('add ext sap: {0}'.format(sap_name
))
261 return Containernet
.addExtSAP(self
, sap_name
, sap_ip
, **params
)
263 def removeExtSAP(self
, sap_name
, **params
):
265 Wrapper for removeExtSAP method to remove SAP also from graph.
267 self
.DCNetwork_graph
.remove_node(sap_name
)
268 return Containernet
.removeExtSAP(self
, sap_name
)
270 def addSwitch( self
, name
, add_to_graph
=True, **params
):
272 Wrapper for addSwitch method to store switch also in graph.
275 # add this switch to the global topology overview
277 self
.DCNetwork_graph
.add_node(name
, type=params
.get('type','switch'))
278 LOG
.info('*** **** *** add switch: {0} type: {1}'.format(name
, params
.get('type')))
280 # set the learning switch behavior
281 if 'failMode' in params
:
282 failMode
= params
['failMode']
284 failMode
= self
.failMode
286 s
= Containernet
.addSwitch(self
, name
, protocols
='OpenFlow10,OpenFlow12,OpenFlow13', failMode
=failMode
, **params
)
290 def getAllContainers(self
):
292 Returns a list with all containers within all data centers.
295 for dc
in self
.dcs
.itervalues():
296 all_containers
+= dc
.listCompute()
297 return all_containers
301 for dc
in self
.dcs
.itervalues():
303 Containernet
.start(self
)
307 # stop the monitor agent
308 if self
.monitor_agent
is not None:
309 self
.monitor_agent
.stop()
312 Containernet
.stop(self
)
314 # stop Ryu controller
321 def setLAN(self
, vnf_list
):
323 setup an E-LAN network by assigning the same VLAN tag to each DC interface of the VNFs in the E-LAN
325 :param vnf_list: names of the VNFs in this E-LAN [{name:,interface:},...]
330 src_sw_inport_name
= None
332 # get a vlan tag for this E-LAN
333 vlan
= self
.vlans
.pop()
336 vnf_src_name
= vnf
['name']
337 vnf_src_interface
= vnf
['interface']
339 # check if port is specified (vnf:port)
340 if vnf_src_interface
is None:
341 # take first interface by default
342 connected_sw
= self
.DCNetwork_graph
.neighbors(vnf_src_name
)[0]
343 link_dict
= self
.DCNetwork_graph
[vnf_src_name
][connected_sw
]
344 vnf_src_interface
= link_dict
[0]['src_port_id']
346 for connected_sw
in self
.DCNetwork_graph
.neighbors(vnf_src_name
):
347 link_dict
= self
.DCNetwork_graph
[vnf_src_name
][connected_sw
]
348 for link
in link_dict
:
349 if (link_dict
[link
]['src_port_id'] == vnf_src_interface
or
350 link_dict
[link
]['src_port_name'] == vnf_src_interface
): # Fix: we might also get interface names, e.g, from a son-emu-cli call
351 # found the right link and connected switch
352 src_sw
= connected_sw
353 src_sw_inport_nr
= link_dict
[link
]['dst_port_nr']
354 src_sw_inport_name
= link_dict
[link
]['dst_port_name']
357 # set the tag on the dc switch interface
358 LOG
.debug('set E-LAN: vnf name: {0} interface: {1} tag: {2}'.format(vnf_src_name
, vnf_src_interface
,vlan
))
359 switch_node
= self
.getNodeByName(src_sw
)
360 self
._set
_vlan
_tag
(switch_node
, src_sw_inport_name
, vlan
)
362 def _addMonitorFlow(self
, vnf_src_name
, vnf_dst_name
, vnf_src_interface
=None, vnf_dst_interface
=None,
365 Add a monitoring flow entry that adds a special flowentry/counter at the begin or end of a chain.
366 So this monitoring flowrule exists on top of a previously defined chain rule and uses the same vlan tag/routing.
369 :param vnf_src_interface:
370 :param vnf_dst_interface:
371 :param tag: vlan tag to be used for this chain (same tag as existing chain)
372 :param monitor_placement: 'tx' or 'rx' indicating to place the extra flowentry resp. at the beginning or end of the chain
378 src_sw_inport_name
= None
380 dst_sw_outport_nr
= 0
381 dst_sw_outport_name
= None
383 LOG
.debug("call AddMonitorFlow vnf_src_name=%r, vnf_src_interface=%r, vnf_dst_name=%r, vnf_dst_interface=%r",
384 vnf_src_name
, vnf_src_interface
, vnf_dst_name
, vnf_dst_interface
)
386 #check if port is specified (vnf:port)
387 if vnf_src_interface
is None:
388 # take first interface by default
389 connected_sw
= self
.DCNetwork_graph
.neighbors(vnf_src_name
)[0]
390 link_dict
= self
.DCNetwork_graph
[vnf_src_name
][connected_sw
]
391 vnf_src_interface
= link_dict
[0]['src_port_id']
393 for connected_sw
in self
.DCNetwork_graph
.neighbors(vnf_src_name
):
394 link_dict
= self
.DCNetwork_graph
[vnf_src_name
][connected_sw
]
395 for link
in link_dict
:
396 if (link_dict
[link
]['src_port_id'] == vnf_src_interface
or
397 link_dict
[link
]['src_port_name'] == vnf_src_interface
): # Fix: we might also get interface names, e.g, from a son-emu-cli call
398 # found the right link and connected switch
399 src_sw
= connected_sw
400 src_sw_inport_nr
= link_dict
[link
]['dst_port_nr']
401 src_sw_inport_name
= link_dict
[link
]['dst_port_name']
404 if vnf_dst_interface
is None:
405 # take first interface by default
406 connected_sw
= self
.DCNetwork_graph
.neighbors(vnf_dst_name
)[0]
407 link_dict
= self
.DCNetwork_graph
[connected_sw
][vnf_dst_name
]
408 vnf_dst_interface
= link_dict
[0]['dst_port_id']
410 vnf_dst_name
= vnf_dst_name
.split(':')[0]
411 for connected_sw
in self
.DCNetwork_graph
.neighbors(vnf_dst_name
):
412 link_dict
= self
.DCNetwork_graph
[connected_sw
][vnf_dst_name
]
413 for link
in link_dict
:
414 if link_dict
[link
]['dst_port_id'] == vnf_dst_interface
or \
415 link_dict
[link
]['dst_port_name'] == vnf_dst_interface
: # Fix: we might also get interface names, e.g, from a son-emu-cli call
416 # found the right link and connected switch
417 dst_sw
= connected_sw
418 dst_sw_outport_nr
= link_dict
[link
]['src_port_nr']
419 dst_sw_outport_name
= link_dict
[link
]['src_port_name']
423 LOG
.exception('tag not valid: {0}'.format(tag
))
427 # returns the first found shortest path
428 # if all shortest paths are wanted, use: all_shortest_paths
429 path
= nx
.shortest_path(self
.DCNetwork_graph
, src_sw
, dst_sw
, weight
=kwargs
.get('weight'))
431 LOG
.exception("No path could be found between {0} and {1} using src_sw={2} and dst_sw={3}".format(
432 vnf_src_name
, vnf_dst_name
, src_sw
, dst_sw
))
433 LOG
.debug("Graph nodes: %r" % self
.DCNetwork_graph
.nodes())
434 LOG
.debug("Graph edges: %r" % self
.DCNetwork_graph
.edges())
435 for e
, v
in self
.DCNetwork_graph
.edges():
436 LOG
.debug("%r" % self
.DCNetwork_graph
[e
][v
])
437 return "No path could be found between {0} and {1}".format(vnf_src_name
, vnf_dst_name
)
439 LOG
.info("Path between {0} and {1}: {2}".format(vnf_src_name
, vnf_dst_name
, path
))
442 switch_inport_nr
= src_sw_inport_nr
444 cmd
= kwargs
.get('cmd')
446 #iterate through the path to install the flow-entries
447 for i
in range(0,len(path
)):
448 current_node
= self
.getNodeByName(current_hop
)
450 if path
.index(current_hop
) < len(path
)-1:
451 next_hop
= path
[path
.index(current_hop
)+1]
454 next_hop
= vnf_dst_name
456 next_node
= self
.getNodeByName(next_hop
)
458 if next_hop
== vnf_dst_name
:
459 switch_outport_nr
= dst_sw_outport_nr
460 LOG
.info("end node reached: {0}".format(vnf_dst_name
))
461 elif not isinstance( next_node
, OVSSwitch
):
462 LOG
.info("Next node: {0} is not a switch".format(next_hop
))
463 return "Next node: {0} is not a switch".format(next_hop
)
465 # take first link between switches by default
467 switch_outport_nr
= self
.DCNetwork_graph
[current_hop
][next_hop
][index_edge_out
]['src_port_nr']
470 # set of entry via ovs-ofctl
471 if isinstance( current_node
, OVSSwitch
):
473 kwargs
['path'] = path
474 kwargs
['current_hop'] = current_hop
475 kwargs
['switch_inport_name'] = src_sw_inport_name
476 kwargs
['switch_outport_name'] = dst_sw_outport_name
477 kwargs
['skip_vlan_tag'] = True
478 kwargs
['pathindex'] = i
480 monitor_placement
= kwargs
.get('monitor_placement').strip()
481 # put monitor flow at the dst switch
483 if monitor_placement
== 'tx' and path
.index(current_hop
) == 0: # first node:
485 # put monitoring flow at the src switch
486 elif monitor_placement
== 'rx' and path
.index(current_hop
) == len(path
) - 1: # last node:
488 elif monitor_placement
not in ['rx', 'tx']:
489 LOG
.exception('invalid monitor command: {0}'.format(monitor_placement
))
492 if self
.controller
== RemoteController
and insert_flow
:
493 ## set flow entry via ryu rest api
494 self
._set
_flow
_entry
_ryu
_rest
(current_node
, switch_inport_nr
, switch_outport_nr
, **kwargs
)
497 ## set flow entry via ovs-ofctl
498 self
._set
_flow
_entry
_dpctl
(current_node
, switch_inport_nr
, switch_outport_nr
, **kwargs
)
501 # take first link between switches by default
502 if isinstance( next_node
, OVSSwitch
):
503 switch_inport_nr
= self
.DCNetwork_graph
[current_hop
][next_hop
][0]['dst_port_nr']
504 current_hop
= next_hop
506 return "path {2} between {0} and {1}".format(vnf_src_name
, vnf_dst_name
, cmd
)
509 def setChain(self
, vnf_src_name
, vnf_dst_name
, vnf_src_interface
=None, vnf_dst_interface
=None, **kwargs
):
511 Chain 2 vnf interfaces together by installing the flowrules in the switches along their path.
512 Currently the path is found using the default networkx shortest path function.
513 Each chain gets a unique vlan id , so different chains wil not interfere.
515 :param vnf_src_name: vnf name (string)
516 :param vnf_dst_name: vnf name (string)
517 :param vnf_src_interface: source interface name (string)
518 :param vnf_dst_interface: destination interface name (string)
519 :param cmd: 'add-flow' (default) to add a chain, 'del-flows' to remove a chain
520 :param cookie: cookie for the installed flowrules (can be used later as identifier for a set of installed chains)
521 :param match: custom match entry to be added to the flowrules (default: only in_port and vlan tag)
522 :param priority: custom flowrule priority
523 :param monitor: boolean to indicate whether this chain is a monitoring chain
524 :param tag: vlan tag to be used for this chain (pre-defined or new one if none is specified)
525 :param skip_vlan_tag: boolean to indicate if a vlan tag should be appointed to this flow or not
526 :param path: custom path between the two VNFs (list of switches)
527 :return: output log string
530 # special procedure for monitoring flows
531 if kwargs
.get('monitor'):
533 # check if chain already exists
534 found_chains
= [chain_dict
for chain_dict
in self
.installed_chains
if
535 (chain_dict
['vnf_src_name'] == vnf_src_name
and chain_dict
['vnf_src_interface'] == vnf_src_interface
536 and chain_dict
['vnf_dst_name'] == vnf_dst_name
and chain_dict
['vnf_dst_interface'] == vnf_dst_interface
)]
538 if len(found_chains
) > 0:
539 # this chain exists, so need an extra monitoring flow
540 # assume only 1 chain per vnf/interface pair
541 LOG
.debug('*** installing monitoring chain on top of pre-defined chain from {0}:{1} -> {2}:{3}'.
542 format(vnf_src_name
, vnf_src_interface
, vnf_dst_name
, vnf_dst_interface
))
543 tag
= found_chains
[0]['tag']
544 ret
= self
._addMonitorFlow
(vnf_src_name
, vnf_dst_name
, vnf_src_interface
, vnf_dst_interface
,
545 tag
=tag
, table_id
=0, **kwargs
)
548 # no chain existing (or E-LAN) -> install normal chain
549 LOG
.warning('*** installing monitoring chain without pre-defined NSD chain from {0}:{1} -> {2}:{3}'.
550 format(vnf_src_name
, vnf_src_interface
, vnf_dst_name
, vnf_dst_interface
))
554 cmd
= kwargs
.get('cmd')
555 if cmd
== 'add-flow' or cmd
== 'del-flows':
556 ret
= self
._chainAddFlow
(vnf_src_name
, vnf_dst_name
, vnf_src_interface
, vnf_dst_interface
, **kwargs
)
557 if kwargs
.get('bidirectional'):
558 if kwargs
.get('path') is not None:
559 kwargs
['path'] = list(reversed(kwargs
.get('path')))
560 ret
= ret
+'\n' + self
._chainAddFlow
(vnf_dst_name
, vnf_src_name
, vnf_dst_interface
, vnf_src_interface
, **kwargs
)
563 ret
= "Command unknown"
568 def _chainAddFlow(self
, vnf_src_name
, vnf_dst_name
, vnf_src_interface
=None, vnf_dst_interface
=None, **kwargs
):
572 src_sw_inport_name
= None
574 dst_sw_outport_nr
= 0
575 dst_sw_outport_name
= None
577 LOG
.debug("call chainAddFlow vnf_src_name=%r, vnf_src_interface=%r, vnf_dst_name=%r, vnf_dst_interface=%r",
578 vnf_src_name
, vnf_src_interface
, vnf_dst_name
, vnf_dst_interface
)
580 #check if port is specified (vnf:port)
581 if vnf_src_interface
is None:
582 # take first interface by default
583 connected_sw
= self
.DCNetwork_graph
.neighbors(vnf_src_name
)[0]
584 link_dict
= self
.DCNetwork_graph
[vnf_src_name
][connected_sw
]
585 vnf_src_interface
= link_dict
[0]['src_port_id']
587 for connected_sw
in self
.DCNetwork_graph
.neighbors(vnf_src_name
):
588 link_dict
= self
.DCNetwork_graph
[vnf_src_name
][connected_sw
]
589 for link
in link_dict
:
590 if (link_dict
[link
]['src_port_id'] == vnf_src_interface
or
591 link_dict
[link
]['src_port_name'] == vnf_src_interface
): # Fix: we might also get interface names, e.g, from a son-emu-cli call
592 # found the right link and connected switch
593 src_sw
= connected_sw
594 src_sw_inport_nr
= link_dict
[link
]['dst_port_nr']
595 src_sw_inport_name
= link_dict
[link
]['dst_port_name']
598 if vnf_dst_interface
is None:
599 # take first interface by default
600 connected_sw
= self
.DCNetwork_graph
.neighbors(vnf_dst_name
)[0]
601 link_dict
= self
.DCNetwork_graph
[connected_sw
][vnf_dst_name
]
602 vnf_dst_interface
= link_dict
[0]['dst_port_id']
604 vnf_dst_name
= vnf_dst_name
.split(':')[0]
605 for connected_sw
in self
.DCNetwork_graph
.neighbors(vnf_dst_name
):
606 link_dict
= self
.DCNetwork_graph
[connected_sw
][vnf_dst_name
]
607 for link
in link_dict
:
608 if link_dict
[link
]['dst_port_id'] == vnf_dst_interface
or \
609 link_dict
[link
]['dst_port_name'] == vnf_dst_interface
: # Fix: we might also get interface names, e.g, from a son-emu-cli call
610 # found the right link and connected switch
611 dst_sw
= connected_sw
612 dst_sw_outport_nr
= link_dict
[link
]['src_port_nr']
613 dst_sw_outport_name
= link_dict
[link
]['src_port_name']
616 path
= kwargs
.get('path')
620 # returns the first found shortest path
621 # if all shortest paths are wanted, use: all_shortest_paths
622 path
= nx
.shortest_path(self
.DCNetwork_graph
, src_sw
, dst_sw
, weight
=kwargs
.get('weight'))
624 LOG
.exception("No path could be found between {0} and {1} using src_sw={2} and dst_sw={3}".format(
625 vnf_src_name
, vnf_dst_name
, src_sw
, dst_sw
))
626 LOG
.debug("Graph nodes: %r" % self
.DCNetwork_graph
.nodes())
627 LOG
.debug("Graph edges: %r" % self
.DCNetwork_graph
.edges())
628 for e
, v
in self
.DCNetwork_graph
.edges():
629 LOG
.debug("%r" % self
.DCNetwork_graph
[e
][v
])
630 return "No path could be found between {0} and {1}".format(vnf_src_name
, vnf_dst_name
)
632 LOG
.info("Path between {0} and {1}: {2}".format(vnf_src_name
, vnf_dst_name
, path
))
635 switch_inport_nr
= src_sw_inport_nr
638 cmd
= kwargs
.get('cmd')
640 if cmd
== 'add-flow':
641 if kwargs
.get('tag'):
642 # use pre-defined tag
643 vlan
= kwargs
.get('tag')
645 vlan
= self
.vlans
.pop()
647 # store the used vlan tag to identify this chain
648 if not kwargs
.get('monitor'):
650 chain_dict
['vnf_src_name'] = vnf_src_name
651 chain_dict
['vnf_dst_name'] = vnf_dst_name
652 chain_dict
['vnf_src_interface'] = vnf_src_interface
653 chain_dict
['vnf_dst_interface'] = vnf_dst_interface
654 chain_dict
['tag'] = vlan
655 self
.installed_chains
.append(chain_dict
)
657 #iterate through the path to install the flow-entries
658 for i
in range(0,len(path
)):
659 current_node
= self
.getNodeByName(current_hop
)
661 if i
< len(path
) - 1:
662 next_hop
= path
[i
+ 1]
664 # last switch reached
665 next_hop
= vnf_dst_name
667 next_node
= self
.getNodeByName(next_hop
)
669 if next_hop
== vnf_dst_name
:
670 switch_outport_nr
= dst_sw_outport_nr
671 LOG
.info("end node reached: {0}".format(vnf_dst_name
))
672 elif not isinstance( next_node
, OVSSwitch
):
673 LOG
.info("Next node: {0} is not a switch".format(next_hop
))
674 return "Next node: {0} is not a switch".format(next_hop
)
676 # take first link between switches by default
678 switch_outport_nr
= self
.DCNetwork_graph
[current_hop
][next_hop
][index_edge_out
]['src_port_nr']
682 if isinstance( current_node
, OVSSwitch
):
683 kwargs
['vlan'] = vlan
684 kwargs
['path'] = path
685 kwargs
['current_hop'] = current_hop
686 kwargs
['switch_inport_name'] = src_sw_inport_name
687 kwargs
['switch_outport_name'] = dst_sw_outport_name
688 kwargs
['pathindex'] = i
690 if self
.controller
== RemoteController
:
691 ## set flow entry via ryu rest api
692 self
._set
_flow
_entry
_ryu
_rest
(current_node
, switch_inport_nr
, switch_outport_nr
, **kwargs
)
694 ## set flow entry via ovs-ofctl
695 self
._set
_flow
_entry
_dpctl
(current_node
, switch_inport_nr
, switch_outport_nr
, **kwargs
)
697 # take first link between switches by default
698 if isinstance( next_node
, OVSSwitch
):
699 switch_inport_nr
= self
.DCNetwork_graph
[current_hop
][next_hop
][0]['dst_port_nr']
700 current_hop
= next_hop
702 return "path {2} between {0} and {1}".format(vnf_src_name
, vnf_dst_name
, cmd
)
704 def _set_flow_entry_ryu_rest(self
, node
, switch_inport_nr
, switch_outport_nr
, **kwargs
):
705 match
= 'in_port=%s' % switch_inport_nr
707 cookie
= kwargs
.get('cookie')
708 match_input
= kwargs
.get('match')
709 cmd
= kwargs
.get('cmd')
710 path
= kwargs
.get('path')
711 index
= kwargs
.get('pathindex')
713 vlan
= kwargs
.get('vlan')
714 priority
= kwargs
.get('priority')
715 # flag to not set the ovs port vlan tag
716 skip_vlan_tag
= kwargs
.get('skip_vlan_tag')
717 # table id to put this flowentry
718 table_id
= kwargs
.get('table_id')
724 match
= s
.join([match
, match_input
])
727 flow
['dpid'] = int(node
.dpid
, 16)
730 flow
['cookie'] = int(cookie
)
732 flow
['priority'] = int(priority
)
734 flow
['table_id'] = table_id
738 # possible Ryu actions, match fields:
739 # http://ryu.readthedocs.io/en/latest/app/ofctl_rest.html#add-a-flow-entry
740 if cmd
== 'add-flow':
741 prefix
= 'stats/flowentry/add'
743 if index
== 0: # first node
744 # set vlan tag in ovs instance (to isolate E-LANs)
745 if not skip_vlan_tag
:
746 in_port_name
= kwargs
.get('switch_inport_name')
747 self
._set
_vlan
_tag
(node
, in_port_name
, vlan
)
748 # set vlan push action if more than 1 switch in the path
751 action
['type'] = 'PUSH_VLAN' # Push a new VLAN tag if a input frame is non-VLAN-tagged
752 action
['ethertype'] = 33024 # Ethertype 0x8100(=33024): IEEE 802.1Q VLAN-tagged frame
753 flow
['actions'].append(action
)
755 action
['type'] = 'SET_FIELD'
756 action
['field'] = 'vlan_vid'
757 # ryu expects the field to be masked
758 action
['value'] = vlan |
0x1000
759 flow
['actions'].append(action
)
761 elif index
== len(path
) - 1: # last node
762 # set vlan tag in ovs instance (to isolate E-LANs)
763 if not skip_vlan_tag
:
764 out_port_name
= kwargs
.get('switch_outport_name')
765 self
._set
_vlan
_tag
(node
, out_port_name
, vlan
)
766 # set vlan pop action if more than 1 switch in the path
768 match
+= ',dl_vlan=%s' % vlan
770 action
['type'] = 'POP_VLAN'
771 flow
['actions'].append(action
)
774 match
+= ',dl_vlan=%s' % vlan
776 # output action must come last
778 action
['type'] = 'OUTPUT'
779 action
['port'] = switch_outport_nr
780 flow
['actions'].append(action
)
782 elif cmd
== 'del-flows':
783 prefix
= 'stats/flowentry/delete'
786 # TODO: add cookie_mask as argument
787 flow
['cookie_mask'] = int('0xffffffffffffffff', 16) # need full mask to match complete cookie
790 action
['type'] = 'OUTPUT'
791 action
['port'] = switch_outport_nr
792 flow
['actions'].append(action
)
794 flow
['match'] = self
._parse
_match
(match
)
795 self
.ryu_REST(prefix
, data
=flow
)
797 def _set_vlan_tag(self
, node
, switch_port
, tag
):
798 node
.vsctl('set', 'port {0} tag={1}'.format(switch_port
,tag
))
799 LOG
.debug("set vlan in switch: {0} in_port: {1} vlan tag: {2}".format(node
.name
, switch_port
, tag
))
801 def _set_flow_entry_dpctl(self
, node
, switch_inport_nr
, switch_outport_nr
, **kwargs
):
803 match
= 'in_port=%s' % switch_inport_nr
805 cookie
= kwargs
.get('cookie')
806 match_input
= kwargs
.get('match')
807 cmd
= kwargs
.get('cmd')
808 path
= kwargs
.get('path')
809 index
= kwargs
.get('pathindex')
810 vlan
= kwargs
.get('vlan')
814 cookie
= 'cookie=%s' % cookie
815 match
= s
.join([cookie
, match
])
817 match
= s
.join([match
, match_input
])
818 if cmd
== 'add-flow':
819 action
= 'action=%s' % switch_outport_nr
821 if index
== 0: # first node
822 action
= ('action=mod_vlan_vid:%s' % vlan
) + (',output=%s' % switch_outport_nr
)
823 match
= '-O OpenFlow13 ' + match
824 elif index
== len(path
) - 1: # last node
825 match
+= ',dl_vlan=%s' % vlan
826 action
= 'action=strip_vlan,output=%s' % switch_outport_nr
828 match
+= ',dl_vlan=%s' % vlan
829 ofcmd
= s
.join([match
, action
])
830 elif cmd
== 'del-flows':
835 node
.dpctl(cmd
, ofcmd
)
836 LOG
.info("{3} in switch: {0} in_port: {1} out_port: {2}".format(node
.name
, switch_inport_nr
,
837 switch_outport_nr
, cmd
))
839 # start Ryu Openflow controller as Remote Controller for the DCNetwork
840 def startRyu(self
, learning_switch
=True):
841 # start Ryu controller with rest-API
842 python_install_path
= site
.getsitepackages()[0]
843 # ryu default learning switch
844 #ryu_path = python_install_path + '/ryu/app/simple_switch_13.py'
845 #custom learning switch that installs a default NORMAL action in the ovs switches
846 dir_path
= os
.path
.dirname(os
.path
.realpath(__file__
))
847 ryu_path
= dir_path
+ '/son_emu_simple_switch_13.py'
848 ryu_path2
= python_install_path
+ '/ryu/app/ofctl_rest.py'
849 # change the default Openflow controller port to 6653 (official IANA-assigned port number), as used by Mininet
850 # Ryu still uses 6633 as default
851 ryu_option
= '--ofp-tcp-listen-port'
853 ryu_cmd
= 'ryu-manager'
854 FNULL
= open("/tmp/ryu.log", 'w')
856 self
.ryu_process
= Popen([ryu_cmd
, ryu_path
, ryu_path2
, ryu_option
, ryu_of_port
], stdout
=FNULL
, stderr
=FNULL
)
857 LOG
.debug('starting ryu-controller with {0}'.format(ryu_path
))
858 LOG
.debug('starting ryu-controller with {0}'.format(ryu_path2
))
860 # no learning switch, but with rest api
861 self
.ryu_process
= Popen([ryu_cmd
, ryu_path2
, ryu_option
, ryu_of_port
], stdout
=FNULL
, stderr
=FNULL
)
862 LOG
.debug('starting ryu-controller with {0}'.format(ryu_path2
))
867 Stop the Ryu controller that might be started by son-emu.
871 if self
.ryu_process
is not None:
872 self
.ryu_process
.terminate()
873 self
.ryu_process
.kill()
874 # ensure its death ;-)
875 Popen(['pkill', '-f', 'ryu-manager'])
877 def ryu_REST(self
, prefix
, dpid
=None, data
=None):
880 url
= self
.ryu_REST_api
+ '/' + str(prefix
) + '/' + str(dpid
)
882 url
= self
.ryu_REST_api
+ '/' + str(prefix
)
884 req
= self
.RyuSession
.post(url
, json
=data
)
886 req
= self
.RyuSession
.get(url
)
889 # do extra logging if status code is not 200 (OK)
890 if req
.status_code
is not requests
.codes
.ok
:
892 'type {0} encoding: {1} text: {2} headers: {3} history: {4}'.format(req
.headers
['content-type'],
893 req
.encoding
, req
.text
,
894 req
.headers
, req
.history
))
895 LOG
.info('url: {0}'.format(str(url
)))
896 if data
: LOG
.info('POST: {0}'.format(str(data
)))
897 LOG
.info('status: {0} reason: {1}'.format(req
.status_code
, req
.reason
))
900 if 'json' in req
.headers
['content-type']:
904 ret
= req
.text
.rstrip()
908 # need to respect that some match fields must be integers
909 # http://ryu.readthedocs.io/en/latest/app/ofctl_rest.html#description-of-match-and-actions
910 def _parse_match(self
, match
):
911 matches
= match
.split(',')
917 m2
= int(match
[1], 0)
921 dict.update({match
[0]:m2
})
924 def find_connected_dc_interface(self
, vnf_src_name
, vnf_src_interface
):
925 for connected_sw
in self
.DCNetwork_graph
.neighbors(vnf_src_name
):
926 link_dict
= self
.DCNetwork_graph
[vnf_src_name
][connected_sw
]
927 for link
in link_dict
:
928 if (link_dict
[link
]['src_port_id'] == vnf_src_interface
or
929 link_dict
[link
]['src_port_name'] == vnf_src_interface
): # Fix: we might also get interface names, e.g, from a son-emu-cli call
930 # found the right link and connected switch
931 src_sw
= connected_sw
932 src_sw_inport_nr
= link_dict
[link
]['dst_port_nr']
933 src_sw_inport_name
= link_dict
[link
]['dst_port_name']
934 return src_sw_inport_name