8973bb988220428f40d6d49b56dbe6aab0f5dd4c
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'))
277 LOG
.info('*** **** *** add switch: {0} type: {1}'.format(name
, params
.get('type')))
279 # set the learning switch behavior
280 if 'failMode' in params
:
281 failMode
= params
['failMode']
283 failMode
= self
.failMode
285 s
= Containernet
.addSwitch(self
, name
, protocols
='OpenFlow10,OpenFlow12,OpenFlow13', failMode
=failMode
, **params
)
289 def getAllContainers(self
):
291 Returns a list with all containers within all data centers.
294 for dc
in self
.dcs
.itervalues():
295 all_containers
+= dc
.listCompute()
296 return all_containers
300 for dc
in self
.dcs
.itervalues():
302 Containernet
.start(self
)
306 # stop the monitor agent
307 if self
.monitor_agent
is not None:
308 self
.monitor_agent
.stop()
311 Containernet
.stop(self
)
313 # stop Ryu controller
320 def setLAN(self
, vnf_list
):
322 setup an E-LAN network by assigning the same VLAN tag to each DC interface of the VNFs in the E-LAN
324 :param vnf_list: names of the VNFs in this E-LAN [{name:,interface:},...]
329 src_sw_inport_name
= None
331 # get a vlan tag for this E-LAN
332 vlan
= self
.vlans
.pop()
335 vnf_src_name
= vnf
['name']
336 vnf_src_interface
= vnf
['interface']
338 # check if port is specified (vnf:port)
339 if vnf_src_interface
is None:
340 # take first interface by default
341 connected_sw
= self
.DCNetwork_graph
.neighbors(vnf_src_name
)[0]
342 link_dict
= self
.DCNetwork_graph
[vnf_src_name
][connected_sw
]
343 vnf_src_interface
= link_dict
[0]['src_port_id']
345 for connected_sw
in self
.DCNetwork_graph
.neighbors(vnf_src_name
):
346 link_dict
= self
.DCNetwork_graph
[vnf_src_name
][connected_sw
]
347 for link
in link_dict
:
348 if (link_dict
[link
]['src_port_id'] == vnf_src_interface
or
349 link_dict
[link
]['src_port_name'] == vnf_src_interface
): # Fix: we might also get interface names, e.g, from a son-emu-cli call
350 # found the right link and connected switch
351 src_sw
= connected_sw
352 src_sw_inport_nr
= link_dict
[link
]['dst_port_nr']
353 src_sw_inport_name
= link_dict
[link
]['dst_port_name']
356 # set the tag on the dc switch interface
357 LOG
.debug('set E-LAN: vnf name: {0} interface: {1} tag: {2}'.format(vnf_src_name
, vnf_src_interface
,vlan
))
358 switch_node
= self
.getNodeByName(src_sw
)
359 self
._set
_vlan
_tag
(switch_node
, src_sw_inport_name
, vlan
)
361 def _addMonitorFlow(self
, vnf_src_name
, vnf_dst_name
, vnf_src_interface
=None, vnf_dst_interface
=None,
364 Add a monitoring flow entry that adds a special flowentry/counter at the begin or end of a chain.
365 So this monitoring flowrule exists on top of a previously defined chain rule and uses the same vlan tag/routing.
368 :param vnf_src_interface:
369 :param vnf_dst_interface:
370 :param tag: vlan tag to be used for this chain (same tag as existing chain)
371 :param monitor_placement: 'tx' or 'rx' indicating to place the extra flowentry resp. at the beginning or end of the chain
377 src_sw_inport_name
= None
379 dst_sw_outport_nr
= 0
380 dst_sw_outport_name
= None
382 LOG
.debug("call AddMonitorFlow vnf_src_name=%r, vnf_src_interface=%r, vnf_dst_name=%r, vnf_dst_interface=%r",
383 vnf_src_name
, vnf_src_interface
, vnf_dst_name
, vnf_dst_interface
)
385 #check if port is specified (vnf:port)
386 if vnf_src_interface
is None:
387 # take first interface by default
388 connected_sw
= self
.DCNetwork_graph
.neighbors(vnf_src_name
)[0]
389 link_dict
= self
.DCNetwork_graph
[vnf_src_name
][connected_sw
]
390 vnf_src_interface
= link_dict
[0]['src_port_id']
392 for connected_sw
in self
.DCNetwork_graph
.neighbors(vnf_src_name
):
393 link_dict
= self
.DCNetwork_graph
[vnf_src_name
][connected_sw
]
394 for link
in link_dict
:
395 if (link_dict
[link
]['src_port_id'] == vnf_src_interface
or
396 link_dict
[link
]['src_port_name'] == vnf_src_interface
): # Fix: we might also get interface names, e.g, from a son-emu-cli call
397 # found the right link and connected switch
398 src_sw
= connected_sw
399 src_sw_inport_nr
= link_dict
[link
]['dst_port_nr']
400 src_sw_inport_name
= link_dict
[link
]['dst_port_name']
403 if vnf_dst_interface
is None:
404 # take first interface by default
405 connected_sw
= self
.DCNetwork_graph
.neighbors(vnf_dst_name
)[0]
406 link_dict
= self
.DCNetwork_graph
[connected_sw
][vnf_dst_name
]
407 vnf_dst_interface
= link_dict
[0]['dst_port_id']
409 vnf_dst_name
= vnf_dst_name
.split(':')[0]
410 for connected_sw
in self
.DCNetwork_graph
.neighbors(vnf_dst_name
):
411 link_dict
= self
.DCNetwork_graph
[connected_sw
][vnf_dst_name
]
412 for link
in link_dict
:
413 if link_dict
[link
]['dst_port_id'] == vnf_dst_interface
or \
414 link_dict
[link
]['dst_port_name'] == vnf_dst_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 dst_sw
= connected_sw
417 dst_sw_outport_nr
= link_dict
[link
]['src_port_nr']
418 dst_sw_outport_name
= link_dict
[link
]['src_port_name']
422 LOG
.exception('tag not valid: {0}'.format(tag
))
426 # returns the first found shortest path
427 # if all shortest paths are wanted, use: all_shortest_paths
428 path
= nx
.shortest_path(self
.DCNetwork_graph
, src_sw
, dst_sw
, weight
=kwargs
.get('weight'))
430 LOG
.exception("No path could be found between {0} and {1} using src_sw={2} and dst_sw={3}".format(
431 vnf_src_name
, vnf_dst_name
, src_sw
, dst_sw
))
432 LOG
.debug("Graph nodes: %r" % self
.DCNetwork_graph
.nodes())
433 LOG
.debug("Graph edges: %r" % self
.DCNetwork_graph
.edges())
434 for e
, v
in self
.DCNetwork_graph
.edges():
435 LOG
.debug("%r" % self
.DCNetwork_graph
[e
][v
])
436 return "No path could be found between {0} and {1}".format(vnf_src_name
, vnf_dst_name
)
438 LOG
.info("Path between {0} and {1}: {2}".format(vnf_src_name
, vnf_dst_name
, path
))
441 switch_inport_nr
= src_sw_inport_nr
443 cmd
= kwargs
.get('cmd')
445 #iterate through the path to install the flow-entries
446 for i
in range(0,len(path
)):
447 current_node
= self
.getNodeByName(current_hop
)
449 if path
.index(current_hop
) < len(path
)-1:
450 next_hop
= path
[path
.index(current_hop
)+1]
453 next_hop
= vnf_dst_name
455 next_node
= self
.getNodeByName(next_hop
)
457 if next_hop
== vnf_dst_name
:
458 switch_outport_nr
= dst_sw_outport_nr
459 LOG
.info("end node reached: {0}".format(vnf_dst_name
))
460 elif not isinstance( next_node
, OVSSwitch
):
461 LOG
.info("Next node: {0} is not a switch".format(next_hop
))
462 return "Next node: {0} is not a switch".format(next_hop
)
464 # take first link between switches by default
466 switch_outport_nr
= self
.DCNetwork_graph
[current_hop
][next_hop
][index_edge_out
]['src_port_nr']
469 # set of entry via ovs-ofctl
470 if isinstance( current_node
, OVSSwitch
):
472 kwargs
['path'] = path
473 kwargs
['current_hop'] = current_hop
474 kwargs
['switch_inport_name'] = src_sw_inport_name
475 kwargs
['switch_outport_name'] = dst_sw_outport_name
476 kwargs
['skip_vlan_tag'] = True
477 kwargs
['pathindex'] = i
479 monitor_placement
= kwargs
.get('monitor_placement').strip()
480 # put monitor flow at the dst switch
482 if monitor_placement
== 'tx' and path
.index(current_hop
) == 0: # first node:
484 # put monitoring flow at the src switch
485 elif monitor_placement
== 'rx' and path
.index(current_hop
) == len(path
) - 1: # last node:
487 elif monitor_placement
not in ['rx', 'tx']:
488 LOG
.exception('invalid monitor command: {0}'.format(monitor_placement
))
491 if self
.controller
== RemoteController
and insert_flow
:
492 ## set flow entry via ryu rest api
493 self
._set
_flow
_entry
_ryu
_rest
(current_node
, switch_inport_nr
, switch_outport_nr
, **kwargs
)
496 ## set flow entry via ovs-ofctl
497 self
._set
_flow
_entry
_dpctl
(current_node
, switch_inport_nr
, switch_outport_nr
, **kwargs
)
500 # take first link between switches by default
501 if isinstance( next_node
, OVSSwitch
):
502 switch_inport_nr
= self
.DCNetwork_graph
[current_hop
][next_hop
][0]['dst_port_nr']
503 current_hop
= next_hop
505 return "path {2} between {0} and {1}".format(vnf_src_name
, vnf_dst_name
, cmd
)
508 def setChain(self
, vnf_src_name
, vnf_dst_name
, vnf_src_interface
=None, vnf_dst_interface
=None, **kwargs
):
510 Chain 2 vnf interfaces together by installing the flowrules in the switches along their path.
511 Currently the path is found using the default networkx shortest path function.
512 Each chain gets a unique vlan id , so different chains wil not interfere.
514 :param vnf_src_name: vnf name (string)
515 :param vnf_dst_name: vnf name (string)
516 :param vnf_src_interface: source interface name (string)
517 :param vnf_dst_interface: destination interface name (string)
518 :param cmd: 'add-flow' (default) to add a chain, 'del-flows' to remove a chain
519 :param cookie: cookie for the installed flowrules (can be used later as identifier for a set of installed chains)
520 :param match: custom match entry to be added to the flowrules (default: only in_port and vlan tag)
521 :param priority: custom flowrule priority
522 :param monitor: boolean to indicate whether this chain is a monitoring chain
523 :param tag: vlan tag to be used for this chain (pre-defined or new one if none is specified)
524 :param skip_vlan_tag: boolean to indicate if a vlan tag should be appointed to this flow or not
525 :param path: custom path between the two VNFs (list of switches)
526 :return: output log string
529 # special procedure for monitoring flows
530 if kwargs
.get('monitor'):
532 # check if chain already exists
533 found_chains
= [chain_dict
for chain_dict
in self
.installed_chains
if
534 (chain_dict
['vnf_src_name'] == vnf_src_name
and chain_dict
['vnf_src_interface'] == vnf_src_interface
535 and chain_dict
['vnf_dst_name'] == vnf_dst_name
and chain_dict
['vnf_dst_interface'] == vnf_dst_interface
)]
537 if len(found_chains
) > 0:
538 # this chain exists, so need an extra monitoring flow
539 # assume only 1 chain per vnf/interface pair
540 LOG
.debug('*** installing monitoring chain on top of pre-defined chain from {0}:{1} -> {2}:{3}'.
541 format(vnf_src_name
, vnf_src_interface
, vnf_dst_name
, vnf_dst_interface
))
542 tag
= found_chains
[0]['tag']
543 ret
= self
._addMonitorFlow
(vnf_src_name
, vnf_dst_name
, vnf_src_interface
, vnf_dst_interface
,
544 tag
=tag
, table_id
=0, **kwargs
)
547 # no chain existing (or E-LAN) -> install normal chain
548 LOG
.warning('*** installing monitoring chain without pre-defined NSD chain from {0}:{1} -> {2}:{3}'.
549 format(vnf_src_name
, vnf_src_interface
, vnf_dst_name
, vnf_dst_interface
))
553 cmd
= kwargs
.get('cmd')
554 if cmd
== 'add-flow' or cmd
== 'del-flows':
555 ret
= self
._chainAddFlow
(vnf_src_name
, vnf_dst_name
, vnf_src_interface
, vnf_dst_interface
, **kwargs
)
556 if kwargs
.get('bidirectional'):
557 if kwargs
.get('path') is not None:
558 kwargs
['path'] = list(reversed(kwargs
.get('path')))
559 ret
= ret
+'\n' + self
._chainAddFlow
(vnf_dst_name
, vnf_src_name
, vnf_dst_interface
, vnf_src_interface
, **kwargs
)
562 ret
= "Command unknown"
567 def _chainAddFlow(self
, vnf_src_name
, vnf_dst_name
, vnf_src_interface
=None, vnf_dst_interface
=None, **kwargs
):
571 src_sw_inport_name
= None
573 dst_sw_outport_nr
= 0
574 dst_sw_outport_name
= None
576 LOG
.debug("call chainAddFlow vnf_src_name=%r, vnf_src_interface=%r, vnf_dst_name=%r, vnf_dst_interface=%r",
577 vnf_src_name
, vnf_src_interface
, vnf_dst_name
, vnf_dst_interface
)
579 #check if port is specified (vnf:port)
580 if vnf_src_interface
is None:
581 # take first interface by default
582 connected_sw
= self
.DCNetwork_graph
.neighbors(vnf_src_name
)[0]
583 link_dict
= self
.DCNetwork_graph
[vnf_src_name
][connected_sw
]
584 vnf_src_interface
= link_dict
[0]['src_port_id']
586 for connected_sw
in self
.DCNetwork_graph
.neighbors(vnf_src_name
):
587 link_dict
= self
.DCNetwork_graph
[vnf_src_name
][connected_sw
]
588 for link
in link_dict
:
589 if (link_dict
[link
]['src_port_id'] == vnf_src_interface
or
590 link_dict
[link
]['src_port_name'] == vnf_src_interface
): # Fix: we might also get interface names, e.g, from a son-emu-cli call
591 # found the right link and connected switch
592 src_sw
= connected_sw
593 src_sw_inport_nr
= link_dict
[link
]['dst_port_nr']
594 src_sw_inport_name
= link_dict
[link
]['dst_port_name']
597 if vnf_dst_interface
is None:
598 # take first interface by default
599 connected_sw
= self
.DCNetwork_graph
.neighbors(vnf_dst_name
)[0]
600 link_dict
= self
.DCNetwork_graph
[connected_sw
][vnf_dst_name
]
601 vnf_dst_interface
= link_dict
[0]['dst_port_id']
603 vnf_dst_name
= vnf_dst_name
.split(':')[0]
604 for connected_sw
in self
.DCNetwork_graph
.neighbors(vnf_dst_name
):
605 link_dict
= self
.DCNetwork_graph
[connected_sw
][vnf_dst_name
]
606 for link
in link_dict
:
607 if link_dict
[link
]['dst_port_id'] == vnf_dst_interface
or \
608 link_dict
[link
]['dst_port_name'] == vnf_dst_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 dst_sw
= connected_sw
611 dst_sw_outport_nr
= link_dict
[link
]['src_port_nr']
612 dst_sw_outport_name
= link_dict
[link
]['src_port_name']
615 path
= kwargs
.get('path')
619 # returns the first found shortest path
620 # if all shortest paths are wanted, use: all_shortest_paths
621 path
= nx
.shortest_path(self
.DCNetwork_graph
, src_sw
, dst_sw
, weight
=kwargs
.get('weight'))
623 LOG
.exception("No path could be found between {0} and {1} using src_sw={2} and dst_sw={3}".format(
624 vnf_src_name
, vnf_dst_name
, src_sw
, dst_sw
))
625 LOG
.debug("Graph nodes: %r" % self
.DCNetwork_graph
.nodes())
626 LOG
.debug("Graph edges: %r" % self
.DCNetwork_graph
.edges())
627 for e
, v
in self
.DCNetwork_graph
.edges():
628 LOG
.debug("%r" % self
.DCNetwork_graph
[e
][v
])
629 return "No path could be found between {0} and {1}".format(vnf_src_name
, vnf_dst_name
)
631 LOG
.info("Path between {0} and {1}: {2}".format(vnf_src_name
, vnf_dst_name
, path
))
634 switch_inport_nr
= src_sw_inport_nr
637 cmd
= kwargs
.get('cmd')
639 if cmd
== 'add-flow':
640 if kwargs
.get('tag'):
641 # use pre-defined tag
642 vlan
= kwargs
.get('tag')
644 vlan
= self
.vlans
.pop()
646 # store the used vlan tag to identify this chain
647 if not kwargs
.get('monitor'):
649 chain_dict
['vnf_src_name'] = vnf_src_name
650 chain_dict
['vnf_dst_name'] = vnf_dst_name
651 chain_dict
['vnf_src_interface'] = vnf_src_interface
652 chain_dict
['vnf_dst_interface'] = vnf_dst_interface
653 chain_dict
['tag'] = vlan
654 self
.installed_chains
.append(chain_dict
)
656 #iterate through the path to install the flow-entries
657 for i
in range(0,len(path
)):
658 current_node
= self
.getNodeByName(current_hop
)
660 if i
< len(path
) - 1:
661 next_hop
= path
[i
+ 1]
663 # last switch reached
664 next_hop
= vnf_dst_name
666 next_node
= self
.getNodeByName(next_hop
)
668 if next_hop
== vnf_dst_name
:
669 switch_outport_nr
= dst_sw_outport_nr
670 LOG
.info("end node reached: {0}".format(vnf_dst_name
))
671 elif not isinstance( next_node
, OVSSwitch
):
672 LOG
.info("Next node: {0} is not a switch".format(next_hop
))
673 return "Next node: {0} is not a switch".format(next_hop
)
675 # take first link between switches by default
677 switch_outport_nr
= self
.DCNetwork_graph
[current_hop
][next_hop
][index_edge_out
]['src_port_nr']
681 if isinstance( current_node
, OVSSwitch
):
682 kwargs
['vlan'] = vlan
683 kwargs
['path'] = path
684 kwargs
['current_hop'] = current_hop
685 kwargs
['switch_inport_name'] = src_sw_inport_name
686 kwargs
['switch_outport_name'] = dst_sw_outport_name
687 kwargs
['pathindex'] = i
689 if self
.controller
== RemoteController
:
690 ## set flow entry via ryu rest api
691 self
._set
_flow
_entry
_ryu
_rest
(current_node
, switch_inport_nr
, switch_outport_nr
, **kwargs
)
693 ## set flow entry via ovs-ofctl
694 self
._set
_flow
_entry
_dpctl
(current_node
, switch_inport_nr
, switch_outport_nr
, **kwargs
)
696 # take first link between switches by default
697 if isinstance( next_node
, OVSSwitch
):
698 switch_inport_nr
= self
.DCNetwork_graph
[current_hop
][next_hop
][0]['dst_port_nr']
699 current_hop
= next_hop
701 return "path {2} between {0} and {1}".format(vnf_src_name
, vnf_dst_name
, cmd
)
703 def _set_flow_entry_ryu_rest(self
, node
, switch_inport_nr
, switch_outport_nr
, **kwargs
):
704 match
= 'in_port=%s' % switch_inport_nr
706 cookie
= kwargs
.get('cookie')
707 match_input
= kwargs
.get('match')
708 cmd
= kwargs
.get('cmd')
709 path
= kwargs
.get('path')
710 index
= kwargs
.get('pathindex')
712 vlan
= kwargs
.get('vlan')
713 priority
= kwargs
.get('priority')
714 # flag to not set the ovs port vlan tag
715 skip_vlan_tag
= kwargs
.get('skip_vlan_tag')
716 # table id to put this flowentry
717 table_id
= kwargs
.get('table_id')
723 match
= s
.join([match
, match_input
])
726 flow
['dpid'] = int(node
.dpid
, 16)
729 flow
['cookie'] = int(cookie
)
731 flow
['priority'] = int(priority
)
733 flow
['table_id'] = table_id
737 # possible Ryu actions, match fields:
738 # http://ryu.readthedocs.io/en/latest/app/ofctl_rest.html#add-a-flow-entry
739 if cmd
== 'add-flow':
740 prefix
= 'stats/flowentry/add'
742 if index
== 0: # first node
743 # set vlan tag in ovs instance (to isolate E-LANs)
744 if not skip_vlan_tag
:
745 in_port_name
= kwargs
.get('switch_inport_name')
746 self
._set
_vlan
_tag
(node
, in_port_name
, vlan
)
747 # set vlan push action if more than 1 switch in the path
750 action
['type'] = 'PUSH_VLAN' # Push a new VLAN tag if a input frame is non-VLAN-tagged
751 action
['ethertype'] = 33024 # Ethertype 0x8100(=33024): IEEE 802.1Q VLAN-tagged frame
752 flow
['actions'].append(action
)
754 action
['type'] = 'SET_FIELD'
755 action
['field'] = 'vlan_vid'
756 # ryu expects the field to be masked
757 action
['value'] = vlan |
0x1000
758 flow
['actions'].append(action
)
760 elif index
== len(path
) - 1: # last node
761 # set vlan tag in ovs instance (to isolate E-LANs)
762 if not skip_vlan_tag
:
763 out_port_name
= kwargs
.get('switch_outport_name')
764 self
._set
_vlan
_tag
(node
, out_port_name
, vlan
)
765 # set vlan pop action if more than 1 switch in the path
767 match
+= ',dl_vlan=%s' % vlan
769 action
['type'] = 'POP_VLAN'
770 flow
['actions'].append(action
)
773 match
+= ',dl_vlan=%s' % vlan
775 # output action must come last
777 action
['type'] = 'OUTPUT'
778 action
['port'] = switch_outport_nr
779 flow
['actions'].append(action
)
781 elif cmd
== 'del-flows':
782 prefix
= 'stats/flowentry/delete'
785 # TODO: add cookie_mask as argument
786 flow
['cookie_mask'] = int('0xffffffffffffffff', 16) # need full mask to match complete cookie
789 action
['type'] = 'OUTPUT'
790 action
['port'] = switch_outport_nr
791 flow
['actions'].append(action
)
793 flow
['match'] = self
._parse
_match
(match
)
794 self
.ryu_REST(prefix
, data
=flow
)
796 def _set_vlan_tag(self
, node
, switch_port
, tag
):
797 node
.vsctl('set', 'port {0} tag={1}'.format(switch_port
,tag
))
798 LOG
.debug("set vlan in switch: {0} in_port: {1} vlan tag: {2}".format(node
.name
, switch_port
, tag
))
800 def _set_flow_entry_dpctl(self
, node
, switch_inport_nr
, switch_outport_nr
, **kwargs
):
802 match
= 'in_port=%s' % switch_inport_nr
804 cookie
= kwargs
.get('cookie')
805 match_input
= kwargs
.get('match')
806 cmd
= kwargs
.get('cmd')
807 path
= kwargs
.get('path')
808 index
= kwargs
.get('pathindex')
809 vlan
= kwargs
.get('vlan')
813 cookie
= 'cookie=%s' % cookie
814 match
= s
.join([cookie
, match
])
816 match
= s
.join([match
, match_input
])
817 if cmd
== 'add-flow':
818 action
= 'action=%s' % switch_outport_nr
820 if index
== 0: # first node
821 action
= ('action=mod_vlan_vid:%s' % vlan
) + (',output=%s' % switch_outport_nr
)
822 match
= '-O OpenFlow13 ' + match
823 elif index
== len(path
) - 1: # last node
824 match
+= ',dl_vlan=%s' % vlan
825 action
= 'action=strip_vlan,output=%s' % switch_outport_nr
827 match
+= ',dl_vlan=%s' % vlan
828 ofcmd
= s
.join([match
, action
])
829 elif cmd
== 'del-flows':
834 node
.dpctl(cmd
, ofcmd
)
835 LOG
.info("{3} in switch: {0} in_port: {1} out_port: {2}".format(node
.name
, switch_inport_nr
,
836 switch_outport_nr
, cmd
))
838 # start Ryu Openflow controller as Remote Controller for the DCNetwork
839 def startRyu(self
, learning_switch
=True):
840 # start Ryu controller with rest-API
841 python_install_path
= site
.getsitepackages()[0]
842 # ryu default learning switch
843 #ryu_path = python_install_path + '/ryu/app/simple_switch_13.py'
844 #custom learning switch that installs a default NORMAL action in the ovs switches
845 dir_path
= os
.path
.dirname(os
.path
.realpath(__file__
))
846 ryu_path
= dir_path
+ '/son_emu_simple_switch_13.py'
847 ryu_path2
= python_install_path
+ '/ryu/app/ofctl_rest.py'
848 # change the default Openflow controller port to 6653 (official IANA-assigned port number), as used by Mininet
849 # Ryu still uses 6633 as default
850 ryu_option
= '--ofp-tcp-listen-port'
852 ryu_cmd
= 'ryu-manager'
853 FNULL
= open("/tmp/ryu.log", 'w')
855 self
.ryu_process
= Popen([ryu_cmd
, ryu_path
, ryu_path2
, ryu_option
, ryu_of_port
], stdout
=FNULL
, stderr
=FNULL
)
856 LOG
.debug('starting ryu-controller with {0}'.format(ryu_path
))
857 LOG
.debug('starting ryu-controller with {0}'.format(ryu_path2
))
859 # no learning switch, but with rest api
860 self
.ryu_process
= Popen([ryu_cmd
, ryu_path2
, ryu_option
, ryu_of_port
], stdout
=FNULL
, stderr
=FNULL
)
861 LOG
.debug('starting ryu-controller with {0}'.format(ryu_path2
))
866 Stop the Ryu controller that might be started by son-emu.
870 if self
.ryu_process
is not None:
871 self
.ryu_process
.terminate()
872 self
.ryu_process
.kill()
873 # ensure its death ;-)
874 Popen(['pkill', '-f', 'ryu-manager'])
876 def ryu_REST(self
, prefix
, dpid
=None, data
=None):
879 url
= self
.ryu_REST_api
+ '/' + str(prefix
) + '/' + str(dpid
)
881 url
= self
.ryu_REST_api
+ '/' + str(prefix
)
883 req
= self
.RyuSession
.post(url
, json
=data
)
885 req
= self
.RyuSession
.get(url
)
888 # do extra logging if status code is not 200 (OK)
889 if req
.status_code
is not requests
.codes
.ok
:
891 'type {0} encoding: {1} text: {2} headers: {3} history: {4}'.format(req
.headers
['content-type'],
892 req
.encoding
, req
.text
,
893 req
.headers
, req
.history
))
894 LOG
.info('url: {0}'.format(str(url
)))
895 if data
: LOG
.info('POST: {0}'.format(str(data
)))
896 LOG
.info('status: {0} reason: {1}'.format(req
.status_code
, req
.reason
))
899 if 'json' in req
.headers
['content-type']:
903 ret
= req
.text
.rstrip()
907 # need to respect that some match fields must be integers
908 # http://ryu.readthedocs.io/en/latest/app/ofctl_rest.html#description-of-match-and-actions
909 def _parse_match(self
, match
):
910 matches
= match
.split(',')
916 m2
= int(match
[1], 0)
920 dict.update({match
[0]:m2
})
923 def find_connected_dc_interface(self
, vnf_src_name
, vnf_src_interface
=None):
925 if vnf_src_interface
is None:
926 # take first interface by default
927 connected_sw
= self
.DCNetwork_graph
.neighbors(vnf_src_name
)[0]
928 link_dict
= self
.DCNetwork_graph
[vnf_src_name
][connected_sw
]
929 vnf_src_interface
= link_dict
[0]['src_port_id']
931 for connected_sw
in self
.DCNetwork_graph
.neighbors(vnf_src_name
):
932 link_dict
= self
.DCNetwork_graph
[vnf_src_name
][connected_sw
]
933 for link
in link_dict
:
934 if (link_dict
[link
]['src_port_id'] == vnf_src_interface
or
935 link_dict
[link
]['src_port_name'] == vnf_src_interface
): # Fix: we might also get interface names, e.g, from a son-emu-cli call
936 # found the right link and connected switch
937 src_sw
= connected_sw
938 src_sw_inport_nr
= link_dict
[link
]['dst_port_nr']
939 src_sw_inport_name
= link_dict
[link
]['dst_port_name']
940 return src_sw_inport_name