1 # Copyright (c) 2015 SONATA-NFV and Paderborn University
4 # Licensed under the Apache License, Version 2.0 (the "License");
5 # you may not use this file except in compliance with the License.
6 # You may obtain a copy of the License at
8 # http://www.apache.org/licenses/LICENSE-2.0
10 # Unless required by applicable law or agreed to in writing, software
11 # distributed under the License is distributed on an "AS IS" BASIS,
12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # See the License for the specific language governing permissions and
14 # limitations under the License.
16 # Neither the name of the SONATA-NFV, Paderborn University
17 # nor the names of its contributors may be used to endorse or promote
18 # products derived from this software without specific prior written
21 # This work has been performed in the framework of the SONATA project,
22 # funded by the European Commission under Grant number 671517 through
23 # the Horizon 2020 and 5G-PPP programmes. The authors would like to
24 # acknowledge the contributions of their colleagues of the SONATA
25 # partner consortium (www.sonata-nfv.eu).
29 from subprocess
import Popen
35 from mininet
.net
import Containernet
36 from mininet
.node
import OVSSwitch
, OVSKernelSwitch
, Docker
, RemoteController
37 from mininet
.cli
import CLI
38 from mininet
.link
import TCLink
39 from mininet
.clean
import cleanup
41 from emuvim
.dcemulator
.monitoring
import DCNetworkMonitor
42 from emuvim
.dcemulator
.node
import Datacenter
, EmulatorCompute
43 from emuvim
.dcemulator
.resourcemodel
import ResourceModelRegistrar
45 LOG
= logging
.getLogger("dcemulator.net")
46 LOG
.setLevel(logging
.DEBUG
)
48 # default CPU period used for cpu percentage-based cfs values (microseconds)
51 # default priority setting for added flow-rules
52 DEFAULT_PRIORITY
= 1000
53 # default cookie number for new flow-rules
57 class DCNetwork(Containernet
):
59 Wraps the original Mininet/Containernet class and provides
60 methods to add data centers, switches, etc.
62 This class is used by topology definition scripts.
65 def __init__(self
, controller
=RemoteController
, monitor
=False,
66 enable_learning
=False,
67 # learning switch behavior of the default ovs switches icw Ryu
68 # controller can be turned off/on, needed for E-LAN
70 dc_emulation_max_cpu
=1.0, # fraction of overall CPU time for emulation
71 dc_emulation_max_mem
=512, # emulation max mem in MB
74 Create an extended version of a Containernet network
75 :param dc_emulation_max_cpu: max. CPU time used by containers in data centers
76 :param kwargs: path through for Mininet parameters
81 self
.ryu_process
= None
82 # list of deployed nsds.E_Lines and E_LANs (uploaded from the dummy
84 self
.deployed_nsds
= []
85 self
.deployed_elines
= []
86 self
.deployed_elans
= []
87 self
.installed_chains
= []
89 # always cleanup environment before we start the emulator
93 # call original Docker.__init__ and setup default controller
94 Containernet
.__init
__(
95 self
, switch
=OVSKernelSwitch
, controller
=controller
, **kwargs
)
97 # default switch configuration
98 enable_ryu_learning
= False
100 self
.failMode
= 'standalone'
101 enable_ryu_learning
= True
103 self
.failMode
= 'secure'
106 if controller
== RemoteController
:
107 # start Ryu controller
108 self
.startRyu(learning_switch
=enable_ryu_learning
)
110 # add the specified controller
111 self
.addController('c0', controller
=controller
)
113 # graph of the complete DC network
114 self
.DCNetwork_graph
= nx
.MultiDiGraph()
116 # initialize pool of vlan tags to setup the SDN paths
117 self
.vlans
= range(1, 4095)[::-1]
119 # link to Ryu REST_API
122 self
.ryu_REST_api
= 'http://{0}:{1}'.format(ryu_ip
, ryu_port
)
123 self
.RyuSession
= requests
.Session()
127 self
.monitor_agent
= DCNetworkMonitor(self
)
129 self
.monitor_agent
= None
131 # initialize resource model registrar
132 self
.rm_registrar
= ResourceModelRegistrar(
133 dc_emulation_max_cpu
, dc_emulation_max_mem
)
134 self
.cpu_period
= CPU_PERIOD
136 def addDatacenter(self
, label
, metadata
={}, resource_log_path
=None):
138 Create and add a logical cloud data center to the network.
140 if label
in self
.dcs
:
141 raise Exception("Data center label already exists: %s" % label
)
142 dc
= Datacenter(label
, metadata
=metadata
,
143 resource_log_path
=resource_log_path
)
144 dc
.net
= self
# set reference to network
146 dc
.create() # finally create the data center in our Mininet instance
147 LOG
.info("added data center: %s" % label
)
150 def addLink(self
, node1
, node2
, **params
):
152 Able to handle Datacenter objects as link
155 assert node1
is not None
156 assert node2
is not None
158 # ensure type of node1
159 if isinstance(node1
, basestring
):
160 if node1
in self
.dcs
:
161 node1
= self
.dcs
[node1
].switch
162 if isinstance(node1
, Datacenter
):
164 # ensure type of node2
165 if isinstance(node2
, basestring
):
166 if node2
in self
.dcs
:
167 node2
= self
.dcs
[node2
].switch
168 if isinstance(node2
, Datacenter
):
170 # try to give containers a default IP
171 if isinstance(node1
, Docker
):
172 if "params1" not in params
:
173 params
["params1"] = {}
174 if "ip" not in params
["params1"]:
175 params
["params1"]["ip"] = self
.getNextIp()
176 if isinstance(node2
, Docker
):
177 if "params2" not in params
:
178 params
["params2"] = {}
179 if "ip" not in params
["params2"]:
180 params
["params2"]["ip"] = self
.getNextIp()
181 # ensure that we allow TCLinks between data centers
182 # TODO this is not optimal, we use cls=Link for containers and TCLink for data centers
183 # see Containernet issue:
184 # https://github.com/mpeuster/containernet/issues/3
185 if "cls" not in params
:
186 params
["cls"] = TCLink
188 link
= Containernet
.addLink(self
, node1
, node2
, **params
)
190 # try to give container interfaces a default id
191 node1_port_id
= node1
.ports
[link
.intf1
]
192 if isinstance(node1
, Docker
):
193 if "id" in params
["params1"]:
194 node1_port_id
= params
["params1"]["id"]
195 node1_port_name
= link
.intf1
.name
197 node2_port_id
= node2
.ports
[link
.intf2
]
198 if isinstance(node2
, Docker
):
199 if "id" in params
["params2"]:
200 node2_port_id
= params
["params2"]["id"]
201 node2_port_name
= link
.intf2
.name
203 # add edge and assigned port number to graph in both directions between node1 and node2
204 # port_id: id given in descriptor (if available, otherwise same as port)
205 # port: portnumber assigned by Containernet
208 # possible weight metrics allowed by TClink class:
209 weight_metrics
= ['bw', 'delay', 'jitter', 'loss']
210 edge_attributes
= [p
for p
in params
if p
in weight_metrics
]
211 for attr
in edge_attributes
:
212 # if delay: strip ms (need number as weight in graph)
213 match
= re
.search('([0-9]*\.?[0-9]+)', str(params
[attr
]))
215 attr_number
= match
.group(1)
218 attr_dict
[attr
] = attr_number
220 attr_dict2
= {'src_port_id': node1_port_id
, 'src_port_nr': node1
.ports
[link
.intf1
],
221 'src_port_name': node1_port_name
,
222 'dst_port_id': node2_port_id
, 'dst_port_nr': node2
.ports
[link
.intf2
],
223 'dst_port_name': node2_port_name
}
224 attr_dict2
.update(attr_dict
)
225 self
.DCNetwork_graph
.add_edge(
226 node1
.name
, node2
.name
, attr_dict
=attr_dict2
)
228 attr_dict2
= {'src_port_id': node2_port_id
, 'src_port_nr': node2
.ports
[link
.intf2
],
229 'src_port_name': node2_port_name
,
230 'dst_port_id': node1_port_id
, 'dst_port_nr': node1
.ports
[link
.intf1
],
231 'dst_port_name': node1_port_name
}
232 attr_dict2
.update(attr_dict
)
233 self
.DCNetwork_graph
.add_edge(
234 node2
.name
, node1
.name
, attr_dict
=attr_dict2
)
236 LOG
.debug("addLink: n1={0} intf1={1} -- n2={2} intf2={3}".format(
237 str(node1
), node1_port_name
, str(node2
), node2_port_name
))
241 def removeLink(self
, link
=None, node1
=None, node2
=None):
243 Remove the link from the Containernet and the networkx graph
246 node1
= link
.intf1
.node
247 node2
= link
.intf2
.node
248 assert node1
is not None
249 assert node2
is not None
250 Containernet
.removeLink(self
, link
=link
, node1
=node1
, node2
=node2
)
251 # TODO we might decrease the loglevel to debug:
253 self
.DCNetwork_graph
.remove_edge(node2
.name
, node1
.name
)
254 except BaseException
:
255 LOG
.warning("%s, %s not found in DCNetwork_graph." %
256 ((node2
.name
, node1
.name
)))
258 self
.DCNetwork_graph
.remove_edge(node1
.name
, node2
.name
)
259 except BaseException
:
260 LOG
.warning("%s, %s not found in DCNetwork_graph." %
261 ((node1
.name
, node2
.name
)))
263 def addDocker(self
, label
, **params
):
265 Wrapper for addDocker method to use custom container class.
267 self
.DCNetwork_graph
.add_node(label
, type=params
.get('type', 'docker'))
268 return Containernet
.addDocker(
269 self
, label
, cls
=EmulatorCompute
, **params
)
271 def removeDocker(self
, label
, **params
):
273 Wrapper for removeDocker method to update graph.
275 self
.DCNetwork_graph
.remove_node(label
)
276 return Containernet
.removeDocker(self
, label
, **params
)
278 def addExtSAP(self
, sap_name
, sap_ip
, **params
):
280 Wrapper for addExtSAP method to store SAP also in graph.
282 # make sure that 'type' is set
283 params
['type'] = params
.get('type', 'sap_ext')
284 self
.DCNetwork_graph
.add_node(sap_name
, type=params
['type'])
285 return Containernet
.addExtSAP(self
, sap_name
, sap_ip
, **params
)
287 def removeExtSAP(self
, sap_name
, **params
):
289 Wrapper for removeExtSAP method to remove SAP also from graph.
291 self
.DCNetwork_graph
.remove_node(sap_name
)
292 return Containernet
.removeExtSAP(self
, sap_name
)
294 def addSwitch(self
, name
, add_to_graph
=True, **params
):
296 Wrapper for addSwitch method to store switch also in graph.
299 # add this switch to the global topology overview
301 self
.DCNetwork_graph
.add_node(
302 name
, type=params
.get('type', 'switch'))
304 # set the learning switch behavior
305 if 'failMode' in params
:
306 failMode
= params
['failMode']
308 failMode
= self
.failMode
310 s
= Containernet
.addSwitch(
311 self
, name
, protocols
='OpenFlow10,OpenFlow12,OpenFlow13', failMode
=failMode
, **params
)
315 def getAllContainers(self
):
317 Returns a list with all containers within all data centers.
320 for dc
in self
.dcs
.itervalues():
321 all_containers
+= dc
.listCompute()
322 return all_containers
326 for dc
in self
.dcs
.itervalues():
328 Containernet
.start(self
)
332 # stop the monitor agent
333 if self
.monitor_agent
is not None:
334 self
.monitor_agent
.stop()
337 Containernet
.stop(self
)
339 # stop Ryu controller
345 def setLAN(self
, vnf_list
):
347 setup an E-LAN network by assigning the same VLAN tag to each DC interface of the VNFs in the E-LAN
349 :param vnf_list: names of the VNFs in this E-LAN [{name:,interface:},...]
353 src_sw_inport_name
= None
355 # get a vlan tag for this E-LAN
356 vlan
= self
.vlans
.pop()
359 vnf_src_name
= vnf
['name']
360 vnf_src_interface
= vnf
['interface']
362 # check if port is specified (vnf:port)
363 if vnf_src_interface
is None:
364 # take first interface by default
365 connected_sw
= self
.DCNetwork_graph
.neighbors(vnf_src_name
)[0]
366 link_dict
= self
.DCNetwork_graph
[vnf_src_name
][connected_sw
]
367 vnf_src_interface
= link_dict
[0]['src_port_id']
369 for connected_sw
in self
.DCNetwork_graph
.neighbors(vnf_src_name
):
370 link_dict
= self
.DCNetwork_graph
[vnf_src_name
][connected_sw
]
371 for link
in link_dict
:
372 if (link_dict
[link
]['src_port_id'] == vnf_src_interface
or
373 link_dict
[link
]['src_port_name'] == vnf_src_interface
): # Fix: we might also get interface names, e.g, from a son-emu-cli call
374 # found the right link and connected switch
375 src_sw
= connected_sw
376 src_sw_inport_name
= link_dict
[link
]['dst_port_name']
379 # set the tag on the dc switch interface
380 LOG
.debug('set E-LAN: vnf name: {0} interface: {1} tag: {2}'.format(
381 vnf_src_name
, vnf_src_interface
, vlan
))
382 switch_node
= self
.getNodeByName(src_sw
)
383 self
._set
_vlan
_tag
(switch_node
, src_sw_inport_name
, vlan
)
385 def _addMonitorFlow(self
, vnf_src_name
, vnf_dst_name
, vnf_src_interface
=None, vnf_dst_interface
=None,
388 Add a monitoring flow entry that adds a special flowentry/counter at the begin or end of a chain.
389 So this monitoring flowrule exists on top of a previously defined chain rule and uses the same vlan tag/routing.
392 :param vnf_src_interface:
393 :param vnf_dst_interface:
394 :param tag: vlan tag to be used for this chain (same tag as existing chain)
395 :param monitor_placement: 'tx' or 'rx' indicating to place the extra flowentry resp. at the beginning or end of the chain
401 src_sw_inport_name
= None
403 dst_sw_outport_nr
= 0
404 dst_sw_outport_name
= None
406 LOG
.debug("call AddMonitorFlow vnf_src_name=%r, vnf_src_interface=%r, vnf_dst_name=%r, vnf_dst_interface=%r",
407 vnf_src_name
, vnf_src_interface
, vnf_dst_name
, vnf_dst_interface
)
409 # check if port is specified (vnf:port)
410 if vnf_src_interface
is None:
411 # take first interface by default
412 connected_sw
= self
.DCNetwork_graph
.neighbors(vnf_src_name
)[0]
413 link_dict
= self
.DCNetwork_graph
[vnf_src_name
][connected_sw
]
414 vnf_src_interface
= link_dict
[0]['src_port_id']
416 for connected_sw
in self
.DCNetwork_graph
.neighbors(vnf_src_name
):
417 link_dict
= self
.DCNetwork_graph
[vnf_src_name
][connected_sw
]
418 for link
in link_dict
:
419 if (link_dict
[link
]['src_port_id'] == vnf_src_interface
or
420 link_dict
[link
]['src_port_name'] == vnf_src_interface
): # Fix: we might also get interface names, e.g, from a son-emu-cli call
421 # found the right link and connected switch
422 src_sw
= connected_sw
423 src_sw_inport_nr
= link_dict
[link
]['dst_port_nr']
424 src_sw_inport_name
= link_dict
[link
]['dst_port_name']
427 if vnf_dst_interface
is None:
428 # take first interface by default
429 connected_sw
= self
.DCNetwork_graph
.neighbors(vnf_dst_name
)[0]
430 link_dict
= self
.DCNetwork_graph
[connected_sw
][vnf_dst_name
]
431 vnf_dst_interface
= link_dict
[0]['dst_port_id']
433 vnf_dst_name
= vnf_dst_name
.split(':')[0]
434 for connected_sw
in self
.DCNetwork_graph
.neighbors(vnf_dst_name
):
435 link_dict
= self
.DCNetwork_graph
[connected_sw
][vnf_dst_name
]
436 for link
in link_dict
:
437 if link_dict
[link
]['dst_port_id'] == vnf_dst_interface
or \
438 link_dict
[link
]['dst_port_name'] == vnf_dst_interface
: # Fix: we might also get interface names, e.g, from a son-emu-cli call
439 # found the right link and connected switch
440 dst_sw
= connected_sw
441 dst_sw_outport_nr
= link_dict
[link
]['src_port_nr']
442 dst_sw_outport_name
= link_dict
[link
]['src_port_name']
446 LOG
.exception('tag not valid: {0}'.format(tag
))
450 # returns the first found shortest path
451 # if all shortest paths are wanted, use: all_shortest_paths
452 path
= nx
.shortest_path(
453 self
.DCNetwork_graph
, src_sw
, dst_sw
, weight
=kwargs
.get('weight'))
454 except BaseException
:
455 LOG
.exception("No path could be found between {0} and {1} using src_sw={2} and dst_sw={3}".format(
456 vnf_src_name
, vnf_dst_name
, src_sw
, dst_sw
))
457 LOG
.debug("Graph nodes: %r" % self
.DCNetwork_graph
.nodes())
458 LOG
.debug("Graph edges: %r" % self
.DCNetwork_graph
.edges())
459 for e
, v
in self
.DCNetwork_graph
.edges():
460 LOG
.debug("%r" % self
.DCNetwork_graph
[e
][v
])
461 return "No path could be found between {0} and {1}".format(
462 vnf_src_name
, vnf_dst_name
)
464 LOG
.debug("Creating path between {0} and {1}: {2}".format(
465 vnf_src_name
, vnf_dst_name
, path
))
468 switch_inport_nr
= src_sw_inport_nr
470 cmd
= kwargs
.get('cmd')
472 # iterate through the path to install the flow-entries
473 for i
in range(0, len(path
)):
474 current_node
= self
.getNodeByName(current_hop
)
476 if path
.index(current_hop
) < len(path
) - 1:
477 next_hop
= path
[path
.index(current_hop
) + 1]
479 # last switch reached
480 next_hop
= vnf_dst_name
482 next_node
= self
.getNodeByName(next_hop
)
484 if next_hop
== vnf_dst_name
:
485 switch_outport_nr
= dst_sw_outport_nr
486 LOG
.debug("end node reached: {0}".format(vnf_dst_name
))
487 elif not isinstance(next_node
, OVSSwitch
):
488 LOG
.info("Next node: {0} is not a switch".format(next_hop
))
489 return "Next node: {0} is not a switch".format(next_hop
)
491 # take first link between switches by default
493 switch_outport_nr
= self
.DCNetwork_graph
[current_hop
][next_hop
][index_edge_out
]['src_port_nr']
495 # set of entry via ovs-ofctl
496 if isinstance(current_node
, OVSSwitch
):
498 kwargs
['path'] = path
499 kwargs
['current_hop'] = current_hop
500 kwargs
['switch_inport_name'] = src_sw_inport_name
501 kwargs
['switch_outport_name'] = dst_sw_outport_name
502 kwargs
['skip_vlan_tag'] = True
503 kwargs
['pathindex'] = i
505 monitor_placement
= kwargs
.get('monitor_placement').strip()
506 # put monitor flow at the dst switch
509 if monitor_placement
== 'tx' and path
.index(current_hop
) == 0:
511 # put monitoring flow at the src switch
513 elif monitor_placement
== 'rx' and path
.index(current_hop
) == len(path
) - 1:
515 elif monitor_placement
not in ['rx', 'tx']:
517 'invalid monitor command: {0}'.format(monitor_placement
))
519 if self
.controller
== RemoteController
and insert_flow
:
520 # set flow entry via ryu rest api
521 self
._set
_flow
_entry
_ryu
_rest
(
522 current_node
, switch_inport_nr
, switch_outport_nr
, **kwargs
)
525 # set flow entry via ovs-ofctl
526 self
._set
_flow
_entry
_dpctl
(
527 current_node
, switch_inport_nr
, switch_outport_nr
, **kwargs
)
530 # take first link between switches by default
531 if isinstance(next_node
, OVSSwitch
):
532 switch_inport_nr
= self
.DCNetwork_graph
[current_hop
][next_hop
][0]['dst_port_nr']
533 current_hop
= next_hop
535 return "path {2} between {0} and {1}".format(
536 vnf_src_name
, vnf_dst_name
, cmd
)
538 def setChain(self
, vnf_src_name
, vnf_dst_name
,
539 vnf_src_interface
=None, vnf_dst_interface
=None, **kwargs
):
541 Chain 2 vnf interfaces together by installing the flowrules in the switches along their path.
542 Currently the path is found using the default networkx shortest path function.
543 Each chain gets a unique vlan id , so different chains wil not interfere.
545 :param vnf_src_name: vnf name (string)
546 :param vnf_dst_name: vnf name (string)
547 :param vnf_src_interface: source interface name (string)
548 :param vnf_dst_interface: destination interface name (string)
549 :param cmd: 'add-flow' (default) to add a chain, 'del-flows' to remove a chain
550 :param cookie: cookie for the installed flowrules (can be used later as identifier for a set of installed chains)
551 :param match: custom match entry to be added to the flowrules (default: only in_port and vlan tag)
552 :param priority: custom flowrule priority
553 :param monitor: boolean to indicate whether this chain is a monitoring chain
554 :param tag: vlan tag to be used for this chain (pre-defined or new one if none is specified)
555 :param skip_vlan_tag: boolean to indicate if a vlan tag should be appointed to this flow or not
556 :param path: custom path between the two VNFs (list of switches)
557 :return: output log string
560 # special procedure for monitoring flows
561 if kwargs
.get('monitor'):
563 # check if chain already exists
564 found_chains
= [chain_dict
for chain_dict
in self
.installed_chains
if
565 (chain_dict
['vnf_src_name'] == vnf_src_name
and
566 chain_dict
['vnf_src_interface'] == vnf_src_interface
and
567 chain_dict
['vnf_dst_name'] == vnf_dst_name
and
568 chain_dict
['vnf_dst_interface'] == vnf_dst_interface
)]
570 if len(found_chains
) > 0:
571 # this chain exists, so need an extra monitoring flow
572 # assume only 1 chain per vnf/interface pair
573 LOG
.debug('*** installing monitoring chain on top of pre-defined chain from {0}:{1} -> {2}:{3}'.
574 format(vnf_src_name
, vnf_src_interface
, vnf_dst_name
, vnf_dst_interface
))
575 tag
= found_chains
[0]['tag']
576 ret
= self
._addMonitorFlow
(vnf_src_name
, vnf_dst_name
, vnf_src_interface
, vnf_dst_interface
,
577 tag
=tag
, table_id
=0, **kwargs
)
580 # no chain existing (or E-LAN) -> install normal chain
581 LOG
.warning('*** installing monitoring chain without pre-defined NSD chain from {0}:{1} -> {2}:{3}'.
582 format(vnf_src_name
, vnf_src_interface
, vnf_dst_name
, vnf_dst_interface
))
585 cmd
= kwargs
.get('cmd', 'add-flow')
586 if cmd
== 'add-flow' or cmd
== 'del-flows':
587 ret
= self
._chainAddFlow
(
588 vnf_src_name
, vnf_dst_name
, vnf_src_interface
, vnf_dst_interface
, **kwargs
)
589 if kwargs
.get('bidirectional'):
590 if kwargs
.get('path') is not None:
591 kwargs
['path'] = list(reversed(kwargs
.get('path')))
594 vnf_dst_name
, vnf_src_name
, vnf_dst_interface
, vnf_src_interface
, **kwargs
)
597 ret
= "Command unknown"
601 def _chainAddFlow(self
, vnf_src_name
, vnf_dst_name
,
602 vnf_src_interface
=None, vnf_dst_interface
=None, **kwargs
):
606 src_sw_inport_name
= None
608 dst_sw_outport_nr
= 0
609 dst_sw_outport_name
= None
611 LOG
.debug("call chainAddFlow vnf_src_name=%r, vnf_src_interface=%r, vnf_dst_name=%r, vnf_dst_interface=%r",
612 vnf_src_name
, vnf_src_interface
, vnf_dst_name
, vnf_dst_interface
)
614 # check if port is specified (vnf:port)
615 if vnf_src_interface
is None:
616 # take first interface by default
617 connected_sw
= self
.DCNetwork_graph
.neighbors(vnf_src_name
)[0]
618 link_dict
= self
.DCNetwork_graph
[vnf_src_name
][connected_sw
]
619 vnf_src_interface
= link_dict
[0]['src_port_id']
621 for connected_sw
in self
.DCNetwork_graph
.neighbors(vnf_src_name
):
622 link_dict
= self
.DCNetwork_graph
[vnf_src_name
][connected_sw
]
623 for link
in link_dict
:
624 if (link_dict
[link
]['src_port_id'] == vnf_src_interface
or
625 link_dict
[link
]['src_port_name'] == vnf_src_interface
): # Fix: we might also get interface names, e.g, from a son-emu-cli call
626 # found the right link and connected switch
627 src_sw
= connected_sw
628 src_sw_inport_nr
= link_dict
[link
]['dst_port_nr']
629 src_sw_inport_name
= link_dict
[link
]['dst_port_name']
632 if vnf_dst_interface
is None:
633 # take first interface by default
634 connected_sw
= self
.DCNetwork_graph
.neighbors(vnf_dst_name
)[0]
635 link_dict
= self
.DCNetwork_graph
[connected_sw
][vnf_dst_name
]
636 vnf_dst_interface
= link_dict
[0]['dst_port_id']
638 vnf_dst_name
= vnf_dst_name
.split(':')[0]
639 for connected_sw
in self
.DCNetwork_graph
.neighbors(vnf_dst_name
):
640 link_dict
= self
.DCNetwork_graph
[connected_sw
][vnf_dst_name
]
641 for link
in link_dict
:
642 if link_dict
[link
]['dst_port_id'] == vnf_dst_interface
or \
643 link_dict
[link
]['dst_port_name'] == vnf_dst_interface
: # Fix: we might also get interface names, e.g, from a son-emu-cli call
644 # found the right link and connected switch
645 dst_sw
= connected_sw
646 dst_sw_outport_nr
= link_dict
[link
]['src_port_nr']
647 dst_sw_outport_name
= link_dict
[link
]['src_port_name']
650 path
= kwargs
.get('path')
654 # returns the first found shortest path
655 # if all shortest paths are wanted, use: all_shortest_paths
656 path
= nx
.shortest_path(
657 self
.DCNetwork_graph
, src_sw
, dst_sw
, weight
=kwargs
.get('weight'))
658 except BaseException
:
659 LOG
.exception("No path could be found between {0} and {1} using src_sw={2} and dst_sw={3}".format(
660 vnf_src_name
, vnf_dst_name
, src_sw
, dst_sw
))
661 LOG
.debug("Graph nodes: %r" % self
.DCNetwork_graph
.nodes())
662 LOG
.debug("Graph edges: %r" % self
.DCNetwork_graph
.edges())
663 for e
, v
in self
.DCNetwork_graph
.edges():
664 LOG
.debug("%r" % self
.DCNetwork_graph
[e
][v
])
665 return "No path could be found between {0} and {1}".format(
666 vnf_src_name
, vnf_dst_name
)
668 LOG
.debug("Creating path between {0} and {1}: {2}".format(
669 vnf_src_name
, vnf_dst_name
, path
))
672 switch_inport_nr
= src_sw_inport_nr
675 cmd
= kwargs
.get('cmd')
677 if cmd
== 'add-flow':
678 if kwargs
.get('tag'):
679 # use pre-defined tag
680 vlan
= kwargs
.get('tag')
682 vlan
= self
.vlans
.pop()
684 # store the used vlan tag to identify this chain
685 if not kwargs
.get('monitor'):
687 chain_dict
['vnf_src_name'] = vnf_src_name
688 chain_dict
['vnf_dst_name'] = vnf_dst_name
689 chain_dict
['vnf_src_interface'] = vnf_src_interface
690 chain_dict
['vnf_dst_interface'] = vnf_dst_interface
691 chain_dict
['tag'] = vlan
692 self
.installed_chains
.append(chain_dict
)
694 # iterate through the path to install the flow-entries
695 for i
in range(0, len(path
)):
696 current_node
= self
.getNodeByName(current_hop
)
698 if i
< len(path
) - 1:
699 next_hop
= path
[i
+ 1]
701 # last switch reached
702 next_hop
= vnf_dst_name
704 next_node
= self
.getNodeByName(next_hop
)
706 if next_hop
== vnf_dst_name
:
707 switch_outport_nr
= dst_sw_outport_nr
708 LOG
.debug("end node reached: {0}".format(vnf_dst_name
))
709 elif not isinstance(next_node
, OVSSwitch
):
710 LOG
.info("Next node: {0} is not a switch".format(next_hop
))
711 return "Next node: {0} is not a switch".format(next_hop
)
713 # take first link between switches by default
715 switch_outport_nr
= self
.DCNetwork_graph
[current_hop
][next_hop
][index_edge_out
]['src_port_nr']
718 if isinstance(current_node
, OVSSwitch
):
719 kwargs
['vlan'] = vlan
720 kwargs
['path'] = path
721 kwargs
['current_hop'] = current_hop
722 kwargs
['switch_inport_name'] = src_sw_inport_name
723 kwargs
['switch_outport_name'] = dst_sw_outport_name
724 kwargs
['pathindex'] = i
726 if self
.controller
== RemoteController
:
727 # set flow entry via ryu rest api
728 self
._set
_flow
_entry
_ryu
_rest
(
729 current_node
, switch_inport_nr
, switch_outport_nr
, **kwargs
)
731 # set flow entry via ovs-ofctl
732 self
._set
_flow
_entry
_dpctl
(
733 current_node
, switch_inport_nr
, switch_outport_nr
, **kwargs
)
735 # take first link between switches by default
736 if isinstance(next_node
, OVSSwitch
):
737 switch_inport_nr
= self
.DCNetwork_graph
[current_hop
][next_hop
][0]['dst_port_nr']
738 current_hop
= next_hop
741 'priority': kwargs
.get('priority', DEFAULT_PRIORITY
),
742 'cookie': kwargs
.get('cookie', DEFAULT_COOKIE
),
743 'vlan': kwargs
['vlan'],
744 'path': kwargs
['path'],
745 'match_input': kwargs
.get('match')
747 flow_options_str
= json
.dumps(flow_options
, indent
=1)
748 LOG
.info("Installed flow rule: ({}:{}) -> ({}:{}) with options: {}"
749 .format(vnf_src_name
, vnf_src_interface
, vnf_dst_name
, vnf_dst_interface
, flow_options
))
750 return "success: {2} between {0} and {1} with options: {3}".format(
751 vnf_src_name
, vnf_dst_name
, cmd
, flow_options_str
)
753 def _set_flow_entry_ryu_rest(
754 self
, node
, switch_inport_nr
, switch_outport_nr
, **kwargs
):
755 match
= 'in_port=%s' % switch_inport_nr
757 cookie
= kwargs
.get('cookie')
758 match_input
= kwargs
.get('match')
759 cmd
= kwargs
.get('cmd')
760 path
= kwargs
.get('path')
761 index
= kwargs
.get('pathindex')
762 mod_dl_dst
= kwargs
.get('mod_dl_dst')
764 vlan
= kwargs
.get('vlan')
765 priority
= kwargs
.get('priority', DEFAULT_PRIORITY
)
766 # flag to not set the ovs port vlan tag
767 skip_vlan_tag
= kwargs
.get('skip_vlan_tag')
768 # table id to put this flowentry
769 table_id
= kwargs
.get('table_id')
775 match
= s
.join([match
, match_input
])
778 flow
['dpid'] = int(node
.dpid
, 16)
781 flow
['cookie'] = int(cookie
)
783 flow
['priority'] = int(priority
)
785 flow
['table_id'] = table_id
789 # possible Ryu actions, match fields:
790 # http://ryu.readthedocs.io/en/latest/app/ofctl_rest.html#add-a-flow-entry
791 if cmd
== 'add-flow':
792 prefix
= 'stats/flowentry/add'
794 if index
== 0: # first node
795 # set vlan tag in ovs instance (to isolate E-LANs)
796 if not skip_vlan_tag
:
797 in_port_name
= kwargs
.get('switch_inport_name')
798 self
._set
_vlan
_tag
(node
, in_port_name
, vlan
)
799 # set vlan push action if more than 1 switch in the path
802 # Push a new VLAN tag if a input frame is
804 action
['type'] = 'PUSH_VLAN'
805 # Ethertype 0x8100(=33024): IEEE 802.1Q VLAN-tagged
807 action
['ethertype'] = 33024
808 flow
['actions'].append(action
)
810 action
['type'] = 'SET_FIELD'
811 action
['field'] = 'vlan_vid'
812 # ryu expects the field to be masked
813 action
['value'] = vlan |
0x1000
814 flow
['actions'].append(action
)
816 elif index
== len(path
) - 1: # last node
817 # set vlan tag in ovs instance (to isolate E-LANs)
818 if not skip_vlan_tag
:
819 out_port_name
= kwargs
.get('switch_outport_name')
820 self
._set
_vlan
_tag
(node
, out_port_name
, vlan
)
821 # set vlan pop action if more than 1 switch in the path
823 match
+= ',dl_vlan=%s' % vlan
825 action
['type'] = 'POP_VLAN'
826 flow
['actions'].append(action
)
829 match
+= ',dl_vlan=%s' % vlan
832 action
['type'] = 'SET_FIELD'
833 action
['field'] = 'eth_dst'
834 action
['value'] = mod_dl_dst
835 flow
['actions'].append(action
)
837 # output action must come last
839 action
['type'] = 'OUTPUT'
840 action
['port'] = switch_outport_nr
841 flow
['actions'].append(action
)
843 elif cmd
== 'del-flows':
844 prefix
= 'stats/flowentry/delete'
847 # TODO: add cookie_mask as argument
848 # need full mask to match complete cookie
849 flow
['cookie_mask'] = int('0xffffffffffffffff', 16)
852 action
['type'] = 'OUTPUT'
853 action
['port'] = switch_outport_nr
854 flow
['actions'].append(action
)
856 flow
['match'] = self
._parse
_match
(match
)
857 self
.ryu_REST(prefix
, data
=flow
)
859 def _set_vlan_tag(self
, node
, switch_port
, tag
):
860 node
.vsctl('set', 'port {0} tag={1}'.format(switch_port
, tag
))
861 LOG
.debug("set vlan in switch: {0} in_port: {1} vlan tag: {2}".format(
862 node
.name
, switch_port
, tag
))
864 def _set_flow_entry_dpctl(
865 self
, node
, switch_inport_nr
, switch_outport_nr
, **kwargs
):
867 match
= 'in_port=%s' % switch_inport_nr
869 cookie
= kwargs
.get('cookie')
870 match_input
= kwargs
.get('match')
871 cmd
= kwargs
.get('cmd')
872 path
= kwargs
.get('path')
873 index
= kwargs
.get('pathindex')
874 vlan
= kwargs
.get('vlan')
878 cookie
= 'cookie=%s' % cookie
879 match
= s
.join([cookie
, match
])
881 match
= s
.join([match
, match_input
])
882 if cmd
== 'add-flow':
883 action
= 'action=%s' % switch_outport_nr
885 if index
== 0: # first node
886 action
= ('action=mod_vlan_vid:%s' % vlan
) + \
887 (',output=%s' % switch_outport_nr
)
888 match
= '-O OpenFlow13 ' + match
889 elif index
== len(path
) - 1: # last node
890 match
+= ',dl_vlan=%s' % vlan
891 action
= 'action=strip_vlan,output=%s' % switch_outport_nr
893 match
+= ',dl_vlan=%s' % vlan
894 ofcmd
= s
.join([match
, action
])
895 elif cmd
== 'del-flows':
900 node
.dpctl(cmd
, ofcmd
)
901 LOG
.info("{3} in switch: {0} in_port: {1} out_port: {2}".format(node
.name
, switch_inport_nr
,
902 switch_outport_nr
, cmd
))
904 # start Ryu Openflow controller as Remote Controller for the DCNetwork
905 def startRyu(self
, learning_switch
=True):
906 # start Ryu controller with rest-API
908 # ryu default learning switch
909 # ryu_learning_app = python_install_path + '/ryu/app/simple_switch_13.py'
910 # custom learning switch that installs a default NORMAL action in the
912 dir_path
= os
.path
.dirname(os
.path
.realpath(__file__
))
913 ryu_learning_app
= dir_path
+ '/son_emu_simple_switch_13.py'
914 ryu_rest_app
= 'ryu.app.ofctl_rest'
915 # change the default Openflow controller port to 6653 (official IANA-assigned port number), as used by Mininet
916 # Ryu still uses 6633 as default
917 ryu_option
= '--ofp-tcp-listen-port'
919 ryu_cmd
= 'ryu-manager'
920 FNULL
= open("/tmp/ryu.log", 'w')
922 # learning and rest api
923 args
= [ryu_cmd
, ryu_learning_app
, ryu_rest_app
, ryu_option
, ryu_of_port
]
925 # no learning switch, but with rest api
926 args
= [ryu_cmd
, ryu_rest_app
, ryu_option
, ryu_of_port
]
927 self
.ryu_process
= Popen(args
, stdout
=FNULL
, stderr
=FNULL
)
928 LOG
.debug('starting ryu-controller with %s' % args
)
933 Stop the Ryu controller that might be started by son-emu.
937 if self
.ryu_process
is not None:
938 self
.ryu_process
.terminate()
939 self
.ryu_process
.kill()
940 # ensure its death ;-)
941 Popen(['pkill', '-f', 'ryu-manager'])
943 def ryu_REST(self
, prefix
, dpid
=None, data
=None):
946 url
= self
.ryu_REST_api
+ '/' + str(prefix
) + '/' + str(dpid
)
948 url
= self
.ryu_REST_api
+ '/' + str(prefix
)
950 req
= self
.RyuSession
.post(url
, json
=data
)
952 req
= self
.RyuSession
.get(url
)
954 # do extra logging if status code is not 200 (OK)
955 if req
.status_code
is not requests
.codes
.ok
:
957 'type {0} encoding: {1} text: {2} headers: {3} history: {4}'.format(req
.headers
['content-type'],
958 req
.encoding
, req
.text
,
959 req
.headers
, req
.history
))
960 LOG
.info('url: {0}'.format(str(url
)))
962 LOG
.info('POST: {0}'.format(str(data
)))
963 LOG
.info('status: {0} reason: {1}'.format(
964 req
.status_code
, req
.reason
))
966 if 'json' in req
.headers
['content-type']:
970 ret
= req
.text
.rstrip()
973 # need to respect that some match fields must be integers
974 # http://ryu.readthedocs.io/en/latest/app/ofctl_rest.html#description-of-match-and-actions
976 def _parse_match(self
, match
):
977 matches
= match
.split(',')
983 m2
= int(match
[1], 0)
984 except BaseException
:
987 dict.update({match
[0]: m2
})
990 def find_connected_dc_interface(
991 self
, vnf_src_name
, vnf_src_interface
=None):
993 if vnf_src_interface
is None:
994 # take first interface by default
995 connected_sw
= self
.DCNetwork_graph
.neighbors(vnf_src_name
)[0]
996 link_dict
= self
.DCNetwork_graph
[vnf_src_name
][connected_sw
]
997 vnf_src_interface
= link_dict
[0]['src_port_id']
999 for connected_sw
in self
.DCNetwork_graph
.neighbors(vnf_src_name
):
1000 link_dict
= self
.DCNetwork_graph
[vnf_src_name
][connected_sw
]
1001 for link
in link_dict
:
1002 if (link_dict
[link
]['src_port_id'] == vnf_src_interface
or
1003 link_dict
[link
]['src_port_name'] == vnf_src_interface
):
1004 # Fix: we might also get interface names, e.g, from a son-emu-cli call
1005 # found the right link and connected switch
1006 src_sw_inport_name
= link_dict
[link
]['dst_port_name']
1007 return src_sw_inport_name