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
)
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 addSwitch( self
, name
, add_to_graph
=True, **params
):
255 Wrapper for addSwitch method to store switch also in graph.
258 # add this switch to the global topology overview
260 self
.DCNetwork_graph
.add_node(name
)
262 # set the learning switch behavior
263 if 'failMode' in params
:
264 failMode
= params
['failMode']
266 failMode
= self
.failMode
268 s
= Containernet
.addSwitch(self
, name
, protocols
='OpenFlow10,OpenFlow12,OpenFlow13', failMode
=failMode
, **params
)
270 # set flow entry that enables learning switch behavior (needed to enable E-LAN functionality)
271 #LOG.info('failmode {0}'.format(failMode))
272 #if failMode == 'standalone' :
273 # LOG.info('add NORMAL')
274 # s.dpctl('add-flow', 'actions=NORMAL')
278 def getAllContainers(self
):
280 Returns a list with all containers within all data centers.
283 for dc
in self
.dcs
.itervalues():
284 all_containers
+= dc
.listCompute()
285 return all_containers
289 for dc
in self
.dcs
.itervalues():
291 Containernet
.start(self
)
295 # stop the monitor agent
296 if self
.monitor_agent
is not None:
297 self
.monitor_agent
.stop()
300 Containernet
.stop(self
)
302 # stop Ryu controller
309 def setLAN(self
, vnf_list
):
311 setup an E-LAN network by assigning the same VLAN tag to each DC interface of the VNFs in the E-LAN
313 :param vnf_list: names of the VNFs in this E-LAN [{name:,interface:},...]
318 src_sw_inport_name
= None
320 # get a vlan tag for this E-LAN
321 vlan
= self
.vlans
.pop()
324 vnf_src_name
= vnf
['name']
325 vnf_src_interface
= vnf
['interface']
327 # check if port is specified (vnf:port)
328 if vnf_src_interface
is None:
329 # take first interface by default
330 connected_sw
= self
.DCNetwork_graph
.neighbors(vnf_src_name
)[0]
331 link_dict
= self
.DCNetwork_graph
[vnf_src_name
][connected_sw
]
332 vnf_src_interface
= link_dict
[0]['src_port_id']
334 for connected_sw
in self
.DCNetwork_graph
.neighbors(vnf_src_name
):
335 link_dict
= self
.DCNetwork_graph
[vnf_src_name
][connected_sw
]
336 for link
in link_dict
:
337 if (link_dict
[link
]['src_port_id'] == vnf_src_interface
or
338 link_dict
[link
]['src_port_name'] == vnf_src_interface
): # Fix: we might also get interface names, e.g, from a son-emu-cli call
339 # found the right link and connected switch
340 src_sw
= connected_sw
341 src_sw_inport_nr
= link_dict
[link
]['dst_port_nr']
342 src_sw_inport_name
= link_dict
[link
]['dst_port_name']
345 # set the tag on the dc switch interface
346 LOG
.debug('set E-LAN: vnf name: {0} interface: {1} tag: {2}'.format(vnf_src_name
, vnf_src_interface
,vlan
))
347 switch_node
= self
.getNodeByName(src_sw
)
348 self
._set
_vlan
_tag
(switch_node
, src_sw_inport_name
, vlan
)
350 def _addMonitorFlow(self
, vnf_src_name
, vnf_dst_name
, vnf_src_interface
=None, vnf_dst_interface
=None,
353 Add a monitoring flow entry that adds a special flowentry/counter at the begin or end of a chain.
354 So this monitoring flowrule exists on top of a previously defined chain rule and uses the same vlan tag/routing.
357 :param vnf_src_interface:
358 :param vnf_dst_interface:
359 :param tag: vlan tag to be used for this chain (same tag as existing chain)
360 :param monitor_placement: 'tx' or 'rx' indicating to place the extra flowentry resp. at the beginning or end of the chain
366 src_sw_inport_name
= None
368 dst_sw_outport_nr
= 0
369 dst_sw_outport_name
= None
371 LOG
.debug("call AddMonitorFlow vnf_src_name=%r, vnf_src_interface=%r, vnf_dst_name=%r, vnf_dst_interface=%r",
372 vnf_src_name
, vnf_src_interface
, vnf_dst_name
, vnf_dst_interface
)
374 #check if port is specified (vnf:port)
375 if vnf_src_interface
is None:
376 # take first interface by default
377 connected_sw
= self
.DCNetwork_graph
.neighbors(vnf_src_name
)[0]
378 link_dict
= self
.DCNetwork_graph
[vnf_src_name
][connected_sw
]
379 vnf_src_interface
= link_dict
[0]['src_port_id']
381 for connected_sw
in self
.DCNetwork_graph
.neighbors(vnf_src_name
):
382 link_dict
= self
.DCNetwork_graph
[vnf_src_name
][connected_sw
]
383 for link
in link_dict
:
384 if (link_dict
[link
]['src_port_id'] == vnf_src_interface
or
385 link_dict
[link
]['src_port_name'] == vnf_src_interface
): # Fix: we might also get interface names, e.g, from a son-emu-cli call
386 # found the right link and connected switch
387 src_sw
= connected_sw
388 src_sw_inport_nr
= link_dict
[link
]['dst_port_nr']
389 src_sw_inport_name
= link_dict
[link
]['dst_port_name']
392 if vnf_dst_interface
is None:
393 # take first interface by default
394 connected_sw
= self
.DCNetwork_graph
.neighbors(vnf_dst_name
)[0]
395 link_dict
= self
.DCNetwork_graph
[connected_sw
][vnf_dst_name
]
396 vnf_dst_interface
= link_dict
[0]['dst_port_id']
398 vnf_dst_name
= vnf_dst_name
.split(':')[0]
399 for connected_sw
in self
.DCNetwork_graph
.neighbors(vnf_dst_name
):
400 link_dict
= self
.DCNetwork_graph
[connected_sw
][vnf_dst_name
]
401 for link
in link_dict
:
402 if link_dict
[link
]['dst_port_id'] == vnf_dst_interface
or \
403 link_dict
[link
]['dst_port_name'] == vnf_dst_interface
: # Fix: we might also get interface names, e.g, from a son-emu-cli call
404 # found the right link and connected switch
405 dst_sw
= connected_sw
406 dst_sw_outport_nr
= link_dict
[link
]['src_port_nr']
407 dst_sw_outport_name
= link_dict
[link
]['src_port_name']
411 LOG
.exception('tag not valid: {0}'.format(tag
))
415 # returns the first found shortest path
416 # if all shortest paths are wanted, use: all_shortest_paths
417 path
= nx
.shortest_path(self
.DCNetwork_graph
, src_sw
, dst_sw
, weight
=kwargs
.get('weight'))
419 LOG
.exception("No path could be found between {0} and {1} using src_sw={2} and dst_sw={3}".format(
420 vnf_src_name
, vnf_dst_name
, src_sw
, dst_sw
))
421 LOG
.debug("Graph nodes: %r" % self
.DCNetwork_graph
.nodes())
422 LOG
.debug("Graph edges: %r" % self
.DCNetwork_graph
.edges())
423 for e
, v
in self
.DCNetwork_graph
.edges():
424 LOG
.debug("%r" % self
.DCNetwork_graph
[e
][v
])
425 return "No path could be found between {0} and {1}".format(vnf_src_name
, vnf_dst_name
)
427 LOG
.info("Path between {0} and {1}: {2}".format(vnf_src_name
, vnf_dst_name
, path
))
430 switch_inport_nr
= src_sw_inport_nr
432 cmd
= kwargs
.get('cmd')
434 #iterate through the path to install the flow-entries
435 for i
in range(0,len(path
)):
436 current_node
= self
.getNodeByName(current_hop
)
438 if path
.index(current_hop
) < len(path
)-1:
439 next_hop
= path
[path
.index(current_hop
)+1]
442 next_hop
= vnf_dst_name
444 next_node
= self
.getNodeByName(next_hop
)
446 if next_hop
== vnf_dst_name
:
447 switch_outport_nr
= dst_sw_outport_nr
448 LOG
.info("end node reached: {0}".format(vnf_dst_name
))
449 elif not isinstance( next_node
, OVSSwitch
):
450 LOG
.info("Next node: {0} is not a switch".format(next_hop
))
451 return "Next node: {0} is not a switch".format(next_hop
)
453 # take first link between switches by default
455 switch_outport_nr
= self
.DCNetwork_graph
[current_hop
][next_hop
][index_edge_out
]['src_port_nr']
458 # set of entry via ovs-ofctl
459 if isinstance( current_node
, OVSSwitch
):
461 kwargs
['path'] = path
462 kwargs
['current_hop'] = current_hop
463 kwargs
['switch_inport_name'] = src_sw_inport_name
464 kwargs
['switch_outport_name'] = dst_sw_outport_name
465 kwargs
['skip_vlan_tag'] = True
466 kwargs
['pathindex'] = i
468 monitor_placement
= kwargs
.get('monitor_placement').strip()
469 # put monitor flow at the dst switch
471 if monitor_placement
== 'tx' and path
.index(current_hop
) == 0: # first node:
473 # put monitoring flow at the src switch
474 elif monitor_placement
== 'rx' and path
.index(current_hop
) == len(path
) - 1: # last node:
476 elif monitor_placement
not in ['rx', 'tx']:
477 LOG
.exception('invalid monitor command: {0}'.format(monitor_placement
))
480 if self
.controller
== RemoteController
and insert_flow
:
481 ## set flow entry via ryu rest api
482 self
._set
_flow
_entry
_ryu
_rest
(current_node
, switch_inport_nr
, switch_outport_nr
, **kwargs
)
485 ## set flow entry via ovs-ofctl
486 self
._set
_flow
_entry
_dpctl
(current_node
, switch_inport_nr
, switch_outport_nr
, **kwargs
)
489 # take first link between switches by default
490 if isinstance( next_node
, OVSSwitch
):
491 switch_inport_nr
= self
.DCNetwork_graph
[current_hop
][next_hop
][0]['dst_port_nr']
492 current_hop
= next_hop
494 return "path {2} between {0} and {1}".format(vnf_src_name
, vnf_dst_name
, cmd
)
497 def setChain(self
, vnf_src_name
, vnf_dst_name
, vnf_src_interface
=None, vnf_dst_interface
=None, **kwargs
):
499 Chain 2 vnf interfaces together by installing the flowrules in the switches along their path.
500 Currently the path is found using the default networkx shortest path function.
501 Each chain gets a unique vlan id , so different chains wil not interfere.
503 :param vnf_src_name: vnf name (string)
504 :param vnf_dst_name: vnf name (string)
505 :param vnf_src_interface: source interface name (string)
506 :param vnf_dst_interface: destination interface name (string)
507 :param cmd: 'add-flow' (default) to add a chain, 'del-flows' to remove a chain
508 :param cookie: cookie for the installed flowrules (can be used later as identifier for a set of installed chains)
509 :param match: custom match entry to be added to the flowrules (default: only in_port and vlan tag)
510 :param priority: custom flowrule priority
511 :param monitor: boolean to indicate whether this chain is a monitoring chain
512 :param tag: vlan tag to be used for this chain (pre-defined or new one if none is specified)
513 :param skip_vlan_tag: boolean to indicate if a vlan tag should be appointed to this flow or not
514 :param path: custom path between the two VNFs (list of switches)
515 :return: output log string
518 # special procedure for monitoring flows
519 if kwargs
.get('monitor'):
521 # check if chain already exists
522 found_chains
= [chain_dict
for chain_dict
in self
.installed_chains
if
523 (chain_dict
['vnf_src_name'] == vnf_src_name
and chain_dict
['vnf_src_interface'] == vnf_src_interface
524 and chain_dict
['vnf_dst_name'] == vnf_dst_name
and chain_dict
['vnf_dst_interface'] == vnf_dst_interface
)]
526 if len(found_chains
) > 0:
527 # this chain exists, so need an extra monitoring flow
528 # assume only 1 chain per vnf/interface pair
529 LOG
.debug('*** installing monitoring chain on top of pre-defined chain from {0}:{1} -> {2}:{3}'.
530 format(vnf_src_name
, vnf_src_interface
, vnf_dst_name
, vnf_dst_interface
))
531 tag
= found_chains
[0]['tag']
532 ret
= self
._addMonitorFlow
(vnf_src_name
, vnf_dst_name
, vnf_src_interface
, vnf_dst_interface
,
533 tag
=tag
, table_id
=0, **kwargs
)
536 # no chain existing (or E-LAN) -> install normal chain
537 LOG
.warning('*** installing monitoring chain without pre-defined NSD chain from {0}:{1} -> {2}:{3}'.
538 format(vnf_src_name
, vnf_src_interface
, vnf_dst_name
, vnf_dst_interface
))
542 cmd
= kwargs
.get('cmd')
543 if cmd
== 'add-flow' or cmd
== 'del-flows':
544 ret
= self
._chainAddFlow
(vnf_src_name
, vnf_dst_name
, vnf_src_interface
, vnf_dst_interface
, **kwargs
)
545 if kwargs
.get('bidirectional'):
546 if kwargs
.get('path') is not None:
547 kwargs
['path'] = list(reversed(kwargs
.get('path')))
548 ret
= ret
+'\n' + self
._chainAddFlow
(vnf_dst_name
, vnf_src_name
, vnf_dst_interface
, vnf_src_interface
, **kwargs
)
551 ret
= "Command unknown"
556 def _chainAddFlow(self
, vnf_src_name
, vnf_dst_name
, vnf_src_interface
=None, vnf_dst_interface
=None, **kwargs
):
560 src_sw_inport_name
= None
562 dst_sw_outport_nr
= 0
563 dst_sw_outport_name
= None
565 LOG
.debug("call chainAddFlow vnf_src_name=%r, vnf_src_interface=%r, vnf_dst_name=%r, vnf_dst_interface=%r",
566 vnf_src_name
, vnf_src_interface
, vnf_dst_name
, vnf_dst_interface
)
568 #check if port is specified (vnf:port)
569 if vnf_src_interface
is None:
570 # take first interface by default
571 connected_sw
= self
.DCNetwork_graph
.neighbors(vnf_src_name
)[0]
572 link_dict
= self
.DCNetwork_graph
[vnf_src_name
][connected_sw
]
573 vnf_src_interface
= link_dict
[0]['src_port_id']
575 for connected_sw
in self
.DCNetwork_graph
.neighbors(vnf_src_name
):
576 link_dict
= self
.DCNetwork_graph
[vnf_src_name
][connected_sw
]
577 for link
in link_dict
:
578 if (link_dict
[link
]['src_port_id'] == vnf_src_interface
or
579 link_dict
[link
]['src_port_name'] == vnf_src_interface
): # Fix: we might also get interface names, e.g, from a son-emu-cli call
580 # found the right link and connected switch
581 src_sw
= connected_sw
582 src_sw_inport_nr
= link_dict
[link
]['dst_port_nr']
583 src_sw_inport_name
= link_dict
[link
]['dst_port_name']
586 if vnf_dst_interface
is None:
587 # take first interface by default
588 connected_sw
= self
.DCNetwork_graph
.neighbors(vnf_dst_name
)[0]
589 link_dict
= self
.DCNetwork_graph
[connected_sw
][vnf_dst_name
]
590 vnf_dst_interface
= link_dict
[0]['dst_port_id']
592 vnf_dst_name
= vnf_dst_name
.split(':')[0]
593 for connected_sw
in self
.DCNetwork_graph
.neighbors(vnf_dst_name
):
594 link_dict
= self
.DCNetwork_graph
[connected_sw
][vnf_dst_name
]
595 for link
in link_dict
:
596 if link_dict
[link
]['dst_port_id'] == vnf_dst_interface
or \
597 link_dict
[link
]['dst_port_name'] == vnf_dst_interface
: # Fix: we might also get interface names, e.g, from a son-emu-cli call
598 # found the right link and connected switch
599 dst_sw
= connected_sw
600 dst_sw_outport_nr
= link_dict
[link
]['src_port_nr']
601 dst_sw_outport_name
= link_dict
[link
]['src_port_name']
604 path
= kwargs
.get('path')
608 # returns the first found shortest path
609 # if all shortest paths are wanted, use: all_shortest_paths
610 path
= nx
.shortest_path(self
.DCNetwork_graph
, src_sw
, dst_sw
, weight
=kwargs
.get('weight'))
612 LOG
.exception("No path could be found between {0} and {1} using src_sw={2} and dst_sw={3}".format(
613 vnf_src_name
, vnf_dst_name
, src_sw
, dst_sw
))
614 LOG
.debug("Graph nodes: %r" % self
.DCNetwork_graph
.nodes())
615 LOG
.debug("Graph edges: %r" % self
.DCNetwork_graph
.edges())
616 for e
, v
in self
.DCNetwork_graph
.edges():
617 LOG
.debug("%r" % self
.DCNetwork_graph
[e
][v
])
618 return "No path could be found between {0} and {1}".format(vnf_src_name
, vnf_dst_name
)
620 LOG
.info("Path between {0} and {1}: {2}".format(vnf_src_name
, vnf_dst_name
, path
))
623 switch_inport_nr
= src_sw_inport_nr
626 cmd
= kwargs
.get('cmd')
628 if cmd
== 'add-flow':
629 if kwargs
.get('tag'):
630 # use pre-defined tag
631 vlan
= kwargs
.get('tag')
633 vlan
= self
.vlans
.pop()
635 # store the used vlan tag to identify this chain
636 if not kwargs
.get('monitor'):
638 chain_dict
['vnf_src_name'] = vnf_src_name
639 chain_dict
['vnf_dst_name'] = vnf_dst_name
640 chain_dict
['vnf_src_interface'] = vnf_src_interface
641 chain_dict
['vnf_dst_interface'] = vnf_dst_interface
642 chain_dict
['tag'] = vlan
643 self
.installed_chains
.append(chain_dict
)
645 #iterate through the path to install the flow-entries
646 for i
in range(0,len(path
)):
647 current_node
= self
.getNodeByName(current_hop
)
649 if i
< len(path
) - 1:
650 next_hop
= path
[i
+ 1]
652 # last switch reached
653 next_hop
= vnf_dst_name
655 next_node
= self
.getNodeByName(next_hop
)
657 if next_hop
== vnf_dst_name
:
658 switch_outport_nr
= dst_sw_outport_nr
659 LOG
.info("end node reached: {0}".format(vnf_dst_name
))
660 elif not isinstance( next_node
, OVSSwitch
):
661 LOG
.info("Next node: {0} is not a switch".format(next_hop
))
662 return "Next node: {0} is not a switch".format(next_hop
)
664 # take first link between switches by default
666 switch_outport_nr
= self
.DCNetwork_graph
[current_hop
][next_hop
][index_edge_out
]['src_port_nr']
670 if isinstance( current_node
, OVSSwitch
):
671 kwargs
['vlan'] = vlan
672 kwargs
['path'] = path
673 kwargs
['current_hop'] = current_hop
674 kwargs
['switch_inport_name'] = src_sw_inport_name
675 kwargs
['switch_outport_name'] = dst_sw_outport_name
676 kwargs
['pathindex'] = i
678 if self
.controller
== RemoteController
:
679 ## set flow entry via ryu rest api
680 self
._set
_flow
_entry
_ryu
_rest
(current_node
, switch_inport_nr
, switch_outport_nr
, **kwargs
)
682 ## set flow entry via ovs-ofctl
683 self
._set
_flow
_entry
_dpctl
(current_node
, switch_inport_nr
, switch_outport_nr
, **kwargs
)
685 # take first link between switches by default
686 if isinstance( next_node
, OVSSwitch
):
687 switch_inport_nr
= self
.DCNetwork_graph
[current_hop
][next_hop
][0]['dst_port_nr']
688 current_hop
= next_hop
690 return "path {2} between {0} and {1}".format(vnf_src_name
, vnf_dst_name
, cmd
)
692 def _set_flow_entry_ryu_rest(self
, node
, switch_inport_nr
, switch_outport_nr
, **kwargs
):
693 match
= 'in_port=%s' % switch_inport_nr
695 cookie
= kwargs
.get('cookie')
696 match_input
= kwargs
.get('match')
697 cmd
= kwargs
.get('cmd')
698 path
= kwargs
.get('path')
699 index
= kwargs
.get('pathindex')
701 vlan
= kwargs
.get('vlan')
702 priority
= kwargs
.get('priority')
703 # flag to not set the ovs port vlan tag
704 skip_vlan_tag
= kwargs
.get('skip_vlan_tag')
705 # table id to put this flowentry
706 table_id
= kwargs
.get('table_id')
712 match
= s
.join([match
, match_input
])
715 flow
['dpid'] = int(node
.dpid
, 16)
718 flow
['cookie'] = int(cookie
)
720 flow
['priority'] = int(priority
)
722 flow
['table_id'] = table_id
726 # possible Ryu actions, match fields:
727 # http://ryu.readthedocs.io/en/latest/app/ofctl_rest.html#add-a-flow-entry
728 if cmd
== 'add-flow':
729 prefix
= 'stats/flowentry/add'
731 if index
== 0: # first node
732 # set vlan tag in ovs instance (to isolate E-LANs)
733 if not skip_vlan_tag
:
734 in_port_name
= kwargs
.get('switch_inport_name')
735 self
._set
_vlan
_tag
(node
, in_port_name
, vlan
)
736 # set vlan push action if more than 1 switch in the path
739 action
['type'] = 'PUSH_VLAN' # Push a new VLAN tag if a input frame is non-VLAN-tagged
740 action
['ethertype'] = 33024 # Ethertype 0x8100(=33024): IEEE 802.1Q VLAN-tagged frame
741 flow
['actions'].append(action
)
743 action
['type'] = 'SET_FIELD'
744 action
['field'] = 'vlan_vid'
745 # ryu expects the field to be masked
746 action
['value'] = vlan |
0x1000
747 flow
['actions'].append(action
)
749 elif index
== len(path
) - 1: # last node
750 # set vlan tag in ovs instance (to isolate E-LANs)
751 if not skip_vlan_tag
:
752 out_port_name
= kwargs
.get('switch_outport_name')
753 self
._set
_vlan
_tag
(node
, out_port_name
, vlan
)
754 # set vlan pop action if more than 1 switch in the path
756 match
+= ',dl_vlan=%s' % vlan
758 action
['type'] = 'POP_VLAN'
759 flow
['actions'].append(action
)
762 match
+= ',dl_vlan=%s' % vlan
764 # output action must come last
766 action
['type'] = 'OUTPUT'
767 action
['port'] = switch_outport_nr
768 flow
['actions'].append(action
)
770 elif cmd
== 'del-flows':
771 prefix
= 'stats/flowentry/delete'
774 # TODO: add cookie_mask as argument
775 flow
['cookie_mask'] = int('0xffffffffffffffff', 16) # need full mask to match complete cookie
778 action
['type'] = 'OUTPUT'
779 action
['port'] = switch_outport_nr
780 flow
['actions'].append(action
)
782 flow
['match'] = self
._parse
_match
(match
)
783 self
.ryu_REST(prefix
, data
=flow
)
785 def _set_vlan_tag(self
, node
, switch_port
, tag
):
786 node
.vsctl('set', 'port {0} tag={1}'.format(switch_port
,tag
))
787 LOG
.debug("set vlan in switch: {0} in_port: {1} vlan tag: {2}".format(node
.name
, switch_port
, tag
))
789 def _set_flow_entry_dpctl(self
, node
, switch_inport_nr
, switch_outport_nr
, **kwargs
):
791 match
= 'in_port=%s' % switch_inport_nr
793 cookie
= kwargs
.get('cookie')
794 match_input
= kwargs
.get('match')
795 cmd
= kwargs
.get('cmd')
796 path
= kwargs
.get('path')
797 index
= kwargs
.get('pathindex')
798 vlan
= kwargs
.get('vlan')
802 cookie
= 'cookie=%s' % cookie
803 match
= s
.join([cookie
, match
])
805 match
= s
.join([match
, match_input
])
806 if cmd
== 'add-flow':
807 action
= 'action=%s' % switch_outport_nr
809 if index
== 0: # first node
810 action
= ('action=mod_vlan_vid:%s' % vlan
) + (',output=%s' % switch_outport_nr
)
811 match
= '-O OpenFlow13 ' + match
812 elif index
== len(path
) - 1: # last node
813 match
+= ',dl_vlan=%s' % vlan
814 action
= 'action=strip_vlan,output=%s' % switch_outport_nr
816 match
+= ',dl_vlan=%s' % vlan
817 ofcmd
= s
.join([match
, action
])
818 elif cmd
== 'del-flows':
823 node
.dpctl(cmd
, ofcmd
)
824 LOG
.info("{3} in switch: {0} in_port: {1} out_port: {2}".format(node
.name
, switch_inport_nr
,
825 switch_outport_nr
, cmd
))
827 # start Ryu Openflow controller as Remote Controller for the DCNetwork
828 def startRyu(self
, learning_switch
=True):
829 # start Ryu controller with rest-API
830 python_install_path
= site
.getsitepackages()[0]
831 # ryu default learning switch
832 #ryu_path = python_install_path + '/ryu/app/simple_switch_13.py'
833 #custom learning switch that installs a default NORMAL action in the ovs switches
834 dir_path
= os
.path
.dirname(os
.path
.realpath(__file__
))
835 ryu_path
= dir_path
+ '/son_emu_simple_switch_13.py'
836 ryu_path2
= python_install_path
+ '/ryu/app/ofctl_rest.py'
837 # change the default Openflow controller port to 6653 (official IANA-assigned port number), as used by Mininet
838 # Ryu still uses 6633 as default
839 ryu_option
= '--ofp-tcp-listen-port'
841 ryu_cmd
= 'ryu-manager'
842 FNULL
= open("/tmp/ryu.log", 'w')
844 self
.ryu_process
= Popen([ryu_cmd
, ryu_path
, ryu_path2
, ryu_option
, ryu_of_port
], stdout
=FNULL
, stderr
=FNULL
)
845 LOG
.debug('starting ryu-controller with {0}'.format(ryu_path
))
846 LOG
.debug('starting ryu-controller with {0}'.format(ryu_path2
))
848 # no learning switch, but with rest api
849 self
.ryu_process
= Popen([ryu_cmd
, ryu_path2
, ryu_option
, ryu_of_port
], stdout
=FNULL
, stderr
=FNULL
)
850 LOG
.debug('starting ryu-controller with {0}'.format(ryu_path2
))
855 Stop the Ryu controller that might be started by son-emu.
859 if self
.ryu_process
is not None:
860 self
.ryu_process
.terminate()
861 self
.ryu_process
.kill()
862 # ensure its death ;-)
863 Popen(['pkill', '-f', 'ryu-manager'])
865 def ryu_REST(self
, prefix
, dpid
=None, data
=None):
868 url
= self
.ryu_REST_api
+ '/' + str(prefix
) + '/' + str(dpid
)
870 url
= self
.ryu_REST_api
+ '/' + str(prefix
)
872 req
= self
.RyuSession
.post(url
, json
=data
)
874 req
= self
.RyuSession
.get(url
)
877 # do extra logging if status code is not 200 (OK)
878 if req
.status_code
is not requests
.codes
.ok
:
880 'type {0} encoding: {1} text: {2} headers: {3} history: {4}'.format(req
.headers
['content-type'],
881 req
.encoding
, req
.text
,
882 req
.headers
, req
.history
))
883 LOG
.info('url: {0}'.format(str(url
)))
884 if data
: LOG
.info('POST: {0}'.format(str(data
)))
885 LOG
.info('status: {0} reason: {1}'.format(req
.status_code
, req
.reason
))
888 if 'json' in req
.headers
['content-type']:
892 ret
= req
.text
.rstrip()
896 # need to respect that some match fields must be integers
897 # http://ryu.readthedocs.io/en/latest/app/ofctl_rest.html#description-of-match-and-actions
898 def _parse_match(self
, match
):
899 matches
= match
.split(',')
905 m2
= int(match
[1], 0)
909 dict.update({match
[0]:m2
})
912 def find_connected_dc_interface(self
, vnf_src_name
, vnf_src_interface
):
913 for connected_sw
in self
.DCNetwork_graph
.neighbors(vnf_src_name
):
914 link_dict
= self
.DCNetwork_graph
[vnf_src_name
][connected_sw
]
915 for link
in link_dict
:
916 if (link_dict
[link
]['src_port_id'] == vnf_src_interface
or
917 link_dict
[link
]['src_port_name'] == vnf_src_interface
): # Fix: we might also get interface names, e.g, from a son-emu-cli call
918 # found the right link and connected switch
919 src_sw
= connected_sw
920 src_sw_inport_nr
= link_dict
[link
]['dst_port_nr']
921 src_sw_inport_name
= link_dict
[link
]['dst_port_name']
922 return src_sw_inport_name