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
38 from mininet
.net
import Containernet
39 from mininet
.node
import Controller
, DefaultController
, OVSSwitch
, OVSKernelSwitch
, Docker
, RemoteController
40 from mininet
.cli
import CLI
41 from mininet
.link
import TCLink
42 from mininet
.clean
import cleanup
44 from emuvim
.dcemulator
.monitoring
import DCNetworkMonitor
45 from emuvim
.dcemulator
.node
import Datacenter
, EmulatorCompute
, EmulatorExtSAP
46 from emuvim
.dcemulator
.resourcemodel
import ResourceModelRegistrar
48 LOG
= logging
.getLogger("dcemulator.net")
49 LOG
.setLevel(logging
.DEBUG
)
51 # default CPU period used for cpu percentage-based cfs values (microseconds)
54 # default priority setting for added flow-rules
55 DEFAULT_PRIORITY
= 1000
56 # default cookie number for new flow-rules
59 class DCNetwork(Containernet
):
61 Wraps the original Mininet/Containernet class and provides
62 methods to add data centers, switches, etc.
64 This class is used by topology definition scripts.
67 def __init__(self
, controller
=RemoteController
, monitor
=False,
68 enable_learning
=False, # learning switch behavior of the default ovs switches icw Ryu controller can be turned off/on, needed for E-LAN functionality
69 dc_emulation_max_cpu
=1.0, # fraction of overall CPU time for emulation
70 dc_emulation_max_mem
=512, # emulation max mem in MB
73 Create an extended version of a Containernet network
74 :param dc_emulation_max_cpu: max. CPU time used by containers in data centers
75 :param kwargs: path through for Mininet parameters
80 self
.ryu_process
= None
81 #list of deployed nsds.E_Lines and E_LANs (uploaded from the dummy gatekeeper)
82 self
.deployed_nsds
= []
83 self
.deployed_elines
= []
84 self
.deployed_elans
= []
85 self
.installed_chains
= []
88 # always cleanup environment before we start the emulator
92 # call original Docker.__init__ and setup default controller
93 Containernet
.__init
__(
94 self
, switch
=OVSKernelSwitch
, controller
=controller
, **kwargs
)
96 # default switch configuration
97 enable_ryu_learning
= False
99 self
.failMode
= 'standalone'
100 enable_ryu_learning
= True
102 self
.failMode
= 'secure'
105 if controller
== RemoteController
:
106 # start Ryu controller
107 self
.startRyu(learning_switch
=enable_ryu_learning
)
109 # add the specified controller
110 self
.addController('c0', controller
=controller
)
112 # graph of the complete DC network
113 self
.DCNetwork_graph
= nx
.MultiDiGraph()
115 # initialize pool of vlan tags to setup the SDN paths
116 self
.vlans
= range(4096)[::-1]
118 # link to Ryu REST_API
121 self
.ryu_REST_api
= 'http://{0}:{1}'.format(ryu_ip
, ryu_port
)
122 self
.RyuSession
= requests
.Session()
126 self
.monitor_agent
= DCNetworkMonitor(self
)
128 self
.monitor_agent
= None
130 # initialize resource model registrar
131 self
.rm_registrar
= ResourceModelRegistrar(
132 dc_emulation_max_cpu
, dc_emulation_max_mem
)
133 self
.cpu_period
= CPU_PERIOD
135 def addDatacenter(self
, label
, metadata
={}, resource_log_path
=None):
137 Create and add a logical cloud data center to the network.
139 if label
in self
.dcs
:
140 raise Exception("Data center label already exists: %s" % label
)
141 dc
= Datacenter(label
, metadata
=metadata
, resource_log_path
=resource_log_path
)
142 dc
.net
= self
# set reference to network
144 dc
.create() # finally create the data center in our Mininet instance
145 LOG
.info("added data center: %s" % label
)
148 def addLink(self
, node1
, node2
, **params
):
150 Able to handle Datacenter objects as link
153 assert node1
is not None
154 assert node2
is not None
156 # ensure type of node1
157 if isinstance( node1
, basestring
):
158 if node1
in self
.dcs
:
159 node1
= self
.dcs
[node1
].switch
160 if isinstance( node1
, Datacenter
):
162 # ensure type of node2
163 if isinstance( node2
, basestring
):
164 if node2
in self
.dcs
:
165 node2
= self
.dcs
[node2
].switch
166 if isinstance( node2
, Datacenter
):
168 # try to give containers a default IP
169 if isinstance( node1
, Docker
):
170 if "params1" not in params
:
171 params
["params1"] = {}
172 if "ip" not in params
["params1"]:
173 params
["params1"]["ip"] = self
.getNextIp()
174 if isinstance( node2
, Docker
):
175 if "params2" not in params
:
176 params
["params2"] = {}
177 if "ip" not in params
["params2"]:
178 params
["params2"]["ip"] = self
.getNextIp()
179 # ensure that we allow TCLinks between data centers
180 # TODO this is not optimal, we use cls=Link for containers and TCLink for data centers
181 # see Containernet issue: https://github.com/mpeuster/containernet/issues/3
182 if "cls" not in params
:
183 params
["cls"] = TCLink
185 link
= Containernet
.addLink(self
, node1
, node2
, **params
)
187 # try to give container interfaces a default id
188 node1_port_id
= node1
.ports
[link
.intf1
]
189 if isinstance(node1
, Docker
):
190 if "id" in params
["params1"]:
191 node1_port_id
= params
["params1"]["id"]
192 node1_port_name
= link
.intf1
.name
194 node2_port_id
= node2
.ports
[link
.intf2
]
195 if isinstance(node2
, Docker
):
196 if "id" in params
["params2"]:
197 node2_port_id
= params
["params2"]["id"]
198 node2_port_name
= link
.intf2
.name
201 # add edge and assigned port number to graph in both directions between node1 and node2
202 # port_id: id given in descriptor (if available, otherwise same as port)
203 # port: portnumber assigned by Containernet
206 # possible weight metrics allowed by TClink class:
207 weight_metrics
= ['bw', 'delay', 'jitter', 'loss']
208 edge_attributes
= [p
for p
in params
if p
in weight_metrics
]
209 for attr
in edge_attributes
:
210 # if delay: strip ms (need number as weight in graph)
211 match
= re
.search('([0-9]*\.?[0-9]+)', str(params
[attr
]))
213 attr_number
= match
.group(1)
216 attr_dict
[attr
] = attr_number
219 attr_dict2
= {'src_port_id': node1_port_id
, 'src_port_nr': node1
.ports
[link
.intf1
],
220 'src_port_name': node1_port_name
,
221 'dst_port_id': node2_port_id
, 'dst_port_nr': node2
.ports
[link
.intf2
],
222 'dst_port_name': node2_port_name
}
223 attr_dict2
.update(attr_dict
)
224 self
.DCNetwork_graph
.add_edge(node1
.name
, node2
.name
, attr_dict
=attr_dict2
)
226 attr_dict2
= {'src_port_id': node2_port_id
, 'src_port_nr': node2
.ports
[link
.intf2
],
227 'src_port_name': node2_port_name
,
228 'dst_port_id': node1_port_id
, 'dst_port_nr': node1
.ports
[link
.intf1
],
229 'dst_port_name': node1_port_name
}
230 attr_dict2
.update(attr_dict
)
231 self
.DCNetwork_graph
.add_edge(node2
.name
, node1
.name
, attr_dict
=attr_dict2
)
233 LOG
.debug("addLink: n1={0} intf1={1} -- n2={2} intf2={3}".format(
234 str(node1
),node1_port_name
, str(node2
), node2_port_name
))
238 def removeLink(self
, link
=None, node1
=None, node2
=None):
240 Remove the link from the Containernet and the networkx graph
243 node1
= link
.intf1
.node
244 node2
= link
.intf2
.node
245 assert node1
is not None
246 assert node2
is not None
247 Containernet
.removeLink(self
, link
=link
, node1
=node1
, node2
=node2
)
248 # TODO we might decrease the loglevel to debug:
250 self
.DCNetwork_graph
.remove_edge(node2
.name
, node1
.name
)
252 LOG
.warning("%s, %s not found in DCNetwork_graph." % ((node2
.name
, node1
.name
)))
254 self
.DCNetwork_graph
.remove_edge(node1
.name
, node2
.name
)
256 LOG
.warning("%s, %s not found in DCNetwork_graph." % ((node1
.name
, node2
.name
)))
258 def addDocker( self
, label
, **params
):
260 Wrapper for addDocker method to use custom container class.
262 self
.DCNetwork_graph
.add_node(label
, type=params
.get('type', 'docker'))
263 return Containernet
.addDocker(self
, label
, cls
=EmulatorCompute
, **params
)
265 def removeDocker( self
, label
, **params
):
267 Wrapper for removeDocker method to update graph.
269 self
.DCNetwork_graph
.remove_node(label
)
270 return Containernet
.removeDocker(self
, label
, **params
)
272 def addExtSAP(self
, sap_name
, sap_ip
, **params
):
274 Wrapper for addExtSAP method to store SAP also in graph.
276 # make sure that 'type' is set
277 params
['type'] = params
.get('type','sap_ext')
278 self
.DCNetwork_graph
.add_node(sap_name
, type=params
['type'])
279 return Containernet
.addExtSAP(self
, sap_name
, sap_ip
, **params
)
281 def removeExtSAP(self
, sap_name
, **params
):
283 Wrapper for removeExtSAP method to remove SAP also from graph.
285 self
.DCNetwork_graph
.remove_node(sap_name
)
286 return Containernet
.removeExtSAP(self
, sap_name
)
288 def addSwitch( self
, name
, add_to_graph
=True, **params
):
290 Wrapper for addSwitch method to store switch also in graph.
293 # add this switch to the global topology overview
295 self
.DCNetwork_graph
.add_node(name
, type=params
.get('type','switch'))
297 # set the learning switch behavior
298 if 'failMode' in params
:
299 failMode
= params
['failMode']
301 failMode
= self
.failMode
303 s
= Containernet
.addSwitch(self
, name
, protocols
='OpenFlow10,OpenFlow12,OpenFlow13', failMode
=failMode
, **params
)
307 def getAllContainers(self
):
309 Returns a list with all containers within all data centers.
312 for dc
in self
.dcs
.itervalues():
313 all_containers
+= dc
.listCompute()
314 return all_containers
318 for dc
in self
.dcs
.itervalues():
320 Containernet
.start(self
)
324 # stop the monitor agent
325 if self
.monitor_agent
is not None:
326 self
.monitor_agent
.stop()
329 Containernet
.stop(self
)
331 # stop Ryu controller
338 def setLAN(self
, vnf_list
):
340 setup an E-LAN network by assigning the same VLAN tag to each DC interface of the VNFs in the E-LAN
342 :param vnf_list: names of the VNFs in this E-LAN [{name:,interface:},...]
347 src_sw_inport_name
= None
349 # get a vlan tag for this E-LAN
350 vlan
= self
.vlans
.pop()
353 vnf_src_name
= vnf
['name']
354 vnf_src_interface
= vnf
['interface']
356 # check if port is specified (vnf:port)
357 if vnf_src_interface
is None:
358 # take first interface by default
359 connected_sw
= self
.DCNetwork_graph
.neighbors(vnf_src_name
)[0]
360 link_dict
= self
.DCNetwork_graph
[vnf_src_name
][connected_sw
]
361 vnf_src_interface
= link_dict
[0]['src_port_id']
363 for connected_sw
in self
.DCNetwork_graph
.neighbors(vnf_src_name
):
364 link_dict
= self
.DCNetwork_graph
[vnf_src_name
][connected_sw
]
365 for link
in link_dict
:
366 if (link_dict
[link
]['src_port_id'] == vnf_src_interface
or
367 link_dict
[link
]['src_port_name'] == vnf_src_interface
): # Fix: we might also get interface names, e.g, from a son-emu-cli call
368 # found the right link and connected switch
369 src_sw
= connected_sw
370 src_sw_inport_nr
= link_dict
[link
]['dst_port_nr']
371 src_sw_inport_name
= link_dict
[link
]['dst_port_name']
374 # set the tag on the dc switch interface
375 LOG
.debug('set E-LAN: vnf name: {0} interface: {1} tag: {2}'.format(vnf_src_name
, vnf_src_interface
,vlan
))
376 switch_node
= self
.getNodeByName(src_sw
)
377 self
._set
_vlan
_tag
(switch_node
, src_sw_inport_name
, vlan
)
379 def _addMonitorFlow(self
, vnf_src_name
, vnf_dst_name
, vnf_src_interface
=None, vnf_dst_interface
=None,
382 Add a monitoring flow entry that adds a special flowentry/counter at the begin or end of a chain.
383 So this monitoring flowrule exists on top of a previously defined chain rule and uses the same vlan tag/routing.
386 :param vnf_src_interface:
387 :param vnf_dst_interface:
388 :param tag: vlan tag to be used for this chain (same tag as existing chain)
389 :param monitor_placement: 'tx' or 'rx' indicating to place the extra flowentry resp. at the beginning or end of the chain
395 src_sw_inport_name
= None
397 dst_sw_outport_nr
= 0
398 dst_sw_outport_name
= None
400 LOG
.debug("call AddMonitorFlow vnf_src_name=%r, vnf_src_interface=%r, vnf_dst_name=%r, vnf_dst_interface=%r",
401 vnf_src_name
, vnf_src_interface
, vnf_dst_name
, vnf_dst_interface
)
403 #check if port is specified (vnf:port)
404 if vnf_src_interface
is None:
405 # take first interface by default
406 connected_sw
= self
.DCNetwork_graph
.neighbors(vnf_src_name
)[0]
407 link_dict
= self
.DCNetwork_graph
[vnf_src_name
][connected_sw
]
408 vnf_src_interface
= link_dict
[0]['src_port_id']
410 for connected_sw
in self
.DCNetwork_graph
.neighbors(vnf_src_name
):
411 link_dict
= self
.DCNetwork_graph
[vnf_src_name
][connected_sw
]
412 for link
in link_dict
:
413 if (link_dict
[link
]['src_port_id'] == vnf_src_interface
or
414 link_dict
[link
]['src_port_name'] == vnf_src_interface
): # Fix: we might also get interface names, e.g, from a son-emu-cli call
415 # found the right link and connected switch
416 src_sw
= connected_sw
417 src_sw_inport_nr
= link_dict
[link
]['dst_port_nr']
418 src_sw_inport_name
= link_dict
[link
]['dst_port_name']
421 if vnf_dst_interface
is None:
422 # take first interface by default
423 connected_sw
= self
.DCNetwork_graph
.neighbors(vnf_dst_name
)[0]
424 link_dict
= self
.DCNetwork_graph
[connected_sw
][vnf_dst_name
]
425 vnf_dst_interface
= link_dict
[0]['dst_port_id']
427 vnf_dst_name
= vnf_dst_name
.split(':')[0]
428 for connected_sw
in self
.DCNetwork_graph
.neighbors(vnf_dst_name
):
429 link_dict
= self
.DCNetwork_graph
[connected_sw
][vnf_dst_name
]
430 for link
in link_dict
:
431 if link_dict
[link
]['dst_port_id'] == vnf_dst_interface
or \
432 link_dict
[link
]['dst_port_name'] == vnf_dst_interface
: # Fix: we might also get interface names, e.g, from a son-emu-cli call
433 # found the right link and connected switch
434 dst_sw
= connected_sw
435 dst_sw_outport_nr
= link_dict
[link
]['src_port_nr']
436 dst_sw_outport_name
= link_dict
[link
]['src_port_name']
440 LOG
.exception('tag not valid: {0}'.format(tag
))
444 # returns the first found shortest path
445 # if all shortest paths are wanted, use: all_shortest_paths
446 path
= nx
.shortest_path(self
.DCNetwork_graph
, src_sw
, dst_sw
, weight
=kwargs
.get('weight'))
448 LOG
.exception("No path could be found between {0} and {1} using src_sw={2} and dst_sw={3}".format(
449 vnf_src_name
, vnf_dst_name
, src_sw
, dst_sw
))
450 LOG
.debug("Graph nodes: %r" % self
.DCNetwork_graph
.nodes())
451 LOG
.debug("Graph edges: %r" % self
.DCNetwork_graph
.edges())
452 for e
, v
in self
.DCNetwork_graph
.edges():
453 LOG
.debug("%r" % self
.DCNetwork_graph
[e
][v
])
454 return "No path could be found between {0} and {1}".format(vnf_src_name
, vnf_dst_name
)
456 LOG
.info("Path between {0} and {1}: {2}".format(vnf_src_name
, vnf_dst_name
, path
))
459 switch_inport_nr
= src_sw_inport_nr
461 cmd
= kwargs
.get('cmd')
463 #iterate through the path to install the flow-entries
464 for i
in range(0,len(path
)):
465 current_node
= self
.getNodeByName(current_hop
)
467 if path
.index(current_hop
) < len(path
)-1:
468 next_hop
= path
[path
.index(current_hop
)+1]
471 next_hop
= vnf_dst_name
473 next_node
= self
.getNodeByName(next_hop
)
475 if next_hop
== vnf_dst_name
:
476 switch_outport_nr
= dst_sw_outport_nr
477 LOG
.info("end node reached: {0}".format(vnf_dst_name
))
478 elif not isinstance( next_node
, OVSSwitch
):
479 LOG
.info("Next node: {0} is not a switch".format(next_hop
))
480 return "Next node: {0} is not a switch".format(next_hop
)
482 # take first link between switches by default
484 switch_outport_nr
= self
.DCNetwork_graph
[current_hop
][next_hop
][index_edge_out
]['src_port_nr']
487 # set of entry via ovs-ofctl
488 if isinstance( current_node
, OVSSwitch
):
490 kwargs
['path'] = path
491 kwargs
['current_hop'] = current_hop
492 kwargs
['switch_inport_name'] = src_sw_inport_name
493 kwargs
['switch_outport_name'] = dst_sw_outport_name
494 kwargs
['skip_vlan_tag'] = True
495 kwargs
['pathindex'] = i
497 monitor_placement
= kwargs
.get('monitor_placement').strip()
498 # put monitor flow at the dst switch
500 if monitor_placement
== 'tx' and path
.index(current_hop
) == 0: # first node:
502 # put monitoring flow at the src switch
503 elif monitor_placement
== 'rx' and path
.index(current_hop
) == len(path
) - 1: # last node:
505 elif monitor_placement
not in ['rx', 'tx']:
506 LOG
.exception('invalid monitor command: {0}'.format(monitor_placement
))
509 if self
.controller
== RemoteController
and insert_flow
:
510 ## set flow entry via ryu rest api
511 self
._set
_flow
_entry
_ryu
_rest
(current_node
, switch_inport_nr
, switch_outport_nr
, **kwargs
)
514 ## set flow entry via ovs-ofctl
515 self
._set
_flow
_entry
_dpctl
(current_node
, switch_inport_nr
, switch_outport_nr
, **kwargs
)
518 # take first link between switches by default
519 if isinstance( next_node
, OVSSwitch
):
520 switch_inport_nr
= self
.DCNetwork_graph
[current_hop
][next_hop
][0]['dst_port_nr']
521 current_hop
= next_hop
523 return "path {2} between {0} and {1}".format(vnf_src_name
, vnf_dst_name
, cmd
)
526 def setChain(self
, vnf_src_name
, vnf_dst_name
, vnf_src_interface
=None, vnf_dst_interface
=None, **kwargs
):
528 Chain 2 vnf interfaces together by installing the flowrules in the switches along their path.
529 Currently the path is found using the default networkx shortest path function.
530 Each chain gets a unique vlan id , so different chains wil not interfere.
532 :param vnf_src_name: vnf name (string)
533 :param vnf_dst_name: vnf name (string)
534 :param vnf_src_interface: source interface name (string)
535 :param vnf_dst_interface: destination interface name (string)
536 :param cmd: 'add-flow' (default) to add a chain, 'del-flows' to remove a chain
537 :param cookie: cookie for the installed flowrules (can be used later as identifier for a set of installed chains)
538 :param match: custom match entry to be added to the flowrules (default: only in_port and vlan tag)
539 :param priority: custom flowrule priority
540 :param monitor: boolean to indicate whether this chain is a monitoring chain
541 :param tag: vlan tag to be used for this chain (pre-defined or new one if none is specified)
542 :param skip_vlan_tag: boolean to indicate if a vlan tag should be appointed to this flow or not
543 :param path: custom path between the two VNFs (list of switches)
544 :return: output log string
547 # special procedure for monitoring flows
548 if kwargs
.get('monitor'):
550 # check if chain already exists
551 found_chains
= [chain_dict
for chain_dict
in self
.installed_chains
if
552 (chain_dict
['vnf_src_name'] == vnf_src_name
and chain_dict
['vnf_src_interface'] == vnf_src_interface
553 and chain_dict
['vnf_dst_name'] == vnf_dst_name
and chain_dict
['vnf_dst_interface'] == vnf_dst_interface
)]
555 if len(found_chains
) > 0:
556 # this chain exists, so need an extra monitoring flow
557 # assume only 1 chain per vnf/interface pair
558 LOG
.debug('*** installing monitoring chain on top of pre-defined chain from {0}:{1} -> {2}:{3}'.
559 format(vnf_src_name
, vnf_src_interface
, vnf_dst_name
, vnf_dst_interface
))
560 tag
= found_chains
[0]['tag']
561 ret
= self
._addMonitorFlow
(vnf_src_name
, vnf_dst_name
, vnf_src_interface
, vnf_dst_interface
,
562 tag
=tag
, table_id
=0, **kwargs
)
565 # no chain existing (or E-LAN) -> install normal chain
566 LOG
.warning('*** installing monitoring chain without pre-defined NSD chain from {0}:{1} -> {2}:{3}'.
567 format(vnf_src_name
, vnf_src_interface
, vnf_dst_name
, vnf_dst_interface
))
571 cmd
= kwargs
.get('cmd', 'add-flow')
572 if cmd
== 'add-flow' or cmd
== 'del-flows':
573 ret
= self
._chainAddFlow
(vnf_src_name
, vnf_dst_name
, vnf_src_interface
, vnf_dst_interface
, **kwargs
)
574 if kwargs
.get('bidirectional'):
575 if kwargs
.get('path') is not None:
576 kwargs
['path'] = list(reversed(kwargs
.get('path')))
577 ret
= ret
+'\n' + self
._chainAddFlow
(vnf_dst_name
, vnf_src_name
, vnf_dst_interface
, vnf_src_interface
, **kwargs
)
580 ret
= "Command unknown"
585 def _chainAddFlow(self
, vnf_src_name
, vnf_dst_name
, vnf_src_interface
=None, vnf_dst_interface
=None, **kwargs
):
589 src_sw_inport_name
= None
591 dst_sw_outport_nr
= 0
592 dst_sw_outport_name
= None
594 LOG
.debug("call chainAddFlow vnf_src_name=%r, vnf_src_interface=%r, vnf_dst_name=%r, vnf_dst_interface=%r",
595 vnf_src_name
, vnf_src_interface
, vnf_dst_name
, vnf_dst_interface
)
597 #check if port is specified (vnf:port)
598 if vnf_src_interface
is None:
599 # take first interface by default
600 connected_sw
= self
.DCNetwork_graph
.neighbors(vnf_src_name
)[0]
601 link_dict
= self
.DCNetwork_graph
[vnf_src_name
][connected_sw
]
602 vnf_src_interface
= link_dict
[0]['src_port_id']
604 for connected_sw
in self
.DCNetwork_graph
.neighbors(vnf_src_name
):
605 link_dict
= self
.DCNetwork_graph
[vnf_src_name
][connected_sw
]
606 for link
in link_dict
:
607 if (link_dict
[link
]['src_port_id'] == vnf_src_interface
or
608 link_dict
[link
]['src_port_name'] == vnf_src_interface
): # Fix: we might also get interface names, e.g, from a son-emu-cli call
609 # found the right link and connected switch
610 src_sw
= connected_sw
611 src_sw_inport_nr
= link_dict
[link
]['dst_port_nr']
612 src_sw_inport_name
= link_dict
[link
]['dst_port_name']
615 if vnf_dst_interface
is None:
616 # take first interface by default
617 connected_sw
= self
.DCNetwork_graph
.neighbors(vnf_dst_name
)[0]
618 link_dict
= self
.DCNetwork_graph
[connected_sw
][vnf_dst_name
]
619 vnf_dst_interface
= link_dict
[0]['dst_port_id']
621 vnf_dst_name
= vnf_dst_name
.split(':')[0]
622 for connected_sw
in self
.DCNetwork_graph
.neighbors(vnf_dst_name
):
623 link_dict
= self
.DCNetwork_graph
[connected_sw
][vnf_dst_name
]
624 for link
in link_dict
:
625 if link_dict
[link
]['dst_port_id'] == vnf_dst_interface
or \
626 link_dict
[link
]['dst_port_name'] == vnf_dst_interface
: # Fix: we might also get interface names, e.g, from a son-emu-cli call
627 # found the right link and connected switch
628 dst_sw
= connected_sw
629 dst_sw_outport_nr
= link_dict
[link
]['src_port_nr']
630 dst_sw_outport_name
= link_dict
[link
]['src_port_name']
633 path
= kwargs
.get('path')
637 # returns the first found shortest path
638 # if all shortest paths are wanted, use: all_shortest_paths
639 path
= nx
.shortest_path(self
.DCNetwork_graph
, src_sw
, dst_sw
, weight
=kwargs
.get('weight'))
641 LOG
.exception("No path could be found between {0} and {1} using src_sw={2} and dst_sw={3}".format(
642 vnf_src_name
, vnf_dst_name
, src_sw
, dst_sw
))
643 LOG
.debug("Graph nodes: %r" % self
.DCNetwork_graph
.nodes())
644 LOG
.debug("Graph edges: %r" % self
.DCNetwork_graph
.edges())
645 for e
, v
in self
.DCNetwork_graph
.edges():
646 LOG
.debug("%r" % self
.DCNetwork_graph
[e
][v
])
647 return "No path could be found between {0} and {1}".format(vnf_src_name
, vnf_dst_name
)
649 LOG
.info("Path between {0} and {1}: {2}".format(vnf_src_name
, vnf_dst_name
, path
))
652 switch_inport_nr
= src_sw_inport_nr
655 cmd
= kwargs
.get('cmd')
657 if cmd
== 'add-flow':
658 if kwargs
.get('tag'):
659 # use pre-defined tag
660 vlan
= kwargs
.get('tag')
662 vlan
= self
.vlans
.pop()
664 # store the used vlan tag to identify this chain
665 if not kwargs
.get('monitor'):
667 chain_dict
['vnf_src_name'] = vnf_src_name
668 chain_dict
['vnf_dst_name'] = vnf_dst_name
669 chain_dict
['vnf_src_interface'] = vnf_src_interface
670 chain_dict
['vnf_dst_interface'] = vnf_dst_interface
671 chain_dict
['tag'] = vlan
672 self
.installed_chains
.append(chain_dict
)
674 #iterate through the path to install the flow-entries
675 for i
in range(0,len(path
)):
676 current_node
= self
.getNodeByName(current_hop
)
678 if i
< len(path
) - 1:
679 next_hop
= path
[i
+ 1]
681 # last switch reached
682 next_hop
= vnf_dst_name
684 next_node
= self
.getNodeByName(next_hop
)
686 if next_hop
== vnf_dst_name
:
687 switch_outport_nr
= dst_sw_outport_nr
688 LOG
.info("end node reached: {0}".format(vnf_dst_name
))
689 elif not isinstance( next_node
, OVSSwitch
):
690 LOG
.info("Next node: {0} is not a switch".format(next_hop
))
691 return "Next node: {0} is not a switch".format(next_hop
)
693 # take first link between switches by default
695 switch_outport_nr
= self
.DCNetwork_graph
[current_hop
][next_hop
][index_edge_out
]['src_port_nr']
699 if isinstance( current_node
, OVSSwitch
):
700 kwargs
['vlan'] = vlan
701 kwargs
['path'] = path
702 kwargs
['current_hop'] = current_hop
703 kwargs
['switch_inport_name'] = src_sw_inport_name
704 kwargs
['switch_outport_name'] = dst_sw_outport_name
705 kwargs
['pathindex'] = i
707 if self
.controller
== RemoteController
:
708 ## set flow entry via ryu rest api
709 self
._set
_flow
_entry
_ryu
_rest
(current_node
, switch_inport_nr
, switch_outport_nr
, **kwargs
)
711 ## set flow entry via ovs-ofctl
712 self
._set
_flow
_entry
_dpctl
(current_node
, switch_inport_nr
, switch_outport_nr
, **kwargs
)
714 # take first link between switches by default
715 if isinstance( next_node
, OVSSwitch
):
716 switch_inport_nr
= self
.DCNetwork_graph
[current_hop
][next_hop
][0]['dst_port_nr']
717 current_hop
= next_hop
720 'priority':kwargs
.get('priority', DEFAULT_PRIORITY
),
721 'cookie':kwargs
.get('cookie', DEFAULT_COOKIE
),
722 'vlan':kwargs
['vlan'],
723 'path':kwargs
['path'],
724 'match_input':kwargs
.get('match')
726 flow_options_str
= json
.dumps(flow_options
, indent
=1)
727 return "success: {2} between {0} and {1} with options: {3}".format(vnf_src_name
, vnf_dst_name
, cmd
, flow_options_str
)
729 def _set_flow_entry_ryu_rest(self
, node
, switch_inport_nr
, switch_outport_nr
, **kwargs
):
730 match
= 'in_port=%s' % switch_inport_nr
732 cookie
= kwargs
.get('cookie')
733 match_input
= kwargs
.get('match')
734 cmd
= kwargs
.get('cmd')
735 path
= kwargs
.get('path')
736 index
= kwargs
.get('pathindex')
738 vlan
= kwargs
.get('vlan')
739 priority
= kwargs
.get('priority', DEFAULT_PRIORITY
)
740 # flag to not set the ovs port vlan tag
741 skip_vlan_tag
= kwargs
.get('skip_vlan_tag')
742 # table id to put this flowentry
743 table_id
= kwargs
.get('table_id')
749 match
= s
.join([match
, match_input
])
752 flow
['dpid'] = int(node
.dpid
, 16)
755 flow
['cookie'] = int(cookie
)
757 flow
['priority'] = int(priority
)
759 flow
['table_id'] = table_id
763 # possible Ryu actions, match fields:
764 # http://ryu.readthedocs.io/en/latest/app/ofctl_rest.html#add-a-flow-entry
765 if cmd
== 'add-flow':
766 prefix
= 'stats/flowentry/add'
768 if index
== 0: # first node
769 # set vlan tag in ovs instance (to isolate E-LANs)
770 if not skip_vlan_tag
:
771 in_port_name
= kwargs
.get('switch_inport_name')
772 self
._set
_vlan
_tag
(node
, in_port_name
, vlan
)
773 # set vlan push action if more than 1 switch in the path
776 action
['type'] = 'PUSH_VLAN' # Push a new VLAN tag if a input frame is non-VLAN-tagged
777 action
['ethertype'] = 33024 # Ethertype 0x8100(=33024): IEEE 802.1Q VLAN-tagged frame
778 flow
['actions'].append(action
)
780 action
['type'] = 'SET_FIELD'
781 action
['field'] = 'vlan_vid'
782 # ryu expects the field to be masked
783 action
['value'] = vlan |
0x1000
784 flow
['actions'].append(action
)
786 elif index
== len(path
) - 1: # last node
787 # set vlan tag in ovs instance (to isolate E-LANs)
788 if not skip_vlan_tag
:
789 out_port_name
= kwargs
.get('switch_outport_name')
790 self
._set
_vlan
_tag
(node
, out_port_name
, vlan
)
791 # set vlan pop action if more than 1 switch in the path
793 match
+= ',dl_vlan=%s' % vlan
795 action
['type'] = 'POP_VLAN'
796 flow
['actions'].append(action
)
799 match
+= ',dl_vlan=%s' % vlan
801 # output action must come last
803 action
['type'] = 'OUTPUT'
804 action
['port'] = switch_outport_nr
805 flow
['actions'].append(action
)
807 elif cmd
== 'del-flows':
808 prefix
= 'stats/flowentry/delete'
811 # TODO: add cookie_mask as argument
812 flow
['cookie_mask'] = int('0xffffffffffffffff', 16) # need full mask to match complete cookie
815 action
['type'] = 'OUTPUT'
816 action
['port'] = switch_outport_nr
817 flow
['actions'].append(action
)
819 flow
['match'] = self
._parse
_match
(match
)
820 self
.ryu_REST(prefix
, data
=flow
)
822 def _set_vlan_tag(self
, node
, switch_port
, tag
):
823 node
.vsctl('set', 'port {0} tag={1}'.format(switch_port
,tag
))
824 LOG
.debug("set vlan in switch: {0} in_port: {1} vlan tag: {2}".format(node
.name
, switch_port
, tag
))
826 def _set_flow_entry_dpctl(self
, node
, switch_inport_nr
, switch_outport_nr
, **kwargs
):
828 match
= 'in_port=%s' % switch_inport_nr
830 cookie
= kwargs
.get('cookie')
831 match_input
= kwargs
.get('match')
832 cmd
= kwargs
.get('cmd')
833 path
= kwargs
.get('path')
834 index
= kwargs
.get('pathindex')
835 vlan
= kwargs
.get('vlan')
839 cookie
= 'cookie=%s' % cookie
840 match
= s
.join([cookie
, match
])
842 match
= s
.join([match
, match_input
])
843 if cmd
== 'add-flow':
844 action
= 'action=%s' % switch_outport_nr
846 if index
== 0: # first node
847 action
= ('action=mod_vlan_vid:%s' % vlan
) + (',output=%s' % switch_outport_nr
)
848 match
= '-O OpenFlow13 ' + match
849 elif index
== len(path
) - 1: # last node
850 match
+= ',dl_vlan=%s' % vlan
851 action
= 'action=strip_vlan,output=%s' % switch_outport_nr
853 match
+= ',dl_vlan=%s' % vlan
854 ofcmd
= s
.join([match
, action
])
855 elif cmd
== 'del-flows':
860 node
.dpctl(cmd
, ofcmd
)
861 LOG
.info("{3} in switch: {0} in_port: {1} out_port: {2}".format(node
.name
, switch_inport_nr
,
862 switch_outport_nr
, cmd
))
864 # start Ryu Openflow controller as Remote Controller for the DCNetwork
865 def startRyu(self
, learning_switch
=True):
866 # start Ryu controller with rest-API
867 python_install_path
= site
.getsitepackages()[0]
868 # ryu default learning switch
869 #ryu_path = python_install_path + '/ryu/app/simple_switch_13.py'
870 #custom learning switch that installs a default NORMAL action in the ovs switches
871 dir_path
= os
.path
.dirname(os
.path
.realpath(__file__
))
872 ryu_path
= dir_path
+ '/son_emu_simple_switch_13.py'
873 ryu_path2
= python_install_path
+ '/ryu/app/ofctl_rest.py'
874 # change the default Openflow controller port to 6653 (official IANA-assigned port number), as used by Mininet
875 # Ryu still uses 6633 as default
876 ryu_option
= '--ofp-tcp-listen-port'
878 ryu_cmd
= 'ryu-manager'
879 FNULL
= open("/tmp/ryu.log", 'w')
881 self
.ryu_process
= Popen([ryu_cmd
, ryu_path
, ryu_path2
, ryu_option
, ryu_of_port
], stdout
=FNULL
, stderr
=FNULL
)
882 LOG
.debug('starting ryu-controller with {0}'.format(ryu_path
))
883 LOG
.debug('starting ryu-controller with {0}'.format(ryu_path2
))
885 # no learning switch, but with rest api
886 self
.ryu_process
= Popen([ryu_cmd
, ryu_path2
, ryu_option
, ryu_of_port
], stdout
=FNULL
, stderr
=FNULL
)
887 LOG
.debug('starting ryu-controller with {0}'.format(ryu_path2
))
892 Stop the Ryu controller that might be started by son-emu.
896 if self
.ryu_process
is not None:
897 self
.ryu_process
.terminate()
898 self
.ryu_process
.kill()
899 # ensure its death ;-)
900 Popen(['pkill', '-f', 'ryu-manager'])
902 def ryu_REST(self
, prefix
, dpid
=None, data
=None):
905 url
= self
.ryu_REST_api
+ '/' + str(prefix
) + '/' + str(dpid
)
907 url
= self
.ryu_REST_api
+ '/' + str(prefix
)
909 req
= self
.RyuSession
.post(url
, json
=data
)
911 req
= self
.RyuSession
.get(url
)
914 # do extra logging if status code is not 200 (OK)
915 if req
.status_code
is not requests
.codes
.ok
:
917 'type {0} encoding: {1} text: {2} headers: {3} history: {4}'.format(req
.headers
['content-type'],
918 req
.encoding
, req
.text
,
919 req
.headers
, req
.history
))
920 LOG
.info('url: {0}'.format(str(url
)))
921 if data
: LOG
.info('POST: {0}'.format(str(data
)))
922 LOG
.info('status: {0} reason: {1}'.format(req
.status_code
, req
.reason
))
925 if 'json' in req
.headers
['content-type']:
929 ret
= req
.text
.rstrip()
933 # need to respect that some match fields must be integers
934 # http://ryu.readthedocs.io/en/latest/app/ofctl_rest.html#description-of-match-and-actions
935 def _parse_match(self
, match
):
936 matches
= match
.split(',')
942 m2
= int(match
[1], 0)
946 dict.update({match
[0]:m2
})
949 def find_connected_dc_interface(self
, vnf_src_name
, vnf_src_interface
=None):
951 if vnf_src_interface
is None:
952 # take first interface by default
953 connected_sw
= self
.DCNetwork_graph
.neighbors(vnf_src_name
)[0]
954 link_dict
= self
.DCNetwork_graph
[vnf_src_name
][connected_sw
]
955 vnf_src_interface
= link_dict
[0]['src_port_id']
957 for connected_sw
in self
.DCNetwork_graph
.neighbors(vnf_src_name
):
958 link_dict
= self
.DCNetwork_graph
[vnf_src_name
][connected_sw
]
959 for link
in link_dict
:
960 if (link_dict
[link
]['src_port_id'] == vnf_src_interface
or
961 link_dict
[link
]['src_port_name'] == vnf_src_interface
): # Fix: we might also get interface names, e.g, from a son-emu-cli call
962 # found the right link and connected switch
963 src_sw
= connected_sw
964 src_sw_inport_nr
= link_dict
[link
]['dst_port_nr']
965 src_sw_inport_name
= link_dict
[link
]['dst_port_name']
966 return src_sw_inport_name