2 Copyright (c) 2015 SONATA-NFV and Paderborn University
5 Licensed under the Apache License, Version 2.0 (the "License");
6 you may not use this file except in compliance with the License.
7 You may obtain a copy of the License at
9 http://www.apache.org/licenses/LICENSE-2.0
11 Unless required by applicable law or agreed to in writing, software
12 distributed under the License is distributed on an "AS IS" BASIS,
13 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 See the License for the specific language governing permissions and
15 limitations under the License.
17 Neither the name of the SONATA-NFV [, ANY ADDITIONAL AFFILIATION]
18 nor the names of its contributors may be used to endorse or promote
19 products derived from this software without specific prior written
22 This work has been performed in the framework of the SONATA project,
23 funded by the European Commission under Grant number 671517 through
24 the Horizon 2020 and 5G-PPP programmes. The authors would like to
25 acknowledge the contributions of their colleagues of the SONATA
26 partner consortium (www.sonata-nfv.eu).
32 from subprocess
import Popen
37 from mininet
.net
import Containernet
38 from mininet
.node
import Controller
, DefaultController
, OVSSwitch
, OVSKernelSwitch
, Docker
, RemoteController
39 from mininet
.cli
import CLI
40 from mininet
.link
import TCLink
41 from mininet
.clean
import cleanup
43 from emuvim
.dcemulator
.monitoring
import DCNetworkMonitor
44 from emuvim
.dcemulator
.node
import Datacenter
, EmulatorCompute
45 from emuvim
.dcemulator
.resourcemodel
import ResourceModelRegistrar
47 LOG
= logging
.getLogger("dcemulator.net")
48 LOG
.setLevel(logging
.DEBUG
)
50 # default CPU period used for cpu percentage-based cfs values (microseconds)
53 class DCNetwork(Containernet
):
55 Wraps the original Mininet/Containernet class and provides
56 methods to add data centers, switches, etc.
58 This class is used by topology definition scripts.
61 def __init__(self
, controller
=RemoteController
, monitor
=False,
62 enable_learning
=False, # learning switch behavior of the default ovs switches icw Ryu controller can be turned off/on, needed for E-LAN functionality
63 dc_emulation_max_cpu
=1.0, # fraction of overall CPU time for emulation
64 dc_emulation_max_mem
=512, # emulation max mem in MB
67 Create an extended version of a Containernet network
68 :param dc_emulation_max_cpu: max. CPU time used by containers in data centers
69 :param kwargs: path through for Mininet parameters
74 self
.ryu_process
= None
75 #list of deployed nsds.E_Lines and E_LANs (uploaded from the dummy gatekeeper)
76 self
.deployed_nsds
= []
77 self
.deployed_elines
= []
78 self
.deployed_elans
= []
79 self
.installed_chains
= []
82 # always cleanup environment before we start the emulator
86 # call original Docker.__init__ and setup default controller
87 Containernet
.__init
__(
88 self
, switch
=OVSKernelSwitch
, controller
=controller
, **kwargs
)
90 # default switch configuration
91 enable_ryu_learning
= False
93 self
.failMode
= 'standalone'
94 enable_ryu_learning
= True
96 self
.failMode
= 'secure'
99 if controller
== RemoteController
:
100 # start Ryu controller
101 self
.startRyu(learning_switch
=enable_ryu_learning
)
103 # add the specified controller
104 self
.addController('c0', controller
=controller
)
106 # graph of the complete DC network
107 self
.DCNetwork_graph
= nx
.MultiDiGraph()
109 # initialize pool of vlan tags to setup the SDN paths
110 self
.vlans
= range(4096)[::-1]
112 # link to Ryu REST_API
115 self
.ryu_REST_api
= 'http://{0}:{1}'.format(ryu_ip
, ryu_port
)
116 self
.RyuSession
= requests
.Session()
120 self
.monitor_agent
= DCNetworkMonitor(self
)
122 self
.monitor_agent
= None
124 # initialize resource model registrar
125 self
.rm_registrar
= ResourceModelRegistrar(
126 dc_emulation_max_cpu
, dc_emulation_max_mem
)
127 self
.cpu_period
= CPU_PERIOD
129 def addDatacenter(self
, label
, metadata
={}, resource_log_path
=None):
131 Create and add a logical cloud data center to the network.
133 if label
in self
.dcs
:
134 raise Exception("Data center label already exists: %s" % label
)
135 dc
= Datacenter(label
, metadata
=metadata
, resource_log_path
=resource_log_path
)
136 dc
.net
= self
# set reference to network
138 dc
.create() # finally create the data center in our Mininet instance
139 LOG
.info("added data center: %s" % label
)
142 def addLink(self
, node1
, node2
, **params
):
144 Able to handle Datacenter objects as link
147 assert node1
is not None
148 assert node2
is not None
150 # ensure type of node1
151 if isinstance( node1
, basestring
):
152 if node1
in self
.dcs
:
153 node1
= self
.dcs
[node1
].switch
154 if isinstance( node1
, Datacenter
):
156 # ensure type of node2
157 if isinstance( node2
, basestring
):
158 if node2
in self
.dcs
:
159 node2
= self
.dcs
[node2
].switch
160 if isinstance( node2
, Datacenter
):
162 # try to give containers a default IP
163 if isinstance( node1
, Docker
):
164 if "params1" not in params
:
165 params
["params1"] = {}
166 if "ip" not in params
["params1"]:
167 params
["params1"]["ip"] = self
.getNextIp()
168 if isinstance( node2
, Docker
):
169 if "params2" not in params
:
170 params
["params2"] = {}
171 if "ip" not in params
["params2"]:
172 params
["params2"]["ip"] = self
.getNextIp()
173 # ensure that we allow TCLinks between data centers
174 # TODO this is not optimal, we use cls=Link for containers and TCLink for data centers
175 # see Containernet issue: https://github.com/mpeuster/containernet/issues/3
176 if "cls" not in params
:
177 params
["cls"] = TCLink
179 link
= Containernet
.addLink(self
, node1
, node2
, **params
)
181 # try to give container interfaces a default id
182 node1_port_id
= node1
.ports
[link
.intf1
]
183 if isinstance(node1
, Docker
):
184 if "id" in params
["params1"]:
185 node1_port_id
= params
["params1"]["id"]
186 node1_port_name
= link
.intf1
.name
188 node2_port_id
= node2
.ports
[link
.intf2
]
189 if isinstance(node2
, Docker
):
190 if "id" in params
["params2"]:
191 node2_port_id
= params
["params2"]["id"]
192 node2_port_name
= link
.intf2
.name
195 # add edge and assigned port number to graph in both directions between node1 and node2
196 # port_id: id given in descriptor (if available, otherwise same as port)
197 # port: portnumber assigned by Containernet
200 # possible weight metrics allowed by TClink class:
201 weight_metrics
= ['bw', 'delay', 'jitter', 'loss']
202 edge_attributes
= [p
for p
in params
if p
in weight_metrics
]
203 for attr
in edge_attributes
:
204 # if delay: strip ms (need number as weight in graph)
205 match
= re
.search('([0-9]*\.?[0-9]+)', str(params
[attr
]))
207 attr_number
= match
.group(1)
210 attr_dict
[attr
] = attr_number
213 attr_dict2
= {'src_port_id': node1_port_id
, 'src_port_nr': node1
.ports
[link
.intf1
],
214 'src_port_name': node1_port_name
,
215 'dst_port_id': node2_port_id
, 'dst_port_nr': node2
.ports
[link
.intf2
],
216 'dst_port_name': node2_port_name
}
217 attr_dict2
.update(attr_dict
)
218 self
.DCNetwork_graph
.add_edge(node1
.name
, node2
.name
, attr_dict
=attr_dict2
)
220 attr_dict2
= {'src_port_id': node2_port_id
, 'src_port_nr': node2
.ports
[link
.intf2
],
221 'src_port_name': node2_port_name
,
222 'dst_port_id': node1_port_id
, 'dst_port_nr': node1
.ports
[link
.intf1
],
223 'dst_port_name': node1_port_name
}
224 attr_dict2
.update(attr_dict
)
225 self
.DCNetwork_graph
.add_edge(node2
.name
, node1
.name
, attr_dict
=attr_dict2
)
227 LOG
.debug("addLink: n1={0} intf1={1} -- n2={2} intf2={3}".format(
228 str(node1
),node1_port_name
, str(node2
), node2_port_name
))
232 def addDocker( self
, label
, **params
):
234 Wrapper for addDocker method to use custom container class.
236 self
.DCNetwork_graph
.add_node(label
)
237 return Containernet
.addDocker(self
, label
, cls
=EmulatorCompute
, **params
)
239 def removeDocker( self
, label
, **params
):
241 Wrapper for removeDocker method to update graph.
243 self
.DCNetwork_graph
.remove_node(label
)
244 return Containernet
.removeDocker(self
, label
, **params
)
246 def addSwitch( self
, name
, add_to_graph
=True, **params
):
248 Wrapper for addSwitch method to store switch also in graph.
251 # add this switch to the global topology overview
253 self
.DCNetwork_graph
.add_node(name
)
255 # set the learning switch behavior
256 if 'failMode' in params
:
257 failMode
= params
['failMode']
259 failMode
= self
.failMode
261 s
= Containernet
.addSwitch(self
, name
, protocols
='OpenFlow10,OpenFlow12,OpenFlow13', failMode
=failMode
, **params
)
263 # set flow entry that enables learning switch behavior (needed to enable E-LAN functionality)
264 #LOG.info('failmode {0}'.format(failMode))
265 #if failMode == 'standalone' :
266 # LOG.info('add NORMAL')
267 # s.dpctl('add-flow', 'actions=NORMAL')
271 def getAllContainers(self
):
273 Returns a list with all containers within all data centers.
276 for dc
in self
.dcs
.itervalues():
277 all_containers
+= dc
.listCompute()
278 return all_containers
282 for dc
in self
.dcs
.itervalues():
284 Containernet
.start(self
)
288 # stop the monitor agent
289 if self
.monitor_agent
is not None:
290 self
.monitor_agent
.stop()
293 Containernet
.stop(self
)
295 # stop Ryu controller
302 def setLAN(self
, vnf_list
):
304 setup an E-LAN network by assigning the same VLAN tag to each DC interface of the VNFs in the E-LAN
306 :param vnf_list: names of the VNFs in this E-LAN [{name:,interface:},...]
311 src_sw_inport_name
= None
313 # get a vlan tag for this E-LAN
314 vlan
= self
.vlans
.pop()
317 vnf_src_name
= vnf
['name']
318 vnf_src_interface
= vnf
['interface']
320 # check if port is specified (vnf:port)
321 if vnf_src_interface
is None:
322 # take first interface by default
323 connected_sw
= self
.DCNetwork_graph
.neighbors(vnf_src_name
)[0]
324 link_dict
= self
.DCNetwork_graph
[vnf_src_name
][connected_sw
]
325 vnf_src_interface
= link_dict
[0]['src_port_id']
327 for connected_sw
in self
.DCNetwork_graph
.neighbors(vnf_src_name
):
328 link_dict
= self
.DCNetwork_graph
[vnf_src_name
][connected_sw
]
329 for link
in link_dict
:
330 if (link_dict
[link
]['src_port_id'] == vnf_src_interface
or
331 link_dict
[link
]['src_port_name'] == vnf_src_interface
): # Fix: we might also get interface names, e.g, from a son-emu-cli call
332 # found the right link and connected switch
333 src_sw
= connected_sw
334 src_sw_inport_nr
= link_dict
[link
]['dst_port_nr']
335 src_sw_inport_name
= link_dict
[link
]['dst_port_name']
338 # set the tag on the dc switch interface
339 LOG
.debug('set E-LAN: vnf name: {0} interface: {1} tag: {2}'.format(vnf_src_name
, vnf_src_interface
,vlan
))
340 switch_node
= self
.getNodeByName(src_sw
)
341 self
._set
_vlan
_tag
(switch_node
, src_sw_inport_name
, vlan
)
343 def _addMonitorFlow(self
, vnf_src_name
, vnf_dst_name
, vnf_src_interface
=None, vnf_dst_interface
=None,
346 Add a monitoring flow entry that adds a special flowentry/counter at the begin or end of a chain.
347 So this monitoring flowrule exists on top of a previously defined chain rule and uses the same vlan tag/routing.
350 :param vnf_src_interface:
351 :param vnf_dst_interface:
352 :param tag: vlan tag to be used for this chain (same tag as existing chain)
353 :param monitor_placement: 'tx' or 'rx' indicating to place the extra flowentry resp. at the beginning or end of the chain
359 src_sw_inport_name
= None
361 dst_sw_outport_nr
= 0
362 dst_sw_outport_name
= None
364 LOG
.debug("call AddMonitorFlow vnf_src_name=%r, vnf_src_interface=%r, vnf_dst_name=%r, vnf_dst_interface=%r",
365 vnf_src_name
, vnf_src_interface
, vnf_dst_name
, vnf_dst_interface
)
367 #check if port is specified (vnf:port)
368 if vnf_src_interface
is None:
369 # take first interface by default
370 connected_sw
= self
.DCNetwork_graph
.neighbors(vnf_src_name
)[0]
371 link_dict
= self
.DCNetwork_graph
[vnf_src_name
][connected_sw
]
372 vnf_src_interface
= link_dict
[0]['src_port_id']
374 for connected_sw
in self
.DCNetwork_graph
.neighbors(vnf_src_name
):
375 link_dict
= self
.DCNetwork_graph
[vnf_src_name
][connected_sw
]
376 for link
in link_dict
:
377 if (link_dict
[link
]['src_port_id'] == vnf_src_interface
or
378 link_dict
[link
]['src_port_name'] == vnf_src_interface
): # Fix: we might also get interface names, e.g, from a son-emu-cli call
379 # found the right link and connected switch
380 src_sw
= connected_sw
381 src_sw_inport_nr
= link_dict
[link
]['dst_port_nr']
382 src_sw_inport_name
= link_dict
[link
]['dst_port_name']
385 if vnf_dst_interface
is None:
386 # take first interface by default
387 connected_sw
= self
.DCNetwork_graph
.neighbors(vnf_dst_name
)[0]
388 link_dict
= self
.DCNetwork_graph
[connected_sw
][vnf_dst_name
]
389 vnf_dst_interface
= link_dict
[0]['dst_port_id']
391 vnf_dst_name
= vnf_dst_name
.split(':')[0]
392 for connected_sw
in self
.DCNetwork_graph
.neighbors(vnf_dst_name
):
393 link_dict
= self
.DCNetwork_graph
[connected_sw
][vnf_dst_name
]
394 for link
in link_dict
:
395 if link_dict
[link
]['dst_port_id'] == vnf_dst_interface
or \
396 link_dict
[link
]['dst_port_name'] == vnf_dst_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 dst_sw
= connected_sw
399 dst_sw_outport_nr
= link_dict
[link
]['src_port_nr']
400 dst_sw_outport_name
= link_dict
[link
]['src_port_name']
404 LOG
.exception('tag not valid: {0}'.format(tag
))
408 # returns the first found shortest path
409 # if all shortest paths are wanted, use: all_shortest_paths
410 path
= nx
.shortest_path(self
.DCNetwork_graph
, src_sw
, dst_sw
, weight
=kwargs
.get('weight'))
412 LOG
.exception("No path could be found between {0} and {1} using src_sw={2} and dst_sw={3}".format(
413 vnf_src_name
, vnf_dst_name
, src_sw
, dst_sw
))
414 LOG
.debug("Graph nodes: %r" % self
.DCNetwork_graph
.nodes())
415 LOG
.debug("Graph edges: %r" % self
.DCNetwork_graph
.edges())
416 for e
, v
in self
.DCNetwork_graph
.edges():
417 LOG
.debug("%r" % self
.DCNetwork_graph
[e
][v
])
418 return "No path could be found between {0} and {1}".format(vnf_src_name
, vnf_dst_name
)
420 LOG
.info("Path between {0} and {1}: {2}".format(vnf_src_name
, vnf_dst_name
, path
))
423 switch_inport_nr
= src_sw_inport_nr
425 cmd
= kwargs
.get('cmd')
427 #iterate through the path to install the flow-entries
428 for i
in range(0,len(path
)):
429 current_node
= self
.getNodeByName(current_hop
)
431 if path
.index(current_hop
) < len(path
)-1:
432 next_hop
= path
[path
.index(current_hop
)+1]
435 next_hop
= vnf_dst_name
437 next_node
= self
.getNodeByName(next_hop
)
439 if next_hop
== vnf_dst_name
:
440 switch_outport_nr
= dst_sw_outport_nr
441 LOG
.info("end node reached: {0}".format(vnf_dst_name
))
442 elif not isinstance( next_node
, OVSSwitch
):
443 LOG
.info("Next node: {0} is not a switch".format(next_hop
))
444 return "Next node: {0} is not a switch".format(next_hop
)
446 # take first link between switches by default
448 switch_outport_nr
= self
.DCNetwork_graph
[current_hop
][next_hop
][index_edge_out
]['src_port_nr']
451 # set of entry via ovs-ofctl
452 if isinstance( current_node
, OVSSwitch
):
454 kwargs
['path'] = path
455 kwargs
['current_hop'] = current_hop
456 kwargs
['switch_inport_name'] = src_sw_inport_name
457 kwargs
['switch_outport_name'] = dst_sw_outport_name
458 kwargs
['skip_vlan_tag'] = True
459 kwargs
['pathindex'] = i
461 monitor_placement
= kwargs
.get('monitor_placement').strip()
462 # put monitor flow at the dst switch
464 if monitor_placement
== 'tx' and path
.index(current_hop
) == 0: # first node:
466 # put monitoring flow at the src switch
467 elif monitor_placement
== 'rx' and path
.index(current_hop
) == len(path
) - 1: # last node:
469 elif monitor_placement
not in ['rx', 'tx']:
470 LOG
.exception('invalid monitor command: {0}'.format(monitor_placement
))
473 if self
.controller
== RemoteController
and insert_flow
:
474 ## set flow entry via ryu rest api
475 self
._set
_flow
_entry
_ryu
_rest
(current_node
, switch_inport_nr
, switch_outport_nr
, **kwargs
)
478 ## set flow entry via ovs-ofctl
479 self
._set
_flow
_entry
_dpctl
(current_node
, switch_inport_nr
, switch_outport_nr
, **kwargs
)
482 # take first link between switches by default
483 if isinstance( next_node
, OVSSwitch
):
484 switch_inport_nr
= self
.DCNetwork_graph
[current_hop
][next_hop
][0]['dst_port_nr']
485 current_hop
= next_hop
487 return "path {2} between {0} and {1}".format(vnf_src_name
, vnf_dst_name
, cmd
)
490 def setChain(self
, vnf_src_name
, vnf_dst_name
, vnf_src_interface
=None, vnf_dst_interface
=None, **kwargs
):
492 Chain 2 vnf interfaces together by installing the flowrules in the switches along their path.
493 Currently the path is found using the default networkx shortest path function.
494 Each chain gets a unique vlan id , so different chains wil not interfere.
496 :param vnf_src_name: vnf name (string)
497 :param vnf_dst_name: vnf name (string)
498 :param vnf_src_interface: source interface name (string)
499 :param vnf_dst_interface: destination interface name (string)
500 :param cmd: 'add-flow' (default) to add a chain, 'del-flows' to remove a chain
501 :param cookie: cookie for the installed flowrules (can be used later as identifier for a set of installed chains)
502 :param match: custom match entry to be added to the flowrules (default: only in_port and vlan tag)
503 :param priority: custom flowrule priority
504 :param monitor: boolean to indicate whether this chain is a monitoring chain
505 :param tag: vlan tag to be used for this chain (pre-defined or new one if none is specified)
506 :param skip_vlan_tag: boolean to indicate if a vlan tag should be appointed to this flow or not
507 :param path: custom path between the two VNFs (list of switches)
508 :return: output log string
511 # special procedure for monitoring flows
512 if kwargs
.get('monitor'):
514 # check if chain already exists
515 found_chains
= [chain_dict
for chain_dict
in self
.installed_chains
if
516 (chain_dict
['vnf_src_name'] == vnf_src_name
and chain_dict
['vnf_src_interface'] == vnf_src_interface
517 and chain_dict
['vnf_dst_name'] == vnf_dst_name
and chain_dict
['vnf_dst_interface'] == vnf_dst_interface
)]
519 if len(found_chains
) > 0:
520 # this chain exists, so need an extra monitoring flow
521 # assume only 1 chain per vnf/interface pair
522 LOG
.debug('*** installing monitoring chain on top of pre-defined chain from {0}:{1} -> {2}:{3}'.
523 format(vnf_src_name
, vnf_src_interface
, vnf_dst_name
, vnf_dst_interface
))
524 tag
= found_chains
[0]['tag']
525 ret
= self
._addMonitorFlow
(vnf_src_name
, vnf_dst_name
, vnf_src_interface
, vnf_dst_interface
,
526 tag
=tag
, table_id
=0, **kwargs
)
529 # no chain existing (or E-LAN) -> install normal chain
530 LOG
.warning('*** installing monitoring chain without pre-defined NSD chain from {0}:{1} -> {2}:{3}'.
531 format(vnf_src_name
, vnf_src_interface
, vnf_dst_name
, vnf_dst_interface
))
535 cmd
= kwargs
.get('cmd')
536 if cmd
== 'add-flow' or cmd
== 'del-flows':
537 ret
= self
._chainAddFlow
(vnf_src_name
, vnf_dst_name
, vnf_src_interface
, vnf_dst_interface
, **kwargs
)
538 if kwargs
.get('bidirectional'):
539 if kwargs
.get('path') is not None:
540 kwargs
['path'] = list(reversed(kwargs
.get('path')))
541 ret
= ret
+'\n' + self
._chainAddFlow
(vnf_dst_name
, vnf_src_name
, vnf_dst_interface
, vnf_src_interface
, **kwargs
)
544 ret
= "Command unknown"
549 def _chainAddFlow(self
, vnf_src_name
, vnf_dst_name
, vnf_src_interface
=None, vnf_dst_interface
=None, **kwargs
):
553 src_sw_inport_name
= None
555 dst_sw_outport_nr
= 0
556 dst_sw_outport_name
= None
558 LOG
.debug("call chainAddFlow vnf_src_name=%r, vnf_src_interface=%r, vnf_dst_name=%r, vnf_dst_interface=%r",
559 vnf_src_name
, vnf_src_interface
, vnf_dst_name
, vnf_dst_interface
)
561 #check if port is specified (vnf:port)
562 if vnf_src_interface
is None:
563 # take first interface by default
564 connected_sw
= self
.DCNetwork_graph
.neighbors(vnf_src_name
)[0]
565 link_dict
= self
.DCNetwork_graph
[vnf_src_name
][connected_sw
]
566 vnf_src_interface
= link_dict
[0]['src_port_id']
568 for connected_sw
in self
.DCNetwork_graph
.neighbors(vnf_src_name
):
569 link_dict
= self
.DCNetwork_graph
[vnf_src_name
][connected_sw
]
570 for link
in link_dict
:
571 if (link_dict
[link
]['src_port_id'] == vnf_src_interface
or
572 link_dict
[link
]['src_port_name'] == vnf_src_interface
): # Fix: we might also get interface names, e.g, from a son-emu-cli call
573 # found the right link and connected switch
574 src_sw
= connected_sw
575 src_sw_inport_nr
= link_dict
[link
]['dst_port_nr']
576 src_sw_inport_name
= link_dict
[link
]['dst_port_name']
579 if vnf_dst_interface
is None:
580 # take first interface by default
581 connected_sw
= self
.DCNetwork_graph
.neighbors(vnf_dst_name
)[0]
582 link_dict
= self
.DCNetwork_graph
[connected_sw
][vnf_dst_name
]
583 vnf_dst_interface
= link_dict
[0]['dst_port_id']
585 vnf_dst_name
= vnf_dst_name
.split(':')[0]
586 for connected_sw
in self
.DCNetwork_graph
.neighbors(vnf_dst_name
):
587 link_dict
= self
.DCNetwork_graph
[connected_sw
][vnf_dst_name
]
588 for link
in link_dict
:
589 if link_dict
[link
]['dst_port_id'] == vnf_dst_interface
or \
590 link_dict
[link
]['dst_port_name'] == vnf_dst_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 dst_sw
= connected_sw
593 dst_sw_outport_nr
= link_dict
[link
]['src_port_nr']
594 dst_sw_outport_name
= link_dict
[link
]['src_port_name']
597 path
= kwargs
.get('path')
601 # returns the first found shortest path
602 # if all shortest paths are wanted, use: all_shortest_paths
603 path
= nx
.shortest_path(self
.DCNetwork_graph
, src_sw
, dst_sw
, weight
=kwargs
.get('weight'))
605 LOG
.exception("No path could be found between {0} and {1} using src_sw={2} and dst_sw={3}".format(
606 vnf_src_name
, vnf_dst_name
, src_sw
, dst_sw
))
607 LOG
.debug("Graph nodes: %r" % self
.DCNetwork_graph
.nodes())
608 LOG
.debug("Graph edges: %r" % self
.DCNetwork_graph
.edges())
609 for e
, v
in self
.DCNetwork_graph
.edges():
610 LOG
.debug("%r" % self
.DCNetwork_graph
[e
][v
])
611 return "No path could be found between {0} and {1}".format(vnf_src_name
, vnf_dst_name
)
613 LOG
.info("Path between {0} and {1}: {2}".format(vnf_src_name
, vnf_dst_name
, path
))
616 switch_inport_nr
= src_sw_inport_nr
619 cmd
= kwargs
.get('cmd')
621 if cmd
== 'add-flow':
622 if kwargs
.get('tag'):
623 # use pre-defined tag
624 vlan
= kwargs
.get('tag')
626 vlan
= self
.vlans
.pop()
628 # store the used vlan tag to identify this chain
629 if not kwargs
.get('monitor'):
631 chain_dict
['vnf_src_name'] = vnf_src_name
632 chain_dict
['vnf_dst_name'] = vnf_dst_name
633 chain_dict
['vnf_src_interface'] = vnf_src_interface
634 chain_dict
['vnf_dst_interface'] = vnf_dst_interface
635 chain_dict
['tag'] = vlan
636 self
.installed_chains
.append(chain_dict
)
638 #iterate through the path to install the flow-entries
639 for i
in range(0,len(path
)):
640 current_node
= self
.getNodeByName(current_hop
)
642 if i
< len(path
) - 1:
643 next_hop
= path
[i
+ 1]
645 # last switch reached
646 next_hop
= vnf_dst_name
648 next_node
= self
.getNodeByName(next_hop
)
650 if next_hop
== vnf_dst_name
:
651 switch_outport_nr
= dst_sw_outport_nr
652 LOG
.info("end node reached: {0}".format(vnf_dst_name
))
653 elif not isinstance( next_node
, OVSSwitch
):
654 LOG
.info("Next node: {0} is not a switch".format(next_hop
))
655 return "Next node: {0} is not a switch".format(next_hop
)
657 # take first link between switches by default
659 switch_outport_nr
= self
.DCNetwork_graph
[current_hop
][next_hop
][index_edge_out
]['src_port_nr']
663 if isinstance( current_node
, OVSSwitch
):
664 kwargs
['vlan'] = vlan
665 kwargs
['path'] = path
666 kwargs
['current_hop'] = current_hop
667 kwargs
['switch_inport_name'] = src_sw_inport_name
668 kwargs
['switch_outport_name'] = dst_sw_outport_name
669 kwargs
['pathindex'] = i
671 if self
.controller
== RemoteController
:
672 ## set flow entry via ryu rest api
673 self
._set
_flow
_entry
_ryu
_rest
(current_node
, switch_inport_nr
, switch_outport_nr
, **kwargs
)
675 ## set flow entry via ovs-ofctl
676 self
._set
_flow
_entry
_dpctl
(current_node
, switch_inport_nr
, switch_outport_nr
, **kwargs
)
678 # take first link between switches by default
679 if isinstance( next_node
, OVSSwitch
):
680 switch_inport_nr
= self
.DCNetwork_graph
[current_hop
][next_hop
][0]['dst_port_nr']
681 current_hop
= next_hop
683 return "path {2} between {0} and {1}".format(vnf_src_name
, vnf_dst_name
, cmd
)
685 def _set_flow_entry_ryu_rest(self
, node
, switch_inport_nr
, switch_outport_nr
, **kwargs
):
686 match
= 'in_port=%s' % switch_inport_nr
688 cookie
= kwargs
.get('cookie')
689 match_input
= kwargs
.get('match')
690 cmd
= kwargs
.get('cmd')
691 path
= kwargs
.get('path')
692 index
= kwargs
.get('pathindex')
694 vlan
= kwargs
.get('vlan')
695 priority
= kwargs
.get('priority')
696 # flag to not set the ovs port vlan tag
697 skip_vlan_tag
= kwargs
.get('skip_vlan_tag')
698 # table id to put this flowentry
699 table_id
= kwargs
.get('table_id')
705 match
= s
.join([match
, match_input
])
708 flow
['dpid'] = int(node
.dpid
, 16)
711 flow
['cookie'] = int(cookie
)
713 flow
['priority'] = int(priority
)
715 flow
['table_id'] = table_id
719 # possible Ryu actions, match fields:
720 # http://ryu.readthedocs.io/en/latest/app/ofctl_rest.html#add-a-flow-entry
721 if cmd
== 'add-flow':
722 prefix
= 'stats/flowentry/add'
724 if index
== 0: # first node
725 # set vlan tag in ovs instance (to isolate E-LANs)
726 if not skip_vlan_tag
:
727 in_port_name
= kwargs
.get('switch_inport_name')
728 self
._set
_vlan
_tag
(node
, in_port_name
, vlan
)
729 # set vlan push action if more than 1 switch in the path
732 action
['type'] = 'PUSH_VLAN' # Push a new VLAN tag if a input frame is non-VLAN-tagged
733 action
['ethertype'] = 33024 # Ethertype 0x8100(=33024): IEEE 802.1Q VLAN-tagged frame
734 flow
['actions'].append(action
)
736 action
['type'] = 'SET_FIELD'
737 action
['field'] = 'vlan_vid'
738 # ryu expects the field to be masked
739 action
['value'] = vlan |
0x1000
740 flow
['actions'].append(action
)
742 elif index
== len(path
) - 1: # last node
743 # set vlan tag in ovs instance (to isolate E-LANs)
744 if not skip_vlan_tag
:
745 out_port_name
= kwargs
.get('switch_outport_name')
746 self
._set
_vlan
_tag
(node
, out_port_name
, vlan
)
747 # set vlan pop action if more than 1 switch in the path
749 match
+= ',dl_vlan=%s' % vlan
751 action
['type'] = 'POP_VLAN'
752 flow
['actions'].append(action
)
755 match
+= ',dl_vlan=%s' % vlan
757 # output action must come last
759 action
['type'] = 'OUTPUT'
760 action
['port'] = switch_outport_nr
761 flow
['actions'].append(action
)
763 elif cmd
== 'del-flows':
764 prefix
= 'stats/flowentry/delete'
767 # TODO: add cookie_mask as argument
768 flow
['cookie_mask'] = int('0xffffffffffffffff', 16) # need full mask to match complete cookie
771 action
['type'] = 'OUTPUT'
772 action
['port'] = switch_outport_nr
773 flow
['actions'].append(action
)
775 flow
['match'] = self
._parse
_match
(match
)
776 self
.ryu_REST(prefix
, data
=flow
)
778 def _set_vlan_tag(self
, node
, switch_port
, tag
):
779 node
.vsctl('set', 'port {0} tag={1}'.format(switch_port
,tag
))
780 LOG
.debug("set vlan in switch: {0} in_port: {1} vlan tag: {2}".format(node
.name
, switch_port
, tag
))
782 def _set_flow_entry_dpctl(self
, node
, switch_inport_nr
, switch_outport_nr
, **kwargs
):
784 match
= 'in_port=%s' % switch_inport_nr
786 cookie
= kwargs
.get('cookie')
787 match_input
= kwargs
.get('match')
788 cmd
= kwargs
.get('cmd')
789 path
= kwargs
.get('path')
790 index
= kwargs
.get('pathindex')
791 vlan
= kwargs
.get('vlan')
795 cookie
= 'cookie=%s' % cookie
796 match
= s
.join([cookie
, match
])
798 match
= s
.join([match
, match_input
])
799 if cmd
== 'add-flow':
800 action
= 'action=%s' % switch_outport_nr
802 if index
== 0: # first node
803 action
= ('action=mod_vlan_vid:%s' % vlan
) + (',output=%s' % switch_outport_nr
)
804 match
= '-O OpenFlow13 ' + match
805 elif index
== len(path
) - 1: # last node
806 match
+= ',dl_vlan=%s' % vlan
807 action
= 'action=strip_vlan,output=%s' % switch_outport_nr
809 match
+= ',dl_vlan=%s' % vlan
810 ofcmd
= s
.join([match
, action
])
811 elif cmd
== 'del-flows':
816 node
.dpctl(cmd
, ofcmd
)
817 LOG
.info("{3} in switch: {0} in_port: {1} out_port: {2}".format(node
.name
, switch_inport_nr
,
818 switch_outport_nr
, cmd
))
820 # start Ryu Openflow controller as Remote Controller for the DCNetwork
821 def startRyu(self
, learning_switch
=True):
822 # start Ryu controller with rest-API
823 python_install_path
= site
.getsitepackages()[0]
824 # ryu default learning switch
825 #ryu_path = python_install_path + '/ryu/app/simple_switch_13.py'
826 #custom learning switch that installs a default NORMAL action in the ovs switches
827 dir_path
= os
.path
.dirname(os
.path
.realpath(__file__
))
828 ryu_path
= dir_path
+ '/son_emu_simple_switch_13.py'
829 ryu_path2
= python_install_path
+ '/ryu/app/ofctl_rest.py'
830 # change the default Openflow controller port to 6653 (official IANA-assigned port number), as used by Mininet
831 # Ryu still uses 6633 as default
832 ryu_option
= '--ofp-tcp-listen-port'
834 ryu_cmd
= 'ryu-manager'
835 FNULL
= open("/tmp/ryu.log", 'w')
837 self
.ryu_process
= Popen([ryu_cmd
, ryu_path
, ryu_path2
, ryu_option
, ryu_of_port
], stdout
=FNULL
, stderr
=FNULL
)
838 LOG
.debug('starting ryu-controller with {0}'.format(ryu_path
))
839 LOG
.debug('starting ryu-controller with {0}'.format(ryu_path2
))
841 # no learning switch, but with rest api
842 self
.ryu_process
= Popen([ryu_cmd
, ryu_path2
, ryu_option
, ryu_of_port
], stdout
=FNULL
, stderr
=FNULL
)
843 LOG
.debug('starting ryu-controller with {0}'.format(ryu_path2
))
848 Stop the Ryu controller that might be started by son-emu.
852 if self
.ryu_process
is not None:
853 self
.ryu_process
.terminate()
854 self
.ryu_process
.kill()
855 # ensure its death ;-)
856 Popen(['pkill', '-f', 'ryu-manager'])
858 def ryu_REST(self
, prefix
, dpid
=None, data
=None):
861 url
= self
.ryu_REST_api
+ '/' + str(prefix
) + '/' + str(dpid
)
863 url
= self
.ryu_REST_api
+ '/' + str(prefix
)
865 req
= self
.RyuSession
.post(url
, json
=data
)
867 req
= self
.RyuSession
.get(url
)
870 # do extra logging if status code is not 200 (OK)
871 if req
.status_code
is not requests
.codes
.ok
:
873 'type {0} encoding: {1} text: {2} headers: {3} history: {4}'.format(req
.headers
['content-type'],
874 req
.encoding
, req
.text
,
875 req
.headers
, req
.history
))
876 LOG
.info('url: {0}'.format(str(url
)))
877 if data
: LOG
.info('POST: {0}'.format(str(data
)))
878 LOG
.info('status: {0} reason: {1}'.format(req
.status_code
, req
.reason
))
881 if 'json' in req
.headers
['content-type']:
885 ret
= req
.text
.rstrip()
889 # need to respect that some match fields must be integers
890 # http://ryu.readthedocs.io/en/latest/app/ofctl_rest.html#description-of-match-and-actions
891 def _parse_match(self
, match
):
892 matches
= match
.split(',')
898 m2
= int(match
[1], 0)
902 dict.update({match
[0]:m2
})
905 def find_connected_dc_interface(self
, vnf_src_name
, vnf_src_interface
):
906 for connected_sw
in self
.DCNetwork_graph
.neighbors(vnf_src_name
):
907 link_dict
= self
.DCNetwork_graph
[vnf_src_name
][connected_sw
]
908 for link
in link_dict
:
909 if (link_dict
[link
]['src_port_id'] == vnf_src_interface
or
910 link_dict
[link
]['src_port_name'] == vnf_src_interface
): # Fix: we might also get interface names, e.g, from a son-emu-cli call
911 # found the right link and connected switch
912 src_sw
= connected_sw
913 src_sw_inport_nr
= link_dict
[link
]['dst_port_nr']
914 src_sw_inport_name
= link_dict
[link
]['dst_port_name']
915 return src_sw_inport_name