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
, EmulatorExtSAP
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
237 node1
= link
.intf1
.node
238 node2
= link
.intf2
.node
239 assert node1
is not None
240 assert node2
is not None
241 Containernet
.removeLink(self
, link
=link
, node1
=node1
, node2
=node2
)
242 # TODO we might decrease the loglevel to debug:
244 self
.DCNetwork_graph
.remove_edge(node2
.name
, node1
.name
)
246 LOG
.warning("%s not found in DCNetwork_graph." % ((node2
.name
, node1
.name
)))
248 self
.DCNetwork_graph
.remove_edge(node1
.name
, node2
.name
)
250 LOG
.warning("%s not found in DCNetwork_graph." % ((node1
.name
, node2
.name
)))
252 def addDocker( self
, label
, **params
):
254 Wrapper for addDocker method to use custom container class.
256 self
.DCNetwork_graph
.add_node(label
, type=params
.get('type', 'docker'))
257 return Containernet
.addDocker(self
, label
, cls
=EmulatorCompute
, **params
)
259 def removeDocker( self
, label
, **params
):
261 Wrapper for removeDocker method to update graph.
263 self
.DCNetwork_graph
.remove_node(label
)
264 return Containernet
.removeDocker(self
, label
, **params
)
266 def addExtSAP(self
, sap_name
, sap_ip
, **params
):
268 Wrapper for addExtSAP method to store SAP also in graph.
270 # make sure that 'type' is set
271 params
['type'] = params
.get('type','sap_ext')
272 self
.DCNetwork_graph
.add_node(sap_name
, type=params
['type'])
273 return Containernet
.addExtSAP(self
, sap_name
, sap_ip
, **params
)
275 def removeExtSAP(self
, sap_name
, **params
):
277 Wrapper for removeExtSAP method to remove SAP also from graph.
279 self
.DCNetwork_graph
.remove_node(sap_name
)
280 return Containernet
.removeExtSAP(self
, sap_name
)
282 def addSwitch( self
, name
, add_to_graph
=True, **params
):
284 Wrapper for addSwitch method to store switch also in graph.
287 # add this switch to the global topology overview
289 self
.DCNetwork_graph
.add_node(name
, type=params
.get('type','switch'))
291 # set the learning switch behavior
292 if 'failMode' in params
:
293 failMode
= params
['failMode']
295 failMode
= self
.failMode
297 s
= Containernet
.addSwitch(self
, name
, protocols
='OpenFlow10,OpenFlow12,OpenFlow13', failMode
=failMode
, **params
)
301 def getAllContainers(self
):
303 Returns a list with all containers within all data centers.
306 for dc
in self
.dcs
.itervalues():
307 all_containers
+= dc
.listCompute()
308 return all_containers
312 for dc
in self
.dcs
.itervalues():
314 Containernet
.start(self
)
318 # stop the monitor agent
319 if self
.monitor_agent
is not None:
320 self
.monitor_agent
.stop()
323 Containernet
.stop(self
)
325 # stop Ryu controller
332 def setLAN(self
, vnf_list
):
334 setup an E-LAN network by assigning the same VLAN tag to each DC interface of the VNFs in the E-LAN
336 :param vnf_list: names of the VNFs in this E-LAN [{name:,interface:},...]
341 src_sw_inport_name
= None
343 # get a vlan tag for this E-LAN
344 vlan
= self
.vlans
.pop()
347 vnf_src_name
= vnf
['name']
348 vnf_src_interface
= vnf
['interface']
350 # check if port is specified (vnf:port)
351 if vnf_src_interface
is None:
352 # take first interface by default
353 connected_sw
= self
.DCNetwork_graph
.neighbors(vnf_src_name
)[0]
354 link_dict
= self
.DCNetwork_graph
[vnf_src_name
][connected_sw
]
355 vnf_src_interface
= link_dict
[0]['src_port_id']
357 for connected_sw
in self
.DCNetwork_graph
.neighbors(vnf_src_name
):
358 link_dict
= self
.DCNetwork_graph
[vnf_src_name
][connected_sw
]
359 for link
in link_dict
:
360 if (link_dict
[link
]['src_port_id'] == vnf_src_interface
or
361 link_dict
[link
]['src_port_name'] == vnf_src_interface
): # Fix: we might also get interface names, e.g, from a son-emu-cli call
362 # found the right link and connected switch
363 src_sw
= connected_sw
364 src_sw_inport_nr
= link_dict
[link
]['dst_port_nr']
365 src_sw_inport_name
= link_dict
[link
]['dst_port_name']
368 # set the tag on the dc switch interface
369 LOG
.debug('set E-LAN: vnf name: {0} interface: {1} tag: {2}'.format(vnf_src_name
, vnf_src_interface
,vlan
))
370 switch_node
= self
.getNodeByName(src_sw
)
371 self
._set
_vlan
_tag
(switch_node
, src_sw_inport_name
, vlan
)
373 def _addMonitorFlow(self
, vnf_src_name
, vnf_dst_name
, vnf_src_interface
=None, vnf_dst_interface
=None,
376 Add a monitoring flow entry that adds a special flowentry/counter at the begin or end of a chain.
377 So this monitoring flowrule exists on top of a previously defined chain rule and uses the same vlan tag/routing.
380 :param vnf_src_interface:
381 :param vnf_dst_interface:
382 :param tag: vlan tag to be used for this chain (same tag as existing chain)
383 :param monitor_placement: 'tx' or 'rx' indicating to place the extra flowentry resp. at the beginning or end of the chain
389 src_sw_inport_name
= None
391 dst_sw_outport_nr
= 0
392 dst_sw_outport_name
= None
394 LOG
.debug("call AddMonitorFlow vnf_src_name=%r, vnf_src_interface=%r, vnf_dst_name=%r, vnf_dst_interface=%r",
395 vnf_src_name
, vnf_src_interface
, vnf_dst_name
, vnf_dst_interface
)
397 #check if port is specified (vnf:port)
398 if vnf_src_interface
is None:
399 # take first interface by default
400 connected_sw
= self
.DCNetwork_graph
.neighbors(vnf_src_name
)[0]
401 link_dict
= self
.DCNetwork_graph
[vnf_src_name
][connected_sw
]
402 vnf_src_interface
= link_dict
[0]['src_port_id']
404 for connected_sw
in self
.DCNetwork_graph
.neighbors(vnf_src_name
):
405 link_dict
= self
.DCNetwork_graph
[vnf_src_name
][connected_sw
]
406 for link
in link_dict
:
407 if (link_dict
[link
]['src_port_id'] == vnf_src_interface
or
408 link_dict
[link
]['src_port_name'] == vnf_src_interface
): # Fix: we might also get interface names, e.g, from a son-emu-cli call
409 # found the right link and connected switch
410 src_sw
= connected_sw
411 src_sw_inport_nr
= link_dict
[link
]['dst_port_nr']
412 src_sw_inport_name
= link_dict
[link
]['dst_port_name']
415 if vnf_dst_interface
is None:
416 # take first interface by default
417 connected_sw
= self
.DCNetwork_graph
.neighbors(vnf_dst_name
)[0]
418 link_dict
= self
.DCNetwork_graph
[connected_sw
][vnf_dst_name
]
419 vnf_dst_interface
= link_dict
[0]['dst_port_id']
421 vnf_dst_name
= vnf_dst_name
.split(':')[0]
422 for connected_sw
in self
.DCNetwork_graph
.neighbors(vnf_dst_name
):
423 link_dict
= self
.DCNetwork_graph
[connected_sw
][vnf_dst_name
]
424 for link
in link_dict
:
425 if link_dict
[link
]['dst_port_id'] == vnf_dst_interface
or \
426 link_dict
[link
]['dst_port_name'] == vnf_dst_interface
: # Fix: we might also get interface names, e.g, from a son-emu-cli call
427 # found the right link and connected switch
428 dst_sw
= connected_sw
429 dst_sw_outport_nr
= link_dict
[link
]['src_port_nr']
430 dst_sw_outport_name
= link_dict
[link
]['src_port_name']
434 LOG
.exception('tag not valid: {0}'.format(tag
))
438 # returns the first found shortest path
439 # if all shortest paths are wanted, use: all_shortest_paths
440 path
= nx
.shortest_path(self
.DCNetwork_graph
, src_sw
, dst_sw
, weight
=kwargs
.get('weight'))
442 LOG
.exception("No path could be found between {0} and {1} using src_sw={2} and dst_sw={3}".format(
443 vnf_src_name
, vnf_dst_name
, src_sw
, dst_sw
))
444 LOG
.debug("Graph nodes: %r" % self
.DCNetwork_graph
.nodes())
445 LOG
.debug("Graph edges: %r" % self
.DCNetwork_graph
.edges())
446 for e
, v
in self
.DCNetwork_graph
.edges():
447 LOG
.debug("%r" % self
.DCNetwork_graph
[e
][v
])
448 return "No path could be found between {0} and {1}".format(vnf_src_name
, vnf_dst_name
)
450 LOG
.info("Path between {0} and {1}: {2}".format(vnf_src_name
, vnf_dst_name
, path
))
453 switch_inport_nr
= src_sw_inport_nr
455 cmd
= kwargs
.get('cmd')
457 #iterate through the path to install the flow-entries
458 for i
in range(0,len(path
)):
459 current_node
= self
.getNodeByName(current_hop
)
461 if path
.index(current_hop
) < len(path
)-1:
462 next_hop
= path
[path
.index(current_hop
)+1]
465 next_hop
= vnf_dst_name
467 next_node
= self
.getNodeByName(next_hop
)
469 if next_hop
== vnf_dst_name
:
470 switch_outport_nr
= dst_sw_outport_nr
471 LOG
.info("end node reached: {0}".format(vnf_dst_name
))
472 elif not isinstance( next_node
, OVSSwitch
):
473 LOG
.info("Next node: {0} is not a switch".format(next_hop
))
474 return "Next node: {0} is not a switch".format(next_hop
)
476 # take first link between switches by default
478 switch_outport_nr
= self
.DCNetwork_graph
[current_hop
][next_hop
][index_edge_out
]['src_port_nr']
481 # set of entry via ovs-ofctl
482 if isinstance( current_node
, OVSSwitch
):
484 kwargs
['path'] = path
485 kwargs
['current_hop'] = current_hop
486 kwargs
['switch_inport_name'] = src_sw_inport_name
487 kwargs
['switch_outport_name'] = dst_sw_outport_name
488 kwargs
['skip_vlan_tag'] = True
489 kwargs
['pathindex'] = i
491 monitor_placement
= kwargs
.get('monitor_placement').strip()
492 # put monitor flow at the dst switch
494 if monitor_placement
== 'tx' and path
.index(current_hop
) == 0: # first node:
496 # put monitoring flow at the src switch
497 elif monitor_placement
== 'rx' and path
.index(current_hop
) == len(path
) - 1: # last node:
499 elif monitor_placement
not in ['rx', 'tx']:
500 LOG
.exception('invalid monitor command: {0}'.format(monitor_placement
))
503 if self
.controller
== RemoteController
and insert_flow
:
504 ## set flow entry via ryu rest api
505 self
._set
_flow
_entry
_ryu
_rest
(current_node
, switch_inport_nr
, switch_outport_nr
, **kwargs
)
508 ## set flow entry via ovs-ofctl
509 self
._set
_flow
_entry
_dpctl
(current_node
, switch_inport_nr
, switch_outport_nr
, **kwargs
)
512 # take first link between switches by default
513 if isinstance( next_node
, OVSSwitch
):
514 switch_inport_nr
= self
.DCNetwork_graph
[current_hop
][next_hop
][0]['dst_port_nr']
515 current_hop
= next_hop
517 return "path {2} between {0} and {1}".format(vnf_src_name
, vnf_dst_name
, cmd
)
520 def setChain(self
, vnf_src_name
, vnf_dst_name
, vnf_src_interface
=None, vnf_dst_interface
=None, **kwargs
):
522 Chain 2 vnf interfaces together by installing the flowrules in the switches along their path.
523 Currently the path is found using the default networkx shortest path function.
524 Each chain gets a unique vlan id , so different chains wil not interfere.
526 :param vnf_src_name: vnf name (string)
527 :param vnf_dst_name: vnf name (string)
528 :param vnf_src_interface: source interface name (string)
529 :param vnf_dst_interface: destination interface name (string)
530 :param cmd: 'add-flow' (default) to add a chain, 'del-flows' to remove a chain
531 :param cookie: cookie for the installed flowrules (can be used later as identifier for a set of installed chains)
532 :param match: custom match entry to be added to the flowrules (default: only in_port and vlan tag)
533 :param priority: custom flowrule priority
534 :param monitor: boolean to indicate whether this chain is a monitoring chain
535 :param tag: vlan tag to be used for this chain (pre-defined or new one if none is specified)
536 :param skip_vlan_tag: boolean to indicate if a vlan tag should be appointed to this flow or not
537 :param path: custom path between the two VNFs (list of switches)
538 :return: output log string
541 # special procedure for monitoring flows
542 if kwargs
.get('monitor'):
544 # check if chain already exists
545 found_chains
= [chain_dict
for chain_dict
in self
.installed_chains
if
546 (chain_dict
['vnf_src_name'] == vnf_src_name
and chain_dict
['vnf_src_interface'] == vnf_src_interface
547 and chain_dict
['vnf_dst_name'] == vnf_dst_name
and chain_dict
['vnf_dst_interface'] == vnf_dst_interface
)]
549 if len(found_chains
) > 0:
550 # this chain exists, so need an extra monitoring flow
551 # assume only 1 chain per vnf/interface pair
552 LOG
.debug('*** installing monitoring chain on top of pre-defined chain from {0}:{1} -> {2}:{3}'.
553 format(vnf_src_name
, vnf_src_interface
, vnf_dst_name
, vnf_dst_interface
))
554 tag
= found_chains
[0]['tag']
555 ret
= self
._addMonitorFlow
(vnf_src_name
, vnf_dst_name
, vnf_src_interface
, vnf_dst_interface
,
556 tag
=tag
, table_id
=0, **kwargs
)
559 # no chain existing (or E-LAN) -> install normal chain
560 LOG
.warning('*** installing monitoring chain without pre-defined NSD chain from {0}:{1} -> {2}:{3}'.
561 format(vnf_src_name
, vnf_src_interface
, vnf_dst_name
, vnf_dst_interface
))
565 cmd
= kwargs
.get('cmd')
566 if cmd
== 'add-flow' or cmd
== 'del-flows':
567 ret
= self
._chainAddFlow
(vnf_src_name
, vnf_dst_name
, vnf_src_interface
, vnf_dst_interface
, **kwargs
)
568 if kwargs
.get('bidirectional'):
569 if kwargs
.get('path') is not None:
570 kwargs
['path'] = list(reversed(kwargs
.get('path')))
571 ret
= ret
+'\n' + self
._chainAddFlow
(vnf_dst_name
, vnf_src_name
, vnf_dst_interface
, vnf_src_interface
, **kwargs
)
574 ret
= "Command unknown"
579 def _chainAddFlow(self
, vnf_src_name
, vnf_dst_name
, vnf_src_interface
=None, vnf_dst_interface
=None, **kwargs
):
583 src_sw_inport_name
= None
585 dst_sw_outport_nr
= 0
586 dst_sw_outport_name
= None
588 LOG
.debug("call chainAddFlow vnf_src_name=%r, vnf_src_interface=%r, vnf_dst_name=%r, vnf_dst_interface=%r",
589 vnf_src_name
, vnf_src_interface
, vnf_dst_name
, vnf_dst_interface
)
591 #check if port is specified (vnf:port)
592 if vnf_src_interface
is None:
593 # take first interface by default
594 connected_sw
= self
.DCNetwork_graph
.neighbors(vnf_src_name
)[0]
595 link_dict
= self
.DCNetwork_graph
[vnf_src_name
][connected_sw
]
596 vnf_src_interface
= link_dict
[0]['src_port_id']
598 for connected_sw
in self
.DCNetwork_graph
.neighbors(vnf_src_name
):
599 link_dict
= self
.DCNetwork_graph
[vnf_src_name
][connected_sw
]
600 for link
in link_dict
:
601 if (link_dict
[link
]['src_port_id'] == vnf_src_interface
or
602 link_dict
[link
]['src_port_name'] == vnf_src_interface
): # Fix: we might also get interface names, e.g, from a son-emu-cli call
603 # found the right link and connected switch
604 src_sw
= connected_sw
605 src_sw_inport_nr
= link_dict
[link
]['dst_port_nr']
606 src_sw_inport_name
= link_dict
[link
]['dst_port_name']
609 if vnf_dst_interface
is None:
610 # take first interface by default
611 connected_sw
= self
.DCNetwork_graph
.neighbors(vnf_dst_name
)[0]
612 link_dict
= self
.DCNetwork_graph
[connected_sw
][vnf_dst_name
]
613 vnf_dst_interface
= link_dict
[0]['dst_port_id']
615 vnf_dst_name
= vnf_dst_name
.split(':')[0]
616 for connected_sw
in self
.DCNetwork_graph
.neighbors(vnf_dst_name
):
617 link_dict
= self
.DCNetwork_graph
[connected_sw
][vnf_dst_name
]
618 for link
in link_dict
:
619 if link_dict
[link
]['dst_port_id'] == vnf_dst_interface
or \
620 link_dict
[link
]['dst_port_name'] == vnf_dst_interface
: # Fix: we might also get interface names, e.g, from a son-emu-cli call
621 # found the right link and connected switch
622 dst_sw
= connected_sw
623 dst_sw_outport_nr
= link_dict
[link
]['src_port_nr']
624 dst_sw_outport_name
= link_dict
[link
]['src_port_name']
627 path
= kwargs
.get('path')
631 # returns the first found shortest path
632 # if all shortest paths are wanted, use: all_shortest_paths
633 path
= nx
.shortest_path(self
.DCNetwork_graph
, src_sw
, dst_sw
, weight
=kwargs
.get('weight'))
635 LOG
.exception("No path could be found between {0} and {1} using src_sw={2} and dst_sw={3}".format(
636 vnf_src_name
, vnf_dst_name
, src_sw
, dst_sw
))
637 LOG
.debug("Graph nodes: %r" % self
.DCNetwork_graph
.nodes())
638 LOG
.debug("Graph edges: %r" % self
.DCNetwork_graph
.edges())
639 for e
, v
in self
.DCNetwork_graph
.edges():
640 LOG
.debug("%r" % self
.DCNetwork_graph
[e
][v
])
641 return "No path could be found between {0} and {1}".format(vnf_src_name
, vnf_dst_name
)
643 LOG
.info("Path between {0} and {1}: {2}".format(vnf_src_name
, vnf_dst_name
, path
))
646 switch_inport_nr
= src_sw_inport_nr
649 cmd
= kwargs
.get('cmd')
651 if cmd
== 'add-flow':
652 if kwargs
.get('tag'):
653 # use pre-defined tag
654 vlan
= kwargs
.get('tag')
656 vlan
= self
.vlans
.pop()
658 # store the used vlan tag to identify this chain
659 if not kwargs
.get('monitor'):
661 chain_dict
['vnf_src_name'] = vnf_src_name
662 chain_dict
['vnf_dst_name'] = vnf_dst_name
663 chain_dict
['vnf_src_interface'] = vnf_src_interface
664 chain_dict
['vnf_dst_interface'] = vnf_dst_interface
665 chain_dict
['tag'] = vlan
666 self
.installed_chains
.append(chain_dict
)
668 #iterate through the path to install the flow-entries
669 for i
in range(0,len(path
)):
670 current_node
= self
.getNodeByName(current_hop
)
672 if i
< len(path
) - 1:
673 next_hop
= path
[i
+ 1]
675 # last switch reached
676 next_hop
= vnf_dst_name
678 next_node
= self
.getNodeByName(next_hop
)
680 if next_hop
== vnf_dst_name
:
681 switch_outport_nr
= dst_sw_outport_nr
682 LOG
.info("end node reached: {0}".format(vnf_dst_name
))
683 elif not isinstance( next_node
, OVSSwitch
):
684 LOG
.info("Next node: {0} is not a switch".format(next_hop
))
685 return "Next node: {0} is not a switch".format(next_hop
)
687 # take first link between switches by default
689 switch_outport_nr
= self
.DCNetwork_graph
[current_hop
][next_hop
][index_edge_out
]['src_port_nr']
693 if isinstance( current_node
, OVSSwitch
):
694 kwargs
['vlan'] = vlan
695 kwargs
['path'] = path
696 kwargs
['current_hop'] = current_hop
697 kwargs
['switch_inport_name'] = src_sw_inport_name
698 kwargs
['switch_outport_name'] = dst_sw_outport_name
699 kwargs
['pathindex'] = i
701 if self
.controller
== RemoteController
:
702 ## set flow entry via ryu rest api
703 self
._set
_flow
_entry
_ryu
_rest
(current_node
, switch_inport_nr
, switch_outport_nr
, **kwargs
)
705 ## set flow entry via ovs-ofctl
706 self
._set
_flow
_entry
_dpctl
(current_node
, switch_inport_nr
, switch_outport_nr
, **kwargs
)
708 # take first link between switches by default
709 if isinstance( next_node
, OVSSwitch
):
710 switch_inport_nr
= self
.DCNetwork_graph
[current_hop
][next_hop
][0]['dst_port_nr']
711 current_hop
= next_hop
713 return "path {2} between {0} and {1}".format(vnf_src_name
, vnf_dst_name
, cmd
)
715 def _set_flow_entry_ryu_rest(self
, node
, switch_inport_nr
, switch_outport_nr
, **kwargs
):
716 match
= 'in_port=%s' % switch_inport_nr
718 cookie
= kwargs
.get('cookie')
719 match_input
= kwargs
.get('match')
720 cmd
= kwargs
.get('cmd')
721 path
= kwargs
.get('path')
722 index
= kwargs
.get('pathindex')
724 vlan
= kwargs
.get('vlan')
725 priority
= kwargs
.get('priority')
726 # flag to not set the ovs port vlan tag
727 skip_vlan_tag
= kwargs
.get('skip_vlan_tag')
728 # table id to put this flowentry
729 table_id
= kwargs
.get('table_id')
735 match
= s
.join([match
, match_input
])
738 flow
['dpid'] = int(node
.dpid
, 16)
741 flow
['cookie'] = int(cookie
)
743 flow
['priority'] = int(priority
)
745 flow
['table_id'] = table_id
749 # possible Ryu actions, match fields:
750 # http://ryu.readthedocs.io/en/latest/app/ofctl_rest.html#add-a-flow-entry
751 if cmd
== 'add-flow':
752 prefix
= 'stats/flowentry/add'
754 if index
== 0: # first node
755 # set vlan tag in ovs instance (to isolate E-LANs)
756 if not skip_vlan_tag
:
757 in_port_name
= kwargs
.get('switch_inport_name')
758 self
._set
_vlan
_tag
(node
, in_port_name
, vlan
)
759 # set vlan push action if more than 1 switch in the path
762 action
['type'] = 'PUSH_VLAN' # Push a new VLAN tag if a input frame is non-VLAN-tagged
763 action
['ethertype'] = 33024 # Ethertype 0x8100(=33024): IEEE 802.1Q VLAN-tagged frame
764 flow
['actions'].append(action
)
766 action
['type'] = 'SET_FIELD'
767 action
['field'] = 'vlan_vid'
768 # ryu expects the field to be masked
769 action
['value'] = vlan |
0x1000
770 flow
['actions'].append(action
)
772 elif index
== len(path
) - 1: # last node
773 # set vlan tag in ovs instance (to isolate E-LANs)
774 if not skip_vlan_tag
:
775 out_port_name
= kwargs
.get('switch_outport_name')
776 self
._set
_vlan
_tag
(node
, out_port_name
, vlan
)
777 # set vlan pop action if more than 1 switch in the path
779 match
+= ',dl_vlan=%s' % vlan
781 action
['type'] = 'POP_VLAN'
782 flow
['actions'].append(action
)
785 match
+= ',dl_vlan=%s' % vlan
787 # output action must come last
789 action
['type'] = 'OUTPUT'
790 action
['port'] = switch_outport_nr
791 flow
['actions'].append(action
)
793 elif cmd
== 'del-flows':
794 prefix
= 'stats/flowentry/delete'
797 # TODO: add cookie_mask as argument
798 flow
['cookie_mask'] = int('0xffffffffffffffff', 16) # need full mask to match complete cookie
801 action
['type'] = 'OUTPUT'
802 action
['port'] = switch_outport_nr
803 flow
['actions'].append(action
)
805 flow
['match'] = self
._parse
_match
(match
)
806 self
.ryu_REST(prefix
, data
=flow
)
808 def _set_vlan_tag(self
, node
, switch_port
, tag
):
809 node
.vsctl('set', 'port {0} tag={1}'.format(switch_port
,tag
))
810 LOG
.debug("set vlan in switch: {0} in_port: {1} vlan tag: {2}".format(node
.name
, switch_port
, tag
))
812 def _set_flow_entry_dpctl(self
, node
, switch_inport_nr
, switch_outport_nr
, **kwargs
):
814 match
= 'in_port=%s' % switch_inport_nr
816 cookie
= kwargs
.get('cookie')
817 match_input
= kwargs
.get('match')
818 cmd
= kwargs
.get('cmd')
819 path
= kwargs
.get('path')
820 index
= kwargs
.get('pathindex')
821 vlan
= kwargs
.get('vlan')
825 cookie
= 'cookie=%s' % cookie
826 match
= s
.join([cookie
, match
])
828 match
= s
.join([match
, match_input
])
829 if cmd
== 'add-flow':
830 action
= 'action=%s' % switch_outport_nr
832 if index
== 0: # first node
833 action
= ('action=mod_vlan_vid:%s' % vlan
) + (',output=%s' % switch_outport_nr
)
834 match
= '-O OpenFlow13 ' + match
835 elif index
== len(path
) - 1: # last node
836 match
+= ',dl_vlan=%s' % vlan
837 action
= 'action=strip_vlan,output=%s' % switch_outport_nr
839 match
+= ',dl_vlan=%s' % vlan
840 ofcmd
= s
.join([match
, action
])
841 elif cmd
== 'del-flows':
846 node
.dpctl(cmd
, ofcmd
)
847 LOG
.info("{3} in switch: {0} in_port: {1} out_port: {2}".format(node
.name
, switch_inport_nr
,
848 switch_outport_nr
, cmd
))
850 # start Ryu Openflow controller as Remote Controller for the DCNetwork
851 def startRyu(self
, learning_switch
=True):
852 # start Ryu controller with rest-API
853 python_install_path
= site
.getsitepackages()[0]
854 # ryu default learning switch
855 #ryu_path = python_install_path + '/ryu/app/simple_switch_13.py'
856 #custom learning switch that installs a default NORMAL action in the ovs switches
857 dir_path
= os
.path
.dirname(os
.path
.realpath(__file__
))
858 ryu_path
= dir_path
+ '/son_emu_simple_switch_13.py'
859 ryu_path2
= python_install_path
+ '/ryu/app/ofctl_rest.py'
860 # change the default Openflow controller port to 6653 (official IANA-assigned port number), as used by Mininet
861 # Ryu still uses 6633 as default
862 ryu_option
= '--ofp-tcp-listen-port'
864 ryu_cmd
= 'ryu-manager'
865 FNULL
= open("/tmp/ryu.log", 'w')
867 self
.ryu_process
= Popen([ryu_cmd
, ryu_path
, ryu_path2
, ryu_option
, ryu_of_port
], stdout
=FNULL
, stderr
=FNULL
)
868 LOG
.debug('starting ryu-controller with {0}'.format(ryu_path
))
869 LOG
.debug('starting ryu-controller with {0}'.format(ryu_path2
))
871 # no learning switch, but with rest api
872 self
.ryu_process
= Popen([ryu_cmd
, ryu_path2
, ryu_option
, ryu_of_port
], stdout
=FNULL
, stderr
=FNULL
)
873 LOG
.debug('starting ryu-controller with {0}'.format(ryu_path2
))
878 Stop the Ryu controller that might be started by son-emu.
882 if self
.ryu_process
is not None:
883 self
.ryu_process
.terminate()
884 self
.ryu_process
.kill()
885 # ensure its death ;-)
886 Popen(['pkill', '-f', 'ryu-manager'])
888 def ryu_REST(self
, prefix
, dpid
=None, data
=None):
891 url
= self
.ryu_REST_api
+ '/' + str(prefix
) + '/' + str(dpid
)
893 url
= self
.ryu_REST_api
+ '/' + str(prefix
)
895 req
= self
.RyuSession
.post(url
, json
=data
)
897 req
= self
.RyuSession
.get(url
)
900 # do extra logging if status code is not 200 (OK)
901 if req
.status_code
is not requests
.codes
.ok
:
903 'type {0} encoding: {1} text: {2} headers: {3} history: {4}'.format(req
.headers
['content-type'],
904 req
.encoding
, req
.text
,
905 req
.headers
, req
.history
))
906 LOG
.info('url: {0}'.format(str(url
)))
907 if data
: LOG
.info('POST: {0}'.format(str(data
)))
908 LOG
.info('status: {0} reason: {1}'.format(req
.status_code
, req
.reason
))
911 if 'json' in req
.headers
['content-type']:
915 ret
= req
.text
.rstrip()
919 # need to respect that some match fields must be integers
920 # http://ryu.readthedocs.io/en/latest/app/ofctl_rest.html#description-of-match-and-actions
921 def _parse_match(self
, match
):
922 matches
= match
.split(',')
928 m2
= int(match
[1], 0)
932 dict.update({match
[0]:m2
})
935 def find_connected_dc_interface(self
, vnf_src_name
, vnf_src_interface
=None):
937 if vnf_src_interface
is None:
938 # take first interface by default
939 connected_sw
= self
.DCNetwork_graph
.neighbors(vnf_src_name
)[0]
940 link_dict
= self
.DCNetwork_graph
[vnf_src_name
][connected_sw
]
941 vnf_src_interface
= link_dict
[0]['src_port_id']
943 for connected_sw
in self
.DCNetwork_graph
.neighbors(vnf_src_name
):
944 link_dict
= self
.DCNetwork_graph
[vnf_src_name
][connected_sw
]
945 for link
in link_dict
:
946 if (link_dict
[link
]['src_port_id'] == vnf_src_interface
or
947 link_dict
[link
]['src_port_name'] == vnf_src_interface
): # Fix: we might also get interface names, e.g, from a son-emu-cli call
948 # found the right link and connected switch
949 src_sw
= connected_sw
950 src_sw_inport_nr
= link_dict
[link
]['dst_port_nr']
951 src_sw_inport_name
= link_dict
[link
]['dst_port_name']
952 return src_sw_inport_name