2 Copyright (c) 2017 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, Paderborn University
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).
28 """Openstack manage component of PG Sandman.
31 :synopsis: Module containing the OpenstackManage class.
32 .. moduleauthor: PG Sandman
43 from emuvim
.api
.openstack
.resources
import Net
, Port
44 from mininet
.node
import OVSSwitch
, RemoteController
, Node
47 class OpenstackManage(object):
49 OpenstackManage is a singleton and management component for the emulator.
50 It is the brain of the Openstack component and manages everything that is not datacenter specific like
51 network chains or load balancers.
56 if OpenstackManage
.__instance
is None:
57 OpenstackManage
.__instance
= object.__new
__(cls
)
58 return OpenstackManage
.__instance
60 def __init__(self
, ip
="0.0.0.0", port
=4000):
61 # we are a singleton, only initialize once!
62 self
.lock
= threading
.Lock()
64 if hasattr(self
, "init"):
68 self
.endpoints
= dict()
74 # to keep track which src_vnf(input port on the switch) handles a load balancer
75 self
.lb_flow_cookies
= dict()
76 self
.chain_flow_cookies
= dict()
78 # for the visualization also store the complete chain data incl. paths
79 self
.full_chain_data
= dict()
80 self
.full_lb_data
= dict()
82 # flow groups could be handled for each switch separately, but this global group counter should be easier to
83 # debug and to maintain
84 self
.flow_groups
= dict()
86 # we want one global chain api. this should not be datacenter dependent!
87 self
.chain
= chain_api
.ChainApi(ip
, port
, self
)
88 self
.thread
= threading
.Thread(target
=self
.chain
._start
_flask
, args
=())
89 self
.thread
.daemon
= True
90 self
.thread
.name
= self
.chain
.__class
__
93 # floating ip network setup
94 self
.floating_switch
= None
95 self
.floating_network
= None
96 self
.floating_netmask
= "192.168.100.0/24"
97 self
.floating_nodes
= dict()
98 self
.floating_cookies
= dict()
99 self
.floating_intf
= None
100 self
.floating_links
= dict()
107 def net(self
, value
):
108 if self
._net
is None:
110 self
.init_floating_network()
113 def init_floating_network(self
):
115 Initialize the floating network component for the emulator.
116 Will not do anything if already initialized.
118 if self
.net
is not None and self
.floating_switch
is None:
119 # create a floating network
120 fn
= self
.floating_network
= Net("default")
121 fn
.id = str(uuid
.uuid4())
122 fn
.set_cidr(self
.floating_netmask
)
125 fn
.subnet_id
= str(uuid
.uuid4())
126 fn
.subnet_name
= fn
.name
+ "-sub"
128 # create a port for the host
129 port
= Port("root-port")
130 #port.id = str(uuid.uuid4())
131 port
.net_name
= fn
.name
134 root_ip
= fn
.get_new_ip_address(port
.name
)
135 port
.ip_address
= root_ip
136 # floating ip network setup
137 # wierd way of getting a datacenter object
138 first_dc
= self
.net
.dcs
.values()[0]
139 # set a dpid for the switch. for this we have to get the id of the next possible dc
140 self
.floating_switch
= self
.net
.addSwitch("fs1", dpid
=hex(first_dc
._get
_next
_dc
_dpid
())[2:])
141 # this is the interface appearing on the physical host
142 self
.floating_root
= Node('root', inNamespace
=False)
143 self
.net
.hosts
.append(self
.floating_root
)
144 self
.net
.nameToNode
['root'] = self
.floating_root
145 self
.floating_intf
= self
.net
.addLink(self
.floating_root
, self
.floating_switch
).intf1
146 self
.floating_root
.setIP(root_ip
, intf
=self
.floating_intf
)
147 self
.floating_nodes
[(self
.floating_root
.name
, root_ip
)] = self
.floating_root
150 def stop_floating_network(self
):
152 self
.floating_switch
= None
154 def add_endpoint(self
, ep
):
156 Registers an openstack endpoint with manage
158 :param ep: Openstack API endpoint
159 :type ep: :class:`heat.openstack_api_endpoint`
161 key
= "%s:%s" % (ep
.ip
, ep
.port
)
162 self
.endpoints
[key
] = ep
164 def get_cookie(self
):
166 Get an unused cookie.
171 cookie
= int(max(self
.cookies
) + 1)
172 self
.cookies
.add(cookie
)
175 def get_flow_group(self
, src_vnf_name
, src_vnf_interface
):
177 Gets free group that is not currently used by any other flow for the specified interface / VNF.
179 :param src_vnf_name: Source VNF name
180 :type src_vnf_name: ``str``
181 :param src_vnf_interface: Source VNF interface name
182 :type src_vnf_interface: ``str``
183 :return: Flow group identifier.
186 if (src_vnf_name
, src_vnf_interface
) not in self
.flow_groups
:
187 grp
= int(len(self
.flow_groups
) + 1)
188 self
.flow_groups
[(src_vnf_name
, src_vnf_interface
)] = grp
190 grp
= self
.flow_groups
[(src_vnf_name
, src_vnf_interface
)]
193 def check_vnf_intf_pair(self
, vnf_name
, vnf_intf_name
):
195 Checks if a VNF exists and has the given interface
197 :param vnf_name: Name of the VNF to be checked
198 :type vnf_name: ``str``
199 :param vnf_intf_name: Name of the interface that belongst to the VNF
200 :type vnf_intf_name: ``str``
201 :return: ``True`` if it is valid pair, else ``False``
205 if vnf_name
in self
.net
:
206 vnf
= self
.net
.getNodeByName(vnf_name
)
207 return vnf_intf_name
in vnf
.nameToIntf
209 def network_action_start(self
, vnf_src_name
, vnf_dst_name
, **kwargs
):
211 Starts a network chain for a source destination pair
213 :param vnf_src_name: Name of the source VNF
214 :type vnf_src_name: ``str``
215 :param vnf_dst_name: Name of the source VNF interface
216 :type vnf_dst_name: ``str``
217 :param \**kwargs: See below
220 * *vnf_src_interface* (``str``): Name of source interface.
221 * *vnf_dst_interface* (``str``): Name of destination interface.
222 * *weight* (``int``): This value is fed into the shortest path computation if no path is specified.
223 * *match* (``str``): A custom match entry for the openflow flow rules. Only vlanid or port possible.
224 * *bidirectional* (``bool``): If set the chain will be set in both directions, else it will just set up \
225 from source to destination.
226 * *cookie* (``int``): Cookie value used by openflow. Used to identify the flows in the switches to be \
227 able to modify the correct flows.
228 * *no_route* (``bool``): If set a layer 3 route to the target interface will not be set up.
229 :return: The cookie chosen for the flow.
233 vnf_src_interface
= kwargs
.get('vnf_src_interface')
234 vnf_dst_interface
= kwargs
.get('vnf_dst_interface')
235 layer2
= kwargs
.get('layer2', True)
236 match
= kwargs
.get('match')
237 flow
= (vnf_src_name
, vnf_src_interface
, vnf_dst_name
, vnf_dst_interface
)
238 if flow
in self
.chain_flow_cookies
:
239 raise Exception("There is already a chain at the specified src/dst pair!")
240 # set up a layer 2 chain, this allows multiple chains for the same interface
241 src_node
= self
.net
.getNodeByName(vnf_src_name
)
242 dst_node
= self
.net
.getNodeByName(vnf_dst_name
)
243 dst_intf
= dst_node
.intf(vnf_dst_interface
)
245 switch
, inport
= self
._get
_connected
_switch
_data
(vnf_src_name
, vnf_src_interface
)
246 self
.setup_arp_reply_at(switch
, inport
, dst_intf
.IP(), dst_intf
.MAC())
247 if isinstance(match
, str):
248 match
+= ",dl_dst=%s" % dst_intf
.MAC()
250 match
= "dl_dst=%s" % dst_intf
.MAC()
252 cookie
= kwargs
.get('cookie', self
.get_cookie())
253 self
.cookies
.add(cookie
)
254 c
= self
.net
.setChain(
255 vnf_src_name
, vnf_dst_name
,
256 vnf_src_interface
=vnf_src_interface
,
257 vnf_dst_interface
=vnf_dst_interface
,
259 weight
=kwargs
.get('weight'),
263 path
=kwargs
.get('path'))
265 # to keep this logic seperate of the core son-emu do the housekeeping here
267 data
["src_vnf"] = vnf_src_name
268 data
["src_intf"] = vnf_src_interface
269 data
["dst_vnf"] = vnf_dst_name
270 data
["dst_intf"] = vnf_dst_interface
271 data
["cookie"] = cookie
272 data
["layer2"] = layer2
273 if kwargs
.get('path') is not None:
274 data
["path"] = kwargs
.get('path')
276 data
["path"] = self
._get
_path
(vnf_src_name
, vnf_dst_name
, vnf_src_interface
,
277 vnf_dst_interface
)[0]
279 # add route to dst ip to this interface
280 # this might block on containers that are still setting up, so start a new thread
281 if not kwargs
.get('no_route'):
282 # son_emu does not like concurrent commands for a container so we need to lock this if multiple chains
283 # on the same interface are created
284 src_node
.setHostRoute(dst_node
.intf(vnf_dst_interface
).IP(), vnf_src_interface
)
287 son_emu_data
= json
.loads(self
.get_son_emu_chain_data(vnf_src_name
))
289 son_emu_data
= dict()
290 if "son_emu_data" not in son_emu_data
:
291 son_emu_data
["son_emu_data"] = dict()
292 if "interfaces" not in son_emu_data
["son_emu_data"]:
293 son_emu_data
["son_emu_data"]["interfaces"] = dict()
294 if vnf_src_interface
not in son_emu_data
["son_emu_data"]["interfaces"]:
295 son_emu_data
["son_emu_data"]["interfaces"][vnf_src_interface
] = list()
296 son_emu_data
["son_emu_data"]["interfaces"][vnf_src_interface
].append(dst_intf
.IP())
298 self
.set_son_emu_chain_data(vnf_src_name
, son_emu_data
)
300 if kwargs
.get('bidirectional', False):
301 # call the reverse direction
302 path
= kwargs
.get('path')
304 path
= list(reversed(path
))
305 self
.network_action_start(vnf_dst_name
, vnf_src_name
, vnf_src_interface
=vnf_dst_interface
,
306 vnf_dst_interface
=vnf_src_interface
, bidirectional
=False,
307 layer2
=kwargs
.get('layer2', False), path
=path
,
308 no_route
=kwargs
.get('no_route'))
310 self
.full_chain_data
[flow
] = data
311 self
.chain_flow_cookies
[flow
] = cookie
313 except Exception as ex
:
314 logging
.exception("RPC error.")
315 raise Exception(ex
.message
)
317 def network_action_stop(self
, vnf_src_name
, vnf_dst_name
, **kwargs
):
319 Starts a network chain for a source destination pair
321 :param vnf_src_name: Name of the source VNF
322 :type vnf_src_name: ``str``
323 :param vnf_dst_name: Name of the source VNF interface
324 :type vnf_dst_name: ``str``
325 :param \**kwargs: See below
328 * *vnf_src_interface* (``str``): Name of source interface.
329 * *vnf_dst_interface* (``str``): Name of destination interface.
330 * *bidirectional* (``bool``): If set the chain will be torn down in both directions, else it will just\
331 be torn down from source to destination.
332 * *cookie* (``int``): Cookie value used by openflow. Used to identify the flows in the switches to be \
333 able to modify the correct flows.
336 if 'cookie' in kwargs
:
337 return self
.delete_flow_by_cookie(kwargs
.get('cookie'))
339 if kwargs
.get('bidirectional', False):
340 self
.delete_chain_by_intf(vnf_dst_name
, kwargs
.get('vnf_dst_interface'),
341 vnf_src_name
, kwargs
.get('vnf_src_interface'))
343 return self
.delete_chain_by_intf(vnf_src_name
, kwargs
.get('vnf_src_interface'),
344 vnf_dst_name
, kwargs
.get('vnf_dst_interface'))
345 except Exception as ex
:
346 logging
.exception("RPC error.")
349 def set_son_emu_chain_data(self
, vnf_name
, data
):
351 Set son-emu chain data for this node.
353 :param vnf_name: The name of the vnf where the data is stored.
354 :type vnf_name: ``str``
355 :param data: Raw data to store on the node.
358 self
.net
.getNodeByName(vnf_name
).cmd("echo \'%s\' > /tmp/son_emu_data.json" % json
.dumps(data
))
360 for intf
in data
['son_emu_data']['interfaces'].values():
363 self
.net
.getNodeByName(vnf_name
).cmd("echo \'%s\' > /tmp/son_emu_data" % "\n".join(ip_list
))
365 def get_son_emu_chain_data(self
, vnf_name
):
367 Get the current son-emu chain data set for this node.
369 :param vnf_name: The name of the vnf where the data is stored.
370 :type vnf_name: ``str``
371 :return: raw data stored on the node
374 return self
.net
.getNodeByName(vnf_name
).cmd("cat /tmp/son_emu_data.json")
376 def _get_connected_switch_data(self
, vnf_name
, vnf_interface
):
378 Get the switch an interface is connected to
379 :param vnf_name: Name of the VNF
380 :type vnf_name: ``str``
381 :param vnf_interface: Name of the VNF interface
382 :type vnf_interface: ``str``
383 :return: List containing the switch, and the inport number
384 :rtype: [``str``, ``int``]
387 src_sw_inport_nr
= None
388 for connected_sw
in self
.net
.DCNetwork_graph
.neighbors(vnf_name
):
389 link_dict
= self
.net
.DCNetwork_graph
[vnf_name
][connected_sw
]
390 for link
in link_dict
:
391 if (link_dict
[link
]['src_port_id'] == vnf_interface
or
393 'src_port_name'] == vnf_interface
):
394 # found the right link and connected switch
395 src_sw
= connected_sw
396 src_sw_inport_nr
= link_dict
[link
]['dst_port_nr']
399 return src_sw
, src_sw_inport_nr
401 def _get_path(self
, src_vnf
, dst_vnf
, src_vnf_intf
, dst_vnf_intf
):
403 Own implementation of the get_path function from DCNetwork, because we just want the path and not set up
406 :param src_vnf: Name of the source VNF
407 :type src_vnf: ``str``
408 :param dst_vnf: Name of the destination VNF
409 :type dst_vnf: ``str``
410 :param src_vnf_intf: Name of the source VNF interface
411 :type src_vnf_intf: ``str``
412 :param dst_vnf_intf: Name of the destination VNF interface
413 :type dst_vnf_intf: ``str``
414 :return: path, src_sw, dst_sw
415 :rtype: ``list``, ``str``, ``str``
417 # modified version of the _chainAddFlow from emuvim.dcemulator.net._chainAddFlow
420 logging
.debug("Find shortest path from vnf %s to %s",
423 for connected_sw
in self
.net
.DCNetwork_graph
.neighbors(src_vnf
):
424 link_dict
= self
.net
.DCNetwork_graph
[src_vnf
][connected_sw
]
425 for link
in link_dict
:
426 if (link_dict
[link
]['src_port_id'] == src_vnf_intf
or
428 'src_port_name'] == src_vnf_intf
):
429 # found the right link and connected switch
430 src_sw
= connected_sw
433 for connected_sw
in self
.net
.DCNetwork_graph
.neighbors(dst_vnf
):
434 link_dict
= self
.net
.DCNetwork_graph
[connected_sw
][dst_vnf
]
435 for link
in link_dict
:
436 if link_dict
[link
]['dst_port_id'] == dst_vnf_intf
or \
438 'dst_port_name'] == dst_vnf_intf
:
439 # found the right link and connected
440 dst_sw
= connected_sw
442 logging
.debug("From switch %s to %s " % (src_sw
, dst_sw
))
446 # returns the first found shortest path
447 # if all shortest paths are wanted, use: all_shortest_paths
448 path
= nx
.shortest_path(self
.net
.DCNetwork_graph
, src_sw
, dst_sw
)
450 logging
.exception("No path could be found between {0} and {1} using src_sw={2} and dst_sw={3}".format(
451 src_vnf
, dst_vnf
, src_sw
, dst_sw
))
452 logging
.debug("Graph nodes: %r" % self
.net
.DCNetwork_graph
.nodes())
453 logging
.debug("Graph edges: %r" % self
.net
.DCNetwork_graph
.edges())
454 for e
, v
in self
.net
.DCNetwork_graph
.edges():
455 logging
.debug("%r" % self
.net
.DCNetwork_graph
[e
][v
])
456 return "No path could be found between {0} and {1}".format(src_vnf
, dst_vnf
)
458 logging
.info("Shortest path between {0} and {1}: {2}".format(src_vnf
, dst_vnf
, path
))
459 return path
, src_sw
, dst_sw
461 def add_loadbalancer(self
, src_vnf_name
, src_vnf_interface
, lb_data
):
463 This function will set up a loadbalancer at the given interface.
465 :param src_vnf_name: Name of the source VNF
466 :type src_vnf_name: ``str``
467 :param src_vnf_interface: Name of the destination VNF
468 :type src_vnf_interface: ``str``
469 :param lb_data: A dictionary containing the destination data as well as custom path settings
470 :type lb_data: ``dict``
473 lbdata = {"dst_vnf_interfaces": {"dc2_man_web0": "port-man-2",
474 "dc3_man_web0": "port-man-4","dc4_man_web0": "port-man-6"}, "path": {"dc2_man_web0": {"port-man-2": [ "dc1.s1",\
480 dest_intfs_mapping
= lb_data
.get('dst_vnf_interfaces', dict())
481 # a custom path can be specified as a list of switches
482 custom_paths
= lb_data
.get('path', dict())
483 dest_vnf_outport_nrs
= list()
485 logging
.debug("Call to add_loadbalancer at %s intfs:%s" % (src_vnf_name
, src_vnf_interface
))
487 if not self
.check_vnf_intf_pair(src_vnf_name
, src_vnf_interface
):
488 raise Exception(u
"Source VNF %s or intfs %s does not exist" % (src_vnf_name
, src_vnf_interface
))
490 # find the switch belonging to the source interface, as well as the inport nr
491 for connected_sw
in net
.DCNetwork_graph
.neighbors(src_vnf_name
):
492 link_dict
= net
.DCNetwork_graph
[src_vnf_name
][connected_sw
]
493 for link
in link_dict
:
494 if link_dict
[link
]['src_port_name'] == src_vnf_interface
:
495 src_sw
= connected_sw
496 src_sw_inport_nr
= link_dict
[link
]['dst_port_nr']
499 if src_sw
is None or src_sw_inport_nr
== 0:
500 raise Exception(u
"Source VNF or interface can not be found.")
502 # get all target interface outport numbers
503 for vnf_name
in dest_intfs_mapping
:
504 if vnf_name
not in net
.DCNetwork_graph
:
505 raise Exception(u
"Target VNF %s is not known." % vnf_name
)
506 for connected_sw
in net
.DCNetwork_graph
.neighbors(vnf_name
):
507 link_dict
= net
.DCNetwork_graph
[vnf_name
][connected_sw
]
508 for link
in link_dict
:
509 if link_dict
[link
]['src_port_name'] == dest_intfs_mapping
[vnf_name
]:
510 dest_vnf_outport_nrs
.append(int(link_dict
[link
]['dst_port_nr']))
512 if (src_vnf_name
, src_vnf_interface
) not in self
.lb_flow_cookies
:
513 self
.lb_flow_cookies
[(src_vnf_name
, src_vnf_interface
)] = list()
518 for intf
in net
[src_vnf_name
].intfs
.values():
519 if intf
.name
== src_vnf_interface
:
524 # set up paths for each destination vnf individually
526 cookie
= self
.get_cookie()
527 main_cmd
= "add-flow -OOpenFlow13"
528 self
.lb_flow_cookies
[(src_vnf_name
, src_vnf_interface
)].append(cookie
)
532 data
["src_vnf"] = src_vnf_name
533 data
["src_intf"] = src_vnf_interface
534 data
["paths"] = list()
535 data
["cookie"] = cookie
537 # lb mac for src -> target connections
538 lb_mac
= "31:33:70:%02x:%02x:%02x" % (random
.randint(0, 255),random
.randint(0, 255),random
.randint(0, 255))
540 # calculate lb ip as src_intf.ip +1
541 octets
= src_ip
.split('.')
542 octets
[3] = str(int(octets
[3]) + 1)
543 plus_one
= '.'.join(octets
)
545 # set up arp reply as well as add the route to the interface
546 self
.setup_arp_reply_at(src_sw
, src_sw_inport_nr
, plus_one
, lb_mac
, cookie
=cookie
)
547 net
.getNodeByName(src_vnf_name
).setHostRoute(plus_one
, src_vnf_interface
)
549 for dst_vnf_name
, dst_vnf_interface
in dest_intfs_mapping
.items():
550 path
, src_sw
, dst_sw
= self
._get
_path
(src_vnf_name
, dst_vnf_name
,
551 src_vnf_interface
, dst_vnf_interface
)
553 # use custom path if one is supplied
554 # json does not support hashing on tuples so we use nested dicts
555 if custom_paths
is not None and dst_vnf_name
in custom_paths
:
556 if dst_vnf_interface
in custom_paths
[dst_vnf_name
]:
557 path
= custom_paths
[dst_vnf_name
][dst_vnf_interface
]
558 logging
.debug("Taking custom path from %s to %s: %s" % (src_vnf_name
, dst_vnf_name
, path
))
560 if not self
.check_vnf_intf_pair(dst_vnf_name
, dst_vnf_interface
):
561 self
.delete_loadbalancer(src_vnf_name
, src_vnf_interface
)
562 raise Exception(u
"VNF %s or intfs %s does not exist" % (dst_vnf_name
, dst_vnf_interface
))
563 if isinstance(path
, dict):
564 self
.delete_loadbalancer(src_vnf_name
, src_vnf_interface
)
565 raise Exception(u
"Can not find a valid path. Are you specifying the right interfaces?.")
567 target_mac
= "fa:17:00:03:13:37"
568 target_ip
= "0.0.0.0"
569 for intf
in net
[dst_vnf_name
].intfs
.values():
570 if intf
.name
== dst_vnf_interface
:
571 target_mac
= str(intf
.mac
)
572 target_ip
= str(intf
.ip
)
573 dst_sw_outport_nr
= dest_vnf_outport_nrs
[index
]
575 switch_inport_nr
= src_sw_inport_nr
577 #self.setup_arp_reply_at(src_sw, src_sw_inport_nr, target_ip, target_mac, cookie=cookie)
578 net
.getNodeByName(dst_vnf_name
).setHostRoute(src_ip
, dst_vnf_interface
)
580 # choose free vlan if path contains more than 1 switch
582 vlan
= net
.vlans
.pop()
584 vlan
= net
.vlans
.pop()
588 single_flow_data
= dict()
589 single_flow_data
["dst_vnf"] = dst_vnf_name
590 single_flow_data
["dst_intf"] = dst_vnf_interface
591 single_flow_data
["path"] = path
592 single_flow_data
["vlan"] = vlan
593 single_flow_data
["cookie"] = cookie
595 data
["paths"].append(single_flow_data
)
598 for i
in range(0, len(path
)):
599 if i
< len(path
) - 1:
600 next_hop
= path
[i
+ 1]
602 # last switch reached
603 next_hop
= dst_vnf_name
604 next_node
= net
.getNodeByName(next_hop
)
605 if next_hop
== dst_vnf_name
:
606 switch_outport_nr
= dst_sw_outport_nr
607 logging
.info("end node reached: {0}".format(dst_vnf_name
))
608 elif not isinstance(next_node
, OVSSwitch
):
609 logging
.info("Next node: {0} is not a switch".format(next_hop
))
610 return "Next node: {0} is not a switch".format(next_hop
)
612 # take first link between switches by default
614 switch_outport_nr
= net
.DCNetwork_graph
[current_hop
][next_hop
][index_edge_out
]['src_port_nr']
616 cmd
= 'priority=1,in_port=%s,cookie=%s' % (switch_inport_nr
, cookie
)
617 cmd_back
= 'priority=1,in_port=%s,cookie=%s' % (switch_outport_nr
, cookie
)
618 # if a vlan is picked, the connection is routed through multiple switches
620 if path
.index(current_hop
) == 0: # first node
622 cmd
= 'in_port=%s' % src_sw_inport_nr
623 cmd
+= ',cookie=%s' % cookie
624 cmd
+= ',table=%s' % cookie
626 cmd
+= ',reg1=%s' % index
629 cmd
+= ',push_vlan:0x8100'
630 masked_vlan
= vlan |
0x1000
631 cmd
+= ',set_field:%s->vlan_vid' % masked_vlan
632 cmd
+= ',set_field:%s->eth_dst' % target_mac
633 cmd
+= ',set_field:%s->ip_dst' % target_ip
634 cmd
+= ',output:%s' % switch_outport_nr
636 # last switch for reverse route
637 # remove any vlan tags
638 cmd_back
+= ',dl_vlan=%s' % vlan
639 cmd_back
+= ',actions=pop_vlan,output:%s' % switch_inport_nr
640 elif next_hop
== dst_vnf_name
: # last switch
641 # remove any vlan tags
642 cmd
+= ',dl_vlan=%s' % vlan
643 cmd
+= ',actions=pop_vlan,output:%s' % switch_outport_nr
644 # set up arp replys at the port so the dst nodes know the src
645 self
.setup_arp_reply_at(current_hop
, switch_outport_nr
, src_ip
, src_mac
, cookie
=cookie
)
648 cmd_back
= 'in_port=%s' % switch_outport_nr
649 cmd_back
+= ',cookie=%s' % cookie
651 cmd_back
+= ',actions='
652 cmd_back
+= 'push_vlan:0x8100'
653 masked_vlan
= vlan |
0x1000
654 cmd_back
+= ',set_field:%s->vlan_vid' % masked_vlan
655 cmd_back
+= ',set_field:%s->eth_src' % lb_mac
656 cmd_back
+= ',set_field:%s->ip_src' % plus_one
657 cmd_back
+= ',output:%s' % switch_inport_nr
659 # if we have a circle in the path we need to specify this, as openflow will ignore the packet
660 # if we just output it on the same port as it came in
661 if switch_inport_nr
== switch_outport_nr
:
662 cmd
+= ',dl_vlan=%s,actions=IN_PORT' % (vlan
)
663 cmd_back
+= ',dl_vlan=%s,actions=IN_PORT' % (vlan
)
665 cmd
+= ',dl_vlan=%s,actions=output:%s' % (vlan
, switch_outport_nr
)
666 cmd_back
+= ',dl_vlan=%s,actions=output:%s' % (vlan
, switch_inport_nr
)
667 # output the packet at the correct outport
669 cmd
= 'in_port=%s' % src_sw_inport_nr
670 cmd
+= ',cookie=%s' % cookie
671 cmd
+= ',table=%s' % cookie
673 cmd
+= ',reg1=%s' % index
675 cmd
+= ',set_field:%s->eth_dst' % target_mac
676 cmd
+= ',set_field:%s->ip_dst' % target_ip
677 cmd
+= ',output:%s' % switch_outport_nr
680 cmd_back
= 'in_port=%s' % switch_outport_nr
681 cmd_back
+= ',cookie=%s' % cookie
683 cmd_back
+= ',actions='
684 cmd_back
+= ',set_field:%s->eth_src' % lb_mac
685 cmd_back
+= ',set_field:%s->ip_src' % plus_one
686 cmd_back
+= ',output:%s' % src_sw_inport_nr
688 self
.setup_arp_reply_at(current_hop
, switch_outport_nr
, src_ip
, src_mac
, cookie
=cookie
)
690 # excecute the command on the target switch
693 cmd_back
= "\"%s\"" % cmd_back
694 net
[current_hop
].dpctl(main_cmd
, cmd
)
695 net
[current_hop
].dpctl(main_cmd
, cmd_back
)
697 # set next hop for the next iteration step
698 if isinstance(next_node
, OVSSwitch
):
699 switch_inport_nr
= net
.DCNetwork_graph
[current_hop
][next_hop
][0]['dst_port_nr']
700 current_hop
= next_hop
702 # advance to next destination
705 # set up the actual load balancing rule as a multipath on the very first switch
706 cmd
= '"in_port=%s' % src_sw_inport_nr
707 cmd
+= ',cookie=%s' % (cookie
)
710 # push 0x01 into the first register
711 cmd
+= 'load:0x1->NXM_NX_REG0[]'
712 # load balance modulo n over all dest interfaces
713 # TODO: in newer openvswitch implementations this should be changed to symmetric_l3l4+udp
714 # to balance any kind of traffic
715 cmd
+= ',multipath(symmetric_l4,1024,modulo_n,%s,0,NXM_NX_REG1[0..12])' % len(dest_intfs_mapping
)
716 # reuse the cookie as table entry as it will be unique
717 cmd
+= ',resubmit(, %s)"' % cookie
719 # actually add the flow
720 logging
.debug("Switch: %s, CMD: %s" % (src_sw
, cmd
))
721 net
[src_sw
].dpctl(main_cmd
, cmd
)
723 # finally add all flow data to the internal data storage
724 self
.full_lb_data
[(src_vnf_name
, src_vnf_interface
)] = data
726 def add_floating_lb(self
, datacenter
, lb_data
):
728 This function will set up a loadbalancer at the given datacenter.
729 This function returns the floating ip assigned to the loadbalancer as multiple ones are possible.
731 :param datacenter: The datacenter entrypoint
732 :type datacenter: ``str``
733 :param lb_data: A dictionary containing the destination data as well as custom path settings
734 :type lb_data: ``dict``
737 lbdata = {"dst_vnf_interfaces": {"dc2_man_web0": "port-man-2",
738 "dc3_man_web0": "port-man-4","dc4_man_web0": "port-man-6"}, "path": {"dc2_man_web0": {"port-man-2": [ "dc1.s1",\
743 src_sw
= self
.floating_switch
.name
744 dest_intfs_mapping
= lb_data
.get('dst_vnf_interfaces', dict())
745 # a custom path can be specified as a list of switches
746 custom_paths
= lb_data
.get('path', dict())
747 dest_vnf_outport_nrs
= list()
749 if datacenter
not in self
.net
.dcs
:
750 raise Exception(u
"Source datacenter can not be found.")
752 # get all target interface outport numbers
753 for vnf_name
in dest_intfs_mapping
:
754 if vnf_name
not in net
.DCNetwork_graph
:
755 raise Exception(u
"Target VNF %s is not known." % vnf_name
)
756 for connected_sw
in net
.DCNetwork_graph
.neighbors(vnf_name
):
757 link_dict
= net
.DCNetwork_graph
[vnf_name
][connected_sw
]
758 for link
in link_dict
:
759 if link_dict
[link
]['src_port_name'] == dest_intfs_mapping
[vnf_name
]:
760 dest_vnf_outport_nrs
.append(int(link_dict
[link
]['dst_port_nr']))
762 if len(dest_vnf_outport_nrs
) == 0:
763 raise Exception("There are no paths specified for the loadbalancer")
764 src_ip
= self
.floating_intf
.IP()
765 src_mac
= self
.floating_intf
.MAC()
767 # set up paths for each destination vnf individually
769 cookie
= self
.get_cookie()
770 main_cmd
= "add-flow -OOpenFlow13"
771 floating_ip
= self
.floating_network
.get_new_ip_address("floating-ip").split("/")[0]
773 for dst_vnf_name
, dst_vnf_interface
in dest_intfs_mapping
.items():
775 # use custom path if one is supplied
776 # json does not support hashing on tuples so we use nested dicts
777 if custom_paths
is not None and dst_vnf_name
in custom_paths
:
778 if dst_vnf_interface
in custom_paths
[dst_vnf_name
]:
779 path
= custom_paths
[dst_vnf_name
][dst_vnf_interface
]
780 logging
.debug("Taking custom path to %s: %s" % (dst_vnf_name
, path
))
782 if datacenter
not in self
.floating_links
:
783 self
.floating_links
[datacenter
] = \
784 net
.addLink(self
.floating_switch
, datacenter
)
786 self
._get
_path
(self
.floating_root
.name
, dst_vnf_name
, self
.floating_intf
.name
, dst_vnf_interface
)[0]
788 if isinstance(path
, dict):
789 self
.delete_flow_by_cookie(cookie
)
790 raise Exception(u
"Can not find a valid path. Are you specifying the right interfaces?.")
792 intf
= net
[dst_vnf_name
].nameToIntf
[dst_vnf_interface
]
793 target_mac
= str(intf
.MAC())
794 target_ip
= str(intf
.IP())
795 dst_sw_outport_nr
= dest_vnf_outport_nrs
[index
]
797 switch_inport_nr
= src_sw_inport_nr
798 vlan
= net
.vlans
.pop()
800 # iterate all switches on the path
801 for i
in range(0, len(path
)):
802 if i
< len(path
) - 1:
803 next_hop
= path
[i
+ 1]
805 # last switch reached
806 next_hop
= dst_vnf_name
807 next_node
= net
.getNodeByName(next_hop
)
810 if next_hop
== dst_vnf_name
:
811 switch_outport_nr
= dst_sw_outport_nr
812 logging
.info("end node reached: {0}".format(dst_vnf_name
))
813 elif not isinstance(next_node
, OVSSwitch
):
814 logging
.info("Next node: {0} is not a switch".format(next_hop
))
815 return "Next node: {0} is not a switch".format(next_hop
)
817 # take first link between switches by default
819 switch_outport_nr
= net
.DCNetwork_graph
[current_hop
][next_hop
][index_edge_out
]['src_port_nr']
821 # default filters, just overwritten on the first node and last node
822 cmd
= 'priority=1,in_port=%s,cookie=%s' % (switch_inport_nr
, cookie
)
823 cmd_back
= 'priority=1,in_port=%s,cookie=%s' % (switch_outport_nr
, cookie
)
824 if i
== 0: # first node
825 cmd
= 'in_port=%s' % src_sw_inport_nr
826 cmd
+= ',cookie=%s' % cookie
827 cmd
+= ',table=%s' % cookie
829 cmd
+= ',ip_dst=%s' % floating_ip
830 cmd
+= ',reg1=%s' % index
833 cmd
+= ',push_vlan:0x8100'
834 masked_vlan
= vlan |
0x1000
835 cmd
+= ',set_field:%s->vlan_vid' % masked_vlan
836 cmd
+= ',set_field:%s->eth_dst' % target_mac
837 cmd
+= ',set_field:%s->ip_dst' % target_ip
838 cmd
+= ',output:%s' % switch_outport_nr
840 # last switch for reverse route
841 # remove any vlan tags
842 cmd_back
+= ',dl_vlan=%s' % vlan
843 cmd_back
+= ',actions=pop_vlan,output:%s' % switch_inport_nr
844 self
.setup_arp_reply_at(current_hop
, src_sw_inport_nr
, floating_ip
, target_mac
, cookie
=cookie
)
845 elif next_hop
== dst_vnf_name
: # last switch
846 # remove any vlan tags
847 cmd
+= ',dl_vlan=%s' % vlan
848 cmd
+= ',actions=pop_vlan,output:%s' % switch_outport_nr
849 # set up arp replys at the port so the dst nodes know the src
850 self
.setup_arp_reply_at(current_hop
, switch_outport_nr
, src_ip
, src_mac
, cookie
=cookie
)
853 cmd_back
= 'in_port=%s' % switch_outport_nr
854 cmd_back
+= ',cookie=%s' % cookie
856 cmd_back
+= ',actions='
857 cmd_back
+= 'push_vlan:0x8100'
858 masked_vlan
= vlan |
0x1000
859 cmd_back
+= ',set_field:%s->vlan_vid' % masked_vlan
860 cmd_back
+= ',set_field:%s->eth_src' % src_mac
861 cmd_back
+= ',set_field:%s->ip_src' % floating_ip
862 cmd_back
+= ',output:%s' % switch_inport_nr
863 net
.getNodeByName(dst_vnf_name
).setHostRoute(src_ip
, dst_vnf_interface
)
865 # if we have a circle in the path we need to specify this, as openflow will ignore the packet
866 # if we just output it on the same port as it came in
867 if switch_inport_nr
== switch_outport_nr
:
868 cmd
+= ',dl_vlan=%s,actions=IN_PORT' % (vlan
)
869 cmd_back
+= ',dl_vlan=%s,actions=IN_PORT' % (vlan
)
871 cmd
+= ',dl_vlan=%s,actions=output:%s' % (vlan
, switch_outport_nr
)
872 cmd_back
+= ',dl_vlan=%s,actions=output:%s' % (vlan
, switch_inport_nr
)
874 # excecute the command on the target switch
877 cmd_back
= "\"%s\"" % cmd_back
878 net
[current_hop
].dpctl(main_cmd
, cmd
)
879 net
[current_hop
].dpctl(main_cmd
, cmd_back
)
881 # set next hop for the next iteration step
882 if isinstance(next_node
, OVSSwitch
):
883 switch_inport_nr
= net
.DCNetwork_graph
[current_hop
][next_hop
][0]['dst_port_nr']
884 current_hop
= next_hop
886 # advance to next destination
889 # set up the actual load balancing rule as a multipath on the very first switch
890 cmd
= '"in_port=%s' % src_sw_inport_nr
891 cmd
+= ',cookie=%s' % (cookie
)
894 # push 0x01 into the first register
895 cmd
+= 'load:0x1->NXM_NX_REG0[]'
896 # load balance modulo n over all dest interfaces
897 # TODO: in newer openvswitch implementations this should be changed to symmetric_l3l4+udp
898 # to balance any kind of traffic
899 cmd
+= ',multipath(symmetric_l4,1024,modulo_n,%s,0,NXM_NX_REG1[0..12])' % len(dest_intfs_mapping
)
900 # reuse the cookie as table entry as it will be unique
901 cmd
+= ',resubmit(, %s)"' % cookie
903 # actually add the flow
904 logging
.debug("Switch: %s, CMD: %s" % (src_sw
, cmd
))
905 net
[src_sw
].dpctl(main_cmd
, cmd
)
907 self
.floating_cookies
[cookie
] = floating_ip
909 return cookie
, floating_ip
911 def setup_arp_reply_at(self
, switch
, port_nr
, target_ip
, target_mac
, cookie
=None):
913 Sets up a custom ARP reply at a switch.
914 An ARP request coming in on the `port_nr` for `target_ip` will be answered with target IP/MAC.
916 :param switch: The switch belonging to the interface
917 :type switch: ``str``
918 :param port_nr: The port number at the switch that is connected to the interface
919 :type port_nr: ``int``
920 :param target_ip: The IP for which to set up the ARP reply
921 :type target_ip: ``str``
922 :param target_mac: The MAC address of the target interface
923 :type target_mac: ``str``
924 :param cookie: cookie to identify the ARP request, if None a new one will be picked
925 :type cookie: ``int`` or ``None``
930 cookie
= self
.get_cookie()
931 main_cmd
= "add-flow -OOpenFlow13"
933 # first set up ARP requests for the source node, so it will always 'find' a partner
934 cmd
= '"in_port=%s' % port_nr
935 cmd
+= ',cookie=%s' % cookie
937 # only answer for target ip arp requests
938 cmd
+= ',arp_tpa=%s' % target_ip
940 # set message type to ARP reply
941 cmd
+= 'load:0x2->NXM_OF_ARP_OP[]'
942 # set src ip as dst ip
943 cmd
+= ',move:NXM_OF_ETH_SRC[]->NXM_OF_ETH_DST[]'
945 cmd
+= ',set_field:%s->eth_src' % target_mac
947 cmd
+= ',move:NXM_NX_ARP_SHA[]->NXM_NX_ARP_THA[], move:NXM_OF_ARP_SPA[]->NXM_OF_ARP_TPA[]'
948 # set target mac as hex
949 cmd
+= ',load:0x%s->NXM_NX_ARP_SHA[]' % "".join(target_mac
.split(':'))
950 # set target ip as hex
951 octets
= target_ip
.split('.')
952 dst_ip_hex
= '{:02X}{:02X}{:02X}{:02X}'.format(*map(int, octets
))
953 cmd
+= ',load:0x%s->NXM_OF_ARP_SPA[]' % dst_ip_hex
954 # output to incoming port remember the closing "
956 self
.net
[switch
].dpctl(main_cmd
, cmd
)
958 "Set up ARP reply at %s port %s." % (switch
, port_nr
))
960 def delete_flow_by_cookie(self
, cookie
):
962 Removes a flow identified by the cookie
964 :param cookie: The cookie for the specified flow
965 :type cookie: ``int``
966 :return: True if successful, else false
971 logging
.debug("Deleting flow by cookie %d" % (cookie
))
973 # we have to call delete-group for each switch
974 for node
in self
.net
.switches
:
976 flow
["dpid"] = int(node
.dpid
, 16)
977 flow
["cookie"] = cookie
978 flow
['cookie_mask'] = int('0xffffffffffffffff', 16)
982 logging
.debug("Deleting flowentry with cookie %d" % (
984 if self
.net
.controller
== RemoteController
:
985 self
.net
.ryu_REST('stats/flowentry/delete', data
=flow
)
987 self
.cookies
.remove(cookie
)
990 def delete_chain_by_intf(self
, src_vnf_name
, src_vnf_intf
, dst_vnf_name
, dst_vnf_intf
):
992 Removes a flow identified by the vnf_name/vnf_intf pairs
994 :param src_vnf_name: The vnf name for the specified flow
995 :type src_vnf_name: ``str``
996 :param src_vnf_intf: The interface name for the specified flow
997 :type src_vnf_intf: ``str``
998 :param dst_vnf_name: The vnf name for the specified flow
999 :type dst_vnf_name: ``str``
1000 :param dst_vnf_intf: The interface name for the specified flow
1001 :type dst_vnf_intf: ``str``
1002 :return: True if successful, else false
1005 logging
.debug("Deleting flow for vnf/intf pair %s %s" % (src_vnf_name
, src_vnf_intf
))
1006 if not self
.check_vnf_intf_pair(src_vnf_name
, src_vnf_intf
):
1008 if not self
.check_vnf_intf_pair(dst_vnf_name
, dst_vnf_intf
):
1010 target_flow
= (src_vnf_name
, src_vnf_intf
, dst_vnf_name
, dst_vnf_intf
)
1011 if not target_flow
in self
.chain_flow_cookies
:
1014 success
= self
.delete_flow_by_cookie(self
.chain_flow_cookies
[target_flow
])
1017 del self
.chain_flow_cookies
[target_flow
]
1018 del self
.full_chain_data
[target_flow
]
1022 def delete_loadbalancer(self
, vnf_src_name
, vnf_src_interface
):
1024 Removes a loadbalancer that is configured for the node and interface
1026 :param src_vnf_name: Name of the source VNF
1027 :param src_vnf_interface: Name of the destination VNF
1030 # we have to call delete-group for each switch
1031 delete_group
= list()
1032 group_id
= self
.get_flow_group(vnf_src_name
, vnf_src_interface
)
1033 for node
in self
.net
.switches
:
1034 for cookie
in self
.lb_flow_cookies
[(vnf_src_name
, vnf_src_interface
)]:
1036 flow
["dpid"] = int(node
.dpid
, 16)
1037 flow
["cookie"] = cookie
1038 flow
['cookie_mask'] = int('0xffffffffffffffff', 16)
1042 group_del
["dpid"] = int(node
.dpid
, 16)
1043 group_del
["group_id"] = group_id
1044 delete_group
.append(group_del
)
1047 logging
.debug("Deleting flowentry with cookie %d belonging to lb at %s:%s" % (
1048 flow
["cookie"], vnf_src_name
, vnf_src_interface
))
1049 if self
.net
.controller
== RemoteController
:
1050 self
.net
.ryu_REST('stats/flowentry/delete', data
=flow
)
1052 logging
.debug("Deleting group with id %s" % group_id
)
1053 for switch_del_group
in delete_group
:
1054 if self
.net
.controller
== RemoteController
:
1055 self
.net
.ryu_REST("stats/groupentry/delete", data
=switch_del_group
)
1057 # unmap groupid from the interface
1058 target_pair
= (vnf_src_name
, vnf_src_interface
)
1059 if target_pair
in self
.flow_groups
:
1060 del self
.flow_groups
[target_pair
]
1061 if target_pair
in self
.full_lb_data
:
1062 del self
.full_lb_data
[target_pair
]
1064 def delete_floating_lb(self
, cookie
):
1066 Delete a floating loadbalancer.
1067 Floating loadbalancers are different from normal ones as there are multiple ones on the same interface.
1068 :param cookie: The cookie of the loadbalancer
1069 :type cookie: ``int``
1071 cookie
= int(cookie
)
1072 if cookie
not in self
.floating_cookies
:
1073 raise Exception("Can not delete floating loadbalancer as the flowcookie is not known")
1075 self
.delete_flow_by_cookie(cookie
)
1076 floating_ip
= self
.floating_cookies
[cookie
]
1077 self
.floating_network
.withdraw_ip_address(floating_ip
)
1079 def set_arp_entry(self
, vnf_name
, vnf_interface
, ip
, mac
):
1081 Sets an arp entry on the specified VNF. This is done on the node directly and not by open vswitch!
1082 :param vnf_name: Name of the VNF
1083 :type vnf_name: ``str``
1084 :param vnf_interface: Name of the interface
1085 :type vnf_interface: ``str``
1086 :param ip: IP to reply to
1088 :param mac: Answer with this MAC
1091 node
= self
.net
.getNodeByName(vnf_name
)
1092 node
.cmd("arp -i %s -s %s %s" % (vnf_interface
, ip
, mac
))