083550ef1264cd2bf9ebbbf2a554cbb58abdf8d0
[osm/vim-emu.git] / src / emuvim / api / openstack / manage.py
1 # Copyright (c) 2015 SONATA-NFV and Paderborn University
2 # ALL RIGHTS RESERVED.
3 #
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
7 #
8 # http://www.apache.org/licenses/LICENSE-2.0
9 #
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.
15 #
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
19 # permission.
20 #
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).
26 import logging
27 import threading
28 import uuid
29 import networkx as nx
30 import chain_api
31 import json
32 import random
33 from emuvim.api.openstack.resources.net import Net
34 from emuvim.api.openstack.resources.port import Port
35 from mininet.node import OVSSwitch, RemoteController, Node
36
37
38 class OpenstackManage(object):
39 """
40 OpenstackManage is a singleton and management component for the emulator.
41 It is the brain of the Openstack component and manages everything that is not datacenter specific like
42 network chains or load balancers.
43 """
44 __instance = None
45
46 def __new__(cls):
47 if OpenstackManage.__instance is None:
48 OpenstackManage.__instance = object.__new__(cls)
49 return OpenstackManage.__instance
50
51 def __init__(self, ip="0.0.0.0", port=4000):
52 # we are a singleton, only initialize once!
53 self.lock = threading.Lock()
54 with self.lock:
55 if hasattr(self, "init"):
56 return
57 self.init = True
58
59 self.endpoints = dict()
60 self.cookies = set()
61 self.cookies.add(0)
62 self.ip = ip
63 self.port = port
64 self._net = None
65 # to keep track which src_vnf(input port on the switch) handles a load
66 # balancer
67 self.lb_flow_cookies = dict()
68 self.chain_flow_cookies = dict()
69
70 # for the visualization also store the complete chain data incl. paths
71 self.full_chain_data = dict()
72 self.full_lb_data = dict()
73
74 # flow groups could be handled for each switch separately, but this global group counter should be easier to
75 # debug and to maintain
76 self.flow_groups = dict()
77
78 # we want one global chain api. this should not be datacenter
79 # dependent!
80 self.chain = chain_api.ChainApi(ip, port, self)
81 self.thread = threading.Thread(target=self.chain._start_flask, args=())
82 self.thread.daemon = True
83 self.thread.name = self.chain.__class__
84 self.thread.start()
85
86 # floating ip network setup
87 self.floating_switch = None
88 self.floating_network = None
89 self.floating_netmask = "192.168.100.0/24"
90 self.floating_nodes = dict()
91 self.floating_cookies = dict()
92 self.floating_intf = None
93 self.floating_links = dict()
94
95 @property
96 def net(self):
97 return self._net
98
99 @net.setter
100 def net(self, value):
101 if self._net is None:
102 self._net = value
103 # create default networks
104 self.init_floating_network()
105 self._net = value
106
107 def init_floating_network(self, name="default"):
108 """
109 Initialize the floating network component for the emulator.
110 Will not do anything if already initialized.
111 """
112 if self.net is not None and self.floating_switch is None:
113 # create a floating network
114 fn = self.floating_network = Net(name)
115 fn.id = str(uuid.uuid4())
116 fn.set_cidr(self.floating_netmask)
117
118 # create a subnet
119 fn.subnet_id = str(uuid.uuid4())
120 fn.subnet_name = fn.name + "-sub"
121
122 # create a port for the host
123 port = Port("root-port")
124 # port.id = str(uuid.uuid4())
125 port.net_name = fn.name
126
127 # get next free ip
128 root_ip = fn.get_new_ip_address(port.name)
129 port.ip_address = root_ip
130 # floating ip network setup
131 # wierd way of getting a datacenter object
132 first_dc = self.net.dcs.values()[0]
133 # set a dpid for the switch. for this we have to get the id of the
134 # next possible dc
135 self.floating_switch = self.net.addSwitch(
136 "fs1", dpid=hex(first_dc._get_next_dc_dpid())[2:])
137 # this is the interface appearing on the physical host
138 self.floating_root = Node('root', inNamespace=False)
139 self.net.hosts.append(self.floating_root)
140 self.net.nameToNode['root'] = self.floating_root
141 self.floating_intf = self.net.addLink(
142 self.floating_root, self.floating_switch).intf1
143 self.floating_root.setIP(root_ip, intf=self.floating_intf)
144 self.floating_nodes[(self.floating_root.name,
145 root_ip)] = self.floating_root
146
147 def stop_floating_network(self):
148 self._net = None
149 self.floating_switch = None
150
151 def add_endpoint(self, ep):
152 """
153 Registers an openstack endpoint with manage
154
155 :param ep: Openstack API endpoint
156 :type ep: :class:`heat.openstack_api_endpoint`
157 """
158 key = "%s:%s" % (ep.ip, ep.port)
159 self.endpoints[key] = ep
160
161 def get_cookie(self):
162 """
163 Get an unused cookie.
164
165 :return: Cookie
166 :rtype: ``int``
167 """
168 cookie = int(max(self.cookies) + 1)
169 self.cookies.add(cookie)
170 return cookie
171
172 def get_flow_group(self, src_vnf_name, src_vnf_interface):
173 """
174 Gets free group that is not currently used by any other flow for the specified interface / VNF.
175
176 :param src_vnf_name: Source VNF name
177 :type src_vnf_name: ``str``
178 :param src_vnf_interface: Source VNF interface name
179 :type src_vnf_interface: ``str``
180 :return: Flow group identifier.
181 :rtype: ``int``
182 """
183 if (src_vnf_name, src_vnf_interface) not in self.flow_groups:
184 grp = int(len(self.flow_groups) + 1)
185 self.flow_groups[(src_vnf_name, src_vnf_interface)] = grp
186 else:
187 grp = self.flow_groups[(src_vnf_name, src_vnf_interface)]
188 return grp
189
190 def check_vnf_intf_pair(self, vnf_name, vnf_intf_name):
191 """
192 Checks if a VNF exists and has the given interface
193
194 :param vnf_name: Name of the VNF to be checked
195 :type vnf_name: ``str``
196 :param vnf_intf_name: Name of the interface that belongst to the VNF
197 :type vnf_intf_name: ``str``
198 :return: ``True`` if it is valid pair, else ``False``
199 :rtype: ``bool``
200 """
201
202 if vnf_name in self.net:
203 vnf = self.net.getNodeByName(vnf_name)
204 return vnf_intf_name in vnf.nameToIntf
205
206 def network_action_start(self, vnf_src_name, vnf_dst_name, **kwargs):
207 """
208 Starts a network chain for a source destination pair
209
210 :param vnf_src_name: Name of the source VNF
211 :type vnf_src_name: ``str``
212 :param vnf_dst_name: Name of the source VNF interface
213 :type vnf_dst_name: ``str``
214 :param \**kwargs: See below
215
216 :Keyword Arguments:
217 * *vnf_src_interface* (``str``): Name of source interface.
218 * *vnf_dst_interface* (``str``): Name of destination interface.
219 * *weight* (``int``): This value is fed into the shortest path computation if no path is specified.
220 * *match* (``str``): A custom match entry for the openflow flow rules. Only vlanid or port possible.
221 * *bidirectional* (``bool``): If set the chain will be set in both directions, else it will just set up \
222 from source to destination.
223 * *cookie* (``int``): Cookie value used by openflow. Used to identify the flows in the switches to be \
224 able to modify the correct flows.
225 * *no_route* (``bool``): If set a layer 3 route to the target interface will not be set up.
226 :return: The cookie chosen for the flow.
227 :rtype: ``int``
228 """
229 try:
230 vnf_src_interface = kwargs.get('vnf_src_interface')
231 vnf_dst_interface = kwargs.get('vnf_dst_interface')
232 layer2 = kwargs.get('layer2', True)
233 match = kwargs.get('match')
234 flow = (vnf_src_name, vnf_src_interface,
235 vnf_dst_name, vnf_dst_interface)
236 if flow in self.chain_flow_cookies:
237 raise Exception(
238 "There is already a chain at the specified src/dst pair!")
239 # set up a layer 2 chain, this allows multiple chains for the same
240 # 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)
244 if layer2:
245 switch, inport = self._get_connected_switch_data(
246 vnf_src_name, vnf_src_interface)
247 self.setup_arp_reply_at(
248 switch, inport, dst_intf.IP(), dst_intf.MAC())
249 if isinstance(match, str):
250 match += ",dl_dst=%s" % dst_intf.MAC()
251 else:
252 match = "dl_dst=%s" % dst_intf.MAC()
253
254 cookie = kwargs.get('cookie', self.get_cookie())
255 self.cookies.add(cookie)
256 self.net.setChain(
257 vnf_src_name, vnf_dst_name,
258 vnf_src_interface=vnf_src_interface,
259 vnf_dst_interface=vnf_dst_interface,
260 cmd='add-flow',
261 weight=kwargs.get('weight'),
262 match=match,
263 bidirectional=False,
264 cookie=cookie,
265 path=kwargs.get('path'))
266
267 # to keep this logic seperate of the core son-emu do the
268 # housekeeping here
269 data = dict()
270 data["src_vnf"] = vnf_src_name
271 data["src_intf"] = vnf_src_interface
272 data["dst_vnf"] = vnf_dst_name
273 data["dst_intf"] = vnf_dst_interface
274 data["cookie"] = cookie
275 data["layer2"] = layer2
276 if kwargs.get('path') is not None:
277 data["path"] = kwargs.get('path')
278 else:
279 data["path"] = self._get_path(vnf_src_name, vnf_dst_name, vnf_src_interface,
280 vnf_dst_interface)[0]
281
282 # add route to dst ip to this interface
283 # this might block on containers that are still setting up, so
284 # start a new thread
285 if not kwargs.get('no_route'):
286 # son_emu does not like concurrent commands for a container so we need to lock this if multiple chains
287 # on the same interface are created
288 src_node.setHostRoute(dst_node.intf(
289 vnf_dst_interface).IP(), vnf_src_interface)
290
291 try:
292 son_emu_data = json.loads(
293 self.get_son_emu_chain_data(vnf_src_name))
294 except BaseException:
295 son_emu_data = dict()
296 if "son_emu_data" not in son_emu_data:
297 son_emu_data["son_emu_data"] = dict()
298 if "interfaces" not in son_emu_data["son_emu_data"]:
299 son_emu_data["son_emu_data"]["interfaces"] = dict()
300 if vnf_src_interface not in son_emu_data["son_emu_data"]["interfaces"]:
301 son_emu_data["son_emu_data"]["interfaces"][vnf_src_interface] = list()
302 son_emu_data["son_emu_data"]["interfaces"][vnf_src_interface].append(
303 dst_intf.IP())
304
305 self.set_son_emu_chain_data(vnf_src_name, son_emu_data)
306
307 if kwargs.get('bidirectional', False):
308 # call the reverse direction
309 path = kwargs.get('path')
310 if path is not None:
311 path = list(reversed(path))
312 self.network_action_start(vnf_dst_name, vnf_src_name, vnf_src_interface=vnf_dst_interface,
313 vnf_dst_interface=vnf_src_interface, bidirectional=False,
314 layer2=kwargs.get('layer2', False), path=path,
315 no_route=kwargs.get('no_route'))
316
317 self.full_chain_data[flow] = data
318 self.chain_flow_cookies[flow] = cookie
319 return cookie
320 except Exception as ex:
321 logging.exception("RPC error.")
322 raise Exception(ex.message)
323
324 def network_action_stop(self, vnf_src_name, vnf_dst_name, **kwargs):
325 """
326 Starts a network chain for a source destination pair
327
328 :param vnf_src_name: Name of the source VNF
329 :type vnf_src_name: ``str``
330 :param vnf_dst_name: Name of the source VNF interface
331 :type vnf_dst_name: ``str``
332 :param \**kwargs: See below
333
334 :Keyword Arguments:
335 * *vnf_src_interface* (``str``): Name of source interface.
336 * *vnf_dst_interface* (``str``): Name of destination interface.
337 * *bidirectional* (``bool``): If set the chain will be torn down in both directions, else it will just\
338 be torn down from source to destination.
339 * *cookie* (``int``): Cookie value used by openflow. Used to identify the flows in the switches to be \
340 able to modify the correct flows.
341 """
342 try:
343 if 'cookie' in kwargs:
344 return self.delete_flow_by_cookie(kwargs.get('cookie'))
345
346 if kwargs.get('bidirectional', False):
347 self.delete_chain_by_intf(vnf_dst_name, kwargs.get('vnf_dst_interface'),
348 vnf_src_name, kwargs.get('vnf_src_interface'))
349
350 return self.delete_chain_by_intf(vnf_src_name, kwargs.get('vnf_src_interface'),
351 vnf_dst_name, kwargs.get('vnf_dst_interface'))
352 except Exception as ex:
353 logging.exception("RPC error.")
354 return ex.message
355
356 def set_son_emu_chain_data(self, vnf_name, data):
357 """
358 Set son-emu chain data for this node.
359
360 :param vnf_name: The name of the vnf where the data is stored.
361 :type vnf_name: ``str``
362 :param data: Raw data to store on the node.
363 :type data: ``str``
364 """
365 self.net.getNodeByName(vnf_name).cmd(
366 "echo \'%s\' > /tmp/son_emu_data.json" % json.dumps(data))
367 ip_list = []
368 for intf in data['son_emu_data']['interfaces'].values():
369 ip_list.extend(intf)
370
371 self.net.getNodeByName(vnf_name).cmd(
372 "echo \'%s\' > /tmp/son_emu_data" % "\n".join(ip_list))
373
374 def get_son_emu_chain_data(self, vnf_name):
375 """
376 Get the current son-emu chain data set for this node.
377
378 :param vnf_name: The name of the vnf where the data is stored.
379 :type vnf_name: ``str``
380 :return: raw data stored on the node
381 :rtype: ``str``
382 """
383 return self.net.getNodeByName(vnf_name).cmd(
384 "cat /tmp/son_emu_data.json")
385
386 def _get_connected_switch_data(self, vnf_name, vnf_interface):
387 """
388 Get the switch an interface is connected to
389 :param vnf_name: Name of the VNF
390 :type vnf_name: ``str``
391 :param vnf_interface: Name of the VNF interface
392 :type vnf_interface: ``str``
393 :return: List containing the switch, and the inport number
394 :rtype: [``str``, ``int``]
395 """
396 src_sw = None
397 src_sw_inport_nr = None
398 for connected_sw in self.net.DCNetwork_graph.neighbors(vnf_name):
399 link_dict = self.net.DCNetwork_graph[vnf_name][connected_sw]
400 for link in link_dict:
401 if (link_dict[link]['src_port_id'] == vnf_interface or
402 link_dict[link][
403 'src_port_name'] == vnf_interface):
404 # found the right link and connected switch
405 src_sw = connected_sw
406 src_sw_inport_nr = link_dict[link]['dst_port_nr']
407 break
408
409 return src_sw, src_sw_inport_nr
410
411 def _get_path(self, src_vnf, dst_vnf, src_vnf_intf, dst_vnf_intf):
412 """
413 Own implementation of the get_path function from DCNetwork, because we just want the path and not set up
414 flows on the way.
415
416 :param src_vnf: Name of the source VNF
417 :type src_vnf: ``str``
418 :param dst_vnf: Name of the destination VNF
419 :type dst_vnf: ``str``
420 :param src_vnf_intf: Name of the source VNF interface
421 :type src_vnf_intf: ``str``
422 :param dst_vnf_intf: Name of the destination VNF interface
423 :type dst_vnf_intf: ``str``
424 :return: path, src_sw, dst_sw
425 :rtype: ``list``, ``str``, ``str``
426 """
427 # modified version of the _chainAddFlow from
428 # emuvim.dcemulator.net._chainAddFlow
429 src_sw = None
430 dst_sw = None
431 logging.debug("Find shortest path from vnf %s to %s",
432 src_vnf, dst_vnf)
433
434 for connected_sw in self.net.DCNetwork_graph.neighbors(src_vnf):
435 link_dict = self.net.DCNetwork_graph[src_vnf][connected_sw]
436 for link in link_dict:
437 if (link_dict[link]['src_port_id'] == src_vnf_intf or
438 link_dict[link][
439 'src_port_name'] == src_vnf_intf):
440 # found the right link and connected switch
441 src_sw = connected_sw
442 break
443
444 for connected_sw in self.net.DCNetwork_graph.neighbors(dst_vnf):
445 link_dict = self.net.DCNetwork_graph[connected_sw][dst_vnf]
446 for link in link_dict:
447 if link_dict[link]['dst_port_id'] == dst_vnf_intf or \
448 link_dict[link][
449 'dst_port_name'] == dst_vnf_intf:
450 # found the right link and connected
451 dst_sw = connected_sw
452 break
453 logging.debug("From switch %s to %s " % (src_sw, dst_sw))
454
455 # get shortest path
456 try:
457 # returns the first found shortest path
458 # if all shortest paths are wanted, use: all_shortest_paths
459 path = nx.shortest_path(self.net.DCNetwork_graph, src_sw, dst_sw)
460 except BaseException:
461 logging.exception("No path could be found between {0} and {1} using src_sw={2} and dst_sw={3}".format(
462 src_vnf, dst_vnf, src_sw, dst_sw))
463 logging.debug("Graph nodes: %r" % self.net.DCNetwork_graph.nodes())
464 logging.debug("Graph edges: %r" % self.net.DCNetwork_graph.edges())
465 for e, v in self.net.DCNetwork_graph.edges():
466 logging.debug("%r" % self.net.DCNetwork_graph[e][v])
467 return "No path could be found between {0} and {1}".format(
468 src_vnf, dst_vnf)
469
470 logging.info("Shortest path between {0} and {1}: {2}".format(
471 src_vnf, dst_vnf, path))
472 return path, src_sw, dst_sw
473
474 def add_loadbalancer(self, src_vnf_name, src_vnf_interface, lb_data):
475 """
476 This function will set up a loadbalancer at the given interface.
477
478 :param src_vnf_name: Name of the source VNF
479 :type src_vnf_name: ``str``
480 :param src_vnf_interface: Name of the destination VNF
481 :type src_vnf_interface: ``str``
482 :param lb_data: A dictionary containing the destination data as well as custom path settings
483 :type lb_data: ``dict``
484
485 :Example:
486 lbdata = {"dst_vnf_interfaces": {"dc2_man_web0": "port-man-2",
487 "dc3_man_web0": "port-man-4","dc4_man_web0": "port-man-6"}, "path": {"dc2_man_web0": {"port-man-2": [ "dc1.s1",\
488 "s1", "dc2.s1"]}}}
489 """
490 net = self.net
491 src_sw_inport_nr = 0
492 src_sw = None
493 dest_intfs_mapping = lb_data.get('dst_vnf_interfaces', dict())
494 # a custom path can be specified as a list of switches
495 custom_paths = lb_data.get('path', dict())
496 dest_vnf_outport_nrs = list()
497
498 logging.debug("Call to add_loadbalancer at %s intfs:%s" %
499 (src_vnf_name, src_vnf_interface))
500
501 if not self.check_vnf_intf_pair(src_vnf_name, src_vnf_interface):
502 raise Exception(u"Source VNF %s or intfs %s does not exist" % (
503 src_vnf_name, src_vnf_interface))
504
505 # find the switch belonging to the source interface, as well as the
506 # inport nr
507 for connected_sw in net.DCNetwork_graph.neighbors(src_vnf_name):
508 link_dict = net.DCNetwork_graph[src_vnf_name][connected_sw]
509 for link in link_dict:
510 if link_dict[link]['src_port_name'] == src_vnf_interface:
511 src_sw = connected_sw
512 src_sw_inport_nr = link_dict[link]['dst_port_nr']
513 break
514
515 if src_sw is None or src_sw_inport_nr == 0:
516 raise Exception(u"Source VNF or interface can not be found.")
517
518 # get all target interface outport numbers
519 for vnf_name in dest_intfs_mapping:
520 if vnf_name not in net.DCNetwork_graph:
521 raise Exception(u"Target VNF %s is not known." % vnf_name)
522 for connected_sw in net.DCNetwork_graph.neighbors(vnf_name):
523 link_dict = net.DCNetwork_graph[vnf_name][connected_sw]
524 for link in link_dict:
525 if link_dict[link]['src_port_name'] == dest_intfs_mapping[vnf_name]:
526 dest_vnf_outport_nrs.append(
527 int(link_dict[link]['dst_port_nr']))
528 # get first switch
529 if (src_vnf_name, src_vnf_interface) not in self.lb_flow_cookies:
530 self.lb_flow_cookies[(src_vnf_name, src_vnf_interface)] = list()
531
532 src_ip = None
533 src_mac = None
534 for intf in net[src_vnf_name].intfs.values():
535 if intf.name == src_vnf_interface:
536 src_mac = intf.mac
537 src_ip = intf.ip
538
539 # set up paths for each destination vnf individually
540 index = 0
541 cookie = self.get_cookie()
542 main_cmd = "add-flow -OOpenFlow13"
543 self.lb_flow_cookies[(src_vnf_name, src_vnf_interface)].append(cookie)
544
545 # bookkeeping
546 data = dict()
547 data["src_vnf"] = src_vnf_name
548 data["src_intf"] = src_vnf_interface
549 data["paths"] = list()
550 data["cookie"] = cookie
551
552 # lb mac for src -> target connections
553 lb_mac = "31:33:70:%02x:%02x:%02x" % (random.randint(
554 0, 255), random.randint(0, 255), random.randint(0, 255))
555
556 # calculate lb ip as src_intf.ip +1
557 octets = src_ip.split('.')
558 octets[3] = str(int(octets[3]) + 1)
559 plus_one = '.'.join(octets)
560
561 # set up arp reply as well as add the route to the interface
562 self.setup_arp_reply_at(src_sw, src_sw_inport_nr,
563 plus_one, lb_mac, cookie=cookie)
564 net.getNodeByName(src_vnf_name).setHostRoute(
565 plus_one, src_vnf_interface)
566
567 for dst_vnf_name, dst_vnf_interface in dest_intfs_mapping.items():
568 path, src_sw, dst_sw = self._get_path(src_vnf_name, dst_vnf_name,
569 src_vnf_interface, dst_vnf_interface)
570
571 # use custom path if one is supplied
572 # json does not support hashing on tuples so we use nested dicts
573 if custom_paths is not None and dst_vnf_name in custom_paths:
574 if dst_vnf_interface in custom_paths[dst_vnf_name]:
575 path = custom_paths[dst_vnf_name][dst_vnf_interface]
576 logging.debug("Taking custom path from %s to %s: %s" % (
577 src_vnf_name, dst_vnf_name, path))
578
579 if not self.check_vnf_intf_pair(dst_vnf_name, dst_vnf_interface):
580 self.delete_loadbalancer(src_vnf_name, src_vnf_interface)
581 raise Exception(u"VNF %s or intfs %s does not exist" %
582 (dst_vnf_name, dst_vnf_interface))
583 if isinstance(path, dict):
584 self.delete_loadbalancer(src_vnf_name, src_vnf_interface)
585 raise Exception(
586 u"Can not find a valid path. Are you specifying the right interfaces?.")
587
588 target_mac = "fa:17:00:03:13:37"
589 target_ip = "0.0.0.0"
590 for intf in net[dst_vnf_name].intfs.values():
591 if intf.name == dst_vnf_interface:
592 target_mac = str(intf.mac)
593 target_ip = str(intf.ip)
594 dst_sw_outport_nr = dest_vnf_outport_nrs[index]
595 current_hop = src_sw
596 switch_inport_nr = src_sw_inport_nr
597
598 # self.setup_arp_reply_at(src_sw, src_sw_inport_nr, target_ip, target_mac, cookie=cookie)
599 net.getNodeByName(dst_vnf_name).setHostRoute(
600 src_ip, dst_vnf_interface)
601
602 # choose free vlan if path contains more than 1 switch
603 if len(path) > 1:
604 vlan = net.vlans.pop()
605 if vlan == 0:
606 vlan = net.vlans.pop()
607 else:
608 vlan = None
609
610 single_flow_data = dict()
611 single_flow_data["dst_vnf"] = dst_vnf_name
612 single_flow_data["dst_intf"] = dst_vnf_interface
613 single_flow_data["path"] = path
614 single_flow_data["vlan"] = vlan
615 single_flow_data["cookie"] = cookie
616
617 data["paths"].append(single_flow_data)
618
619 # src to target
620 for i in range(0, len(path)):
621 if i < len(path) - 1:
622 next_hop = path[i + 1]
623 else:
624 # last switch reached
625 next_hop = dst_vnf_name
626 next_node = net.getNodeByName(next_hop)
627 if next_hop == dst_vnf_name:
628 switch_outport_nr = dst_sw_outport_nr
629 logging.info("end node reached: {0}".format(dst_vnf_name))
630 elif not isinstance(next_node, OVSSwitch):
631 logging.info(
632 "Next node: {0} is not a switch".format(next_hop))
633 return "Next node: {0} is not a switch".format(next_hop)
634 else:
635 # take first link between switches by default
636 index_edge_out = 0
637 switch_outport_nr = net.DCNetwork_graph[current_hop][next_hop][index_edge_out]['src_port_nr']
638
639 cmd = 'priority=1,in_port=%s,cookie=%s' % (
640 switch_inport_nr, cookie)
641 cmd_back = 'priority=1,in_port=%s,cookie=%s' % (
642 switch_outport_nr, cookie)
643 # if a vlan is picked, the connection is routed through
644 # multiple switches
645 if vlan is not None:
646 if path.index(current_hop) == 0: # first node
647 # flow #index set up
648 cmd = 'in_port=%s' % src_sw_inport_nr
649 cmd += ',cookie=%s' % cookie
650 cmd += ',table=%s' % cookie
651 cmd += ',ip'
652 cmd += ',reg1=%s' % index
653 cmd += ',actions='
654 # set vlan id
655 cmd += ',push_vlan:0x8100'
656 masked_vlan = vlan | 0x1000
657 cmd += ',set_field:%s->vlan_vid' % masked_vlan
658 cmd += ',set_field:%s->eth_dst' % target_mac
659 cmd += ',set_field:%s->ip_dst' % target_ip
660 cmd += ',output:%s' % switch_outport_nr
661
662 # last switch for reverse route
663 # remove any vlan tags
664 cmd_back += ',dl_vlan=%s' % vlan
665 cmd_back += ',actions=pop_vlan,output:%s' % switch_inport_nr
666 elif next_hop == dst_vnf_name: # last switch
667 # remove any vlan tags
668 cmd += ',dl_vlan=%s' % vlan
669 cmd += ',actions=pop_vlan,output:%s' % switch_outport_nr
670 # set up arp replys at the port so the dst nodes know
671 # the src
672 self.setup_arp_reply_at(
673 current_hop, switch_outport_nr, src_ip, src_mac, cookie=cookie)
674
675 # reverse route
676 cmd_back = 'in_port=%s' % switch_outport_nr
677 cmd_back += ',cookie=%s' % cookie
678 cmd_back += ',ip'
679 cmd_back += ',actions='
680 cmd_back += 'push_vlan:0x8100'
681 masked_vlan = vlan | 0x1000
682 cmd_back += ',set_field:%s->vlan_vid' % masked_vlan
683 cmd_back += ',set_field:%s->eth_src' % lb_mac
684 cmd_back += ',set_field:%s->ip_src' % plus_one
685 cmd_back += ',output:%s' % switch_inport_nr
686 else: # middle nodes
687 # if we have a circle in the path we need to specify this, as openflow will ignore the packet
688 # if we just output it on the same port as it came in
689 if switch_inport_nr == switch_outport_nr:
690 cmd += ',dl_vlan=%s,actions=IN_PORT' % (vlan)
691 cmd_back += ',dl_vlan=%s,actions=IN_PORT' % (vlan)
692 else:
693 cmd += ',dl_vlan=%s,actions=output:%s' % (
694 vlan, switch_outport_nr)
695 cmd_back += ',dl_vlan=%s,actions=output:%s' % (
696 vlan, switch_inport_nr)
697 # output the packet at the correct outport
698 else:
699 cmd = 'in_port=%s' % src_sw_inport_nr
700 cmd += ',cookie=%s' % cookie
701 cmd += ',table=%s' % cookie
702 cmd += ',ip'
703 cmd += ',reg1=%s' % index
704 cmd += ',actions='
705 cmd += ',set_field:%s->eth_dst' % target_mac
706 cmd += ',set_field:%s->ip_dst' % target_ip
707 cmd += ',output:%s' % switch_outport_nr
708
709 # reverse route
710 cmd_back = 'in_port=%s' % switch_outport_nr
711 cmd_back += ',cookie=%s' % cookie
712 cmd_back += ',ip'
713 cmd_back += ',actions='
714 cmd_back += ',set_field:%s->eth_src' % lb_mac
715 cmd_back += ',set_field:%s->ip_src' % plus_one
716 cmd_back += ',output:%s' % src_sw_inport_nr
717
718 self.setup_arp_reply_at(
719 current_hop, switch_outport_nr, src_ip, src_mac, cookie=cookie)
720
721 # excecute the command on the target switch
722 logging.debug(cmd)
723 cmd = "\"%s\"" % cmd
724 cmd_back = "\"%s\"" % cmd_back
725 net[current_hop].dpctl(main_cmd, cmd)
726 net[current_hop].dpctl(main_cmd, cmd_back)
727
728 # set next hop for the next iteration step
729 if isinstance(next_node, OVSSwitch):
730 switch_inport_nr = net.DCNetwork_graph[current_hop][next_hop][0]['dst_port_nr']
731 current_hop = next_hop
732
733 # advance to next destination
734 index += 1
735
736 # set up the actual load balancing rule as a multipath on the very
737 # first switch
738 cmd = '"in_port=%s' % src_sw_inport_nr
739 cmd += ',cookie=%s' % (cookie)
740 cmd += ',ip'
741 cmd += ',actions='
742 # push 0x01 into the first register
743 cmd += 'load:0x1->NXM_NX_REG0[]'
744 # load balance modulo n over all dest interfaces
745 # TODO: in newer openvswitch implementations this should be changed to symmetric_l3l4+udp
746 # to balance any kind of traffic
747 cmd += ',multipath(symmetric_l4,1024,modulo_n,%s,0,NXM_NX_REG1[0..12])' % len(
748 dest_intfs_mapping)
749 # reuse the cookie as table entry as it will be unique
750 cmd += ',resubmit(, %s)"' % cookie
751
752 # actually add the flow
753 logging.debug("Switch: %s, CMD: %s" % (src_sw, cmd))
754 net[src_sw].dpctl(main_cmd, cmd)
755
756 # finally add all flow data to the internal data storage
757 self.full_lb_data[(src_vnf_name, src_vnf_interface)] = data
758
759 def add_floating_lb(self, datacenter, lb_data):
760 """
761 This function will set up a loadbalancer at the given datacenter.
762 This function returns the floating ip assigned to the loadbalancer as multiple ones are possible.
763
764 :param datacenter: The datacenter entrypoint
765 :type datacenter: ``str``
766 :param lb_data: A dictionary containing the destination data as well as custom path settings
767 :type lb_data: ``dict``
768
769 :Example:
770 lbdata = {"dst_vnf_interfaces": {"dc2_man_web0": "port-man-2",
771 "dc3_man_web0": "port-man-4","dc4_man_web0": "port-man-6"}, "path": {"dc2_man_web0": {"port-man-2": [ "dc1.s1",\
772 "s1", "dc2.s1"]}}}
773 """
774 net = self.net
775 src_sw_inport_nr = 1
776 src_sw = self.floating_switch.name
777 dest_intfs_mapping = lb_data.get('dst_vnf_interfaces', dict())
778 # a custom path can be specified as a list of switches
779 custom_paths = lb_data.get('path', dict())
780 dest_vnf_outport_nrs = list()
781
782 if datacenter not in self.net.dcs:
783 raise Exception(u"Source datacenter can not be found.")
784
785 # get all target interface outport numbers
786 for vnf_name in dest_intfs_mapping:
787 if vnf_name not in net.DCNetwork_graph:
788 raise Exception(u"Target VNF %s is not known." % vnf_name)
789 for connected_sw in net.DCNetwork_graph.neighbors(vnf_name):
790 link_dict = net.DCNetwork_graph[vnf_name][connected_sw]
791 for link in link_dict:
792 if link_dict[link]['src_port_name'] == dest_intfs_mapping[vnf_name]:
793 dest_vnf_outport_nrs.append(
794 int(link_dict[link]['dst_port_nr']))
795
796 if len(dest_vnf_outport_nrs) == 0:
797 raise Exception(
798 "There are no paths specified for the loadbalancer")
799 src_ip = self.floating_intf.IP()
800 src_mac = self.floating_intf.MAC()
801
802 # set up paths for each destination vnf individually
803 index = 0
804 cookie = self.get_cookie()
805 main_cmd = "add-flow -OOpenFlow13"
806 floating_ip = self.floating_network.get_new_ip_address(
807 "floating-ip").split("/")[0]
808
809 for dst_vnf_name, dst_vnf_interface in dest_intfs_mapping.items():
810 path = None
811 # use custom path if one is supplied
812 # json does not support hashing on tuples so we use nested dicts
813 if custom_paths is not None and dst_vnf_name in custom_paths:
814 if dst_vnf_interface in custom_paths[dst_vnf_name]:
815 path = custom_paths[dst_vnf_name][dst_vnf_interface]
816 logging.debug("Taking custom path to %s: %s" %
817 (dst_vnf_name, path))
818 else:
819 if datacenter not in self.floating_links:
820 self.floating_links[datacenter] = \
821 net.addLink(self.floating_switch, datacenter)
822 path = \
823 self._get_path(self.floating_root.name, dst_vnf_name,
824 self.floating_intf.name, dst_vnf_interface)[0]
825
826 if isinstance(path, dict):
827 self.delete_flow_by_cookie(cookie)
828 raise Exception(
829 u"Can not find a valid path. Are you specifying the right interfaces?.")
830
831 intf = net[dst_vnf_name].nameToIntf[dst_vnf_interface]
832 target_mac = str(intf.MAC())
833 target_ip = str(intf.IP())
834 dst_sw_outport_nr = dest_vnf_outport_nrs[index]
835 current_hop = src_sw
836 switch_inport_nr = src_sw_inport_nr
837 vlan = net.vlans.pop()
838
839 # iterate all switches on the path
840 for i in range(0, len(path)):
841 if i < len(path) - 1:
842 next_hop = path[i + 1]
843 else:
844 # last switch reached
845 next_hop = dst_vnf_name
846 next_node = net.getNodeByName(next_hop)
847
848 # sanity checks
849 if next_hop == dst_vnf_name:
850 switch_outport_nr = dst_sw_outport_nr
851 logging.info("end node reached: {0}".format(dst_vnf_name))
852 elif not isinstance(next_node, OVSSwitch):
853 logging.info(
854 "Next node: {0} is not a switch".format(next_hop))
855 return "Next node: {0} is not a switch".format(next_hop)
856 else:
857 # take first link between switches by default
858 index_edge_out = 0
859 switch_outport_nr = net.DCNetwork_graph[current_hop][next_hop][index_edge_out]['src_port_nr']
860
861 # default filters, just overwritten on the first node and last
862 # node
863 cmd = 'priority=1,in_port=%s,cookie=%s' % (
864 switch_inport_nr, cookie)
865 cmd_back = 'priority=1,in_port=%s,cookie=%s' % (
866 switch_outport_nr, cookie)
867 if i == 0: # first node
868 cmd = 'in_port=%s' % src_sw_inport_nr
869 cmd += ',cookie=%s' % cookie
870 cmd += ',table=%s' % cookie
871 cmd += ',ip'
872 cmd += ',ip_dst=%s' % floating_ip
873 cmd += ',reg1=%s' % index
874 cmd += ',actions='
875 # set vlan id
876 cmd += ',push_vlan:0x8100'
877 masked_vlan = vlan | 0x1000
878 cmd += ',set_field:%s->vlan_vid' % masked_vlan
879 cmd += ',set_field:%s->eth_dst' % target_mac
880 cmd += ',set_field:%s->ip_dst' % target_ip
881 cmd += ',output:%s' % switch_outport_nr
882
883 # last switch for reverse route
884 # remove any vlan tags
885 cmd_back += ',dl_vlan=%s' % vlan
886 cmd_back += ',actions=pop_vlan,output:%s' % switch_inport_nr
887 self.setup_arp_reply_at(
888 current_hop, src_sw_inport_nr, floating_ip, target_mac, cookie=cookie)
889 elif next_hop == dst_vnf_name: # last switch
890 # remove any vlan tags
891 cmd += ',dl_vlan=%s' % vlan
892 cmd += ',actions=pop_vlan,output:%s' % switch_outport_nr
893 # set up arp replys at the port so the dst nodes know the
894 # src
895 self.setup_arp_reply_at(
896 current_hop, switch_outport_nr, src_ip, src_mac, cookie=cookie)
897
898 # reverse route
899 cmd_back = 'in_port=%s' % switch_outport_nr
900 cmd_back += ',cookie=%s' % cookie
901 cmd_back += ',ip'
902 cmd_back += ',actions='
903 cmd_back += 'push_vlan:0x8100'
904 masked_vlan = vlan | 0x1000
905 cmd_back += ',set_field:%s->vlan_vid' % masked_vlan
906 cmd_back += ',set_field:%s->eth_src' % src_mac
907 cmd_back += ',set_field:%s->ip_src' % floating_ip
908 cmd_back += ',output:%s' % switch_inport_nr
909 net.getNodeByName(dst_vnf_name).setHostRoute(
910 src_ip, dst_vnf_interface)
911 else: # middle node
912 # if we have a circle in the path we need to specify this, as openflow will ignore the packet
913 # if we just output it on the same port as it came in
914 if switch_inport_nr == switch_outport_nr:
915 cmd += ',dl_vlan=%s,actions=IN_PORT' % (vlan)
916 cmd_back += ',dl_vlan=%s,actions=IN_PORT' % (vlan)
917 else:
918 cmd += ',dl_vlan=%s,actions=output:%s' % (
919 vlan, switch_outport_nr)
920 cmd_back += ',dl_vlan=%s,actions=output:%s' % (
921 vlan, switch_inport_nr)
922
923 # excecute the command on the target switch
924 logging.debug(cmd)
925 cmd = "\"%s\"" % cmd
926 cmd_back = "\"%s\"" % cmd_back
927 net[current_hop].dpctl(main_cmd, cmd)
928 net[current_hop].dpctl(main_cmd, cmd_back)
929
930 # set next hop for the next iteration step
931 if isinstance(next_node, OVSSwitch):
932 switch_inport_nr = net.DCNetwork_graph[current_hop][next_hop][0]['dst_port_nr']
933 current_hop = next_hop
934
935 # advance to next destination
936 index += 1
937
938 # set up the actual load balancing rule as a multipath on the very
939 # first switch
940 cmd = '"in_port=%s' % src_sw_inport_nr
941 cmd += ',cookie=%s' % (cookie)
942 cmd += ',ip'
943 cmd += ',actions='
944 # push 0x01 into the first register
945 cmd += 'load:0x1->NXM_NX_REG0[]'
946 # load balance modulo n over all dest interfaces
947 # TODO: in newer openvswitch implementations this should be changed to symmetric_l3l4+udp
948 # to balance any kind of traffic
949 cmd += ',multipath(symmetric_l4,1024,modulo_n,%s,0,NXM_NX_REG1[0..12])' % len(
950 dest_intfs_mapping)
951 # reuse the cookie as table entry as it will be unique
952 cmd += ',resubmit(, %s)"' % cookie
953
954 # actually add the flow
955 logging.debug("Switch: %s, CMD: %s" % (src_sw, cmd))
956 net[src_sw].dpctl(main_cmd, cmd)
957
958 self.floating_cookies[cookie] = floating_ip
959
960 return cookie, floating_ip
961
962 def setup_arp_reply_at(self, switch, port_nr,
963 target_ip, target_mac, cookie=None):
964 """
965 Sets up a custom ARP reply at a switch.
966 An ARP request coming in on the `port_nr` for `target_ip` will be answered with target IP/MAC.
967
968 :param switch: The switch belonging to the interface
969 :type switch: ``str``
970 :param port_nr: The port number at the switch that is connected to the interface
971 :type port_nr: ``int``
972 :param target_ip: The IP for which to set up the ARP reply
973 :type target_ip: ``str``
974 :param target_mac: The MAC address of the target interface
975 :type target_mac: ``str``
976 :param cookie: cookie to identify the ARP request, if None a new one will be picked
977 :type cookie: ``int`` or ``None``
978 :return: cookie
979 :rtype: ``int``
980 """
981 if cookie is None:
982 cookie = self.get_cookie()
983 main_cmd = "add-flow -OOpenFlow13"
984
985 # first set up ARP requests for the source node, so it will always
986 # 'find' a partner
987 cmd = '"in_port=%s' % port_nr
988 cmd += ',cookie=%s' % cookie
989 cmd += ',arp'
990 # only answer for target ip arp requests
991 cmd += ',arp_tpa=%s' % target_ip
992 cmd += ',actions='
993 # set message type to ARP reply
994 cmd += 'load:0x2->NXM_OF_ARP_OP[]'
995 # set src ip as dst ip
996 cmd += ',move:NXM_OF_ETH_SRC[]->NXM_OF_ETH_DST[]'
997 # set src mac
998 cmd += ',set_field:%s->eth_src' % target_mac
999 # set src as target
1000 cmd += ',move:NXM_NX_ARP_SHA[]->NXM_NX_ARP_THA[], move:NXM_OF_ARP_SPA[]->NXM_OF_ARP_TPA[]'
1001 # set target mac as hex
1002 cmd += ',load:0x%s->NXM_NX_ARP_SHA[]' % "".join(target_mac.split(':'))
1003 # set target ip as hex
1004 octets = target_ip.split('.')
1005 dst_ip_hex = '{:02X}{:02X}{:02X}{:02X}'.format(*map(int, octets))
1006 cmd += ',load:0x%s->NXM_OF_ARP_SPA[]' % dst_ip_hex
1007 # output to incoming port remember the closing "
1008 cmd += ',IN_PORT"'
1009 self.net[switch].dpctl(main_cmd, cmd)
1010 logging.debug(
1011 "Set up ARP reply at %s port %s." % (switch, port_nr))
1012
1013 def delete_flow_by_cookie(self, cookie):
1014 """
1015 Removes a flow identified by the cookie
1016
1017 :param cookie: The cookie for the specified flow
1018 :type cookie: ``int``
1019 :return: True if successful, else false
1020 :rtype: ``bool``
1021 """
1022 if not cookie:
1023 return False
1024 logging.debug("Deleting flow by cookie %d" % (cookie))
1025 flows = list()
1026 # we have to call delete-group for each switch
1027 for node in self.net.switches:
1028 flow = dict()
1029 flow["dpid"] = int(node.dpid, 16)
1030 flow["cookie"] = cookie
1031 flow['cookie_mask'] = int('0xffffffffffffffff', 16)
1032
1033 flows.append(flow)
1034 for flow in flows:
1035 logging.debug("Deleting flowentry with cookie %d" % (
1036 flow["cookie"]))
1037 if self.net.controller == RemoteController:
1038 self.net.ryu_REST('stats/flowentry/delete', data=flow)
1039
1040 self.cookies.remove(cookie)
1041 return True
1042
1043 def delete_chain_by_intf(
1044 self, src_vnf_name, src_vnf_intf, dst_vnf_name, dst_vnf_intf):
1045 """
1046 Removes a flow identified by the vnf_name/vnf_intf pairs
1047
1048 :param src_vnf_name: The vnf name for the specified flow
1049 :type src_vnf_name: ``str``
1050 :param src_vnf_intf: The interface name for the specified flow
1051 :type src_vnf_intf: ``str``
1052 :param dst_vnf_name: The vnf name for the specified flow
1053 :type dst_vnf_name: ``str``
1054 :param dst_vnf_intf: The interface name for the specified flow
1055 :type dst_vnf_intf: ``str``
1056 :return: True if successful, else false
1057 :rtype: ``bool``
1058 """
1059 logging.debug("Deleting flow for vnf/intf pair %s %s" %
1060 (src_vnf_name, src_vnf_intf))
1061 if not self.check_vnf_intf_pair(src_vnf_name, src_vnf_intf):
1062 return False
1063 if not self.check_vnf_intf_pair(dst_vnf_name, dst_vnf_intf):
1064 return False
1065 target_flow = (src_vnf_name, src_vnf_intf, dst_vnf_name, dst_vnf_intf)
1066 if target_flow not in self.chain_flow_cookies:
1067 return False
1068
1069 success = self.delete_flow_by_cookie(
1070 self.chain_flow_cookies[target_flow])
1071
1072 if success:
1073 del self.chain_flow_cookies[target_flow]
1074 del self.full_chain_data[target_flow]
1075 return True
1076 return False
1077
1078 def delete_loadbalancer(self, vnf_src_name, vnf_src_interface):
1079 '''
1080 Removes a loadbalancer that is configured for the node and interface
1081
1082 :param src_vnf_name: Name of the source VNF
1083 :param src_vnf_interface: Name of the destination VNF
1084 '''
1085 flows = list()
1086 # we have to call delete-group for each switch
1087 delete_group = list()
1088 group_id = self.get_flow_group(vnf_src_name, vnf_src_interface)
1089 for node in self.net.switches:
1090 for cookie in self.lb_flow_cookies[(
1091 vnf_src_name, vnf_src_interface)]:
1092 flow = dict()
1093 flow["dpid"] = int(node.dpid, 16)
1094 flow["cookie"] = cookie
1095 flow['cookie_mask'] = int('0xffffffffffffffff', 16)
1096
1097 flows.append(flow)
1098 group_del = dict()
1099 group_del["dpid"] = int(node.dpid, 16)
1100 group_del["group_id"] = group_id
1101 delete_group.append(group_del)
1102
1103 for flow in flows:
1104 logging.debug("Deleting flowentry with cookie %d belonging to lb at %s:%s" % (
1105 flow["cookie"], vnf_src_name, vnf_src_interface))
1106 if self.net.controller == RemoteController:
1107 self.net.ryu_REST('stats/flowentry/delete', data=flow)
1108
1109 logging.debug("Deleting group with id %s" % group_id)
1110 for switch_del_group in delete_group:
1111 if self.net.controller == RemoteController:
1112 self.net.ryu_REST("stats/groupentry/delete",
1113 data=switch_del_group)
1114
1115 # unmap groupid from the interface
1116 target_pair = (vnf_src_name, vnf_src_interface)
1117 if target_pair in self.flow_groups:
1118 del self.flow_groups[target_pair]
1119 if target_pair in self.full_lb_data:
1120 del self.full_lb_data[target_pair]
1121
1122 def delete_floating_lb(self, cookie):
1123 """
1124 Delete a floating loadbalancer.
1125 Floating loadbalancers are different from normal ones as there are multiple ones on the same interface.
1126 :param cookie: The cookie of the loadbalancer
1127 :type cookie: ``int``
1128 """
1129 cookie = int(cookie)
1130 if cookie not in self.floating_cookies:
1131 raise Exception(
1132 "Can not delete floating loadbalancer as the flowcookie is not known")
1133
1134 self.delete_flow_by_cookie(cookie)
1135 floating_ip = self.floating_cookies[cookie]
1136 self.floating_network.withdraw_ip_address(floating_ip)
1137
1138 def set_arp_entry(self, vnf_name, vnf_interface, ip, mac):
1139 """
1140 Sets an arp entry on the specified VNF. This is done on the node directly and not by open vswitch!
1141 :param vnf_name: Name of the VNF
1142 :type vnf_name: ``str``
1143 :param vnf_interface: Name of the interface
1144 :type vnf_interface: ``str``
1145 :param ip: IP to reply to
1146 :type ip: ``str``
1147 :param mac: Answer with this MAC
1148 :type mac: ``str``
1149 """
1150 node = self.net.getNodeByName(vnf_name)
1151 node.cmd("arp -i %s -s %s %s" % (vnf_interface, ip, mac))