1f30ba7abfcce8422fcd8189622944049d53edcd
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).
30 from subprocess
import Popen
36 from mininet
.net
import Containernet
37 from mininet
.node
import OVSSwitch
, OVSKernelSwitch
, Docker
, RemoteController
38 from mininet
.cli
import CLI
39 from mininet
.link
import TCLink
40 from mininet
.clean
import cleanup
42 from emuvim
.dcemulator
.monitoring
import DCNetworkMonitor
43 from emuvim
.dcemulator
.node
import Datacenter
, EmulatorCompute
44 from emuvim
.dcemulator
.resourcemodel
import ResourceModelRegistrar
46 LOG
= logging
.getLogger("dcemulator.net")
47 LOG
.setLevel(logging
.DEBUG
)
49 # default CPU period used for cpu percentage-based cfs values (microseconds)
52 # default priority setting for added flow-rules
53 DEFAULT_PRIORITY
= 1000
54 # default cookie number for new flow-rules
58 class DCNetwork(Containernet
):
60 Wraps the original Mininet/Containernet class and provides
61 methods to add data centers, switches, etc.
63 This class is used by topology definition scripts.
66 def __init__(self
, controller
=RemoteController
, monitor
=False,
67 enable_learning
=False,
68 # learning switch behavior of the default ovs switches icw Ryu
69 # controller can be turned off/on, needed for E-LAN
71 dc_emulation_max_cpu
=1.0, # fraction of overall CPU time for emulation
72 dc_emulation_max_mem
=512, # emulation max mem in MB
75 Create an extended version of a Containernet network
76 :param dc_emulation_max_cpu: max. CPU time used by containers in data centers
77 :param kwargs: path through for Mininet parameters
82 self
.ryu_process
= None
83 # list of deployed nsds.E_Lines and E_LANs (uploaded from the dummy
85 self
.deployed_nsds
= []
86 self
.deployed_elines
= []
87 self
.deployed_elans
= []
88 self
.installed_chains
= []
90 # always cleanup environment before we start the emulator
94 # call original Docker.__init__ and setup default controller
95 Containernet
.__init
__(
96 self
, switch
=OVSKernelSwitch
, controller
=controller
, **kwargs
)
98 # default switch configuration
99 enable_ryu_learning
= False
101 self
.failMode
= 'standalone'
102 enable_ryu_learning
= True
104 self
.failMode
= 'secure'
107 if controller
== RemoteController
:
108 # start Ryu controller
109 self
.startRyu(learning_switch
=enable_ryu_learning
)
111 # add the specified controller
112 self
.addController('c0', controller
=controller
)
114 # graph of the complete DC network
115 self
.DCNetwork_graph
= nx
.MultiDiGraph()
117 # initialize pool of vlan tags to setup the SDN paths
118 self
.vlans
= range(1, 4095)[::-1]
120 # link to Ryu REST_API
123 self
.ryu_REST_api
= 'http://{0}:{1}'.format(ryu_ip
, ryu_port
)
124 self
.RyuSession
= requests
.Session()
128 self
.monitor_agent
= DCNetworkMonitor(self
)
130 self
.monitor_agent
= None
132 # initialize resource model registrar
133 self
.rm_registrar
= ResourceModelRegistrar(
134 dc_emulation_max_cpu
, dc_emulation_max_mem
)
135 self
.cpu_period
= CPU_PERIOD
137 def addDatacenter(self
, label
, metadata
={}, resource_log_path
=None):
139 Create and add a logical cloud data center to the network.
141 if label
in self
.dcs
:
142 raise Exception("Data center label already exists: %s" % label
)
143 dc
= Datacenter(label
, metadata
=metadata
,
144 resource_log_path
=resource_log_path
)
145 dc
.net
= self
# set reference to network
147 dc
.create() # finally create the data center in our Mininet instance
148 LOG
.info("added data center: %s" % label
)
151 def addLink(self
, node1
, node2
, **params
):
153 Able to handle Datacenter objects as link
156 assert node1
is not None
157 assert node2
is not None
159 # ensure type of node1
160 if isinstance(node1
, basestring
):
161 if node1
in self
.dcs
:
162 node1
= self
.dcs
[node1
].switch
163 if isinstance(node1
, Datacenter
):
165 # ensure type of node2
166 if isinstance(node2
, basestring
):
167 if node2
in self
.dcs
:
168 node2
= self
.dcs
[node2
].switch
169 if isinstance(node2
, Datacenter
):
171 # try to give containers a default IP
172 if isinstance(node1
, Docker
):
173 if "params1" not in params
:
174 params
["params1"] = {}
175 if "ip" not in params
["params1"]:
176 params
["params1"]["ip"] = self
.getNextIp()
177 if isinstance(node2
, Docker
):
178 if "params2" not in params
:
179 params
["params2"] = {}
180 if "ip" not in params
["params2"]:
181 params
["params2"]["ip"] = self
.getNextIp()
182 # ensure that we allow TCLinks between data centers
183 # TODO this is not optimal, we use cls=Link for containers and TCLink for data centers
184 # see Containernet issue:
185 # https://github.com/mpeuster/containernet/issues/3
186 if "cls" not in params
:
187 params
["cls"] = TCLink
189 link
= Containernet
.addLink(self
, node1
, node2
, **params
)
191 # try to give container interfaces a default id
192 node1_port_id
= node1
.ports
[link
.intf1
]
193 if isinstance(node1
, Docker
):
194 if "id" in params
["params1"]:
195 node1_port_id
= params
["params1"]["id"]
196 node1_port_name
= link
.intf1
.name
198 node2_port_id
= node2
.ports
[link
.intf2
]
199 if isinstance(node2
, Docker
):
200 if "id" in params
["params2"]:
201 node2_port_id
= params
["params2"]["id"]
202 node2_port_name
= link
.intf2
.name
204 # add edge and assigned port number to graph in both directions between node1 and node2
205 # port_id: id given in descriptor (if available, otherwise same as port)
206 # port: portnumber assigned by Containernet
209 # possible weight metrics allowed by TClink class:
210 weight_metrics
= ['bw', 'delay', 'jitter', 'loss']
211 edge_attributes
= [p
for p
in params
if p
in weight_metrics
]
212 for attr
in edge_attributes
:
213 # if delay: strip ms (need number as weight in graph)
214 match
= re
.search('([0-9]*\.?[0-9]+)', str(params
[attr
]))
216 attr_number
= match
.group(1)
219 attr_dict
[attr
] = attr_number
221 attr_dict2
= {'src_port_id': node1_port_id
, 'src_port_nr': node1
.ports
[link
.intf1
],
222 'src_port_name': node1_port_name
,
223 'dst_port_id': node2_port_id
, 'dst_port_nr': node2
.ports
[link
.intf2
],
224 'dst_port_name': node2_port_name
}
225 attr_dict2
.update(attr_dict
)
226 self
.DCNetwork_graph
.add_edge(
227 node1
.name
, node2
.name
, attr_dict
=attr_dict2
)
229 attr_dict2
= {'src_port_id': node2_port_id
, 'src_port_nr': node2
.ports
[link
.intf2
],
230 'src_port_name': node2_port_name
,
231 'dst_port_id': node1_port_id
, 'dst_port_nr': node1
.ports
[link
.intf1
],
232 'dst_port_name': node1_port_name
}
233 attr_dict2
.update(attr_dict
)
234 self
.DCNetwork_graph
.add_edge(
235 node2
.name
, node1
.name
, attr_dict
=attr_dict2
)
237 LOG
.debug("addLink: n1={0} intf1={1} -- n2={2} intf2={3}".format(
238 str(node1
), node1_port_name
, str(node2
), node2_port_name
))
242 def removeLink(self
, link
=None, node1
=None, node2
=None):
244 Remove the link from the Containernet and the networkx graph
247 node1
= link
.intf1
.node
248 node2
= link
.intf2
.node
249 assert node1
is not None
250 assert node2
is not None
251 Containernet
.removeLink(self
, link
=link
, node1
=node1
, node2
=node2
)
252 # TODO we might decrease the loglevel to debug:
254 self
.DCNetwork_graph
.remove_edge(node2
.name
, node1
.name
)
255 except BaseException
:
256 LOG
.warning("%s, %s not found in DCNetwork_graph." %
257 ((node2
.name
, node1
.name
)))
259 self
.DCNetwork_graph
.remove_edge(node1
.name
, node2
.name
)
260 except BaseException
:
261 LOG
.warning("%s, %s not found in DCNetwork_graph." %
262 ((node1
.name
, node2
.name
)))
264 def addDocker(self
, label
, **params
):
266 Wrapper for addDocker method to use custom container class.
268 self
.DCNetwork_graph
.add_node(label
, type=params
.get('type', 'docker'))
269 return Containernet
.addDocker(
270 self
, label
, cls
=EmulatorCompute
, **params
)
272 def removeDocker(self
, label
, **params
):
274 Wrapper for removeDocker method to update graph.
276 self
.DCNetwork_graph
.remove_node(label
)
277 return Containernet
.removeDocker(self
, label
, **params
)
279 def addExtSAP(self
, sap_name
, sap_ip
, **params
):
281 Wrapper for addExtSAP method to store SAP also in graph.
283 # make sure that 'type' is set
284 params
['type'] = params
.get('type', 'sap_ext')
285 self
.DCNetwork_graph
.add_node(sap_name
, type=params
['type'])
286 return Containernet
.addExtSAP(self
, sap_name
, sap_ip
, **params
)
288 def removeExtSAP(self
, sap_name
, **params
):
290 Wrapper for removeExtSAP method to remove SAP also from graph.
292 self
.DCNetwork_graph
.remove_node(sap_name
)
293 return Containernet
.removeExtSAP(self
, sap_name
)
295 def addSwitch(self
, name
, add_to_graph
=True, **params
):
297 Wrapper for addSwitch method to store switch also in graph.
300 # add this switch to the global topology overview
302 self
.DCNetwork_graph
.add_node(
303 name
, type=params
.get('type', 'switch'))
305 # set the learning switch behavior
306 if 'failMode' in params
:
307 failMode
= params
['failMode']
309 failMode
= self
.failMode
311 s
= Containernet
.addSwitch(
312 self
, name
, protocols
='OpenFlow10,OpenFlow12,OpenFlow13', failMode
=failMode
, **params
)
316 def getAllContainers(self
):
318 Returns a list with all containers within all data centers.
321 for dc
in self
.dcs
.itervalues():
322 all_containers
+= dc
.listCompute()
323 return all_containers
327 for dc
in self
.dcs
.itervalues():
329 Containernet
.start(self
)
333 # stop the monitor agent
334 if self
.monitor_agent
is not None:
335 self
.monitor_agent
.stop()
338 Containernet
.stop(self
)
340 # stop Ryu controller
346 def setLAN(self
, vnf_list
):
348 setup an E-LAN network by assigning the same VLAN tag to each DC interface of the VNFs in the E-LAN
350 :param vnf_list: names of the VNFs in this E-LAN [{name:,interface:},...]
354 src_sw_inport_name
= None
356 # get a vlan tag for this E-LAN
357 vlan
= self
.vlans
.pop()
360 vnf_src_name
= vnf
['name']
361 vnf_src_interface
= vnf
['interface']
363 # check if port is specified (vnf:port)
364 if vnf_src_interface
is None:
365 # take first interface by default
366 connected_sw
= self
.DCNetwork_graph
.neighbors(vnf_src_name
)[0]
367 link_dict
= self
.DCNetwork_graph
[vnf_src_name
][connected_sw
]
368 vnf_src_interface
= link_dict
[0]['src_port_id']
370 for connected_sw
in self
.DCNetwork_graph
.neighbors(vnf_src_name
):
371 link_dict
= self
.DCNetwork_graph
[vnf_src_name
][connected_sw
]
372 for link
in link_dict
:
373 if (link_dict
[link
]['src_port_id'] == vnf_src_interface
or
374 link_dict
[link
]['src_port_name'] == vnf_src_interface
): # Fix: we might also get interface names, e.g, from a son-emu-cli call
375 # found the right link and connected switch
376 src_sw
= connected_sw
377 src_sw_inport_name
= link_dict
[link
]['dst_port_name']
380 # set the tag on the dc switch interface
381 LOG
.debug('set E-LAN: vnf name: {0} interface: {1} tag: {2}'.format(
382 vnf_src_name
, vnf_src_interface
, vlan
))
383 switch_node
= self
.getNodeByName(src_sw
)
384 self
._set
_vlan
_tag
(switch_node
, src_sw_inport_name
, vlan
)
386 def _addMonitorFlow(self
, vnf_src_name
, vnf_dst_name
, vnf_src_interface
=None, vnf_dst_interface
=None,
389 Add a monitoring flow entry that adds a special flowentry/counter at the begin or end of a chain.
390 So this monitoring flowrule exists on top of a previously defined chain rule and uses the same vlan tag/routing.
393 :param vnf_src_interface:
394 :param vnf_dst_interface:
395 :param tag: vlan tag to be used for this chain (same tag as existing chain)
396 :param monitor_placement: 'tx' or 'rx' indicating to place the extra flowentry resp. at the beginning or end of the chain
402 src_sw_inport_name
= None
404 dst_sw_outport_nr
= 0
405 dst_sw_outport_name
= None
407 LOG
.debug("call AddMonitorFlow vnf_src_name=%r, vnf_src_interface=%r, vnf_dst_name=%r, vnf_dst_interface=%r",
408 vnf_src_name
, vnf_src_interface
, vnf_dst_name
, vnf_dst_interface
)
410 # check if port is specified (vnf:port)
411 if vnf_src_interface
is None:
412 # take first interface by default
413 connected_sw
= self
.DCNetwork_graph
.neighbors(vnf_src_name
)[0]
414 link_dict
= self
.DCNetwork_graph
[vnf_src_name
][connected_sw
]
415 vnf_src_interface
= link_dict
[0]['src_port_id']
417 for connected_sw
in self
.DCNetwork_graph
.neighbors(vnf_src_name
):
418 link_dict
= self
.DCNetwork_graph
[vnf_src_name
][connected_sw
]
419 for link
in link_dict
:
420 if (link_dict
[link
]['src_port_id'] == vnf_src_interface
or
421 link_dict
[link
]['src_port_name'] == vnf_src_interface
): # Fix: we might also get interface names, e.g, from a son-emu-cli call
422 # found the right link and connected switch
423 src_sw
= connected_sw
424 src_sw_inport_nr
= link_dict
[link
]['dst_port_nr']
425 src_sw_inport_name
= link_dict
[link
]['dst_port_name']
428 if vnf_dst_interface
is None:
429 # take first interface by default
430 connected_sw
= self
.DCNetwork_graph
.neighbors(vnf_dst_name
)[0]
431 link_dict
= self
.DCNetwork_graph
[connected_sw
][vnf_dst_name
]
432 vnf_dst_interface
= link_dict
[0]['dst_port_id']
434 vnf_dst_name
= vnf_dst_name
.split(':')[0]
435 for connected_sw
in self
.DCNetwork_graph
.neighbors(vnf_dst_name
):
436 link_dict
= self
.DCNetwork_graph
[connected_sw
][vnf_dst_name
]
437 for link
in link_dict
:
438 if link_dict
[link
]['dst_port_id'] == vnf_dst_interface
or \
439 link_dict
[link
]['dst_port_name'] == vnf_dst_interface
: # Fix: we might also get interface names, e.g, from a son-emu-cli call
440 # found the right link and connected switch
441 dst_sw
= connected_sw
442 dst_sw_outport_nr
= link_dict
[link
]['src_port_nr']
443 dst_sw_outport_name
= link_dict
[link
]['src_port_name']
447 LOG
.exception('tag not valid: {0}'.format(tag
))
451 # returns the first found shortest path
452 # if all shortest paths are wanted, use: all_shortest_paths
453 path
= nx
.shortest_path(
454 self
.DCNetwork_graph
, src_sw
, dst_sw
, weight
=kwargs
.get('weight'))
455 except BaseException
:
456 LOG
.exception("No path could be found between {0} and {1} using src_sw={2} and dst_sw={3}".format(
457 vnf_src_name
, vnf_dst_name
, src_sw
, dst_sw
))
458 LOG
.debug("Graph nodes: %r" % self
.DCNetwork_graph
.nodes())
459 LOG
.debug("Graph edges: %r" % self
.DCNetwork_graph
.edges())
460 for e
, v
in self
.DCNetwork_graph
.edges():
461 LOG
.debug("%r" % self
.DCNetwork_graph
[e
][v
])
462 return "No path could be found between {0} and {1}".format(
463 vnf_src_name
, vnf_dst_name
)
465 LOG
.debug("Creating path between {0} and {1}: {2}".format(
466 vnf_src_name
, vnf_dst_name
, path
))
469 switch_inport_nr
= src_sw_inport_nr
471 cmd
= kwargs
.get('cmd')
473 # iterate through the path to install the flow-entries
474 for i
in range(0, len(path
)):
475 current_node
= self
.getNodeByName(current_hop
)
477 if path
.index(current_hop
) < len(path
) - 1:
478 next_hop
= path
[path
.index(current_hop
) + 1]
480 # last switch reached
481 next_hop
= vnf_dst_name
483 next_node
= self
.getNodeByName(next_hop
)
485 if next_hop
== vnf_dst_name
:
486 switch_outport_nr
= dst_sw_outport_nr
487 LOG
.debug("end node reached: {0}".format(vnf_dst_name
))
488 elif not isinstance(next_node
, OVSSwitch
):
489 LOG
.info("Next node: {0} is not a switch".format(next_hop
))
490 return "Next node: {0} is not a switch".format(next_hop
)
492 # take first link between switches by default
494 switch_outport_nr
= self
.DCNetwork_graph
[current_hop
][next_hop
][index_edge_out
]['src_port_nr']
496 # set of entry via ovs-ofctl
497 if isinstance(current_node
, OVSSwitch
):
499 kwargs
['path'] = path
500 kwargs
['current_hop'] = current_hop
501 kwargs
['switch_inport_name'] = src_sw_inport_name
502 kwargs
['switch_outport_name'] = dst_sw_outport_name
503 kwargs
['skip_vlan_tag'] = True
504 kwargs
['pathindex'] = i
506 monitor_placement
= kwargs
.get('monitor_placement').strip()
507 # put monitor flow at the dst switch
510 if monitor_placement
== 'tx' and path
.index(current_hop
) == 0:
512 # put monitoring flow at the src switch
514 elif monitor_placement
== 'rx' and path
.index(current_hop
) == len(path
) - 1:
516 elif monitor_placement
not in ['rx', 'tx']:
518 'invalid monitor command: {0}'.format(monitor_placement
))
520 if self
.controller
== RemoteController
and insert_flow
:
521 # set flow entry via ryu rest api
522 self
._set
_flow
_entry
_ryu
_rest
(
523 current_node
, switch_inport_nr
, switch_outport_nr
, **kwargs
)
526 # set flow entry via ovs-ofctl
527 self
._set
_flow
_entry
_dpctl
(
528 current_node
, switch_inport_nr
, switch_outport_nr
, **kwargs
)
531 # take first link between switches by default
532 if isinstance(next_node
, OVSSwitch
):
533 switch_inport_nr
= self
.DCNetwork_graph
[current_hop
][next_hop
][0]['dst_port_nr']
534 current_hop
= next_hop
536 return "path {2} between {0} and {1}".format(
537 vnf_src_name
, vnf_dst_name
, cmd
)
539 def setChain(self
, vnf_src_name
, vnf_dst_name
,
540 vnf_src_interface
=None, vnf_dst_interface
=None, **kwargs
):
542 Chain 2 vnf interfaces together by installing the flowrules in the switches along their path.
543 Currently the path is found using the default networkx shortest path function.
544 Each chain gets a unique vlan id , so different chains wil not interfere.
546 :param vnf_src_name: vnf name (string)
547 :param vnf_dst_name: vnf name (string)
548 :param vnf_src_interface: source interface name (string)
549 :param vnf_dst_interface: destination interface name (string)
550 :param cmd: 'add-flow' (default) to add a chain, 'del-flows' to remove a chain
551 :param cookie: cookie for the installed flowrules (can be used later as identifier for a set of installed chains)
552 :param match: custom match entry to be added to the flowrules (default: only in_port and vlan tag)
553 :param priority: custom flowrule priority
554 :param monitor: boolean to indicate whether this chain is a monitoring chain
555 :param tag: vlan tag to be used for this chain (pre-defined or new one if none is specified)
556 :param skip_vlan_tag: boolean to indicate if a vlan tag should be appointed to this flow or not
557 :param path: custom path between the two VNFs (list of switches)
558 :return: output log string
561 # special procedure for monitoring flows
562 if kwargs
.get('monitor'):
564 # check if chain already exists
565 found_chains
= [chain_dict
for chain_dict
in self
.installed_chains
if
566 (chain_dict
['vnf_src_name'] == vnf_src_name
and
567 chain_dict
['vnf_src_interface'] == vnf_src_interface
and
568 chain_dict
['vnf_dst_name'] == vnf_dst_name
and
569 chain_dict
['vnf_dst_interface'] == vnf_dst_interface
)]
571 if len(found_chains
) > 0:
572 # this chain exists, so need an extra monitoring flow
573 # assume only 1 chain per vnf/interface pair
574 LOG
.debug('*** installing monitoring chain on top of pre-defined chain from {0}:{1} -> {2}:{3}'.
575 format(vnf_src_name
, vnf_src_interface
, vnf_dst_name
, vnf_dst_interface
))
576 tag
= found_chains
[0]['tag']
577 ret
= self
._addMonitorFlow
(vnf_src_name
, vnf_dst_name
, vnf_src_interface
, vnf_dst_interface
,
578 tag
=tag
, table_id
=0, **kwargs
)
581 # no chain existing (or E-LAN) -> install normal chain
582 LOG
.warning('*** installing monitoring chain without pre-defined NSD chain from {0}:{1} -> {2}:{3}'.
583 format(vnf_src_name
, vnf_src_interface
, vnf_dst_name
, vnf_dst_interface
))
586 cmd
= kwargs
.get('cmd', 'add-flow')
587 if cmd
== 'add-flow' or cmd
== 'del-flows':
588 ret
= self
._chainAddFlow
(
589 vnf_src_name
, vnf_dst_name
, vnf_src_interface
, vnf_dst_interface
, **kwargs
)
590 if kwargs
.get('bidirectional'):
591 if kwargs
.get('path') is not None:
592 kwargs
['path'] = list(reversed(kwargs
.get('path')))
595 vnf_dst_name
, vnf_src_name
, vnf_dst_interface
, vnf_src_interface
, **kwargs
)
598 ret
= "Command unknown"
602 def _chainAddFlow(self
, vnf_src_name
, vnf_dst_name
,
603 vnf_src_interface
=None, vnf_dst_interface
=None, **kwargs
):
607 src_sw_inport_name
= None
609 dst_sw_outport_nr
= 0
610 dst_sw_outport_name
= None
612 LOG
.debug("call chainAddFlow vnf_src_name=%r, vnf_src_interface=%r, vnf_dst_name=%r, vnf_dst_interface=%r",
613 vnf_src_name
, vnf_src_interface
, vnf_dst_name
, vnf_dst_interface
)
615 # check if port is specified (vnf:port)
616 if vnf_src_interface
is None:
617 # take first interface by default
618 connected_sw
= self
.DCNetwork_graph
.neighbors(vnf_src_name
)[0]
619 link_dict
= self
.DCNetwork_graph
[vnf_src_name
][connected_sw
]
620 vnf_src_interface
= link_dict
[0]['src_port_id']
622 for connected_sw
in self
.DCNetwork_graph
.neighbors(vnf_src_name
):
623 link_dict
= self
.DCNetwork_graph
[vnf_src_name
][connected_sw
]
624 for link
in link_dict
:
625 if (link_dict
[link
]['src_port_id'] == vnf_src_interface
or
626 link_dict
[link
]['src_port_name'] == vnf_src_interface
): # Fix: we might also get interface names, e.g, from a son-emu-cli call
627 # found the right link and connected switch
628 src_sw
= connected_sw
629 src_sw_inport_nr
= link_dict
[link
]['dst_port_nr']
630 src_sw_inport_name
= link_dict
[link
]['dst_port_name']
633 if vnf_dst_interface
is None:
634 # take first interface by default
635 connected_sw
= self
.DCNetwork_graph
.neighbors(vnf_dst_name
)[0]
636 link_dict
= self
.DCNetwork_graph
[connected_sw
][vnf_dst_name
]
637 vnf_dst_interface
= link_dict
[0]['dst_port_id']
639 vnf_dst_name
= vnf_dst_name
.split(':')[0]
640 for connected_sw
in self
.DCNetwork_graph
.neighbors(vnf_dst_name
):
641 link_dict
= self
.DCNetwork_graph
[connected_sw
][vnf_dst_name
]
642 for link
in link_dict
:
643 if link_dict
[link
]['dst_port_id'] == vnf_dst_interface
or \
644 link_dict
[link
]['dst_port_name'] == vnf_dst_interface
: # Fix: we might also get interface names, e.g, from a son-emu-cli call
645 # found the right link and connected switch
646 dst_sw
= connected_sw
647 dst_sw_outport_nr
= link_dict
[link
]['src_port_nr']
648 dst_sw_outport_name
= link_dict
[link
]['src_port_name']
651 path
= kwargs
.get('path')
655 # returns the first found shortest path
656 # if all shortest paths are wanted, use: all_shortest_paths
657 path
= nx
.shortest_path(
658 self
.DCNetwork_graph
, src_sw
, dst_sw
, weight
=kwargs
.get('weight'))
659 except BaseException
:
660 LOG
.exception("No path could be found between {0} and {1} using src_sw={2} and dst_sw={3}".format(
661 vnf_src_name
, vnf_dst_name
, src_sw
, dst_sw
))
662 LOG
.debug("Graph nodes: %r" % self
.DCNetwork_graph
.nodes())
663 LOG
.debug("Graph edges: %r" % self
.DCNetwork_graph
.edges())
664 for e
, v
in self
.DCNetwork_graph
.edges():
665 LOG
.debug("%r" % self
.DCNetwork_graph
[e
][v
])
666 return "No path could be found between {0} and {1}".format(
667 vnf_src_name
, vnf_dst_name
)
669 LOG
.debug("Creating path between {0} and {1}: {2}".format(
670 vnf_src_name
, vnf_dst_name
, path
))
673 switch_inport_nr
= src_sw_inport_nr
676 cmd
= kwargs
.get('cmd')
678 if cmd
== 'add-flow':
679 if kwargs
.get('tag'):
680 # use pre-defined tag
681 vlan
= kwargs
.get('tag')
683 vlan
= self
.vlans
.pop()
685 # store the used vlan tag to identify this chain
686 if not kwargs
.get('monitor'):
688 chain_dict
['vnf_src_name'] = vnf_src_name
689 chain_dict
['vnf_dst_name'] = vnf_dst_name
690 chain_dict
['vnf_src_interface'] = vnf_src_interface
691 chain_dict
['vnf_dst_interface'] = vnf_dst_interface
692 chain_dict
['tag'] = vlan
693 self
.installed_chains
.append(chain_dict
)
695 # iterate through the path to install the flow-entries
696 for i
in range(0, len(path
)):
697 current_node
= self
.getNodeByName(current_hop
)
699 if i
< len(path
) - 1:
700 next_hop
= path
[i
+ 1]
702 # last switch reached
703 next_hop
= vnf_dst_name
705 next_node
= self
.getNodeByName(next_hop
)
707 if next_hop
== vnf_dst_name
:
708 switch_outport_nr
= dst_sw_outport_nr
709 LOG
.debug("end node reached: {0}".format(vnf_dst_name
))
710 elif not isinstance(next_node
, OVSSwitch
):
711 LOG
.info("Next node: {0} is not a switch".format(next_hop
))
712 return "Next node: {0} is not a switch".format(next_hop
)
714 # take first link between switches by default
716 switch_outport_nr
= self
.DCNetwork_graph
[current_hop
][next_hop
][index_edge_out
]['src_port_nr']
719 if isinstance(current_node
, OVSSwitch
):
720 kwargs
['vlan'] = vlan
721 kwargs
['path'] = path
722 kwargs
['current_hop'] = current_hop
723 kwargs
['switch_inport_name'] = src_sw_inport_name
724 kwargs
['switch_outport_name'] = dst_sw_outport_name
725 kwargs
['pathindex'] = i
727 if self
.controller
== RemoteController
:
728 # set flow entry via ryu rest api
729 self
._set
_flow
_entry
_ryu
_rest
(
730 current_node
, switch_inport_nr
, switch_outport_nr
, **kwargs
)
732 # set flow entry via ovs-ofctl
733 self
._set
_flow
_entry
_dpctl
(
734 current_node
, switch_inport_nr
, switch_outport_nr
, **kwargs
)
736 # take first link between switches by default
737 if isinstance(next_node
, OVSSwitch
):
738 switch_inport_nr
= self
.DCNetwork_graph
[current_hop
][next_hop
][0]['dst_port_nr']
739 current_hop
= next_hop
742 'priority': kwargs
.get('priority', DEFAULT_PRIORITY
),
743 'cookie': kwargs
.get('cookie', DEFAULT_COOKIE
),
744 'vlan': kwargs
['vlan'],
745 'path': kwargs
['path'],
746 'match_input': kwargs
.get('match')
748 flow_options_str
= json
.dumps(flow_options
, indent
=1)
749 LOG
.info("Installed flow rule: ({}:{}) -> ({}:{}) with options: {}"
750 .format(vnf_src_name
, vnf_src_interface
, vnf_dst_name
, vnf_dst_interface
, flow_options
))
751 return "success: {2} between {0} and {1} with options: {3}".format(
752 vnf_src_name
, vnf_dst_name
, cmd
, flow_options_str
)
754 def _set_flow_entry_ryu_rest(
755 self
, node
, switch_inport_nr
, switch_outport_nr
, **kwargs
):
756 match
= 'in_port=%s' % switch_inport_nr
758 cookie
= kwargs
.get('cookie')
759 match_input
= kwargs
.get('match')
760 cmd
= kwargs
.get('cmd')
761 path
= kwargs
.get('path')
762 index
= kwargs
.get('pathindex')
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
831 # output action must come last
833 action
['type'] = 'OUTPUT'
834 action
['port'] = switch_outport_nr
835 flow
['actions'].append(action
)
837 elif cmd
== 'del-flows':
838 prefix
= 'stats/flowentry/delete'
841 # TODO: add cookie_mask as argument
842 # need full mask to match complete cookie
843 flow
['cookie_mask'] = int('0xffffffffffffffff', 16)
846 action
['type'] = 'OUTPUT'
847 action
['port'] = switch_outport_nr
848 flow
['actions'].append(action
)
850 flow
['match'] = self
._parse
_match
(match
)
851 self
.ryu_REST(prefix
, data
=flow
)
853 def _set_vlan_tag(self
, node
, switch_port
, tag
):
854 node
.vsctl('set', 'port {0} tag={1}'.format(switch_port
, tag
))
855 LOG
.debug("set vlan in switch: {0} in_port: {1} vlan tag: {2}".format(
856 node
.name
, switch_port
, tag
))
858 def _set_flow_entry_dpctl(
859 self
, node
, switch_inport_nr
, switch_outport_nr
, **kwargs
):
861 match
= 'in_port=%s' % switch_inport_nr
863 cookie
= kwargs
.get('cookie')
864 match_input
= kwargs
.get('match')
865 cmd
= kwargs
.get('cmd')
866 path
= kwargs
.get('path')
867 index
= kwargs
.get('pathindex')
868 vlan
= kwargs
.get('vlan')
872 cookie
= 'cookie=%s' % cookie
873 match
= s
.join([cookie
, match
])
875 match
= s
.join([match
, match_input
])
876 if cmd
== 'add-flow':
877 action
= 'action=%s' % switch_outport_nr
879 if index
== 0: # first node
880 action
= ('action=mod_vlan_vid:%s' % vlan
) + \
881 (',output=%s' % switch_outport_nr
)
882 match
= '-O OpenFlow13 ' + match
883 elif index
== len(path
) - 1: # last node
884 match
+= ',dl_vlan=%s' % vlan
885 action
= 'action=strip_vlan,output=%s' % switch_outport_nr
887 match
+= ',dl_vlan=%s' % vlan
888 ofcmd
= s
.join([match
, action
])
889 elif cmd
== 'del-flows':
894 node
.dpctl(cmd
, ofcmd
)
895 LOG
.info("{3} in switch: {0} in_port: {1} out_port: {2}".format(node
.name
, switch_inport_nr
,
896 switch_outport_nr
, cmd
))
898 # start Ryu Openflow controller as Remote Controller for the DCNetwork
899 def startRyu(self
, learning_switch
=True):
900 # start Ryu controller with rest-API
901 python_install_path
= site
.getsitepackages()[0]
902 # ryu default learning switch
903 # ryu_path = python_install_path + '/ryu/app/simple_switch_13.py'
904 # custom learning switch that installs a default NORMAL action in the
906 dir_path
= os
.path
.dirname(os
.path
.realpath(__file__
))
907 ryu_path
= dir_path
+ '/son_emu_simple_switch_13.py'
908 ryu_path2
= python_install_path
+ '/ryu/app/ofctl_rest.py'
909 # change the default Openflow controller port to 6653 (official IANA-assigned port number), as used by Mininet
910 # Ryu still uses 6633 as default
911 ryu_option
= '--ofp-tcp-listen-port'
913 ryu_cmd
= 'ryu-manager'
914 FNULL
= open("/tmp/ryu.log", 'w')
916 self
.ryu_process
= Popen(
917 [ryu_cmd
, ryu_path
, ryu_path2
, ryu_option
, ryu_of_port
], stdout
=FNULL
, stderr
=FNULL
)
918 LOG
.debug('starting ryu-controller with {0}'.format(ryu_path
))
919 LOG
.debug('starting ryu-controller with {0}'.format(ryu_path2
))
921 # no learning switch, but with rest api
922 self
.ryu_process
= Popen(
923 [ryu_cmd
, ryu_path2
, ryu_option
, ryu_of_port
], stdout
=FNULL
, stderr
=FNULL
)
924 LOG
.debug('starting ryu-controller with {0}'.format(ryu_path2
))
929 Stop the Ryu controller that might be started by son-emu.
933 if self
.ryu_process
is not None:
934 self
.ryu_process
.terminate()
935 self
.ryu_process
.kill()
936 # ensure its death ;-)
937 Popen(['pkill', '-f', 'ryu-manager'])
939 def ryu_REST(self
, prefix
, dpid
=None, data
=None):
942 url
= self
.ryu_REST_api
+ '/' + str(prefix
) + '/' + str(dpid
)
944 url
= self
.ryu_REST_api
+ '/' + str(prefix
)
946 req
= self
.RyuSession
.post(url
, json
=data
)
948 req
= self
.RyuSession
.get(url
)
950 # do extra logging if status code is not 200 (OK)
951 if req
.status_code
is not requests
.codes
.ok
:
953 'type {0} encoding: {1} text: {2} headers: {3} history: {4}'.format(req
.headers
['content-type'],
954 req
.encoding
, req
.text
,
955 req
.headers
, req
.history
))
956 LOG
.info('url: {0}'.format(str(url
)))
958 LOG
.info('POST: {0}'.format(str(data
)))
959 LOG
.info('status: {0} reason: {1}'.format(
960 req
.status_code
, req
.reason
))
962 if 'json' in req
.headers
['content-type']:
966 ret
= req
.text
.rstrip()
969 # need to respect that some match fields must be integers
970 # http://ryu.readthedocs.io/en/latest/app/ofctl_rest.html#description-of-match-and-actions
972 def _parse_match(self
, match
):
973 matches
= match
.split(',')
979 m2
= int(match
[1], 0)
980 except BaseException
:
983 dict.update({match
[0]: m2
})
986 def find_connected_dc_interface(
987 self
, vnf_src_name
, vnf_src_interface
=None):
989 if vnf_src_interface
is None:
990 # take first interface by default
991 connected_sw
= self
.DCNetwork_graph
.neighbors(vnf_src_name
)[0]
992 link_dict
= self
.DCNetwork_graph
[vnf_src_name
][connected_sw
]
993 vnf_src_interface
= link_dict
[0]['src_port_id']
995 for connected_sw
in self
.DCNetwork_graph
.neighbors(vnf_src_name
):
996 link_dict
= self
.DCNetwork_graph
[vnf_src_name
][connected_sw
]
997 for link
in link_dict
:
998 if (link_dict
[link
]['src_port_id'] == vnf_src_interface
or
999 link_dict
[link
]['src_port_name'] == vnf_src_interface
):
1000 # Fix: we might also get interface names, e.g, from a son-emu-cli call
1001 # found the right link and connected switch
1002 src_sw_inport_name
= link_dict
[link
]['dst_port_name']
1003 return src_sw_inport_name