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
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 return Containernet
.addExtSAP(self
, sap_name
, sap_ip
, **params
)
262 def removeExtSAP(self
, sap_name
, **params
):
264 Wrapper for removeExtSAP method to remove SAP also from graph.
266 self
.DCNetwork_graph
.remove_node(sap_name
)
267 return Containernet
.removeExtSAP(self
, sap_name
)
269 def addSwitch( self
, name
, add_to_graph
=True, **params
):
271 Wrapper for addSwitch method to store switch also in graph.
274 # add this switch to the global topology overview
276 self
.DCNetwork_graph
.add_node(name
, type=params
.get('type','switch'))
278 # set the learning switch behavior
279 if 'failMode' in params
:
280 failMode
= params
['failMode']
282 failMode
= self
.failMode
284 s
= Containernet
.addSwitch(self
, name
, protocols
='OpenFlow10,OpenFlow12,OpenFlow13', failMode
=failMode
, **params
)
288 def getAllContainers(self
):
290 Returns a list with all containers within all data centers.
293 for dc
in self
.dcs
.itervalues():
294 all_containers
+= dc
.listCompute()
295 return all_containers
299 for dc
in self
.dcs
.itervalues():
301 Containernet
.start(self
)
305 # stop the monitor agent
306 if self
.monitor_agent
is not None:
307 self
.monitor_agent
.stop()
310 Containernet
.stop(self
)
312 # stop Ryu controller
319 def setLAN(self
, vnf_list
):
321 setup an E-LAN network by assigning the same VLAN tag to each DC interface of the VNFs in the E-LAN
323 :param vnf_list: names of the VNFs in this E-LAN [{name:,interface:},...]
328 src_sw_inport_name
= None
330 # get a vlan tag for this E-LAN
331 vlan
= self
.vlans
.pop()
334 vnf_src_name
= vnf
['name']
335 vnf_src_interface
= vnf
['interface']
337 # check if port is specified (vnf:port)
338 if vnf_src_interface
is None:
339 # take first interface by default
340 connected_sw
= self
.DCNetwork_graph
.neighbors(vnf_src_name
)[0]
341 link_dict
= self
.DCNetwork_graph
[vnf_src_name
][connected_sw
]
342 vnf_src_interface
= link_dict
[0]['src_port_id']
344 for connected_sw
in self
.DCNetwork_graph
.neighbors(vnf_src_name
):
345 link_dict
= self
.DCNetwork_graph
[vnf_src_name
][connected_sw
]
346 for link
in link_dict
:
347 if (link_dict
[link
]['src_port_id'] == vnf_src_interface
or
348 link_dict
[link
]['src_port_name'] == vnf_src_interface
): # Fix: we might also get interface names, e.g, from a son-emu-cli call
349 # found the right link and connected switch
350 src_sw
= connected_sw
351 src_sw_inport_nr
= link_dict
[link
]['dst_port_nr']
352 src_sw_inport_name
= link_dict
[link
]['dst_port_name']
355 # set the tag on the dc switch interface
356 LOG
.debug('set E-LAN: vnf name: {0} interface: {1} tag: {2}'.format(vnf_src_name
, vnf_src_interface
,vlan
))
357 switch_node
= self
.getNodeByName(src_sw
)
358 self
._set
_vlan
_tag
(switch_node
, src_sw_inport_name
, vlan
)
360 def _addMonitorFlow(self
, vnf_src_name
, vnf_dst_name
, vnf_src_interface
=None, vnf_dst_interface
=None,
363 Add a monitoring flow entry that adds a special flowentry/counter at the begin or end of a chain.
364 So this monitoring flowrule exists on top of a previously defined chain rule and uses the same vlan tag/routing.
367 :param vnf_src_interface:
368 :param vnf_dst_interface:
369 :param tag: vlan tag to be used for this chain (same tag as existing chain)
370 :param monitor_placement: 'tx' or 'rx' indicating to place the extra flowentry resp. at the beginning or end of the chain
376 src_sw_inport_name
= None
378 dst_sw_outport_nr
= 0
379 dst_sw_outport_name
= None
381 LOG
.debug("call AddMonitorFlow vnf_src_name=%r, vnf_src_interface=%r, vnf_dst_name=%r, vnf_dst_interface=%r",
382 vnf_src_name
, vnf_src_interface
, vnf_dst_name
, vnf_dst_interface
)
384 #check if port is specified (vnf:port)
385 if vnf_src_interface
is None:
386 # take first interface by default
387 connected_sw
= self
.DCNetwork_graph
.neighbors(vnf_src_name
)[0]
388 link_dict
= self
.DCNetwork_graph
[vnf_src_name
][connected_sw
]
389 vnf_src_interface
= link_dict
[0]['src_port_id']
391 for connected_sw
in self
.DCNetwork_graph
.neighbors(vnf_src_name
):
392 link_dict
= self
.DCNetwork_graph
[vnf_src_name
][connected_sw
]
393 for link
in link_dict
:
394 if (link_dict
[link
]['src_port_id'] == vnf_src_interface
or
395 link_dict
[link
]['src_port_name'] == vnf_src_interface
): # Fix: we might also get interface names, e.g, from a son-emu-cli call
396 # found the right link and connected switch
397 src_sw
= connected_sw
398 src_sw_inport_nr
= link_dict
[link
]['dst_port_nr']
399 src_sw_inport_name
= link_dict
[link
]['dst_port_name']
402 if vnf_dst_interface
is None:
403 # take first interface by default
404 connected_sw
= self
.DCNetwork_graph
.neighbors(vnf_dst_name
)[0]
405 link_dict
= self
.DCNetwork_graph
[connected_sw
][vnf_dst_name
]
406 vnf_dst_interface
= link_dict
[0]['dst_port_id']
408 vnf_dst_name
= vnf_dst_name
.split(':')[0]
409 for connected_sw
in self
.DCNetwork_graph
.neighbors(vnf_dst_name
):
410 link_dict
= self
.DCNetwork_graph
[connected_sw
][vnf_dst_name
]
411 for link
in link_dict
:
412 if link_dict
[link
]['dst_port_id'] == vnf_dst_interface
or \
413 link_dict
[link
]['dst_port_name'] == vnf_dst_interface
: # Fix: we might also get interface names, e.g, from a son-emu-cli call
414 # found the right link and connected switch
415 dst_sw
= connected_sw
416 dst_sw_outport_nr
= link_dict
[link
]['src_port_nr']
417 dst_sw_outport_name
= link_dict
[link
]['src_port_name']
421 LOG
.exception('tag not valid: {0}'.format(tag
))
425 # returns the first found shortest path
426 # if all shortest paths are wanted, use: all_shortest_paths
427 path
= nx
.shortest_path(self
.DCNetwork_graph
, src_sw
, dst_sw
, weight
=kwargs
.get('weight'))
429 LOG
.exception("No path could be found between {0} and {1} using src_sw={2} and dst_sw={3}".format(
430 vnf_src_name
, vnf_dst_name
, src_sw
, dst_sw
))
431 LOG
.debug("Graph nodes: %r" % self
.DCNetwork_graph
.nodes())
432 LOG
.debug("Graph edges: %r" % self
.DCNetwork_graph
.edges())
433 for e
, v
in self
.DCNetwork_graph
.edges():
434 LOG
.debug("%r" % self
.DCNetwork_graph
[e
][v
])
435 return "No path could be found between {0} and {1}".format(vnf_src_name
, vnf_dst_name
)
437 LOG
.info("Path between {0} and {1}: {2}".format(vnf_src_name
, vnf_dst_name
, path
))
440 switch_inport_nr
= src_sw_inport_nr
442 cmd
= kwargs
.get('cmd')
444 #iterate through the path to install the flow-entries
445 for i
in range(0,len(path
)):
446 current_node
= self
.getNodeByName(current_hop
)
448 if path
.index(current_hop
) < len(path
)-1:
449 next_hop
= path
[path
.index(current_hop
)+1]
452 next_hop
= vnf_dst_name
454 next_node
= self
.getNodeByName(next_hop
)
456 if next_hop
== vnf_dst_name
:
457 switch_outport_nr
= dst_sw_outport_nr
458 LOG
.info("end node reached: {0}".format(vnf_dst_name
))
459 elif not isinstance( next_node
, OVSSwitch
):
460 LOG
.info("Next node: {0} is not a switch".format(next_hop
))
461 return "Next node: {0} is not a switch".format(next_hop
)
463 # take first link between switches by default
465 switch_outport_nr
= self
.DCNetwork_graph
[current_hop
][next_hop
][index_edge_out
]['src_port_nr']
468 # set of entry via ovs-ofctl
469 if isinstance( current_node
, OVSSwitch
):
471 kwargs
['path'] = path
472 kwargs
['current_hop'] = current_hop
473 kwargs
['switch_inport_name'] = src_sw_inport_name
474 kwargs
['switch_outport_name'] = dst_sw_outport_name
475 kwargs
['skip_vlan_tag'] = True
476 kwargs
['pathindex'] = i
478 monitor_placement
= kwargs
.get('monitor_placement').strip()
479 # put monitor flow at the dst switch
481 if monitor_placement
== 'tx' and path
.index(current_hop
) == 0: # first node:
483 # put monitoring flow at the src switch
484 elif monitor_placement
== 'rx' and path
.index(current_hop
) == len(path
) - 1: # last node:
486 elif monitor_placement
not in ['rx', 'tx']:
487 LOG
.exception('invalid monitor command: {0}'.format(monitor_placement
))
490 if self
.controller
== RemoteController
and insert_flow
:
491 ## set flow entry via ryu rest api
492 self
._set
_flow
_entry
_ryu
_rest
(current_node
, switch_inport_nr
, switch_outport_nr
, **kwargs
)
495 ## set flow entry via ovs-ofctl
496 self
._set
_flow
_entry
_dpctl
(current_node
, switch_inport_nr
, switch_outport_nr
, **kwargs
)
499 # take first link between switches by default
500 if isinstance( next_node
, OVSSwitch
):
501 switch_inport_nr
= self
.DCNetwork_graph
[current_hop
][next_hop
][0]['dst_port_nr']
502 current_hop
= next_hop
504 return "path {2} between {0} and {1}".format(vnf_src_name
, vnf_dst_name
, cmd
)
507 def setChain(self
, vnf_src_name
, vnf_dst_name
, vnf_src_interface
=None, vnf_dst_interface
=None, **kwargs
):
509 Chain 2 vnf interfaces together by installing the flowrules in the switches along their path.
510 Currently the path is found using the default networkx shortest path function.
511 Each chain gets a unique vlan id , so different chains wil not interfere.
513 :param vnf_src_name: vnf name (string)
514 :param vnf_dst_name: vnf name (string)
515 :param vnf_src_interface: source interface name (string)
516 :param vnf_dst_interface: destination interface name (string)
517 :param cmd: 'add-flow' (default) to add a chain, 'del-flows' to remove a chain
518 :param cookie: cookie for the installed flowrules (can be used later as identifier for a set of installed chains)
519 :param match: custom match entry to be added to the flowrules (default: only in_port and vlan tag)
520 :param priority: custom flowrule priority
521 :param monitor: boolean to indicate whether this chain is a monitoring chain
522 :param tag: vlan tag to be used for this chain (pre-defined or new one if none is specified)
523 :param skip_vlan_tag: boolean to indicate if a vlan tag should be appointed to this flow or not
524 :param path: custom path between the two VNFs (list of switches)
525 :return: output log string
528 # special procedure for monitoring flows
529 if kwargs
.get('monitor'):
531 # check if chain already exists
532 found_chains
= [chain_dict
for chain_dict
in self
.installed_chains
if
533 (chain_dict
['vnf_src_name'] == vnf_src_name
and chain_dict
['vnf_src_interface'] == vnf_src_interface
534 and chain_dict
['vnf_dst_name'] == vnf_dst_name
and chain_dict
['vnf_dst_interface'] == vnf_dst_interface
)]
536 if len(found_chains
) > 0:
537 # this chain exists, so need an extra monitoring flow
538 # assume only 1 chain per vnf/interface pair
539 LOG
.debug('*** installing monitoring chain on top of pre-defined chain from {0}:{1} -> {2}:{3}'.
540 format(vnf_src_name
, vnf_src_interface
, vnf_dst_name
, vnf_dst_interface
))
541 tag
= found_chains
[0]['tag']
542 ret
= self
._addMonitorFlow
(vnf_src_name
, vnf_dst_name
, vnf_src_interface
, vnf_dst_interface
,
543 tag
=tag
, table_id
=0, **kwargs
)
546 # no chain existing (or E-LAN) -> install normal chain
547 LOG
.warning('*** installing monitoring chain without pre-defined NSD chain from {0}:{1} -> {2}:{3}'.
548 format(vnf_src_name
, vnf_src_interface
, vnf_dst_name
, vnf_dst_interface
))
552 cmd
= kwargs
.get('cmd')
553 if cmd
== 'add-flow' or cmd
== 'del-flows':
554 ret
= self
._chainAddFlow
(vnf_src_name
, vnf_dst_name
, vnf_src_interface
, vnf_dst_interface
, **kwargs
)
555 if kwargs
.get('bidirectional'):
556 if kwargs
.get('path') is not None:
557 kwargs
['path'] = list(reversed(kwargs
.get('path')))
558 ret
= ret
+'\n' + self
._chainAddFlow
(vnf_dst_name
, vnf_src_name
, vnf_dst_interface
, vnf_src_interface
, **kwargs
)
561 ret
= "Command unknown"
566 def _chainAddFlow(self
, vnf_src_name
, vnf_dst_name
, vnf_src_interface
=None, vnf_dst_interface
=None, **kwargs
):
570 src_sw_inport_name
= None
572 dst_sw_outport_nr
= 0
573 dst_sw_outport_name
= None
575 LOG
.debug("call chainAddFlow vnf_src_name=%r, vnf_src_interface=%r, vnf_dst_name=%r, vnf_dst_interface=%r",
576 vnf_src_name
, vnf_src_interface
, vnf_dst_name
, vnf_dst_interface
)
578 #check if port is specified (vnf:port)
579 if vnf_src_interface
is None:
580 # take first interface by default
581 connected_sw
= self
.DCNetwork_graph
.neighbors(vnf_src_name
)[0]
582 link_dict
= self
.DCNetwork_graph
[vnf_src_name
][connected_sw
]
583 vnf_src_interface
= link_dict
[0]['src_port_id']
585 for connected_sw
in self
.DCNetwork_graph
.neighbors(vnf_src_name
):
586 link_dict
= self
.DCNetwork_graph
[vnf_src_name
][connected_sw
]
587 for link
in link_dict
:
588 if (link_dict
[link
]['src_port_id'] == vnf_src_interface
or
589 link_dict
[link
]['src_port_name'] == vnf_src_interface
): # Fix: we might also get interface names, e.g, from a son-emu-cli call
590 # found the right link and connected switch
591 src_sw
= connected_sw
592 src_sw_inport_nr
= link_dict
[link
]['dst_port_nr']
593 src_sw_inport_name
= link_dict
[link
]['dst_port_name']
596 if vnf_dst_interface
is None:
597 # take first interface by default
598 connected_sw
= self
.DCNetwork_graph
.neighbors(vnf_dst_name
)[0]
599 link_dict
= self
.DCNetwork_graph
[connected_sw
][vnf_dst_name
]
600 vnf_dst_interface
= link_dict
[0]['dst_port_id']
602 vnf_dst_name
= vnf_dst_name
.split(':')[0]
603 for connected_sw
in self
.DCNetwork_graph
.neighbors(vnf_dst_name
):
604 link_dict
= self
.DCNetwork_graph
[connected_sw
][vnf_dst_name
]
605 for link
in link_dict
:
606 if link_dict
[link
]['dst_port_id'] == vnf_dst_interface
or \
607 link_dict
[link
]['dst_port_name'] == vnf_dst_interface
: # Fix: we might also get interface names, e.g, from a son-emu-cli call
608 # found the right link and connected switch
609 dst_sw
= connected_sw
610 dst_sw_outport_nr
= link_dict
[link
]['src_port_nr']
611 dst_sw_outport_name
= link_dict
[link
]['src_port_name']
614 path
= kwargs
.get('path')
618 # returns the first found shortest path
619 # if all shortest paths are wanted, use: all_shortest_paths
620 path
= nx
.shortest_path(self
.DCNetwork_graph
, src_sw
, dst_sw
, weight
=kwargs
.get('weight'))
622 LOG
.exception("No path could be found between {0} and {1} using src_sw={2} and dst_sw={3}".format(
623 vnf_src_name
, vnf_dst_name
, src_sw
, dst_sw
))
624 LOG
.debug("Graph nodes: %r" % self
.DCNetwork_graph
.nodes())
625 LOG
.debug("Graph edges: %r" % self
.DCNetwork_graph
.edges())
626 for e
, v
in self
.DCNetwork_graph
.edges():
627 LOG
.debug("%r" % self
.DCNetwork_graph
[e
][v
])
628 return "No path could be found between {0} and {1}".format(vnf_src_name
, vnf_dst_name
)
630 LOG
.info("Path between {0} and {1}: {2}".format(vnf_src_name
, vnf_dst_name
, path
))
633 switch_inport_nr
= src_sw_inport_nr
636 cmd
= kwargs
.get('cmd')
638 if cmd
== 'add-flow':
639 if kwargs
.get('tag'):
640 # use pre-defined tag
641 vlan
= kwargs
.get('tag')
643 vlan
= self
.vlans
.pop()
645 # store the used vlan tag to identify this chain
646 if not kwargs
.get('monitor'):
648 chain_dict
['vnf_src_name'] = vnf_src_name
649 chain_dict
['vnf_dst_name'] = vnf_dst_name
650 chain_dict
['vnf_src_interface'] = vnf_src_interface
651 chain_dict
['vnf_dst_interface'] = vnf_dst_interface
652 chain_dict
['tag'] = vlan
653 self
.installed_chains
.append(chain_dict
)
655 #iterate through the path to install the flow-entries
656 for i
in range(0,len(path
)):
657 current_node
= self
.getNodeByName(current_hop
)
659 if i
< len(path
) - 1:
660 next_hop
= path
[i
+ 1]
662 # last switch reached
663 next_hop
= vnf_dst_name
665 next_node
= self
.getNodeByName(next_hop
)
667 if next_hop
== vnf_dst_name
:
668 switch_outport_nr
= dst_sw_outport_nr
669 LOG
.info("end node reached: {0}".format(vnf_dst_name
))
670 elif not isinstance( next_node
, OVSSwitch
):
671 LOG
.info("Next node: {0} is not a switch".format(next_hop
))
672 return "Next node: {0} is not a switch".format(next_hop
)
674 # take first link between switches by default
676 switch_outport_nr
= self
.DCNetwork_graph
[current_hop
][next_hop
][index_edge_out
]['src_port_nr']
680 if isinstance( current_node
, OVSSwitch
):
681 kwargs
['vlan'] = vlan
682 kwargs
['path'] = path
683 kwargs
['current_hop'] = current_hop
684 kwargs
['switch_inport_name'] = src_sw_inport_name
685 kwargs
['switch_outport_name'] = dst_sw_outport_name
686 kwargs
['pathindex'] = i
688 if self
.controller
== RemoteController
:
689 ## set flow entry via ryu rest api
690 self
._set
_flow
_entry
_ryu
_rest
(current_node
, switch_inport_nr
, switch_outport_nr
, **kwargs
)
692 ## set flow entry via ovs-ofctl
693 self
._set
_flow
_entry
_dpctl
(current_node
, switch_inport_nr
, switch_outport_nr
, **kwargs
)
695 # take first link between switches by default
696 if isinstance( next_node
, OVSSwitch
):
697 switch_inport_nr
= self
.DCNetwork_graph
[current_hop
][next_hop
][0]['dst_port_nr']
698 current_hop
= next_hop
700 return "path {2} between {0} and {1}".format(vnf_src_name
, vnf_dst_name
, cmd
)
702 def _set_flow_entry_ryu_rest(self
, node
, switch_inport_nr
, switch_outport_nr
, **kwargs
):
703 match
= 'in_port=%s' % switch_inport_nr
705 cookie
= kwargs
.get('cookie')
706 match_input
= kwargs
.get('match')
707 cmd
= kwargs
.get('cmd')
708 path
= kwargs
.get('path')
709 index
= kwargs
.get('pathindex')
711 vlan
= kwargs
.get('vlan')
712 priority
= kwargs
.get('priority')
713 # flag to not set the ovs port vlan tag
714 skip_vlan_tag
= kwargs
.get('skip_vlan_tag')
715 # table id to put this flowentry
716 table_id
= kwargs
.get('table_id')
722 match
= s
.join([match
, match_input
])
725 flow
['dpid'] = int(node
.dpid
, 16)
728 flow
['cookie'] = int(cookie
)
730 flow
['priority'] = int(priority
)
732 flow
['table_id'] = table_id
736 # possible Ryu actions, match fields:
737 # http://ryu.readthedocs.io/en/latest/app/ofctl_rest.html#add-a-flow-entry
738 if cmd
== 'add-flow':
739 prefix
= 'stats/flowentry/add'
741 if index
== 0: # first node
742 # set vlan tag in ovs instance (to isolate E-LANs)
743 if not skip_vlan_tag
:
744 in_port_name
= kwargs
.get('switch_inport_name')
745 self
._set
_vlan
_tag
(node
, in_port_name
, vlan
)
746 # set vlan push action if more than 1 switch in the path
749 action
['type'] = 'PUSH_VLAN' # Push a new VLAN tag if a input frame is non-VLAN-tagged
750 action
['ethertype'] = 33024 # Ethertype 0x8100(=33024): IEEE 802.1Q VLAN-tagged frame
751 flow
['actions'].append(action
)
753 action
['type'] = 'SET_FIELD'
754 action
['field'] = 'vlan_vid'
755 # ryu expects the field to be masked
756 action
['value'] = vlan |
0x1000
757 flow
['actions'].append(action
)
759 elif index
== len(path
) - 1: # last node
760 # set vlan tag in ovs instance (to isolate E-LANs)
761 if not skip_vlan_tag
:
762 out_port_name
= kwargs
.get('switch_outport_name')
763 self
._set
_vlan
_tag
(node
, out_port_name
, vlan
)
764 # set vlan pop action if more than 1 switch in the path
766 match
+= ',dl_vlan=%s' % vlan
768 action
['type'] = 'POP_VLAN'
769 flow
['actions'].append(action
)
772 match
+= ',dl_vlan=%s' % vlan
774 # output action must come last
776 action
['type'] = 'OUTPUT'
777 action
['port'] = switch_outport_nr
778 flow
['actions'].append(action
)
780 elif cmd
== 'del-flows':
781 prefix
= 'stats/flowentry/delete'
784 # TODO: add cookie_mask as argument
785 flow
['cookie_mask'] = int('0xffffffffffffffff', 16) # need full mask to match complete cookie
788 action
['type'] = 'OUTPUT'
789 action
['port'] = switch_outport_nr
790 flow
['actions'].append(action
)
792 flow
['match'] = self
._parse
_match
(match
)
793 self
.ryu_REST(prefix
, data
=flow
)
795 def _set_vlan_tag(self
, node
, switch_port
, tag
):
796 node
.vsctl('set', 'port {0} tag={1}'.format(switch_port
,tag
))
797 LOG
.debug("set vlan in switch: {0} in_port: {1} vlan tag: {2}".format(node
.name
, switch_port
, tag
))
799 def _set_flow_entry_dpctl(self
, node
, switch_inport_nr
, switch_outport_nr
, **kwargs
):
801 match
= 'in_port=%s' % switch_inport_nr
803 cookie
= kwargs
.get('cookie')
804 match_input
= kwargs
.get('match')
805 cmd
= kwargs
.get('cmd')
806 path
= kwargs
.get('path')
807 index
= kwargs
.get('pathindex')
808 vlan
= kwargs
.get('vlan')
812 cookie
= 'cookie=%s' % cookie
813 match
= s
.join([cookie
, match
])
815 match
= s
.join([match
, match_input
])
816 if cmd
== 'add-flow':
817 action
= 'action=%s' % switch_outport_nr
819 if index
== 0: # first node
820 action
= ('action=mod_vlan_vid:%s' % vlan
) + (',output=%s' % switch_outport_nr
)
821 match
= '-O OpenFlow13 ' + match
822 elif index
== len(path
) - 1: # last node
823 match
+= ',dl_vlan=%s' % vlan
824 action
= 'action=strip_vlan,output=%s' % switch_outport_nr
826 match
+= ',dl_vlan=%s' % vlan
827 ofcmd
= s
.join([match
, action
])
828 elif cmd
== 'del-flows':
833 node
.dpctl(cmd
, ofcmd
)
834 LOG
.info("{3} in switch: {0} in_port: {1} out_port: {2}".format(node
.name
, switch_inport_nr
,
835 switch_outport_nr
, cmd
))
837 # start Ryu Openflow controller as Remote Controller for the DCNetwork
838 def startRyu(self
, learning_switch
=True):
839 # start Ryu controller with rest-API
840 python_install_path
= site
.getsitepackages()[0]
841 # ryu default learning switch
842 #ryu_path = python_install_path + '/ryu/app/simple_switch_13.py'
843 #custom learning switch that installs a default NORMAL action in the ovs switches
844 dir_path
= os
.path
.dirname(os
.path
.realpath(__file__
))
845 ryu_path
= dir_path
+ '/son_emu_simple_switch_13.py'
846 ryu_path2
= python_install_path
+ '/ryu/app/ofctl_rest.py'
847 # change the default Openflow controller port to 6653 (official IANA-assigned port number), as used by Mininet
848 # Ryu still uses 6633 as default
849 ryu_option
= '--ofp-tcp-listen-port'
851 ryu_cmd
= 'ryu-manager'
852 FNULL
= open("/tmp/ryu.log", 'w')
854 self
.ryu_process
= Popen([ryu_cmd
, ryu_path
, ryu_path2
, ryu_option
, ryu_of_port
], stdout
=FNULL
, stderr
=FNULL
)
855 LOG
.debug('starting ryu-controller with {0}'.format(ryu_path
))
856 LOG
.debug('starting ryu-controller with {0}'.format(ryu_path2
))
858 # no learning switch, but with rest api
859 self
.ryu_process
= Popen([ryu_cmd
, ryu_path2
, ryu_option
, ryu_of_port
], stdout
=FNULL
, stderr
=FNULL
)
860 LOG
.debug('starting ryu-controller with {0}'.format(ryu_path2
))
865 Stop the Ryu controller that might be started by son-emu.
869 if self
.ryu_process
is not None:
870 self
.ryu_process
.terminate()
871 self
.ryu_process
.kill()
872 # ensure its death ;-)
873 Popen(['pkill', '-f', 'ryu-manager'])
875 def ryu_REST(self
, prefix
, dpid
=None, data
=None):
878 url
= self
.ryu_REST_api
+ '/' + str(prefix
) + '/' + str(dpid
)
880 url
= self
.ryu_REST_api
+ '/' + str(prefix
)
882 req
= self
.RyuSession
.post(url
, json
=data
)
884 req
= self
.RyuSession
.get(url
)
887 # do extra logging if status code is not 200 (OK)
888 if req
.status_code
is not requests
.codes
.ok
:
890 'type {0} encoding: {1} text: {2} headers: {3} history: {4}'.format(req
.headers
['content-type'],
891 req
.encoding
, req
.text
,
892 req
.headers
, req
.history
))
893 LOG
.info('url: {0}'.format(str(url
)))
894 if data
: LOG
.info('POST: {0}'.format(str(data
)))
895 LOG
.info('status: {0} reason: {1}'.format(req
.status_code
, req
.reason
))
898 if 'json' in req
.headers
['content-type']:
902 ret
= req
.text
.rstrip()
906 # need to respect that some match fields must be integers
907 # http://ryu.readthedocs.io/en/latest/app/ofctl_rest.html#description-of-match-and-actions
908 def _parse_match(self
, match
):
909 matches
= match
.split(',')
915 m2
= int(match
[1], 0)
919 dict.update({match
[0]:m2
})
922 def find_connected_dc_interface(self
, vnf_src_name
, vnf_src_interface
=None):
924 if vnf_src_interface
is None:
925 # take first interface by default
926 connected_sw
= self
.DCNetwork_graph
.neighbors(vnf_src_name
)[0]
927 link_dict
= self
.DCNetwork_graph
[vnf_src_name
][connected_sw
]
928 vnf_src_interface
= link_dict
[0]['src_port_id']
930 for connected_sw
in self
.DCNetwork_graph
.neighbors(vnf_src_name
):
931 link_dict
= self
.DCNetwork_graph
[vnf_src_name
][connected_sw
]
932 for link
in link_dict
:
933 if (link_dict
[link
]['src_port_id'] == vnf_src_interface
or
934 link_dict
[link
]['src_port_name'] == vnf_src_interface
): # Fix: we might also get interface names, e.g, from a son-emu-cli call
935 # found the right link and connected switch
936 src_sw
= connected_sw
937 src_sw_inport_nr
= link_dict
[link
]['dst_port_nr']
938 src_sw_inport_name
= link_dict
[link
]['dst_port_name']
939 return src_sw_inport_name