2c5ce14227133d9b5f89ca9d03f8b37273d8f3d7
2 Copyright (c) 2015 SONATA-NFV and Paderborn University
5 Licensed under the Apache License, Version 2.0 (the "License");
6 you may not use this file except in compliance with the License.
7 You may obtain a copy of the License at
9 http://www.apache.org/licenses/LICENSE-2.0
11 Unless required by applicable law or agreed to in writing, software
12 distributed under the License is distributed on an "AS IS" BASIS,
13 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 See the License for the specific language governing permissions and
15 limitations under the License.
17 Neither the name of the SONATA-NFV [, ANY ADDITIONAL AFFILIATION]
18 nor the names of its contributors may be used to endorse or promote
19 products derived from this software without specific prior written
22 This work has been performed in the framework of the SONATA project,
23 funded by the European Commission under Grant number 671517 through
24 the Horizon 2020 and 5G-PPP programmes. The authors would like to
25 acknowledge the contributions of their colleagues of the SONATA
26 partner consortium (www.sonata-nfv.eu).
32 from subprocess
import Popen
36 from functools
import partial
38 from mininet
.net
import Containernet
39 from mininet
.node
import Controller
, DefaultController
, OVSSwitch
, OVSKernelSwitch
, Docker
, RemoteController
40 from mininet
.cli
import CLI
41 from mininet
.link
import TCLink
43 from emuvim
.dcemulator
.monitoring
import DCNetworkMonitor
44 from emuvim
.dcemulator
.node
import Datacenter
, EmulatorCompute
45 from emuvim
.dcemulator
.resourcemodel
import ResourceModelRegistrar
47 class DCNetwork(Containernet
):
49 Wraps the original Mininet/Containernet class and provides
50 methods to add data centers, switches, etc.
52 This class is used by topology definition scripts.
55 def __init__(self
, controller
=RemoteController
, monitor
=False,
56 enable_learning
= True, # in case of RemoteController (Ryu), learning switch behavior can be turned off/on
57 dc_emulation_max_cpu
=1.0, # fraction of overall CPU time for emulation
58 dc_emulation_max_mem
=512, # emulation max mem in MB
61 Create an extended version of a Containernet network
62 :param dc_emulation_max_cpu: max. CPU time used by containers in data centers
63 :param kwargs: path through for Mininet parameters
68 # make sure any remaining Ryu processes are killed
70 # make sure no containers are left over from a previous emulator run.
71 self
.removeLeftoverContainers()
73 # call original Docker.__init__ and setup default controller
74 Containernet
.__init
__(
75 self
, switch
=OVSKernelSwitch
, controller
=controller
, **kwargs
)
79 self
.ryu_process
= None
80 if controller
== RemoteController
:
81 # start Ryu controller
82 self
.startRyu(learning_switch
=enable_learning
)
84 # add the specified controller
85 self
.addController('c0', controller
=controller
)
87 # graph of the complete DC network
88 self
.DCNetwork_graph
= nx
.MultiDiGraph()
90 # initialize pool of vlan tags to setup the SDN paths
91 self
.vlans
= range(4096)[::-1]
93 # link to Ryu REST_API
96 self
.ryu_REST_api
= 'http://{0}:{1}'.format(ryu_ip
, ryu_port
)
100 self
.monitor_agent
= DCNetworkMonitor(self
)
102 self
.monitor_agent
= None
104 # initialize resource model registrar
105 self
.rm_registrar
= ResourceModelRegistrar(
106 dc_emulation_max_cpu
, dc_emulation_max_mem
)
108 def addDatacenter(self
, label
, metadata
={}, resource_log_path
=None):
110 Create and add a logical cloud data center to the network.
112 if label
in self
.dcs
:
113 raise Exception("Data center label already exists: %s" % label
)
114 dc
= Datacenter(label
, metadata
=metadata
, resource_log_path
=resource_log_path
)
115 dc
.net
= self
# set reference to network
117 dc
.create() # finally create the data center in our Mininet instance
118 logging
.info("added data center: %s" % label
)
121 def addLink(self
, node1
, node2
, **params
):
123 Able to handle Datacenter objects as link
126 assert node1
is not None
127 assert node2
is not None
128 logging
.debug("addLink: n1=%s n2=%s" % (str(node1
), str(node2
)))
129 # ensure type of node1
130 if isinstance( node1
, basestring
):
131 if node1
in self
.dcs
:
132 node1
= self
.dcs
[node1
].switch
133 if isinstance( node1
, Datacenter
):
135 # ensure type of node2
136 if isinstance( node2
, basestring
):
137 if node2
in self
.dcs
:
138 node2
= self
.dcs
[node2
].switch
139 if isinstance( node2
, Datacenter
):
141 # try to give containers a default IP
142 if isinstance( node1
, Docker
):
143 if "params1" not in params
:
144 params
["params1"] = {}
145 if "ip" not in params
["params1"]:
146 params
["params1"]["ip"] = self
.getNextIp()
147 if isinstance( node2
, Docker
):
148 if "params2" not in params
:
149 params
["params2"] = {}
150 if "ip" not in params
["params2"]:
151 params
["params2"]["ip"] = self
.getNextIp()
152 # ensure that we allow TCLinks between data centers
153 # TODO this is not optimal, we use cls=Link for containers and TCLink for data centers
154 # see Containernet issue: https://github.com/mpeuster/containernet/issues/3
155 if "cls" not in params
:
156 params
["cls"] = TCLink
158 link
= Containernet
.addLink(self
, node1
, node2
, **params
)
160 # try to give container interfaces a default id
161 node1_port_id
= node1
.ports
[link
.intf1
]
162 if isinstance(node1
, Docker
):
163 if "id" in params
["params1"]:
164 node1_port_id
= params
["params1"]["id"]
165 node1_port_name
= link
.intf1
.name
167 node2_port_id
= node2
.ports
[link
.intf2
]
168 if isinstance(node2
, Docker
):
169 if "id" in params
["params2"]:
170 node2_port_id
= params
["params2"]["id"]
171 node2_port_name
= link
.intf2
.name
174 # add edge and assigned port number to graph in both directions between node1 and node2
175 # port_id: id given in descriptor (if available, otherwise same as port)
176 # port: portnumber assigned by Containernet
179 # possible weight metrics allowed by TClink class:
180 weight_metrics
= ['bw', 'delay', 'jitter', 'loss']
181 edge_attributes
= [p
for p
in params
if p
in weight_metrics
]
182 for attr
in edge_attributes
:
183 # if delay: strip ms (need number as weight in graph)
184 match
= re
.search('([0-9]*\.?[0-9]+)', params
[attr
])
186 attr_number
= match
.group(1)
189 attr_dict
[attr
] = attr_number
192 attr_dict2
= {'src_port_id': node1_port_id
, 'src_port_nr': node1
.ports
[link
.intf1
],
193 'src_port_name': node1_port_name
,
194 'dst_port_id': node2_port_id
, 'dst_port_nr': node2
.ports
[link
.intf2
],
195 'dst_port_name': node2_port_name
}
196 attr_dict2
.update(attr_dict
)
197 self
.DCNetwork_graph
.add_edge(node1
.name
, node2
.name
, attr_dict
=attr_dict2
)
199 attr_dict2
= {'src_port_id': node2_port_id
, 'src_port_nr': node2
.ports
[link
.intf2
],
200 'src_port_name': node2_port_name
,
201 'dst_port_id': node1_port_id
, 'dst_port_nr': node1
.ports
[link
.intf1
],
202 'dst_port_name': node1_port_name
}
203 attr_dict2
.update(attr_dict
)
204 self
.DCNetwork_graph
.add_edge(node2
.name
, node1
.name
, attr_dict
=attr_dict2
)
208 def addDocker( self
, label
, **params
):
210 Wrapper for addDocker method to use custom container class.
212 self
.DCNetwork_graph
.add_node(label
)
213 return Containernet
.addDocker(self
, label
, cls
=EmulatorCompute
, **params
)
215 def removeDocker( self
, label
, **params
):
217 Wrapper for removeDocker method to update graph.
219 self
.DCNetwork_graph
.remove_node(label
)
220 return Containernet
.removeDocker(self
, label
, **params
)
222 def addSwitch( self
, name
, add_to_graph
=True, **params
):
224 Wrapper for addSwitch method to store switch also in graph.
227 self
.DCNetwork_graph
.add_node(name
)
228 return Containernet
.addSwitch(self
, name
, protocols
='OpenFlow10,OpenFlow12,OpenFlow13', **params
)
230 def getAllContainers(self
):
232 Returns a list with all containers within all data centers.
235 for dc
in self
.dcs
.itervalues():
236 all_containers
+= dc
.listCompute()
237 return all_containers
241 for dc
in self
.dcs
.itervalues():
243 Containernet
.start(self
)
247 # stop the monitor agent
248 if self
.monitor_agent
is not None:
249 self
.monitor_agent
.stop()
252 Containernet
.stop(self
)
254 # stop Ryu controller
261 # to remove chain do setChain( src, dst, cmd='del-flows')
262 def setChain(self
, vnf_src_name
, vnf_dst_name
, vnf_src_interface
=None, vnf_dst_interface
=None, **kwargs
):
263 cmd
= kwargs
.get('cmd')
264 if cmd
== 'add-flow':
265 ret
= self
._chainAddFlow
(vnf_src_name
, vnf_dst_name
, vnf_src_interface
, vnf_dst_interface
, **kwargs
)
266 if kwargs
.get('bidirectional'):
267 ret
= ret
+'\n' + self
._chainAddFlow
(vnf_dst_name
, vnf_src_name
, vnf_dst_interface
, vnf_src_interface
, **kwargs
)
269 elif cmd
== 'del-flows':
270 ret
= self
._chainAddFlow
(vnf_src_name
, vnf_dst_name
, vnf_src_interface
, vnf_dst_interface
, **kwargs
)
271 if kwargs
.get('bidirectional'):
272 ret
= ret
+ '\n' + self
._chainAddFlow
(vnf_dst_name
, vnf_src_name
, vnf_dst_interface
, vnf_src_interface
, **kwargs
)
275 ret
= "Command unknown"
280 def _chainAddFlow(self
, vnf_src_name
, vnf_dst_name
, vnf_src_interface
=None, vnf_dst_interface
=None, **kwargs
):
282 #check if port is specified (vnf:port)
283 if vnf_src_interface
is None:
284 # take first interface by default
285 connected_sw
= self
.DCNetwork_graph
.neighbors(vnf_src_name
)[0]
286 link_dict
= self
.DCNetwork_graph
[vnf_src_name
][connected_sw
]
287 vnf_src_interface
= link_dict
[0]['src_port_id']
289 for connected_sw
in self
.DCNetwork_graph
.neighbors(vnf_src_name
):
290 link_dict
= self
.DCNetwork_graph
[vnf_src_name
][connected_sw
]
291 for link
in link_dict
:
292 if link_dict
[link
]['src_port_id'] == vnf_src_interface
:
293 # found the right link and connected switch
294 src_sw
= connected_sw
296 src_sw_inport_nr
= link_dict
[link
]['dst_port_nr']
299 if vnf_dst_interface
is None:
300 # take first interface by default
301 connected_sw
= self
.DCNetwork_graph
.neighbors(vnf_dst_name
)[0]
302 link_dict
= self
.DCNetwork_graph
[connected_sw
][vnf_dst_name
]
303 vnf_dst_interface
= link_dict
[0]['dst_port_id']
305 vnf_dst_name
= vnf_dst_name
.split(':')[0]
306 for connected_sw
in self
.DCNetwork_graph
.neighbors(vnf_dst_name
):
307 link_dict
= self
.DCNetwork_graph
[connected_sw
][vnf_dst_name
]
308 for link
in link_dict
:
309 if link_dict
[link
]['dst_port_id'] == vnf_dst_interface
:
310 # found the right link and connected switch
311 dst_sw
= connected_sw
312 dst_sw_outport_nr
= link_dict
[link
]['src_port_nr']
318 # returns the first found shortest path
319 # if all shortest paths are wanted, use: all_shortest_paths
320 path
= nx
.shortest_path(self
.DCNetwork_graph
, src_sw
, dst_sw
, weight
=kwargs
.get('weight'))
322 logging
.info("No path could be found between {0} and {1}".format(vnf_src_name
, vnf_dst_name
))
323 return "No path could be found between {0} and {1}".format(vnf_src_name
, vnf_dst_name
)
325 logging
.info("Path between {0} and {1}: {2}".format(vnf_src_name
, vnf_dst_name
, path
))
328 switch_inport_nr
= src_sw_inport_nr
330 # choose free vlan if path contains more than 1 switch
331 cmd
= kwargs
.get('cmd')
333 if cmd
== 'add-flow':
335 vlan
= self
.vlans
.pop()
337 for i
in range(0,len(path
)):
338 current_node
= self
.getNodeByName(current_hop
)
340 if path
.index(current_hop
) < len(path
)-1:
341 next_hop
= path
[path
.index(current_hop
)+1]
344 next_hop
= vnf_dst_name
346 next_node
= self
.getNodeByName(next_hop
)
348 if next_hop
== vnf_dst_name
:
349 switch_outport_nr
= dst_sw_outport_nr
350 logging
.info("end node reached: {0}".format(vnf_dst_name
))
351 elif not isinstance( next_node
, OVSSwitch
):
352 logging
.info("Next node: {0} is not a switch".format(next_hop
))
353 return "Next node: {0} is not a switch".format(next_hop
)
355 # take first link between switches by default
357 switch_outport_nr
= self
.DCNetwork_graph
[current_hop
][next_hop
][index_edge_out
]['src_port_nr']
360 # set of entry via ovs-ofctl
361 if isinstance( current_node
, OVSSwitch
):
362 kwargs
['vlan'] = vlan
363 kwargs
['path'] = path
364 kwargs
['current_hop'] = current_hop
366 if self
.controller
== RemoteController
:
367 ## set flow entry via ryu rest api
368 self
._set
_flow
_entry
_ryu
_rest
(current_node
, switch_inport_nr
, switch_outport_nr
, **kwargs
)
370 ## set flow entry via ovs-ofctl
371 self
._set
_flow
_entry
_dpctl
(current_node
, switch_inport_nr
, switch_outport_nr
, **kwargs
)
375 # take first link between switches by default
376 if isinstance( next_node
, OVSSwitch
):
377 switch_inport_nr
= self
.DCNetwork_graph
[current_hop
][next_hop
][0]['dst_port_nr']
378 current_hop
= next_hop
380 return "path {2} between {0} and {1}".format(vnf_src_name
, vnf_dst_name
, cmd
)
382 def _set_flow_entry_ryu_rest(self
, node
, switch_inport_nr
, switch_outport_nr
, **kwargs
):
383 match
= 'in_port=%s' % switch_inport_nr
385 cookie
= kwargs
.get('cookie')
386 match_input
= kwargs
.get('match')
387 cmd
= kwargs
.get('cmd')
388 path
= kwargs
.get('path')
389 current_hop
= kwargs
.get('current_hop')
390 vlan
= kwargs
.get('vlan')
394 match
= s
.join([match
, match_input
])
397 flow
['dpid'] = int(node
.dpid
, 16)
400 flow
['cookie'] = int(cookie
)
405 # possible Ryu actions, match fields:
406 # http://ryu.readthedocs.io/en/latest/app/ofctl_rest.html#add-a-flow-entry
407 if cmd
== 'add-flow':
408 prefix
= 'stats/flowentry/add'
410 if path
.index(current_hop
) == 0: # first node
412 action
['type'] = 'PUSH_VLAN' # Push a new VLAN tag if a input frame is non-VLAN-tagged
413 action
['ethertype'] = 33024 # Ethertype 0x8100(=33024): IEEE 802.1Q VLAN-tagged frame
414 flow
['actions'].append(action
)
416 action
['type'] = 'SET_FIELD'
417 action
['field'] = 'vlan_vid'
418 action
['value'] = vlan
419 flow
['actions'].append(action
)
420 elif path
.index(current_hop
) == len(path
) - 1: # last node
421 match
+= ',dl_vlan=%s' % vlan
423 action
['type'] = 'POP_VLAN'
424 flow
['actions'].append(action
)
426 match
+= ',dl_vlan=%s' % vlan
427 # output action must come last
429 action
['type'] = 'OUTPUT'
430 action
['port'] = switch_outport_nr
431 flow
['actions'].append(action
)
433 elif cmd
== 'del-flows':
434 prefix
= 'stats/flowentry/delete'
437 # TODO: add cookie_mask as argument
438 flow
['cookie_mask'] = int('0xffffffffffffffff', 16) # need full mask to match complete cookie
441 action
['type'] = 'OUTPUT'
442 action
['port'] = switch_outport_nr
443 flow
['actions'].append(action
)
445 flow
['match'] = self
._parse
_match
(match
)
446 self
.ryu_REST(prefix
, data
=flow
)
448 def _set_flow_entry_dpctl(self
, node
, switch_inport_nr
, switch_outport_nr
, **kwargs
):
449 match
= 'in_port=%s' % switch_inport_nr
451 cookie
= kwargs
.get('cookie')
452 match_input
= kwargs
.get('match')
453 cmd
= kwargs
.get('cmd')
454 path
= kwargs
.get('path')
455 current_hop
= kwargs
.get('current_hop')
456 vlan
= kwargs
.get('vlan')
460 cookie
= 'cookie=%s' % cookie
461 match
= s
.join([cookie
, match
])
463 match
= s
.join([match
, match_input
])
464 if cmd
== 'add-flow':
465 action
= 'action=%s' % switch_outport_nr
467 if path
.index(current_hop
) == 0: # first node
468 action
= ('action=mod_vlan_vid:%s' % vlan
) + (',output=%s' % switch_outport_nr
)
469 match
= '-O OpenFlow13 ' + match
470 elif path
.index(current_hop
) == len(path
) - 1: # last node
471 match
+= ',dl_vlan=%s' % vlan
472 action
= 'action=strip_vlan,output=%s' % switch_outport_nr
474 match
+= ',dl_vlan=%s' % vlan
475 ofcmd
= s
.join([match
, action
])
476 elif cmd
== 'del-flows':
481 node
.dpctl(cmd
, ofcmd
)
482 logging
.info("{3} in switch: {0} in_port: {1} out_port: {2}".format(node
.name
, switch_inport_nr
,
483 switch_outport_nr
, cmd
))
485 # start Ryu Openflow controller as Remote Controller for the DCNetwork
486 def startRyu(self
, learning_switch
=True):
487 # start Ryu controller with rest-API
488 python_install_path
= site
.getsitepackages()[0]
489 ryu_path
= python_install_path
+ '/ryu/app/simple_switch_13.py'
490 ryu_path2
= python_install_path
+ '/ryu/app/ofctl_rest.py'
491 # change the default Openflow controller port to 6653 (official IANA-assigned port number), as used by Mininet
492 # Ryu still uses 6633 as default
493 ryu_option
= '--ofp-tcp-listen-port'
495 ryu_cmd
= 'ryu-manager'
496 FNULL
= open("/tmp/ryu.log", 'w')
498 self
.ryu_process
= Popen([ryu_cmd
, ryu_path
, ryu_path2
, ryu_option
, ryu_of_port
], stdout
=FNULL
, stderr
=FNULL
)
500 # no learning switch, but with rest api
501 self
.ryu_process
= Popen([ryu_cmd
, ryu_path2
, ryu_option
, ryu_of_port
], stdout
=FNULL
, stderr
=FNULL
)
505 if self
.ryu_process
is not None:
506 self
.ryu_process
.terminate()
507 self
.ryu_process
.kill()
511 def removeLeftoverContainers():
512 # TODO can be more python-based using eg. docker-py?
513 Popen('docker ps -a -q --filter="name=mn.*" | xargs -r docker rm -f', shell
=True)
517 Popen(['pkill', '-f', 'ryu-manager'])
519 def ryu_REST(self
, prefix
, dpid
=None, data
=None):
522 url
= self
.ryu_REST_api
+ '/' + str(prefix
) + '/' + str(dpid
)
524 url
= self
.ryu_REST_api
+ '/' + str(prefix
)
526 #logging.info('POST: {0}'.format(str(data)))
527 req
= urllib2
.Request(url
, str(data
))
529 req
= urllib2
.Request(url
)
531 ret
= urllib2
.urlopen(req
).read()
534 logging
.info('error url: {0}'.format(str(url
)))
535 if data
: logging
.info('error POST: {0}'.format(str(data
)))
537 # need to respect that some match fields must be integers
538 # http://ryu.readthedocs.io/en/latest/app/ofctl_rest.html#description-of-match-and-actions
539 def _parse_match(self
, match
):
540 matches
= match
.split(',')
546 m2
= int(match
[1], 0)
550 dict.update({match
[0]:m2
})