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).
33 from subprocess
import Popen
34 # from gevent import monkey
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
40 from emuvim
.dcemulator
.monitoring
import DCNetworkMonitor
41 from emuvim
.dcemulator
.node
import Datacenter
, EmulatorCompute
42 from emuvim
.dcemulator
.resourcemodel
import ResourceModelRegistrar
44 # ensure correct functionality of all gevent based REST servers
48 LOG
= logging
.getLogger("dcemulator.net")
49 LOG
.setLevel(logging
.DEBUG
)
51 # default CPU period used for cpu percentage-based cfs values (microseconds)
54 # default priority setting for added flow-rules
55 DEFAULT_PRIORITY
= 1000
56 # default cookie number for new flow-rules
60 class DCNetwork(Containernet
):
62 Wraps the original Mininet/Containernet class and provides
63 methods to add data centers, switches, etc.
65 This class is used by topology definition scripts.
68 def __init__(self
, controller
=RemoteController
, monitor
=False,
69 enable_learning
=False,
70 # learning switch behavior of the default ovs switches icw Ryu
71 # controller can be turned off/on, needed for E-LAN
73 dc_emulation_max_cpu
=1.0, # fraction of overall CPU time for emulation
74 dc_emulation_max_mem
=512, # emulation max mem in MB
77 Create an extended version of a Containernet network
78 :param dc_emulation_max_cpu: max. CPU time used by containers in data centers
79 :param kwargs: path through for Mininet parameters
84 self
.ryu_process
= None
85 # list of deployed nsds.E_Lines and E_LANs (uploaded from the dummy
87 self
.deployed_nsds
= []
88 self
.deployed_elines
= []
89 self
.deployed_elans
= []
90 self
.installed_chains
= []
92 # always cleanup environment before we start the emulator
96 # call original Docker.__init__ and setup default controller
97 Containernet
.__init
__(
98 self
, switch
=OVSKernelSwitch
, controller
=controller
, **kwargs
)
100 # default switch configuration
101 enable_ryu_learning
= False
103 self
.failMode
= 'standalone'
104 enable_ryu_learning
= True
106 self
.failMode
= 'secure'
109 if controller
== RemoteController
:
110 # start Ryu controller
111 self
.startRyu(learning_switch
=enable_ryu_learning
)
113 # add the specified controller
114 self
.addController('c0', controller
=controller
)
116 # graph of the complete DC network
117 self
.DCNetwork_graph
= nx
.MultiDiGraph()
119 # initialize pool of vlan tags to setup the SDN paths
120 self
.vlans
= range(1, 4095)[::-1]
122 # link to Ryu REST_API
125 self
.ryu_REST_api
= 'http://{0}:{1}'.format(ryu_ip
, ryu_port
)
126 self
.RyuSession
= requests
.Session()
130 self
.monitor_agent
= DCNetworkMonitor(self
)
132 self
.monitor_agent
= None
134 # initialize resource model registrar
135 self
.rm_registrar
= ResourceModelRegistrar(
136 dc_emulation_max_cpu
, dc_emulation_max_mem
)
137 self
.cpu_period
= CPU_PERIOD
139 def addDatacenter(self
, label
, metadata
={}, resource_log_path
=None):
141 Create and add a logical cloud data center to the network.
143 if label
in self
.dcs
:
144 raise Exception("Data center label already exists: %s" % label
)
145 dc
= Datacenter(label
, metadata
=metadata
,
146 resource_log_path
=resource_log_path
)
147 dc
.net
= self
# set reference to network
149 dc
.create() # finally create the data center in our Mininet instance
150 LOG
.info("added data center: %s" % label
)
153 def addLink(self
, node1
, node2
, **params
):
155 Able to handle Datacenter objects as link
158 assert node1
is not None
159 assert node2
is not None
161 # ensure type of node1
162 if isinstance(node1
, basestring
):
163 if node1
in self
.dcs
:
164 node1
= self
.dcs
[node1
].switch
165 if isinstance(node1
, Datacenter
):
167 # ensure type of node2
168 if isinstance(node2
, basestring
):
169 if node2
in self
.dcs
:
170 node2
= self
.dcs
[node2
].switch
171 if isinstance(node2
, Datacenter
):
173 # try to give containers a default IP
174 if isinstance(node1
, Docker
):
175 if "params1" not in params
:
176 params
["params1"] = {}
177 if "ip" not in params
["params1"]:
178 params
["params1"]["ip"] = self
.getNextIp()
179 if isinstance(node2
, Docker
):
180 if "params2" not in params
:
181 params
["params2"] = {}
182 if "ip" not in params
["params2"]:
183 params
["params2"]["ip"] = self
.getNextIp()
184 # ensure that we allow TCLinks between data centers
185 # TODO this is not optimal, we use cls=Link for containers and TCLink for data centers
186 # see Containernet issue:
187 # https://github.com/mpeuster/containernet/issues/3
188 if "cls" not in params
:
189 params
["cls"] = TCLink
191 link
= Containernet
.addLink(self
, node1
, node2
, **params
)
193 # try to give container interfaces a default id
194 node1_port_id
= node1
.ports
[link
.intf1
]
195 if isinstance(node1
, Docker
):
196 if "id" in params
["params1"]:
197 node1_port_id
= params
["params1"]["id"]
198 node1_port_name
= link
.intf1
.name
200 node2_port_id
= node2
.ports
[link
.intf2
]
201 if isinstance(node2
, Docker
):
202 if "id" in params
["params2"]:
203 node2_port_id
= params
["params2"]["id"]
204 node2_port_name
= link
.intf2
.name
206 # add edge and assigned port number to graph in both directions between node1 and node2
207 # port_id: id given in descriptor (if available, otherwise same as port)
208 # port: portnumber assigned by Containernet
211 # possible weight metrics allowed by TClink class:
212 weight_metrics
= ['bw', 'delay', 'jitter', 'loss']
213 edge_attributes
= [p
for p
in params
if p
in weight_metrics
]
214 for attr
in edge_attributes
:
215 # if delay: strip ms (need number as weight in graph)
216 match
= re
.search('([0-9]*\.?[0-9]+)', str(params
[attr
]))
218 attr_number
= match
.group(1)
221 attr_dict
[attr
] = attr_number
223 attr_dict2
= {'src_port_id': node1_port_id
, 'src_port_nr': node1
.ports
[link
.intf1
],
224 'src_port_name': node1_port_name
,
225 'dst_port_id': node2_port_id
, 'dst_port_nr': node2
.ports
[link
.intf2
],
226 'dst_port_name': node2_port_name
}
227 attr_dict2
.update(attr_dict
)
228 self
.DCNetwork_graph
.add_edge(
229 node1
.name
, node2
.name
, attr_dict
=attr_dict2
)
231 attr_dict2
= {'src_port_id': node2_port_id
, 'src_port_nr': node2
.ports
[link
.intf2
],
232 'src_port_name': node2_port_name
,
233 'dst_port_id': node1_port_id
, 'dst_port_nr': node1
.ports
[link
.intf1
],
234 'dst_port_name': node1_port_name
}
235 attr_dict2
.update(attr_dict
)
236 self
.DCNetwork_graph
.add_edge(
237 node2
.name
, node1
.name
, attr_dict
=attr_dict2
)
239 LOG
.debug("addLink: n1={0} intf1={1} -- n2={2} intf2={3}".format(
240 str(node1
), node1_port_name
, str(node2
), node2_port_name
))
244 def removeLink(self
, link
=None, node1
=None, node2
=None):
246 Remove the link from the Containernet and the networkx graph
249 node1
= link
.intf1
.node
250 node2
= link
.intf2
.node
251 assert node1
is not None
252 assert node2
is not None
253 Containernet
.removeLink(self
, link
=link
, node1
=node1
, node2
=node2
)
254 # TODO we might decrease the loglevel to debug:
256 self
.DCNetwork_graph
.remove_edge(node2
.name
, node1
.name
)
257 except BaseException
:
258 LOG
.warning("%s, %s not found in DCNetwork_graph." %
259 ((node2
.name
, node1
.name
)))
261 self
.DCNetwork_graph
.remove_edge(node1
.name
, node2
.name
)
262 except BaseException
:
263 LOG
.warning("%s, %s not found in DCNetwork_graph." %
264 ((node1
.name
, node2
.name
)))
266 def addDocker(self
, label
, **params
):
268 Wrapper for addDocker method to use custom container class.
270 self
.DCNetwork_graph
.add_node(label
, type=params
.get('type', 'docker'))
271 return Containernet
.addDocker(
272 self
, label
, cls
=EmulatorCompute
, **params
)
274 def removeDocker(self
, label
, **params
):
276 Wrapper for removeDocker method to update graph.
278 self
.DCNetwork_graph
.remove_node(label
)
279 return Containernet
.removeDocker(self
, label
, **params
)
281 def addExtSAP(self
, sap_name
, sap_ip
, **params
):
283 Wrapper for addExtSAP method to store SAP also in graph.
285 # make sure that 'type' is set
286 params
['type'] = params
.get('type', 'sap_ext')
287 self
.DCNetwork_graph
.add_node(sap_name
, type=params
['type'])
288 return Containernet
.addExtSAP(self
, sap_name
, sap_ip
, **params
)
290 def removeExtSAP(self
, sap_name
, **params
):
292 Wrapper for removeExtSAP method to remove SAP also from graph.
294 self
.DCNetwork_graph
.remove_node(sap_name
)
295 return Containernet
.removeExtSAP(self
, sap_name
)
297 def addSwitch(self
, name
, add_to_graph
=True, **params
):
299 Wrapper for addSwitch method to store switch also in graph.
302 # add this switch to the global topology overview
304 self
.DCNetwork_graph
.add_node(
305 name
, type=params
.get('type', 'switch'))
307 # set the learning switch behavior
308 if 'failMode' in params
:
309 failMode
= params
['failMode']
311 failMode
= self
.failMode
313 s
= Containernet
.addSwitch(
314 self
, name
, protocols
='OpenFlow10,OpenFlow12,OpenFlow13', failMode
=failMode
, **params
)
318 def getAllContainers(self
):
320 Returns a list with all containers within all data centers.
323 for dc
in self
.dcs
.itervalues():
324 all_containers
+= dc
.listCompute()
325 return all_containers
329 for dc
in self
.dcs
.itervalues():
331 Containernet
.start(self
)
335 # stop the monitor agent
336 if self
.monitor_agent
is not None:
337 self
.monitor_agent
.stop()
340 Containernet
.stop(self
)
342 # stop Ryu controller
348 def setLAN(self
, vnf_list
):
350 setup an E-LAN network by assigning the same VLAN tag to each DC interface of the VNFs in the E-LAN
352 :param vnf_list: names of the VNFs in this E-LAN [{name:,interface:},...]
356 src_sw_inport_name
= None
358 # get a vlan tag for this E-LAN
359 vlan
= self
.vlans
.pop()
362 vnf_src_name
= vnf
['name']
363 vnf_src_interface
= vnf
['interface']
365 # check if port is specified (vnf:port)
366 if vnf_src_interface
is None:
367 # take first interface by default
368 connected_sw
= self
.DCNetwork_graph
.neighbors(vnf_src_name
)[0]
369 link_dict
= self
.DCNetwork_graph
[vnf_src_name
][connected_sw
]
370 vnf_src_interface
= link_dict
[0]['src_port_id']
372 for connected_sw
in self
.DCNetwork_graph
.neighbors(vnf_src_name
):
373 link_dict
= self
.DCNetwork_graph
[vnf_src_name
][connected_sw
]
374 for link
in link_dict
:
375 if (link_dict
[link
]['src_port_id'] == vnf_src_interface
or
376 link_dict
[link
]['src_port_name'] == vnf_src_interface
): # Fix: we might also get interface names, e.g, from a son-emu-cli call
377 # found the right link and connected switch
378 src_sw
= connected_sw
379 src_sw_inport_name
= link_dict
[link
]['dst_port_name']
382 # set the tag on the dc switch interface
383 LOG
.debug('set E-LAN: vnf name: {0} interface: {1} tag: {2}'.format(
384 vnf_src_name
, vnf_src_interface
, vlan
))
385 switch_node
= self
.getNodeByName(src_sw
)
386 self
._set
_vlan
_tag
(switch_node
, src_sw_inport_name
, vlan
)
388 def getNodeByName(self
, name
):
390 Wraps Containernet's getNodeByName method to avoid
391 key not found exceptions.
394 return super(DCNetwork
, self
).getNodeByName(name
)
395 except BaseException
as ex
:
396 LOG
.warning("Node not found: {}".format(name
))
397 LOG
.debug("Node not found: {}".format(ex
))
400 def _addMonitorFlow(self
, vnf_src_name
, vnf_dst_name
, vnf_src_interface
=None, vnf_dst_interface
=None,
403 Add a monitoring flow entry that adds a special flowentry/counter at the begin or end of a chain.
404 So this monitoring flowrule exists on top of a previously defined chain rule and uses the same vlan tag/routing.
407 :param vnf_src_interface:
408 :param vnf_dst_interface:
409 :param tag: vlan tag to be used for this chain (same tag as existing chain)
410 :param monitor_placement: 'tx' or 'rx' indicating to place the extra flowentry resp. at the beginning or end of the chain
416 src_sw_inport_name
= None
418 dst_sw_outport_nr
= 0
419 dst_sw_outport_name
= None
421 LOG
.debug("call AddMonitorFlow vnf_src_name=%r, vnf_src_interface=%r, vnf_dst_name=%r, vnf_dst_interface=%r",
422 vnf_src_name
, vnf_src_interface
, vnf_dst_name
, vnf_dst_interface
)
424 # check if port is specified (vnf:port)
425 if vnf_src_interface
is None:
426 # take first interface by default
427 connected_sw
= self
.DCNetwork_graph
.neighbors(vnf_src_name
)[0]
428 link_dict
= self
.DCNetwork_graph
[vnf_src_name
][connected_sw
]
429 vnf_src_interface
= link_dict
[0]['src_port_id']
431 for connected_sw
in self
.DCNetwork_graph
.neighbors(vnf_src_name
):
432 link_dict
= self
.DCNetwork_graph
[vnf_src_name
][connected_sw
]
433 for link
in link_dict
:
434 if (link_dict
[link
]['src_port_id'] == vnf_src_interface
or
435 link_dict
[link
]['src_port_name'] == vnf_src_interface
): # Fix: we might also get interface names, e.g, from a son-emu-cli call
436 # found the right link and connected switch
437 src_sw
= connected_sw
438 src_sw_inport_nr
= link_dict
[link
]['dst_port_nr']
439 src_sw_inport_name
= link_dict
[link
]['dst_port_name']
442 if vnf_dst_interface
is None:
443 # take first interface by default
444 connected_sw
= self
.DCNetwork_graph
.neighbors(vnf_dst_name
)[0]
445 link_dict
= self
.DCNetwork_graph
[connected_sw
][vnf_dst_name
]
446 vnf_dst_interface
= link_dict
[0]['dst_port_id']
448 vnf_dst_name
= vnf_dst_name
.split(':')[0]
449 for connected_sw
in self
.DCNetwork_graph
.neighbors(vnf_dst_name
):
450 link_dict
= self
.DCNetwork_graph
[connected_sw
][vnf_dst_name
]
451 for link
in link_dict
:
452 if link_dict
[link
]['dst_port_id'] == vnf_dst_interface
or \
453 link_dict
[link
]['dst_port_name'] == vnf_dst_interface
: # Fix: we might also get interface names, e.g, from a son-emu-cli call
454 # found the right link and connected switch
455 dst_sw
= connected_sw
456 dst_sw_outport_nr
= link_dict
[link
]['src_port_nr']
457 dst_sw_outport_name
= link_dict
[link
]['src_port_name']
461 LOG
.exception('tag not valid: {0}'.format(tag
))
465 # returns the first found shortest path
466 # if all shortest paths are wanted, use: all_shortest_paths
467 path
= nx
.shortest_path(
468 self
.DCNetwork_graph
, src_sw
, dst_sw
, weight
=kwargs
.get('weight'))
469 except BaseException
:
470 LOG
.exception("No path could be found between {0} and {1} using src_sw={2} and dst_sw={3}".format(
471 vnf_src_name
, vnf_dst_name
, src_sw
, dst_sw
))
472 LOG
.debug("Graph nodes: %r" % self
.DCNetwork_graph
.nodes())
473 LOG
.debug("Graph edges: %r" % self
.DCNetwork_graph
.edges())
474 for e
, v
in self
.DCNetwork_graph
.edges():
475 LOG
.debug("%r" % self
.DCNetwork_graph
[e
][v
])
476 return "No path could be found between {0} and {1}".format(
477 vnf_src_name
, vnf_dst_name
)
479 LOG
.debug("Creating path between {0} and {1}: {2}".format(
480 vnf_src_name
, vnf_dst_name
, path
))
483 switch_inport_nr
= src_sw_inport_nr
485 cmd
= kwargs
.get('cmd')
487 # iterate through the path to install the flow-entries
488 for i
in range(0, len(path
)):
489 current_node
= self
.getNodeByName(current_hop
)
491 if path
.index(current_hop
) < len(path
) - 1:
492 next_hop
= path
[path
.index(current_hop
) + 1]
494 # last switch reached
495 next_hop
= vnf_dst_name
497 next_node
= self
.getNodeByName(next_hop
)
499 if next_hop
== vnf_dst_name
:
500 switch_outport_nr
= dst_sw_outport_nr
501 LOG
.debug("end node reached: {0}".format(vnf_dst_name
))
502 elif not isinstance(next_node
, OVSSwitch
):
503 LOG
.info("Next node: {0} is not a switch".format(next_hop
))
504 return "Next node: {0} is not a switch".format(next_hop
)
506 # take first link between switches by default
508 switch_outport_nr
= self
.DCNetwork_graph
[current_hop
][next_hop
][index_edge_out
]['src_port_nr']
510 # set of entry via ovs-ofctl
511 if isinstance(current_node
, OVSSwitch
):
513 kwargs
['path'] = path
514 kwargs
['current_hop'] = current_hop
515 kwargs
['switch_inport_name'] = src_sw_inport_name
516 kwargs
['switch_outport_name'] = dst_sw_outport_name
517 kwargs
['skip_vlan_tag'] = True
518 kwargs
['pathindex'] = i
520 monitor_placement
= kwargs
.get('monitor_placement').strip()
521 # put monitor flow at the dst switch
524 if monitor_placement
== 'tx' and path
.index(current_hop
) == 0:
526 # put monitoring flow at the src switch
528 elif monitor_placement
== 'rx' and path
.index(current_hop
) == len(path
) - 1:
530 elif monitor_placement
not in ['rx', 'tx']:
532 'invalid monitor command: {0}'.format(monitor_placement
))
534 if self
.controller
== RemoteController
and insert_flow
:
535 # set flow entry via ryu rest api
536 self
._set
_flow
_entry
_ryu
_rest
(
537 current_node
, switch_inport_nr
, switch_outport_nr
, **kwargs
)
540 # set flow entry via ovs-ofctl
541 self
._set
_flow
_entry
_dpctl
(
542 current_node
, switch_inport_nr
, switch_outport_nr
, **kwargs
)
545 # take first link between switches by default
546 if isinstance(next_node
, OVSSwitch
):
547 switch_inport_nr
= self
.DCNetwork_graph
[current_hop
][next_hop
][0]['dst_port_nr']
548 current_hop
= next_hop
550 return "path {2} between {0} and {1}".format(
551 vnf_src_name
, vnf_dst_name
, cmd
)
553 def setChain(self
, vnf_src_name
, vnf_dst_name
,
554 vnf_src_interface
=None, vnf_dst_interface
=None, **kwargs
):
556 Chain 2 vnf interfaces together by installing the flowrules in the switches along their path.
557 Currently the path is found using the default networkx shortest path function.
558 Each chain gets a unique vlan id , so different chains wil not interfere.
560 :param vnf_src_name: vnf name (string)
561 :param vnf_dst_name: vnf name (string)
562 :param vnf_src_interface: source interface name (string)
563 :param vnf_dst_interface: destination interface name (string)
564 :param cmd: 'add-flow' (default) to add a chain, 'del-flows' to remove a chain
565 :param cookie: cookie for the installed flowrules (can be used later as identifier for a set of installed chains)
566 :param match: custom match entry to be added to the flowrules (default: only in_port and vlan tag)
567 :param priority: custom flowrule priority
568 :param monitor: boolean to indicate whether this chain is a monitoring chain
569 :param tag: vlan tag to be used for this chain (pre-defined or new one if none is specified)
570 :param skip_vlan_tag: boolean to indicate if a vlan tag should be appointed to this flow or not
571 :param path: custom path between the two VNFs (list of switches)
572 :return: output log string
575 # special procedure for monitoring flows
576 if kwargs
.get('monitor'):
578 # check if chain already exists
579 found_chains
= [chain_dict
for chain_dict
in self
.installed_chains
if
580 (chain_dict
['vnf_src_name'] == vnf_src_name
and
581 chain_dict
['vnf_src_interface'] == vnf_src_interface
and
582 chain_dict
['vnf_dst_name'] == vnf_dst_name
and
583 chain_dict
['vnf_dst_interface'] == vnf_dst_interface
)]
585 if len(found_chains
) > 0:
586 # this chain exists, so need an extra monitoring flow
587 # assume only 1 chain per vnf/interface pair
588 LOG
.debug('*** installing monitoring chain on top of pre-defined chain from {0}:{1} -> {2}:{3}'.
589 format(vnf_src_name
, vnf_src_interface
, vnf_dst_name
, vnf_dst_interface
))
590 tag
= found_chains
[0]['tag']
591 ret
= self
._addMonitorFlow
(vnf_src_name
, vnf_dst_name
, vnf_src_interface
, vnf_dst_interface
,
592 tag
=tag
, table_id
=0, **kwargs
)
595 # no chain existing (or E-LAN) -> install normal chain
596 LOG
.warning('*** installing monitoring chain without pre-defined NSD chain from {0}:{1} -> {2}:{3}'.
597 format(vnf_src_name
, vnf_src_interface
, vnf_dst_name
, vnf_dst_interface
))
600 cmd
= kwargs
.get('cmd', 'add-flow')
601 if cmd
== 'add-flow' or cmd
== 'del-flows':
602 ret
= self
._chainAddFlow
(
603 vnf_src_name
, vnf_dst_name
, vnf_src_interface
, vnf_dst_interface
, **kwargs
)
604 if kwargs
.get('bidirectional'):
605 if kwargs
.get('path') is not None:
606 kwargs
['path'] = list(reversed(kwargs
.get('path')))
609 vnf_dst_name
, vnf_src_name
, vnf_dst_interface
, vnf_src_interface
, **kwargs
)
612 ret
= "Command unknown"
616 def _chainAddFlow(self
, vnf_src_name
, vnf_dst_name
,
617 vnf_src_interface
=None, vnf_dst_interface
=None, **kwargs
):
621 src_sw_inport_name
= None
623 dst_sw_outport_nr
= 0
624 dst_sw_outport_name
= None
626 LOG
.debug("call chainAddFlow vnf_src_name=%r, vnf_src_interface=%r, vnf_dst_name=%r, vnf_dst_interface=%r",
627 vnf_src_name
, vnf_src_interface
, vnf_dst_name
, vnf_dst_interface
)
629 # check if port is specified (vnf:port)
630 if vnf_src_interface
is None:
631 # take first interface by default
632 connected_sw
= self
.DCNetwork_graph
.neighbors(vnf_src_name
)[0]
633 link_dict
= self
.DCNetwork_graph
[vnf_src_name
][connected_sw
]
634 vnf_src_interface
= link_dict
[0]['src_port_id']
636 for connected_sw
in self
.DCNetwork_graph
.neighbors(vnf_src_name
):
637 link_dict
= self
.DCNetwork_graph
[vnf_src_name
][connected_sw
]
638 for link
in link_dict
:
639 if (link_dict
[link
]['src_port_id'] == vnf_src_interface
or
640 link_dict
[link
]['src_port_name'] == vnf_src_interface
): # Fix: we might also get interface names, e.g, from a son-emu-cli call
641 # found the right link and connected switch
642 src_sw
= connected_sw
643 src_sw_inport_nr
= link_dict
[link
]['dst_port_nr']
644 src_sw_inport_name
= link_dict
[link
]['dst_port_name']
647 if vnf_dst_interface
is None:
648 # take first interface by default
649 connected_sw
= self
.DCNetwork_graph
.neighbors(vnf_dst_name
)[0]
650 link_dict
= self
.DCNetwork_graph
[connected_sw
][vnf_dst_name
]
651 vnf_dst_interface
= link_dict
[0]['dst_port_id']
653 vnf_dst_name
= vnf_dst_name
.split(':')[0]
654 for connected_sw
in self
.DCNetwork_graph
.neighbors(vnf_dst_name
):
655 link_dict
= self
.DCNetwork_graph
[connected_sw
][vnf_dst_name
]
656 for link
in link_dict
:
657 if link_dict
[link
]['dst_port_id'] == vnf_dst_interface
or \
658 link_dict
[link
]['dst_port_name'] == vnf_dst_interface
: # Fix: we might also get interface names, e.g, from a son-emu-cli call
659 # found the right link and connected switch
660 dst_sw
= connected_sw
661 dst_sw_outport_nr
= link_dict
[link
]['src_port_nr']
662 dst_sw_outport_name
= link_dict
[link
]['src_port_name']
665 path
= kwargs
.get('path')
669 # returns the first found shortest path
670 # if all shortest paths are wanted, use: all_shortest_paths
671 path
= nx
.shortest_path(
672 self
.DCNetwork_graph
, src_sw
, dst_sw
, weight
=kwargs
.get('weight'))
673 except BaseException
:
674 LOG
.exception("No path could be found between {0} and {1} using src_sw={2} and dst_sw={3}".format(
675 vnf_src_name
, vnf_dst_name
, src_sw
, dst_sw
))
676 LOG
.debug("Graph nodes: %r" % self
.DCNetwork_graph
.nodes())
677 LOG
.debug("Graph edges: %r" % self
.DCNetwork_graph
.edges())
678 for e
, v
in self
.DCNetwork_graph
.edges():
679 LOG
.debug("%r" % self
.DCNetwork_graph
[e
][v
])
680 return "No path could be found between {0} and {1}".format(
681 vnf_src_name
, vnf_dst_name
)
683 LOG
.debug("Creating path between {0} and {1}: {2}".format(
684 vnf_src_name
, vnf_dst_name
, path
))
687 switch_inport_nr
= src_sw_inport_nr
690 cmd
= kwargs
.get('cmd')
692 if cmd
== 'add-flow':
693 if kwargs
.get('tag'):
694 # use pre-defined tag
695 vlan
= kwargs
.get('tag')
697 vlan
= self
.vlans
.pop()
699 # store the used vlan tag to identify this chain
700 if not kwargs
.get('monitor'):
702 chain_dict
['vnf_src_name'] = vnf_src_name
703 chain_dict
['vnf_dst_name'] = vnf_dst_name
704 chain_dict
['vnf_src_interface'] = vnf_src_interface
705 chain_dict
['vnf_dst_interface'] = vnf_dst_interface
706 chain_dict
['tag'] = vlan
707 self
.installed_chains
.append(chain_dict
)
709 # iterate through the path to install the flow-entries
710 for i
in range(0, len(path
)):
711 current_node
= self
.getNodeByName(current_hop
)
713 if i
< len(path
) - 1:
714 next_hop
= path
[i
+ 1]
716 # last switch reached
717 next_hop
= vnf_dst_name
719 next_node
= self
.getNodeByName(next_hop
)
721 if next_hop
== vnf_dst_name
:
722 switch_outport_nr
= dst_sw_outport_nr
723 LOG
.debug("end node reached: {0}".format(vnf_dst_name
))
724 elif not isinstance(next_node
, OVSSwitch
):
725 LOG
.info("Next node: {0} is not a switch".format(next_hop
))
726 return "Next node: {0} is not a switch".format(next_hop
)
728 # take first link between switches by default
730 switch_outport_nr
= self
.DCNetwork_graph
[current_hop
][next_hop
][index_edge_out
]['src_port_nr']
733 if isinstance(current_node
, OVSSwitch
):
734 kwargs
['vlan'] = vlan
735 kwargs
['path'] = path
736 kwargs
['current_hop'] = current_hop
737 kwargs
['switch_inport_name'] = src_sw_inport_name
738 kwargs
['switch_outport_name'] = dst_sw_outport_name
739 kwargs
['pathindex'] = i
741 if self
.controller
== RemoteController
:
742 # set flow entry via ryu rest api
743 self
._set
_flow
_entry
_ryu
_rest
(
744 current_node
, switch_inport_nr
, switch_outport_nr
, **kwargs
)
746 # set flow entry via ovs-ofctl
747 self
._set
_flow
_entry
_dpctl
(
748 current_node
, switch_inport_nr
, switch_outport_nr
, **kwargs
)
750 # take first link between switches by default
751 if isinstance(next_node
, OVSSwitch
):
752 switch_inport_nr
= self
.DCNetwork_graph
[current_hop
][next_hop
][0]['dst_port_nr']
753 current_hop
= next_hop
756 'priority': kwargs
.get('priority', DEFAULT_PRIORITY
),
757 'cookie': kwargs
.get('cookie', DEFAULT_COOKIE
),
758 'vlan': kwargs
['vlan'],
759 'path': kwargs
['path'],
760 'match_input': kwargs
.get('match')
762 flow_options_str
= json
.dumps(flow_options
, indent
=1)
763 LOG
.info("Installed flow rule: ({}:{}) -> ({}:{}) with options: {}"
764 .format(vnf_src_name
, vnf_src_interface
, vnf_dst_name
, vnf_dst_interface
, flow_options
))
765 return "success: {2} between {0} and {1} with options: {3}".format(
766 vnf_src_name
, vnf_dst_name
, cmd
, flow_options_str
)
768 def _set_flow_entry_ryu_rest(
769 self
, node
, switch_inport_nr
, switch_outport_nr
, **kwargs
):
770 match
= 'in_port=%s' % switch_inport_nr
772 cookie
= kwargs
.get('cookie')
773 match_input
= kwargs
.get('match')
774 cmd
= kwargs
.get('cmd')
775 path
= kwargs
.get('path')
776 index
= kwargs
.get('pathindex')
777 mod_dl_dst
= kwargs
.get('mod_dl_dst')
779 vlan
= kwargs
.get('vlan')
780 priority
= kwargs
.get('priority', DEFAULT_PRIORITY
)
781 # flag to not set the ovs port vlan tag
782 skip_vlan_tag
= kwargs
.get('skip_vlan_tag')
783 # table id to put this flowentry
784 table_id
= kwargs
.get('table_id')
790 match
= s
.join([match
, match_input
])
793 flow
['dpid'] = int(node
.dpid
, 16)
796 flow
['cookie'] = int(cookie
)
798 flow
['priority'] = int(priority
)
800 flow
['table_id'] = table_id
804 # possible Ryu actions, match fields:
805 # http://ryu.readthedocs.io/en/latest/app/ofctl_rest.html#add-a-flow-entry
806 if cmd
== 'add-flow':
807 prefix
= 'stats/flowentry/add'
809 if index
== 0: # first node
810 # set vlan tag in ovs instance (to isolate E-LANs)
811 if not skip_vlan_tag
:
812 in_port_name
= kwargs
.get('switch_inport_name')
813 self
._set
_vlan
_tag
(node
, in_port_name
, vlan
)
814 # set vlan push action if more than 1 switch in the path
817 # Push a new VLAN tag if a input frame is
819 action
['type'] = 'PUSH_VLAN'
820 # Ethertype 0x8100(=33024): IEEE 802.1Q VLAN-tagged
822 action
['ethertype'] = 33024
823 flow
['actions'].append(action
)
825 action
['type'] = 'SET_FIELD'
826 action
['field'] = 'vlan_vid'
827 # ryu expects the field to be masked
828 action
['value'] = vlan |
0x1000
829 flow
['actions'].append(action
)
831 elif index
== len(path
) - 1: # last node
832 # set vlan tag in ovs instance (to isolate E-LANs)
833 if not skip_vlan_tag
:
834 out_port_name
= kwargs
.get('switch_outport_name')
835 self
._set
_vlan
_tag
(node
, out_port_name
, vlan
)
836 # set vlan pop action if more than 1 switch in the path
838 match
+= ',dl_vlan=%s' % vlan
840 action
['type'] = 'POP_VLAN'
841 flow
['actions'].append(action
)
844 match
+= ',dl_vlan=%s' % vlan
847 action
['type'] = 'SET_FIELD'
848 action
['field'] = 'eth_dst'
849 action
['value'] = mod_dl_dst
850 flow
['actions'].append(action
)
852 # output action must come last
854 action
['type'] = 'OUTPUT'
855 action
['port'] = switch_outport_nr
856 flow
['actions'].append(action
)
858 elif cmd
== 'del-flows':
859 prefix
= 'stats/flowentry/delete'
862 # TODO: add cookie_mask as argument
863 # need full mask to match complete cookie
864 flow
['cookie_mask'] = int('0xffffffffffffffff', 16)
867 action
['type'] = 'OUTPUT'
868 action
['port'] = switch_outport_nr
869 flow
['actions'].append(action
)
871 flow
['match'] = self
._parse
_match
(match
)
872 self
.ryu_REST(prefix
, data
=flow
)
874 def _set_vlan_tag(self
, node
, switch_port
, tag
):
875 node
.vsctl('set', 'port {0} tag={1}'.format(switch_port
, tag
))
876 LOG
.debug("set vlan in switch: {0} in_port: {1} vlan tag: {2}".format(
877 node
.name
, switch_port
, tag
))
879 def _set_flow_entry_dpctl(
880 self
, node
, switch_inport_nr
, switch_outport_nr
, **kwargs
):
882 match
= 'in_port=%s' % switch_inport_nr
884 cookie
= kwargs
.get('cookie')
885 match_input
= kwargs
.get('match')
886 cmd
= kwargs
.get('cmd')
887 path
= kwargs
.get('path')
888 index
= kwargs
.get('pathindex')
889 vlan
= kwargs
.get('vlan')
893 cookie
= 'cookie=%s' % cookie
894 match
= s
.join([cookie
, match
])
896 match
= s
.join([match
, match_input
])
897 if cmd
== 'add-flow':
898 action
= 'action=%s' % switch_outport_nr
900 if index
== 0: # first node
901 action
= ('action=mod_vlan_vid:%s' % vlan
) + \
902 (',output=%s' % switch_outport_nr
)
903 match
= '-O OpenFlow13 ' + match
904 elif index
== len(path
) - 1: # last node
905 match
+= ',dl_vlan=%s' % vlan
906 action
= 'action=strip_vlan,output=%s' % switch_outport_nr
908 match
+= ',dl_vlan=%s' % vlan
909 ofcmd
= s
.join([match
, action
])
910 elif cmd
== 'del-flows':
915 node
.dpctl(cmd
, ofcmd
)
916 LOG
.info("{3} in switch: {0} in_port: {1} out_port: {2}".format(node
.name
, switch_inport_nr
,
917 switch_outport_nr
, cmd
))
919 # start Ryu Openflow controller as Remote Controller for the DCNetwork
920 def startRyu(self
, learning_switch
=True):
921 # start Ryu controller with rest-API
923 # ryu default learning switch
924 # ryu_learning_app = python_install_path + '/ryu/app/simple_switch_13.py'
925 # custom learning switch that installs a default NORMAL action in the
927 dir_path
= os
.path
.dirname(os
.path
.realpath(__file__
))
928 ryu_learning_app
= dir_path
+ '/son_emu_simple_switch_13.py'
929 ryu_rest_app
= 'ryu.app.ofctl_rest'
930 # change the default Openflow controller port to 6653 (official IANA-assigned port number), as used by Mininet
931 # Ryu still uses 6633 as default
932 ryu_option
= '--ofp-tcp-listen-port'
934 ryu_cmd
= 'ryu-manager'
935 FNULL
= open("/tmp/ryu.log", 'w')
937 # learning and rest api
938 args
= [ryu_cmd
, ryu_learning_app
, ryu_rest_app
, ryu_option
, ryu_of_port
]
940 # no learning switch, but with rest api
941 args
= [ryu_cmd
, ryu_rest_app
, ryu_option
, ryu_of_port
]
942 self
.ryu_process
= Popen(args
, stdout
=FNULL
, stderr
=FNULL
)
943 LOG
.debug('starting ryu-controller with %s' % args
)
948 Stop the Ryu controller that might be started by son-emu.
953 if self
.ryu_process
is not None:
954 self
.ryu_process
.terminate()
955 self
.ryu_process
.kill()
956 except BaseException
as ex
:
957 LOG
.warning("Error during Ryu stop: {}".format(ex
))
958 # ensure its death ;-)
959 Popen(['pkill', '-f', 'ryu-manager'])
961 def ryu_REST(self
, prefix
, dpid
=None, data
=None):
964 url
= self
.ryu_REST_api
+ '/' + str(prefix
) + '/' + str(dpid
)
966 url
= self
.ryu_REST_api
+ '/' + str(prefix
)
968 LOG
.debug('sending RYU command: %s, payload: %s', url
, data
)
970 req
= self
.RyuSession
.post(url
, json
=data
)
972 req
= self
.RyuSession
.get(url
)
974 # do extra logging if status code is not 200 (OK)
975 if req
.status_code
is not requests
.codes
.ok
:
977 'type {0} encoding: {1} text: {2} headers: {3} history: {4}'.format(req
.headers
['content-type'],
978 req
.encoding
, req
.text
,
979 req
.headers
, req
.history
))
980 LOG
.info('url: {0}'.format(str(url
)))
982 LOG
.info('POST: {0}'.format(str(data
)))
983 LOG
.info('status: {0} reason: {1}'.format(
984 req
.status_code
, req
.reason
))
986 if 'json' in req
.headers
['content-type']:
990 ret
= req
.text
.rstrip()
993 # need to respect that some match fields must be integers
994 # http://ryu.readthedocs.io/en/latest/app/ofctl_rest.html#description-of-match-and-actions
996 def _parse_match(self
, match
):
997 matches
= match
.split(',')
1000 match
= m
.split('=')
1003 m2
= int(match
[1], 0)
1004 except BaseException
:
1007 dict.update({match
[0]: m2
})
1010 def find_connected_dc_interface(
1011 self
, vnf_src_name
, vnf_src_interface
=None):
1013 if vnf_src_interface
is None:
1014 # take first interface by default
1015 connected_sw
= self
.DCNetwork_graph
.neighbors(vnf_src_name
)[0]
1016 link_dict
= self
.DCNetwork_graph
[vnf_src_name
][connected_sw
]
1017 vnf_src_interface
= link_dict
[0]['src_port_id']
1019 for connected_sw
in self
.DCNetwork_graph
.neighbors(vnf_src_name
):
1020 link_dict
= self
.DCNetwork_graph
[vnf_src_name
][connected_sw
]
1021 for link
in link_dict
:
1022 if (link_dict
[link
]['src_port_id'] == vnf_src_interface
or
1023 link_dict
[link
]['src_port_name'] == vnf_src_interface
):
1024 # Fix: we might also get interface names, e.g, from a son-emu-cli call
1025 # found the right link and connected switch
1026 src_sw_inport_name
= link_dict
[link
]['dst_port_name']
1027 return src_sw_inport_name