58c2bff46e9e569a3228bb2832c39aabe09bccd7
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
45 monkey
.patch_all(sys
=True)
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 _addMonitorFlow(self
, vnf_src_name
, vnf_dst_name
, vnf_src_interface
=None, vnf_dst_interface
=None,
391 Add a monitoring flow entry that adds a special flowentry/counter at the begin or end of a chain.
392 So this monitoring flowrule exists on top of a previously defined chain rule and uses the same vlan tag/routing.
395 :param vnf_src_interface:
396 :param vnf_dst_interface:
397 :param tag: vlan tag to be used for this chain (same tag as existing chain)
398 :param monitor_placement: 'tx' or 'rx' indicating to place the extra flowentry resp. at the beginning or end of the chain
404 src_sw_inport_name
= None
406 dst_sw_outport_nr
= 0
407 dst_sw_outport_name
= None
409 LOG
.debug("call AddMonitorFlow vnf_src_name=%r, vnf_src_interface=%r, vnf_dst_name=%r, vnf_dst_interface=%r",
410 vnf_src_name
, vnf_src_interface
, vnf_dst_name
, vnf_dst_interface
)
412 # check if port is specified (vnf:port)
413 if vnf_src_interface
is None:
414 # take first interface by default
415 connected_sw
= self
.DCNetwork_graph
.neighbors(vnf_src_name
)[0]
416 link_dict
= self
.DCNetwork_graph
[vnf_src_name
][connected_sw
]
417 vnf_src_interface
= link_dict
[0]['src_port_id']
419 for connected_sw
in self
.DCNetwork_graph
.neighbors(vnf_src_name
):
420 link_dict
= self
.DCNetwork_graph
[vnf_src_name
][connected_sw
]
421 for link
in link_dict
:
422 if (link_dict
[link
]['src_port_id'] == vnf_src_interface
or
423 link_dict
[link
]['src_port_name'] == vnf_src_interface
): # Fix: we might also get interface names, e.g, from a son-emu-cli call
424 # found the right link and connected switch
425 src_sw
= connected_sw
426 src_sw_inport_nr
= link_dict
[link
]['dst_port_nr']
427 src_sw_inport_name
= link_dict
[link
]['dst_port_name']
430 if vnf_dst_interface
is None:
431 # take first interface by default
432 connected_sw
= self
.DCNetwork_graph
.neighbors(vnf_dst_name
)[0]
433 link_dict
= self
.DCNetwork_graph
[connected_sw
][vnf_dst_name
]
434 vnf_dst_interface
= link_dict
[0]['dst_port_id']
436 vnf_dst_name
= vnf_dst_name
.split(':')[0]
437 for connected_sw
in self
.DCNetwork_graph
.neighbors(vnf_dst_name
):
438 link_dict
= self
.DCNetwork_graph
[connected_sw
][vnf_dst_name
]
439 for link
in link_dict
:
440 if link_dict
[link
]['dst_port_id'] == vnf_dst_interface
or \
441 link_dict
[link
]['dst_port_name'] == vnf_dst_interface
: # Fix: we might also get interface names, e.g, from a son-emu-cli call
442 # found the right link and connected switch
443 dst_sw
= connected_sw
444 dst_sw_outport_nr
= link_dict
[link
]['src_port_nr']
445 dst_sw_outport_name
= link_dict
[link
]['src_port_name']
449 LOG
.exception('tag not valid: {0}'.format(tag
))
453 # returns the first found shortest path
454 # if all shortest paths are wanted, use: all_shortest_paths
455 path
= nx
.shortest_path(
456 self
.DCNetwork_graph
, src_sw
, dst_sw
, weight
=kwargs
.get('weight'))
457 except BaseException
:
458 LOG
.exception("No path could be found between {0} and {1} using src_sw={2} and dst_sw={3}".format(
459 vnf_src_name
, vnf_dst_name
, src_sw
, dst_sw
))
460 LOG
.debug("Graph nodes: %r" % self
.DCNetwork_graph
.nodes())
461 LOG
.debug("Graph edges: %r" % self
.DCNetwork_graph
.edges())
462 for e
, v
in self
.DCNetwork_graph
.edges():
463 LOG
.debug("%r" % self
.DCNetwork_graph
[e
][v
])
464 return "No path could be found between {0} and {1}".format(
465 vnf_src_name
, vnf_dst_name
)
467 LOG
.debug("Creating path between {0} and {1}: {2}".format(
468 vnf_src_name
, vnf_dst_name
, path
))
471 switch_inport_nr
= src_sw_inport_nr
473 cmd
= kwargs
.get('cmd')
475 # iterate through the path to install the flow-entries
476 for i
in range(0, len(path
)):
477 current_node
= self
.getNodeByName(current_hop
)
479 if path
.index(current_hop
) < len(path
) - 1:
480 next_hop
= path
[path
.index(current_hop
) + 1]
482 # last switch reached
483 next_hop
= vnf_dst_name
485 next_node
= self
.getNodeByName(next_hop
)
487 if next_hop
== vnf_dst_name
:
488 switch_outport_nr
= dst_sw_outport_nr
489 LOG
.debug("end node reached: {0}".format(vnf_dst_name
))
490 elif not isinstance(next_node
, OVSSwitch
):
491 LOG
.info("Next node: {0} is not a switch".format(next_hop
))
492 return "Next node: {0} is not a switch".format(next_hop
)
494 # take first link between switches by default
496 switch_outport_nr
= self
.DCNetwork_graph
[current_hop
][next_hop
][index_edge_out
]['src_port_nr']
498 # set of entry via ovs-ofctl
499 if isinstance(current_node
, OVSSwitch
):
501 kwargs
['path'] = path
502 kwargs
['current_hop'] = current_hop
503 kwargs
['switch_inport_name'] = src_sw_inport_name
504 kwargs
['switch_outport_name'] = dst_sw_outport_name
505 kwargs
['skip_vlan_tag'] = True
506 kwargs
['pathindex'] = i
508 monitor_placement
= kwargs
.get('monitor_placement').strip()
509 # put monitor flow at the dst switch
512 if monitor_placement
== 'tx' and path
.index(current_hop
) == 0:
514 # put monitoring flow at the src switch
516 elif monitor_placement
== 'rx' and path
.index(current_hop
) == len(path
) - 1:
518 elif monitor_placement
not in ['rx', 'tx']:
520 'invalid monitor command: {0}'.format(monitor_placement
))
522 if self
.controller
== RemoteController
and insert_flow
:
523 # set flow entry via ryu rest api
524 self
._set
_flow
_entry
_ryu
_rest
(
525 current_node
, switch_inport_nr
, switch_outport_nr
, **kwargs
)
528 # set flow entry via ovs-ofctl
529 self
._set
_flow
_entry
_dpctl
(
530 current_node
, switch_inport_nr
, switch_outport_nr
, **kwargs
)
533 # take first link between switches by default
534 if isinstance(next_node
, OVSSwitch
):
535 switch_inport_nr
= self
.DCNetwork_graph
[current_hop
][next_hop
][0]['dst_port_nr']
536 current_hop
= next_hop
538 return "path {2} between {0} and {1}".format(
539 vnf_src_name
, vnf_dst_name
, cmd
)
541 def setChain(self
, vnf_src_name
, vnf_dst_name
,
542 vnf_src_interface
=None, vnf_dst_interface
=None, **kwargs
):
544 Chain 2 vnf interfaces together by installing the flowrules in the switches along their path.
545 Currently the path is found using the default networkx shortest path function.
546 Each chain gets a unique vlan id , so different chains wil not interfere.
548 :param vnf_src_name: vnf name (string)
549 :param vnf_dst_name: vnf name (string)
550 :param vnf_src_interface: source interface name (string)
551 :param vnf_dst_interface: destination interface name (string)
552 :param cmd: 'add-flow' (default) to add a chain, 'del-flows' to remove a chain
553 :param cookie: cookie for the installed flowrules (can be used later as identifier for a set of installed chains)
554 :param match: custom match entry to be added to the flowrules (default: only in_port and vlan tag)
555 :param priority: custom flowrule priority
556 :param monitor: boolean to indicate whether this chain is a monitoring chain
557 :param tag: vlan tag to be used for this chain (pre-defined or new one if none is specified)
558 :param skip_vlan_tag: boolean to indicate if a vlan tag should be appointed to this flow or not
559 :param path: custom path between the two VNFs (list of switches)
560 :return: output log string
563 # special procedure for monitoring flows
564 if kwargs
.get('monitor'):
566 # check if chain already exists
567 found_chains
= [chain_dict
for chain_dict
in self
.installed_chains
if
568 (chain_dict
['vnf_src_name'] == vnf_src_name
and
569 chain_dict
['vnf_src_interface'] == vnf_src_interface
and
570 chain_dict
['vnf_dst_name'] == vnf_dst_name
and
571 chain_dict
['vnf_dst_interface'] == vnf_dst_interface
)]
573 if len(found_chains
) > 0:
574 # this chain exists, so need an extra monitoring flow
575 # assume only 1 chain per vnf/interface pair
576 LOG
.debug('*** installing monitoring chain on top of pre-defined chain from {0}:{1} -> {2}:{3}'.
577 format(vnf_src_name
, vnf_src_interface
, vnf_dst_name
, vnf_dst_interface
))
578 tag
= found_chains
[0]['tag']
579 ret
= self
._addMonitorFlow
(vnf_src_name
, vnf_dst_name
, vnf_src_interface
, vnf_dst_interface
,
580 tag
=tag
, table_id
=0, **kwargs
)
583 # no chain existing (or E-LAN) -> install normal chain
584 LOG
.warning('*** installing monitoring chain without pre-defined NSD chain from {0}:{1} -> {2}:{3}'.
585 format(vnf_src_name
, vnf_src_interface
, vnf_dst_name
, vnf_dst_interface
))
588 cmd
= kwargs
.get('cmd', 'add-flow')
589 if cmd
== 'add-flow' or cmd
== 'del-flows':
590 ret
= self
._chainAddFlow
(
591 vnf_src_name
, vnf_dst_name
, vnf_src_interface
, vnf_dst_interface
, **kwargs
)
592 if kwargs
.get('bidirectional'):
593 if kwargs
.get('path') is not None:
594 kwargs
['path'] = list(reversed(kwargs
.get('path')))
597 vnf_dst_name
, vnf_src_name
, vnf_dst_interface
, vnf_src_interface
, **kwargs
)
600 ret
= "Command unknown"
604 def _chainAddFlow(self
, vnf_src_name
, vnf_dst_name
,
605 vnf_src_interface
=None, vnf_dst_interface
=None, **kwargs
):
609 src_sw_inport_name
= None
611 dst_sw_outport_nr
= 0
612 dst_sw_outport_name
= None
614 LOG
.debug("call chainAddFlow vnf_src_name=%r, vnf_src_interface=%r, vnf_dst_name=%r, vnf_dst_interface=%r",
615 vnf_src_name
, vnf_src_interface
, vnf_dst_name
, vnf_dst_interface
)
617 # check if port is specified (vnf:port)
618 if vnf_src_interface
is None:
619 # take first interface by default
620 connected_sw
= self
.DCNetwork_graph
.neighbors(vnf_src_name
)[0]
621 link_dict
= self
.DCNetwork_graph
[vnf_src_name
][connected_sw
]
622 vnf_src_interface
= link_dict
[0]['src_port_id']
624 for connected_sw
in self
.DCNetwork_graph
.neighbors(vnf_src_name
):
625 link_dict
= self
.DCNetwork_graph
[vnf_src_name
][connected_sw
]
626 for link
in link_dict
:
627 if (link_dict
[link
]['src_port_id'] == vnf_src_interface
or
628 link_dict
[link
]['src_port_name'] == vnf_src_interface
): # Fix: we might also get interface names, e.g, from a son-emu-cli call
629 # found the right link and connected switch
630 src_sw
= connected_sw
631 src_sw_inport_nr
= link_dict
[link
]['dst_port_nr']
632 src_sw_inport_name
= link_dict
[link
]['dst_port_name']
635 if vnf_dst_interface
is None:
636 # take first interface by default
637 connected_sw
= self
.DCNetwork_graph
.neighbors(vnf_dst_name
)[0]
638 link_dict
= self
.DCNetwork_graph
[connected_sw
][vnf_dst_name
]
639 vnf_dst_interface
= link_dict
[0]['dst_port_id']
641 vnf_dst_name
= vnf_dst_name
.split(':')[0]
642 for connected_sw
in self
.DCNetwork_graph
.neighbors(vnf_dst_name
):
643 link_dict
= self
.DCNetwork_graph
[connected_sw
][vnf_dst_name
]
644 for link
in link_dict
:
645 if link_dict
[link
]['dst_port_id'] == vnf_dst_interface
or \
646 link_dict
[link
]['dst_port_name'] == vnf_dst_interface
: # Fix: we might also get interface names, e.g, from a son-emu-cli call
647 # found the right link and connected switch
648 dst_sw
= connected_sw
649 dst_sw_outport_nr
= link_dict
[link
]['src_port_nr']
650 dst_sw_outport_name
= link_dict
[link
]['src_port_name']
653 path
= kwargs
.get('path')
657 # returns the first found shortest path
658 # if all shortest paths are wanted, use: all_shortest_paths
659 path
= nx
.shortest_path(
660 self
.DCNetwork_graph
, src_sw
, dst_sw
, weight
=kwargs
.get('weight'))
661 except BaseException
:
662 LOG
.exception("No path could be found between {0} and {1} using src_sw={2} and dst_sw={3}".format(
663 vnf_src_name
, vnf_dst_name
, src_sw
, dst_sw
))
664 LOG
.debug("Graph nodes: %r" % self
.DCNetwork_graph
.nodes())
665 LOG
.debug("Graph edges: %r" % self
.DCNetwork_graph
.edges())
666 for e
, v
in self
.DCNetwork_graph
.edges():
667 LOG
.debug("%r" % self
.DCNetwork_graph
[e
][v
])
668 return "No path could be found between {0} and {1}".format(
669 vnf_src_name
, vnf_dst_name
)
671 LOG
.debug("Creating path between {0} and {1}: {2}".format(
672 vnf_src_name
, vnf_dst_name
, path
))
675 switch_inport_nr
= src_sw_inport_nr
678 cmd
= kwargs
.get('cmd')
680 if cmd
== 'add-flow':
681 if kwargs
.get('tag'):
682 # use pre-defined tag
683 vlan
= kwargs
.get('tag')
685 vlan
= self
.vlans
.pop()
687 # store the used vlan tag to identify this chain
688 if not kwargs
.get('monitor'):
690 chain_dict
['vnf_src_name'] = vnf_src_name
691 chain_dict
['vnf_dst_name'] = vnf_dst_name
692 chain_dict
['vnf_src_interface'] = vnf_src_interface
693 chain_dict
['vnf_dst_interface'] = vnf_dst_interface
694 chain_dict
['tag'] = vlan
695 self
.installed_chains
.append(chain_dict
)
697 # iterate through the path to install the flow-entries
698 for i
in range(0, len(path
)):
699 current_node
= self
.getNodeByName(current_hop
)
701 if i
< len(path
) - 1:
702 next_hop
= path
[i
+ 1]
704 # last switch reached
705 next_hop
= vnf_dst_name
707 next_node
= self
.getNodeByName(next_hop
)
709 if next_hop
== vnf_dst_name
:
710 switch_outport_nr
= dst_sw_outport_nr
711 LOG
.debug("end node reached: {0}".format(vnf_dst_name
))
712 elif not isinstance(next_node
, OVSSwitch
):
713 LOG
.info("Next node: {0} is not a switch".format(next_hop
))
714 return "Next node: {0} is not a switch".format(next_hop
)
716 # take first link between switches by default
718 switch_outport_nr
= self
.DCNetwork_graph
[current_hop
][next_hop
][index_edge_out
]['src_port_nr']
721 if isinstance(current_node
, OVSSwitch
):
722 kwargs
['vlan'] = vlan
723 kwargs
['path'] = path
724 kwargs
['current_hop'] = current_hop
725 kwargs
['switch_inport_name'] = src_sw_inport_name
726 kwargs
['switch_outport_name'] = dst_sw_outport_name
727 kwargs
['pathindex'] = i
729 if self
.controller
== RemoteController
:
730 # set flow entry via ryu rest api
731 self
._set
_flow
_entry
_ryu
_rest
(
732 current_node
, switch_inport_nr
, switch_outport_nr
, **kwargs
)
734 # set flow entry via ovs-ofctl
735 self
._set
_flow
_entry
_dpctl
(
736 current_node
, switch_inport_nr
, switch_outport_nr
, **kwargs
)
738 # take first link between switches by default
739 if isinstance(next_node
, OVSSwitch
):
740 switch_inport_nr
= self
.DCNetwork_graph
[current_hop
][next_hop
][0]['dst_port_nr']
741 current_hop
= next_hop
744 'priority': kwargs
.get('priority', DEFAULT_PRIORITY
),
745 'cookie': kwargs
.get('cookie', DEFAULT_COOKIE
),
746 'vlan': kwargs
['vlan'],
747 'path': kwargs
['path'],
748 'match_input': kwargs
.get('match')
750 flow_options_str
= json
.dumps(flow_options
, indent
=1)
751 LOG
.info("Installed flow rule: ({}:{}) -> ({}:{}) with options: {}"
752 .format(vnf_src_name
, vnf_src_interface
, vnf_dst_name
, vnf_dst_interface
, flow_options
))
753 return "success: {2} between {0} and {1} with options: {3}".format(
754 vnf_src_name
, vnf_dst_name
, cmd
, flow_options_str
)
756 def _set_flow_entry_ryu_rest(
757 self
, node
, switch_inport_nr
, switch_outport_nr
, **kwargs
):
758 match
= 'in_port=%s' % switch_inport_nr
760 cookie
= kwargs
.get('cookie')
761 match_input
= kwargs
.get('match')
762 cmd
= kwargs
.get('cmd')
763 path
= kwargs
.get('path')
764 index
= kwargs
.get('pathindex')
765 mod_dl_dst
= kwargs
.get('mod_dl_dst')
767 vlan
= kwargs
.get('vlan')
768 priority
= kwargs
.get('priority', DEFAULT_PRIORITY
)
769 # flag to not set the ovs port vlan tag
770 skip_vlan_tag
= kwargs
.get('skip_vlan_tag')
771 # table id to put this flowentry
772 table_id
= kwargs
.get('table_id')
778 match
= s
.join([match
, match_input
])
781 flow
['dpid'] = int(node
.dpid
, 16)
784 flow
['cookie'] = int(cookie
)
786 flow
['priority'] = int(priority
)
788 flow
['table_id'] = table_id
792 # possible Ryu actions, match fields:
793 # http://ryu.readthedocs.io/en/latest/app/ofctl_rest.html#add-a-flow-entry
794 if cmd
== 'add-flow':
795 prefix
= 'stats/flowentry/add'
797 if index
== 0: # first node
798 # set vlan tag in ovs instance (to isolate E-LANs)
799 if not skip_vlan_tag
:
800 in_port_name
= kwargs
.get('switch_inport_name')
801 self
._set
_vlan
_tag
(node
, in_port_name
, vlan
)
802 # set vlan push action if more than 1 switch in the path
805 # Push a new VLAN tag if a input frame is
807 action
['type'] = 'PUSH_VLAN'
808 # Ethertype 0x8100(=33024): IEEE 802.1Q VLAN-tagged
810 action
['ethertype'] = 33024
811 flow
['actions'].append(action
)
813 action
['type'] = 'SET_FIELD'
814 action
['field'] = 'vlan_vid'
815 # ryu expects the field to be masked
816 action
['value'] = vlan |
0x1000
817 flow
['actions'].append(action
)
819 elif index
== len(path
) - 1: # last node
820 # set vlan tag in ovs instance (to isolate E-LANs)
821 if not skip_vlan_tag
:
822 out_port_name
= kwargs
.get('switch_outport_name')
823 self
._set
_vlan
_tag
(node
, out_port_name
, vlan
)
824 # set vlan pop action if more than 1 switch in the path
826 match
+= ',dl_vlan=%s' % vlan
828 action
['type'] = 'POP_VLAN'
829 flow
['actions'].append(action
)
832 match
+= ',dl_vlan=%s' % vlan
835 action
['type'] = 'SET_FIELD'
836 action
['field'] = 'eth_dst'
837 action
['value'] = mod_dl_dst
838 flow
['actions'].append(action
)
840 # output action must come last
842 action
['type'] = 'OUTPUT'
843 action
['port'] = switch_outport_nr
844 flow
['actions'].append(action
)
846 elif cmd
== 'del-flows':
847 prefix
= 'stats/flowentry/delete'
850 # TODO: add cookie_mask as argument
851 # need full mask to match complete cookie
852 flow
['cookie_mask'] = int('0xffffffffffffffff', 16)
855 action
['type'] = 'OUTPUT'
856 action
['port'] = switch_outport_nr
857 flow
['actions'].append(action
)
859 flow
['match'] = self
._parse
_match
(match
)
860 self
.ryu_REST(prefix
, data
=flow
)
862 def _set_vlan_tag(self
, node
, switch_port
, tag
):
863 node
.vsctl('set', 'port {0} tag={1}'.format(switch_port
, tag
))
864 LOG
.debug("set vlan in switch: {0} in_port: {1} vlan tag: {2}".format(
865 node
.name
, switch_port
, tag
))
867 def _set_flow_entry_dpctl(
868 self
, node
, switch_inport_nr
, switch_outport_nr
, **kwargs
):
870 match
= 'in_port=%s' % switch_inport_nr
872 cookie
= kwargs
.get('cookie')
873 match_input
= kwargs
.get('match')
874 cmd
= kwargs
.get('cmd')
875 path
= kwargs
.get('path')
876 index
= kwargs
.get('pathindex')
877 vlan
= kwargs
.get('vlan')
881 cookie
= 'cookie=%s' % cookie
882 match
= s
.join([cookie
, match
])
884 match
= s
.join([match
, match_input
])
885 if cmd
== 'add-flow':
886 action
= 'action=%s' % switch_outport_nr
888 if index
== 0: # first node
889 action
= ('action=mod_vlan_vid:%s' % vlan
) + \
890 (',output=%s' % switch_outport_nr
)
891 match
= '-O OpenFlow13 ' + match
892 elif index
== len(path
) - 1: # last node
893 match
+= ',dl_vlan=%s' % vlan
894 action
= 'action=strip_vlan,output=%s' % switch_outport_nr
896 match
+= ',dl_vlan=%s' % vlan
897 ofcmd
= s
.join([match
, action
])
898 elif cmd
== 'del-flows':
903 node
.dpctl(cmd
, ofcmd
)
904 LOG
.info("{3} in switch: {0} in_port: {1} out_port: {2}".format(node
.name
, switch_inport_nr
,
905 switch_outport_nr
, cmd
))
907 # start Ryu Openflow controller as Remote Controller for the DCNetwork
908 def startRyu(self
, learning_switch
=True):
909 # start Ryu controller with rest-API
911 # ryu default learning switch
912 # ryu_learning_app = python_install_path + '/ryu/app/simple_switch_13.py'
913 # custom learning switch that installs a default NORMAL action in the
915 dir_path
= os
.path
.dirname(os
.path
.realpath(__file__
))
916 ryu_learning_app
= dir_path
+ '/son_emu_simple_switch_13.py'
917 ryu_rest_app
= 'ryu.app.ofctl_rest'
918 # change the default Openflow controller port to 6653 (official IANA-assigned port number), as used by Mininet
919 # Ryu still uses 6633 as default
920 ryu_option
= '--ofp-tcp-listen-port'
922 ryu_cmd
= 'ryu-manager'
923 FNULL
= open("/tmp/ryu.log", 'w')
925 # learning and rest api
926 args
= [ryu_cmd
, ryu_learning_app
, ryu_rest_app
, ryu_option
, ryu_of_port
]
928 # no learning switch, but with rest api
929 args
= [ryu_cmd
, ryu_rest_app
, ryu_option
, ryu_of_port
]
930 self
.ryu_process
= Popen(args
, stdout
=FNULL
, stderr
=FNULL
)
931 LOG
.debug('starting ryu-controller with %s' % args
)
936 Stop the Ryu controller that might be started by son-emu.
940 if self
.ryu_process
is not None:
941 self
.ryu_process
.terminate()
942 self
.ryu_process
.kill()
943 # ensure its death ;-)
944 Popen(['pkill', '-f', 'ryu-manager'])
946 def ryu_REST(self
, prefix
, dpid
=None, data
=None):
949 url
= self
.ryu_REST_api
+ '/' + str(prefix
) + '/' + str(dpid
)
951 url
= self
.ryu_REST_api
+ '/' + str(prefix
)
953 LOG
.debug('sending RYU command: %s, payload: %s', url
, data
)
955 req
= self
.RyuSession
.post(url
, json
=data
)
957 req
= self
.RyuSession
.get(url
)
959 # do extra logging if status code is not 200 (OK)
960 if req
.status_code
is not requests
.codes
.ok
:
962 'type {0} encoding: {1} text: {2} headers: {3} history: {4}'.format(req
.headers
['content-type'],
963 req
.encoding
, req
.text
,
964 req
.headers
, req
.history
))
965 LOG
.info('url: {0}'.format(str(url
)))
967 LOG
.info('POST: {0}'.format(str(data
)))
968 LOG
.info('status: {0} reason: {1}'.format(
969 req
.status_code
, req
.reason
))
971 if 'json' in req
.headers
['content-type']:
975 ret
= req
.text
.rstrip()
978 # need to respect that some match fields must be integers
979 # http://ryu.readthedocs.io/en/latest/app/ofctl_rest.html#description-of-match-and-actions
981 def _parse_match(self
, match
):
982 matches
= match
.split(',')
988 m2
= int(match
[1], 0)
989 except BaseException
:
992 dict.update({match
[0]: m2
})
995 def find_connected_dc_interface(
996 self
, vnf_src_name
, vnf_src_interface
=None):
998 if vnf_src_interface
is None:
999 # take first interface by default
1000 connected_sw
= self
.DCNetwork_graph
.neighbors(vnf_src_name
)[0]
1001 link_dict
= self
.DCNetwork_graph
[vnf_src_name
][connected_sw
]
1002 vnf_src_interface
= link_dict
[0]['src_port_id']
1004 for connected_sw
in self
.DCNetwork_graph
.neighbors(vnf_src_name
):
1005 link_dict
= self
.DCNetwork_graph
[vnf_src_name
][connected_sw
]
1006 for link
in link_dict
:
1007 if (link_dict
[link
]['src_port_id'] == vnf_src_interface
or
1008 link_dict
[link
]['src_port_name'] == vnf_src_interface
):
1009 # Fix: we might also get interface names, e.g, from a son-emu-cli call
1010 # found the right link and connected switch
1011 src_sw_inport_name
= link_dict
[link
]['dst_port_name']
1012 return src_sw_inport_name