| """Openstack manage component of PG Sandman. |
| |
| .. module:: manage |
| :synopsis: Module containing the OpenstackManage class. |
| .. moduleauthor: PG Sandman |
| |
| """ |
| |
| import logging |
| import threading |
| import uuid |
| import networkx as nx |
| import chain_api |
| import json |
| import random |
| from emuvim.api.openstack.resources import Net, Port |
| from mininet.node import OVSSwitch, RemoteController, Node |
| |
| |
| class OpenstackManage(object): |
| """ |
| OpenstackManage is a singleton and management component for the emulator. |
| It is the brain of the Openstack component and manages everything that is not datacenter specific like |
| network chains or load balancers. |
| """ |
| __instance = None |
| |
| def __new__(cls): |
| if OpenstackManage.__instance is None: |
| OpenstackManage.__instance = object.__new__(cls) |
| return OpenstackManage.__instance |
| |
| def __init__(self, ip="0.0.0.0", port=4000): |
| # we are a singleton, only initialize once! |
| self.lock = threading.Lock() |
| with self.lock: |
| if hasattr(self, "init"): |
| return |
| self.init = True |
| |
| self.endpoints = dict() |
| self.cookies = set() |
| self.cookies.add(0) |
| self.ip = ip |
| self.port = port |
| self._net = None |
| # to keep track which src_vnf(input port on the switch) handles a load balancer |
| self.lb_flow_cookies = dict() |
| self.chain_flow_cookies = dict() |
| |
| # for the visualization also store the complete chain data incl. paths |
| self.full_chain_data = dict() |
| self.full_lb_data = dict() |
| |
| # flow groups could be handled for each switch separately, but this global group counter should be easier to |
| # debug and to maintain |
| self.flow_groups = dict() |
| |
| # we want one global chain api. this should not be datacenter dependent! |
| self.chain = chain_api.ChainApi(ip, port, self) |
| self.thread = threading.Thread(target=self.chain._start_flask, args=()) |
| self.thread.daemon = True |
| self.thread.name = self.chain.__class__ |
| self.thread.start() |
| |
| # floating ip network setup |
| self.floating_switch = None |
| self.floating_network = None |
| self.floating_netmask = "192.168.100.0/24" |
| self.floating_nodes = dict() |
| self.floating_cookies = dict() |
| self.floating_intf = None |
| self.floating_links = dict() |
| |
| @property |
| def net(self): |
| return self._net |
| |
| @net.setter |
| def net(self, value): |
| if self._net is None: |
| self._net = value |
| self.init_floating_network() |
| self._net = value |
| |
| def init_floating_network(self): |
| """ |
| Initialize the floating network component for the emulator. |
| Will not do anything if already initialized. |
| """ |
| if self.net is not None and self.floating_switch is None: |
| # create a floating network |
| fn = self.floating_network = Net("default") |
| fn.id = str(uuid.uuid4()) |
| fn.set_cidr(self.floating_netmask) |
| |
| # create a subnet |
| fn.subnet_id = str(uuid.uuid4()) |
| fn.subnet_name = fn.name + "-sub" |
| |
| # create a port for the host |
| port = Port("root-port") |
| #port.id = str(uuid.uuid4()) |
| port.net_name = fn.name |
| |
| # get next free ip |
| root_ip = fn.get_new_ip_address(port.name) |
| port.ip_address = root_ip |
| # floating ip network setup |
| # wierd way of getting a datacenter object |
| first_dc = self.net.dcs.values()[0] |
| # set a dpid for the switch. for this we have to get the id of the next possible dc |
| self.floating_switch = self.net.addSwitch("fs1", dpid=hex(first_dc._get_next_dc_dpid())[2:]) |
| # this is the interface appearing on the physical host |
| self.floating_root = Node('root', inNamespace=False) |
| self.net.hosts.append(self.floating_root) |
| self.net.nameToNode['root'] = self.floating_root |
| self.floating_intf = self.net.addLink(self.floating_root, self.floating_switch).intf1 |
| self.floating_root.setIP(root_ip, intf=self.floating_intf) |
| self.floating_nodes[(self.floating_root.name, root_ip)] = self.floating_root |
| |
| |
| def stop_floating_network(self): |
| self._net = None |
| self.floating_switch = None |
| |
| def add_endpoint(self, ep): |
| """ |
| Registers an openstack endpoint with manage |
| |
| :param ep: Openstack API endpoint |
| :type ep: :class:`heat.openstack_api_endpoint` |
| """ |
| key = "%s:%s" % (ep.ip, ep.port) |
| self.endpoints[key] = ep |
| |
| def get_cookie(self): |
| """ |
| Get an unused cookie. |
| |
| :return: Cookie |
| :rtype: ``int`` |
| """ |
| cookie = int(max(self.cookies) + 1) |
| self.cookies.add(cookie) |
| return cookie |
| |
| def get_flow_group(self, src_vnf_name, src_vnf_interface): |
| """ |
| Gets free group that is not currently used by any other flow for the specified interface / VNF. |
| |
| :param src_vnf_name: Source VNF name |
| :type src_vnf_name: ``str`` |
| :param src_vnf_interface: Source VNF interface name |
| :type src_vnf_interface: ``str`` |
| :return: Flow group identifier. |
| :rtype: ``int`` |
| """ |
| if (src_vnf_name, src_vnf_interface) not in self.flow_groups: |
| grp = int(len(self.flow_groups) + 1) |
| self.flow_groups[(src_vnf_name, src_vnf_interface)] = grp |
| else: |
| grp = self.flow_groups[(src_vnf_name, src_vnf_interface)] |
| return grp |
| |
| def check_vnf_intf_pair(self, vnf_name, vnf_intf_name): |
| """ |
| Checks if a VNF exists and has the given interface |
| |
| :param vnf_name: Name of the VNF to be checked |
| :type vnf_name: ``str`` |
| :param vnf_intf_name: Name of the interface that belongst to the VNF |
| :type vnf_intf_name: ``str`` |
| :return: ``True`` if it is valid pair, else ``False`` |
| :rtype: ``bool`` |
| """ |
| |
| if vnf_name in self.net: |
| vnf = self.net.getNodeByName(vnf_name) |
| return vnf_intf_name in vnf.nameToIntf |
| |
| def network_action_start(self, vnf_src_name, vnf_dst_name, **kwargs): |
| """ |
| Starts a network chain for a source destination pair |
| |
| :param vnf_src_name: Name of the source VNF |
| :type vnf_src_name: ``str`` |
| :param vnf_dst_name: Name of the source VNF interface |
| :type vnf_dst_name: ``str`` |
| :param \**kwargs: See below |
| |
| :Keyword Arguments: |
| * *vnf_src_interface* (``str``): Name of source interface. |
| * *vnf_dst_interface* (``str``): Name of destination interface. |
| * *weight* (``int``): This value is fed into the shortest path computation if no path is specified. |
| * *match* (``str``): A custom match entry for the openflow flow rules. Only vlanid or port possible. |
| * *bidirectional* (``bool``): If set the chain will be set in both directions, else it will just set up \ |
| from source to destination. |
| * *cookie* (``int``): Cookie value used by openflow. Used to identify the flows in the switches to be \ |
| able to modify the correct flows. |
| * *no_route* (``bool``): If set a layer 3 route to the target interface will not be set up. |
| :return: The cookie chosen for the flow. |
| :rtype: ``int`` |
| """ |
| try: |
| vnf_src_interface = kwargs.get('vnf_src_interface') |
| vnf_dst_interface = kwargs.get('vnf_dst_interface') |
| layer2 = kwargs.get('layer2', True) |
| match = kwargs.get('match') |
| flow = (vnf_src_name, vnf_src_interface, vnf_dst_name, vnf_dst_interface) |
| if flow in self.chain_flow_cookies: |
| raise Exception("There is already a chain at the specified src/dst pair!") |
| # set up a layer 2 chain, this allows multiple chains for the same interface |
| src_node = self.net.getNodeByName(vnf_src_name) |
| dst_node = self.net.getNodeByName(vnf_dst_name) |
| dst_intf = dst_node.intf(vnf_dst_interface) |
| if layer2: |
| switch, inport = self._get_connected_switch_data(vnf_src_name, vnf_src_interface) |
| self.setup_arp_reply_at(switch, inport, dst_intf.IP(), dst_intf.MAC()) |
| if isinstance(match, str): |
| match += ",dl_dst=%s" % dst_intf.MAC() |
| else: |
| match = "dl_dst=%s" % dst_intf.MAC() |
| |
| cookie = kwargs.get('cookie', self.get_cookie()) |
| self.cookies.add(cookie) |
| c = self.net.setChain( |
| vnf_src_name, vnf_dst_name, |
| vnf_src_interface=vnf_src_interface, |
| vnf_dst_interface=vnf_dst_interface, |
| cmd='add-flow', |
| weight=kwargs.get('weight'), |
| match=match, |
| bidirectional=False, |
| cookie=cookie, |
| path=kwargs.get('path')) |
| |
| # to keep this logic seperate of the core son-emu do the housekeeping here |
| data = dict() |
| data["src_vnf"] = vnf_src_name |
| data["src_intf"] = vnf_src_interface |
| data["dst_vnf"] = vnf_dst_name |
| data["dst_intf"] = vnf_dst_interface |
| data["cookie"] = cookie |
| data["layer2"] = layer2 |
| if kwargs.get('path') is not None: |
| data["path"] = kwargs.get('path') |
| else: |
| data["path"] = self._get_path(vnf_src_name, vnf_dst_name, vnf_src_interface, |
| vnf_dst_interface)[0] |
| |
| # add route to dst ip to this interface |
| # this might block on containers that are still setting up, so start a new thread |
| if not kwargs.get('no_route'): |
| # son_emu does not like concurrent commands for a container so we need to lock this if multiple chains |
| # on the same interface are created |
| src_node.setHostRoute(dst_node.intf(vnf_dst_interface).IP(), vnf_src_interface) |
| |
| try: |
| son_emu_data = json.loads(self.get_son_emu_chain_data(vnf_src_name)) |
| except: |
| son_emu_data = dict() |
| if "son_emu_data" not in son_emu_data: |
| son_emu_data["son_emu_data"] = dict() |
| if "interfaces" not in son_emu_data["son_emu_data"]: |
| son_emu_data["son_emu_data"]["interfaces"] = dict() |
| if vnf_src_interface not in son_emu_data["son_emu_data"]["interfaces"]: |
| son_emu_data["son_emu_data"]["interfaces"][vnf_src_interface] = list() |
| son_emu_data["son_emu_data"]["interfaces"][vnf_src_interface].append(dst_intf.IP()) |
| |
| self.set_son_emu_chain_data(vnf_src_name, son_emu_data) |
| |
| if kwargs.get('bidirectional', False): |
| # call the reverse direction |
| path = kwargs.get('path') |
| if path is not None: |
| path = list(reversed(path)) |
| self.network_action_start(vnf_dst_name, vnf_src_name, vnf_src_interface=vnf_dst_interface, |
| vnf_dst_interface=vnf_src_interface, bidirectional=False, |
| layer2=kwargs.get('layer2', False), path=path, |
| no_route=kwargs.get('no_route')) |
| |
| self.full_chain_data[flow] = data |
| self.chain_flow_cookies[flow] = cookie |
| return cookie |
| except Exception as ex: |
| logging.exception("RPC error.") |
| raise Exception(ex.message) |
| |
| def network_action_stop(self, vnf_src_name, vnf_dst_name, **kwargs): |
| """ |
| Starts a network chain for a source destination pair |
| |
| :param vnf_src_name: Name of the source VNF |
| :type vnf_src_name: ``str`` |
| :param vnf_dst_name: Name of the source VNF interface |
| :type vnf_dst_name: ``str`` |
| :param \**kwargs: See below |
| |
| :Keyword Arguments: |
| * *vnf_src_interface* (``str``): Name of source interface. |
| * *vnf_dst_interface* (``str``): Name of destination interface. |
| * *bidirectional* (``bool``): If set the chain will be torn down in both directions, else it will just\ |
| be torn down from source to destination. |
| * *cookie* (``int``): Cookie value used by openflow. Used to identify the flows in the switches to be \ |
| able to modify the correct flows. |
| """ |
| try: |
| if 'cookie' in kwargs: |
| return self.delete_flow_by_cookie(kwargs.get('cookie')) |
| |
| if kwargs.get('bidirectional', False): |
| self.delete_chain_by_intf(vnf_dst_name, kwargs.get('vnf_dst_interface'), |
| vnf_src_name, kwargs.get('vnf_src_interface')) |
| |
| return self.delete_chain_by_intf(vnf_src_name, kwargs.get('vnf_src_interface'), |
| vnf_dst_name, kwargs.get('vnf_dst_interface')) |
| except Exception as ex: |
| logging.exception("RPC error.") |
| return ex.message |
| |
| def set_son_emu_chain_data(self, vnf_name, data): |
| """ |
| Set son-emu chain data for this node. |
| |
| :param vnf_name: The name of the vnf where the data is stored. |
| :type vnf_name: ``str`` |
| :param data: Raw data to store on the node. |
| :type data: ``str`` |
| """ |
| self.net.getNodeByName(vnf_name).cmd("echo \'%s\' > /tmp/son_emu_data.json" % json.dumps(data)) |
| ip_list = [] |
| for intf in data['son_emu_data']['interfaces'].values(): |
| ip_list.extend(intf) |
| |
| self.net.getNodeByName(vnf_name).cmd("echo \'%s\' > /tmp/son_emu_data" % "\n".join(ip_list)) |
| |
| def get_son_emu_chain_data(self, vnf_name): |
| """ |
| Get the current son-emu chain data set for this node. |
| |
| :param vnf_name: The name of the vnf where the data is stored. |
| :type vnf_name: ``str`` |
| :return: raw data stored on the node |
| :rtype: ``str`` |
| """ |
| return self.net.getNodeByName(vnf_name).cmd("cat /tmp/son_emu_data.json") |
| |
| def _get_connected_switch_data(self, vnf_name, vnf_interface): |
| """ |
| Get the switch an interface is connected to |
| :param vnf_name: Name of the VNF |
| :type vnf_name: ``str`` |
| :param vnf_interface: Name of the VNF interface |
| :type vnf_interface: ``str`` |
| :return: List containing the switch, and the inport number |
| :rtype: [``str``, ``int``] |
| """ |
| src_sw = None |
| src_sw_inport_nr = None |
| for connected_sw in self.net.DCNetwork_graph.neighbors(vnf_name): |
| link_dict = self.net.DCNetwork_graph[vnf_name][connected_sw] |
| for link in link_dict: |
| if (link_dict[link]['src_port_id'] == vnf_interface or |
| link_dict[link][ |
| 'src_port_name'] == vnf_interface): |
| # found the right link and connected switch |
| src_sw = connected_sw |
| src_sw_inport_nr = link_dict[link]['dst_port_nr'] |
| break |
| |
| return src_sw, src_sw_inport_nr |
| |
| def _get_path(self, src_vnf, dst_vnf, src_vnf_intf, dst_vnf_intf): |
| """ |
| Own implementation of the get_path function from DCNetwork, because we just want the path and not set up |
| flows on the way. |
| |
| :param src_vnf: Name of the source VNF |
| :type src_vnf: ``str`` |
| :param dst_vnf: Name of the destination VNF |
| :type dst_vnf: ``str`` |
| :param src_vnf_intf: Name of the source VNF interface |
| :type src_vnf_intf: ``str`` |
| :param dst_vnf_intf: Name of the destination VNF interface |
| :type dst_vnf_intf: ``str`` |
| :return: path, src_sw, dst_sw |
| :rtype: ``list``, ``str``, ``str`` |
| """ |
| # modified version of the _chainAddFlow from emuvim.dcemulator.net._chainAddFlow |
| src_sw = None |
| dst_sw = None |
| logging.debug("Find shortest path from vnf %s to %s", |
| src_vnf, dst_vnf) |
| |
| for connected_sw in self.net.DCNetwork_graph.neighbors(src_vnf): |
| link_dict = self.net.DCNetwork_graph[src_vnf][connected_sw] |
| for link in link_dict: |
| if (link_dict[link]['src_port_id'] == src_vnf_intf or |
| link_dict[link][ |
| 'src_port_name'] == src_vnf_intf): |
| # found the right link and connected switch |
| src_sw = connected_sw |
| break |
| |
| for connected_sw in self.net.DCNetwork_graph.neighbors(dst_vnf): |
| link_dict = self.net.DCNetwork_graph[connected_sw][dst_vnf] |
| for link in link_dict: |
| if link_dict[link]['dst_port_id'] == dst_vnf_intf or \ |
| link_dict[link][ |
| 'dst_port_name'] == dst_vnf_intf: |
| # found the right link and connected |
| dst_sw = connected_sw |
| break |
| logging.debug("From switch %s to %s " % (src_sw, dst_sw)) |
| |
| # get shortest path |
| try: |
| # returns the first found shortest path |
| # if all shortest paths are wanted, use: all_shortest_paths |
| path = nx.shortest_path(self.net.DCNetwork_graph, src_sw, dst_sw) |
| except: |
| logging.exception("No path could be found between {0} and {1} using src_sw={2} and dst_sw={3}".format( |
| src_vnf, dst_vnf, src_sw, dst_sw)) |
| logging.debug("Graph nodes: %r" % self.net.DCNetwork_graph.nodes()) |
| logging.debug("Graph edges: %r" % self.net.DCNetwork_graph.edges()) |
| for e, v in self.net.DCNetwork_graph.edges(): |
| logging.debug("%r" % self.net.DCNetwork_graph[e][v]) |
| return "No path could be found between {0} and {1}".format(src_vnf, dst_vnf) |
| |
| logging.info("Shortest path between {0} and {1}: {2}".format(src_vnf, dst_vnf, path)) |
| return path, src_sw, dst_sw |
| |
| def add_loadbalancer(self, src_vnf_name, src_vnf_interface, lb_data): |
| """ |
| This function will set up a loadbalancer at the given interface. |
| |
| :param src_vnf_name: Name of the source VNF |
| :type src_vnf_name: ``str`` |
| :param src_vnf_interface: Name of the destination VNF |
| :type src_vnf_interface: ``str`` |
| :param lb_data: A dictionary containing the destination data as well as custom path settings |
| :type lb_data: ``dict`` |
| |
| :Example: |
| lbdata = {"dst_vnf_interfaces": {"dc2_man_web0": "port-man-2", |
| "dc3_man_web0": "port-man-4","dc4_man_web0": "port-man-6"}, "path": {"dc2_man_web0": {"port-man-2": [ "dc1.s1",\ |
| "s1", "dc2.s1"]}}} |
| """ |
| net = self.net |
| src_sw_inport_nr = 0 |
| src_sw = None |
| dest_intfs_mapping = lb_data.get('dst_vnf_interfaces', dict()) |
| # a custom path can be specified as a list of switches |
| custom_paths = lb_data.get('path', dict()) |
| dest_vnf_outport_nrs = list() |
| |
| logging.debug("Call to add_loadbalancer at %s intfs:%s" % (src_vnf_name, src_vnf_interface)) |
| |
| if not self.check_vnf_intf_pair(src_vnf_name, src_vnf_interface): |
| raise Exception(u"Source VNF %s or intfs %s does not exist" % (src_vnf_name, src_vnf_interface)) |
| |
| # find the switch belonging to the source interface, as well as the inport nr |
| for connected_sw in net.DCNetwork_graph.neighbors(src_vnf_name): |
| link_dict = net.DCNetwork_graph[src_vnf_name][connected_sw] |
| for link in link_dict: |
| if link_dict[link]['src_port_name'] == src_vnf_interface: |
| src_sw = connected_sw |
| src_sw_inport_nr = link_dict[link]['dst_port_nr'] |
| break |
| |
| if src_sw is None or src_sw_inport_nr == 0: |
| raise Exception(u"Source VNF or interface can not be found.") |
| |
| # get all target interface outport numbers |
| for vnf_name in dest_intfs_mapping: |
| if vnf_name not in net.DCNetwork_graph: |
| raise Exception(u"Target VNF %s is not known." % vnf_name) |
| for connected_sw in net.DCNetwork_graph.neighbors(vnf_name): |
| link_dict = net.DCNetwork_graph[vnf_name][connected_sw] |
| for link in link_dict: |
| if link_dict[link]['src_port_name'] == dest_intfs_mapping[vnf_name]: |
| dest_vnf_outport_nrs.append(int(link_dict[link]['dst_port_nr'])) |
| # get first switch |
| if (src_vnf_name, src_vnf_interface) not in self.lb_flow_cookies: |
| self.lb_flow_cookies[(src_vnf_name, src_vnf_interface)] = list() |
| |
| src_intf = None |
| src_ip = None |
| src_mac = None |
| for intf in net[src_vnf_name].intfs.values(): |
| if intf.name == src_vnf_interface: |
| src_mac = intf.mac |
| src_ip = intf.ip |
| src_intf = intf |
| |
| # set up paths for each destination vnf individually |
| index = 0 |
| cookie = self.get_cookie() |
| main_cmd = "add-flow -OOpenFlow13" |
| self.lb_flow_cookies[(src_vnf_name, src_vnf_interface)].append(cookie) |
| |
| # bookkeeping |
| data = dict() |
| data["src_vnf"] = src_vnf_name |
| data["src_intf"] = src_vnf_interface |
| data["paths"] = list() |
| data["cookie"] = cookie |
| |
| # lb mac for src -> target connections |
| lb_mac = "31:33:70:%02x:%02x:%02x" % (random.randint(0, 255),random.randint(0, 255),random.randint(0, 255)) |
| |
| # calculate lb ip as src_intf.ip +1 |
| octets = src_ip.split('.') |
| octets[3] = str(int(octets[3]) + 1) |
| plus_one = '.'.join(octets) |
| |
| # set up arp reply as well as add the route to the interface |
| self.setup_arp_reply_at(src_sw, src_sw_inport_nr, plus_one, lb_mac, cookie=cookie) |
| net.getNodeByName(src_vnf_name).setHostRoute(plus_one, src_vnf_interface) |
| |
| for dst_vnf_name, dst_vnf_interface in dest_intfs_mapping.items(): |
| path, src_sw, dst_sw = self._get_path(src_vnf_name, dst_vnf_name, |
| src_vnf_interface, dst_vnf_interface) |
| |
| # use custom path if one is supplied |
| # json does not support hashing on tuples so we use nested dicts |
| if custom_paths is not None and dst_vnf_name in custom_paths: |
| if dst_vnf_interface in custom_paths[dst_vnf_name]: |
| path = custom_paths[dst_vnf_name][dst_vnf_interface] |
| logging.debug("Taking custom path from %s to %s: %s" % (src_vnf_name, dst_vnf_name, path)) |
| |
| if not self.check_vnf_intf_pair(dst_vnf_name, dst_vnf_interface): |
| self.delete_loadbalancer(src_vnf_name, src_vnf_interface) |
| raise Exception(u"VNF %s or intfs %s does not exist" % (dst_vnf_name, dst_vnf_interface)) |
| if isinstance(path, dict): |
| self.delete_loadbalancer(src_vnf_name, src_vnf_interface) |
| raise Exception(u"Can not find a valid path. Are you specifying the right interfaces?.") |
| |
| target_mac = "fa:17:00:03:13:37" |
| target_ip = "0.0.0.0" |
| for intf in net[dst_vnf_name].intfs.values(): |
| if intf.name == dst_vnf_interface: |
| target_mac = str(intf.mac) |
| target_ip = str(intf.ip) |
| dst_sw_outport_nr = dest_vnf_outport_nrs[index] |
| current_hop = src_sw |
| switch_inport_nr = src_sw_inport_nr |
| |
| #self.setup_arp_reply_at(src_sw, src_sw_inport_nr, target_ip, target_mac, cookie=cookie) |
| net.getNodeByName(dst_vnf_name).setHostRoute(src_ip, dst_vnf_interface) |
| |
| # choose free vlan if path contains more than 1 switch |
| if len(path) > 1: |
| vlan = net.vlans.pop() |
| if vlan == 0: |
| vlan = net.vlans.pop() |
| else: |
| vlan = None |
| |
| single_flow_data = dict() |
| single_flow_data["dst_vnf"] = dst_vnf_name |
| single_flow_data["dst_intf"] = dst_vnf_interface |
| single_flow_data["path"] = path |
| single_flow_data["vlan"] = vlan |
| single_flow_data["cookie"] = cookie |
| |
| data["paths"].append(single_flow_data) |
| |
| # src to target |
| for i in range(0, len(path)): |
| if i < len(path) - 1: |
| next_hop = path[i + 1] |
| else: |
| # last switch reached |
| next_hop = dst_vnf_name |
| next_node = net.getNodeByName(next_hop) |
| if next_hop == dst_vnf_name: |
| switch_outport_nr = dst_sw_outport_nr |
| logging.info("end node reached: {0}".format(dst_vnf_name)) |
| elif not isinstance(next_node, OVSSwitch): |
| logging.info("Next node: {0} is not a switch".format(next_hop)) |
| return "Next node: {0} is not a switch".format(next_hop) |
| else: |
| # take first link between switches by default |
| index_edge_out = 0 |
| switch_outport_nr = net.DCNetwork_graph[current_hop][next_hop][index_edge_out]['src_port_nr'] |
| |
| cmd = 'priority=1,in_port=%s,cookie=%s' % (switch_inport_nr, cookie) |
| cmd_back = 'priority=1,in_port=%s,cookie=%s' % (switch_outport_nr, cookie) |
| # if a vlan is picked, the connection is routed through multiple switches |
| if vlan is not None: |
| if path.index(current_hop) == 0: # first node |
| # flow #index set up |
| cmd = 'in_port=%s' % src_sw_inport_nr |
| cmd += ',cookie=%s' % cookie |
| cmd += ',table=%s' % cookie |
| cmd += ',ip' |
| cmd += ',reg1=%s' % index |
| cmd += ',actions=' |
| # set vlan id |
| cmd += ',push_vlan:0x8100' |
| masked_vlan = vlan | 0x1000 |
| cmd += ',set_field:%s->vlan_vid' % masked_vlan |
| cmd += ',set_field:%s->eth_dst' % target_mac |
| cmd += ',set_field:%s->ip_dst' % target_ip |
| cmd += ',output:%s' % switch_outport_nr |
| |
| # last switch for reverse route |
| # remove any vlan tags |
| cmd_back += ',dl_vlan=%s' % vlan |
| cmd_back += ',actions=pop_vlan,output:%s' % switch_inport_nr |
| elif next_hop == dst_vnf_name: # last switch |
| # remove any vlan tags |
| cmd += ',dl_vlan=%s' % vlan |
| cmd += ',actions=pop_vlan,output:%s' % switch_outport_nr |
| # set up arp replys at the port so the dst nodes know the src |
| self.setup_arp_reply_at(current_hop, switch_outport_nr, src_ip, src_mac, cookie=cookie) |
| |
| # reverse route |
| cmd_back = 'in_port=%s' % switch_outport_nr |
| cmd_back += ',cookie=%s' % cookie |
| cmd_back += ',ip' |
| cmd_back += ',actions=' |
| cmd_back += 'push_vlan:0x8100' |
| masked_vlan = vlan | 0x1000 |
| cmd_back += ',set_field:%s->vlan_vid' % masked_vlan |
| cmd_back += ',set_field:%s->eth_src' % lb_mac |
| cmd_back += ',set_field:%s->ip_src' % plus_one |
| cmd_back += ',output:%s' % switch_inport_nr |
| else: # middle nodes |
| # if we have a circle in the path we need to specify this, as openflow will ignore the packet |
| # if we just output it on the same port as it came in |
| if switch_inport_nr == switch_outport_nr: |
| cmd += ',dl_vlan=%s,actions=IN_PORT' % (vlan) |
| cmd_back += ',dl_vlan=%s,actions=IN_PORT' % (vlan) |
| else: |
| cmd += ',dl_vlan=%s,actions=output:%s' % (vlan, switch_outport_nr) |
| cmd_back += ',dl_vlan=%s,actions=output:%s' % (vlan, switch_inport_nr) |
| # output the packet at the correct outport |
| else: |
| cmd = 'in_port=%s' % src_sw_inport_nr |
| cmd += ',cookie=%s' % cookie |
| cmd += ',table=%s' % cookie |
| cmd += ',ip' |
| cmd += ',reg1=%s' % index |
| cmd += ',actions=' |
| cmd += ',set_field:%s->eth_dst' % target_mac |
| cmd += ',set_field:%s->ip_dst' % target_ip |
| cmd += ',output:%s' % switch_outport_nr |
| |
| # reverse route |
| cmd_back = 'in_port=%s' % switch_outport_nr |
| cmd_back += ',cookie=%s' % cookie |
| cmd_back += ',ip' |
| cmd_back += ',actions=' |
| cmd_back += ',set_field:%s->eth_src' % lb_mac |
| cmd_back += ',set_field:%s->ip_src' % plus_one |
| cmd_back += ',output:%s' % src_sw_inport_nr |
| |
| self.setup_arp_reply_at(current_hop, switch_outport_nr, src_ip, src_mac, cookie=cookie) |
| |
| # excecute the command on the target switch |
| logging.debug(cmd) |
| cmd = "\"%s\"" % cmd |
| cmd_back = "\"%s\"" % cmd_back |
| net[current_hop].dpctl(main_cmd, cmd) |
| net[current_hop].dpctl(main_cmd, cmd_back) |
| |
| # set next hop for the next iteration step |
| if isinstance(next_node, OVSSwitch): |
| switch_inport_nr = net.DCNetwork_graph[current_hop][next_hop][0]['dst_port_nr'] |
| current_hop = next_hop |
| |
| # advance to next destination |
| index += 1 |
| |
| # set up the actual load balancing rule as a multipath on the very first switch |
| cmd = '"in_port=%s' % src_sw_inport_nr |
| cmd += ',cookie=%s' % (cookie) |
| cmd += ',ip' |
| cmd += ',actions=' |
| # push 0x01 into the first register |
| cmd += 'load:0x1->NXM_NX_REG0[]' |
| # load balance modulo n over all dest interfaces |
| # TODO: in newer openvswitch implementations this should be changed to symmetric_l3l4+udp |
| # to balance any kind of traffic |
| cmd += ',multipath(symmetric_l4,1024,modulo_n,%s,0,NXM_NX_REG1[0..12])' % len(dest_intfs_mapping) |
| # reuse the cookie as table entry as it will be unique |
| cmd += ',resubmit(, %s)"' % cookie |
| |
| # actually add the flow |
| logging.debug("Switch: %s, CMD: %s" % (src_sw, cmd)) |
| net[src_sw].dpctl(main_cmd, cmd) |
| |
| # finally add all flow data to the internal data storage |
| self.full_lb_data[(src_vnf_name, src_vnf_interface)] = data |
| |
| def add_floating_lb(self, datacenter, lb_data): |
| """ |
| This function will set up a loadbalancer at the given datacenter. |
| This function returns the floating ip assigned to the loadbalancer as multiple ones are possible. |
| |
| :param datacenter: The datacenter entrypoint |
| :type datacenter: ``str`` |
| :param lb_data: A dictionary containing the destination data as well as custom path settings |
| :type lb_data: ``dict`` |
| |
| :Example: |
| lbdata = {"dst_vnf_interfaces": {"dc2_man_web0": "port-man-2", |
| "dc3_man_web0": "port-man-4","dc4_man_web0": "port-man-6"}, "path": {"dc2_man_web0": {"port-man-2": [ "dc1.s1",\ |
| "s1", "dc2.s1"]}}} |
| """ |
| net = self.net |
| src_sw_inport_nr = 1 |
| src_sw = self.floating_switch.name |
| dest_intfs_mapping = lb_data.get('dst_vnf_interfaces', dict()) |
| # a custom path can be specified as a list of switches |
| custom_paths = lb_data.get('path', dict()) |
| dest_vnf_outport_nrs = list() |
| |
| if datacenter not in self.net.dcs: |
| raise Exception(u"Source datacenter can not be found.") |
| |
| # get all target interface outport numbers |
| for vnf_name in dest_intfs_mapping: |
| if vnf_name not in net.DCNetwork_graph: |
| raise Exception(u"Target VNF %s is not known." % vnf_name) |
| for connected_sw in net.DCNetwork_graph.neighbors(vnf_name): |
| link_dict = net.DCNetwork_graph[vnf_name][connected_sw] |
| for link in link_dict: |
| if link_dict[link]['src_port_name'] == dest_intfs_mapping[vnf_name]: |
| dest_vnf_outport_nrs.append(int(link_dict[link]['dst_port_nr'])) |
| |
| if len(dest_vnf_outport_nrs) == 0: |
| raise Exception("There are no paths specified for the loadbalancer") |
| src_ip = self.floating_intf.IP() |
| src_mac = self.floating_intf.MAC() |
| |
| # set up paths for each destination vnf individually |
| index = 0 |
| cookie = self.get_cookie() |
| main_cmd = "add-flow -OOpenFlow13" |
| floating_ip = self.floating_network.get_new_ip_address("floating-ip").split("/")[0] |
| |
| for dst_vnf_name, dst_vnf_interface in dest_intfs_mapping.items(): |
| path = None |
| # use custom path if one is supplied |
| # json does not support hashing on tuples so we use nested dicts |
| if custom_paths is not None and dst_vnf_name in custom_paths: |
| if dst_vnf_interface in custom_paths[dst_vnf_name]: |
| path = custom_paths[dst_vnf_name][dst_vnf_interface] |
| logging.debug("Taking custom path to %s: %s" % (dst_vnf_name, path)) |
| else: |
| if datacenter not in self.floating_links: |
| self.floating_links[datacenter] = \ |
| net.addLink(self.floating_switch, datacenter) |
| path = \ |
| self._get_path(self.floating_root.name, dst_vnf_name, self.floating_intf.name, dst_vnf_interface)[0] |
| |
| if isinstance(path, dict): |
| self.delete_flow_by_cookie(cookie) |
| raise Exception(u"Can not find a valid path. Are you specifying the right interfaces?.") |
| |
| intf = net[dst_vnf_name].nameToIntf[dst_vnf_interface] |
| target_mac = str(intf.MAC()) |
| target_ip = str(intf.IP()) |
| dst_sw_outport_nr = dest_vnf_outport_nrs[index] |
| current_hop = src_sw |
| switch_inport_nr = src_sw_inport_nr |
| vlan = net.vlans.pop() |
| |
| # iterate all switches on the path |
| for i in range(0, len(path)): |
| if i < len(path) - 1: |
| next_hop = path[i + 1] |
| else: |
| # last switch reached |
| next_hop = dst_vnf_name |
| next_node = net.getNodeByName(next_hop) |
| |
| # sanity checks |
| if next_hop == dst_vnf_name: |
| switch_outport_nr = dst_sw_outport_nr |
| logging.info("end node reached: {0}".format(dst_vnf_name)) |
| elif not isinstance(next_node, OVSSwitch): |
| logging.info("Next node: {0} is not a switch".format(next_hop)) |
| return "Next node: {0} is not a switch".format(next_hop) |
| else: |
| # take first link between switches by default |
| index_edge_out = 0 |
| switch_outport_nr = net.DCNetwork_graph[current_hop][next_hop][index_edge_out]['src_port_nr'] |
| |
| # default filters, just overwritten on the first node and last node |
| cmd = 'priority=1,in_port=%s,cookie=%s' % (switch_inport_nr, cookie) |
| cmd_back = 'priority=1,in_port=%s,cookie=%s' % (switch_outport_nr, cookie) |
| if i == 0: # first node |
| cmd = 'in_port=%s' % src_sw_inport_nr |
| cmd += ',cookie=%s' % cookie |
| cmd += ',table=%s' % cookie |
| cmd += ',ip' |
| cmd += ',ip_dst=%s' % floating_ip |
| cmd += ',reg1=%s' % index |
| cmd += ',actions=' |
| # set vlan id |
| cmd += ',push_vlan:0x8100' |
| masked_vlan = vlan | 0x1000 |
| cmd += ',set_field:%s->vlan_vid' % masked_vlan |
| cmd += ',set_field:%s->eth_dst' % target_mac |
| cmd += ',set_field:%s->ip_dst' % target_ip |
| cmd += ',output:%s' % switch_outport_nr |
| |
| # last switch for reverse route |
| # remove any vlan tags |
| cmd_back += ',dl_vlan=%s' % vlan |
| cmd_back += ',actions=pop_vlan,output:%s' % switch_inport_nr |
| self.setup_arp_reply_at(current_hop, src_sw_inport_nr, floating_ip, target_mac, cookie=cookie) |
| elif next_hop == dst_vnf_name: # last switch |
| # remove any vlan tags |
| cmd += ',dl_vlan=%s' % vlan |
| cmd += ',actions=pop_vlan,output:%s' % switch_outport_nr |
| # set up arp replys at the port so the dst nodes know the src |
| self.setup_arp_reply_at(current_hop, switch_outport_nr, src_ip, src_mac, cookie=cookie) |
| |
| # reverse route |
| cmd_back = 'in_port=%s' % switch_outport_nr |
| cmd_back += ',cookie=%s' % cookie |
| cmd_back += ',ip' |
| cmd_back += ',actions=' |
| cmd_back += 'push_vlan:0x8100' |
| masked_vlan = vlan | 0x1000 |
| cmd_back += ',set_field:%s->vlan_vid' % masked_vlan |
| cmd_back += ',set_field:%s->eth_src' % src_mac |
| cmd_back += ',set_field:%s->ip_src' % floating_ip |
| cmd_back += ',output:%s' % switch_inport_nr |
| net.getNodeByName(dst_vnf_name).setHostRoute(src_ip, dst_vnf_interface) |
| else: # middle node |
| # if we have a circle in the path we need to specify this, as openflow will ignore the packet |
| # if we just output it on the same port as it came in |
| if switch_inport_nr == switch_outport_nr: |
| cmd += ',dl_vlan=%s,actions=IN_PORT' % (vlan) |
| cmd_back += ',dl_vlan=%s,actions=IN_PORT' % (vlan) |
| else: |
| cmd += ',dl_vlan=%s,actions=output:%s' % (vlan, switch_outport_nr) |
| cmd_back += ',dl_vlan=%s,actions=output:%s' % (vlan, switch_inport_nr) |
| |
| # excecute the command on the target switch |
| logging.debug(cmd) |
| cmd = "\"%s\"" % cmd |
| cmd_back = "\"%s\"" % cmd_back |
| net[current_hop].dpctl(main_cmd, cmd) |
| net[current_hop].dpctl(main_cmd, cmd_back) |
| |
| # set next hop for the next iteration step |
| if isinstance(next_node, OVSSwitch): |
| switch_inport_nr = net.DCNetwork_graph[current_hop][next_hop][0]['dst_port_nr'] |
| current_hop = next_hop |
| |
| # advance to next destination |
| index += 1 |
| |
| # set up the actual load balancing rule as a multipath on the very first switch |
| cmd = '"in_port=%s' % src_sw_inport_nr |
| cmd += ',cookie=%s' % (cookie) |
| cmd += ',ip' |
| cmd += ',actions=' |
| # push 0x01 into the first register |
| cmd += 'load:0x1->NXM_NX_REG0[]' |
| # load balance modulo n over all dest interfaces |
| # TODO: in newer openvswitch implementations this should be changed to symmetric_l3l4+udp |
| # to balance any kind of traffic |
| cmd += ',multipath(symmetric_l4,1024,modulo_n,%s,0,NXM_NX_REG1[0..12])' % len(dest_intfs_mapping) |
| # reuse the cookie as table entry as it will be unique |
| cmd += ',resubmit(, %s)"' % cookie |
| |
| # actually add the flow |
| logging.debug("Switch: %s, CMD: %s" % (src_sw, cmd)) |
| net[src_sw].dpctl(main_cmd, cmd) |
| |
| self.floating_cookies[cookie] = floating_ip |
| |
| return cookie, floating_ip |
| |
| def setup_arp_reply_at(self, switch, port_nr, target_ip, target_mac, cookie=None): |
| """ |
| Sets up a custom ARP reply at a switch. |
| An ARP request coming in on the `port_nr` for `target_ip` will be answered with target IP/MAC. |
| |
| :param switch: The switch belonging to the interface |
| :type switch: ``str`` |
| :param port_nr: The port number at the switch that is connected to the interface |
| :type port_nr: ``int`` |
| :param target_ip: The IP for which to set up the ARP reply |
| :type target_ip: ``str`` |
| :param target_mac: The MAC address of the target interface |
| :type target_mac: ``str`` |
| :param cookie: cookie to identify the ARP request, if None a new one will be picked |
| :type cookie: ``int`` or ``None`` |
| :return: cookie |
| :rtype: ``int`` |
| """ |
| if cookie is None: |
| cookie = self.get_cookie() |
| main_cmd = "add-flow -OOpenFlow13" |
| |
| # first set up ARP requests for the source node, so it will always 'find' a partner |
| cmd = '"in_port=%s' % port_nr |
| cmd += ',cookie=%s' % cookie |
| cmd += ',arp' |
| # only answer for target ip arp requests |
| cmd += ',arp_tpa=%s' % target_ip |
| cmd += ',actions=' |
| # set message type to ARP reply |
| cmd += 'load:0x2->NXM_OF_ARP_OP[]' |
| # set src ip as dst ip |
| cmd += ',move:NXM_OF_ETH_SRC[]->NXM_OF_ETH_DST[]' |
| # set src mac |
| cmd += ',set_field:%s->eth_src' % target_mac |
| # set src as target |
| cmd += ',move:NXM_NX_ARP_SHA[]->NXM_NX_ARP_THA[], move:NXM_OF_ARP_SPA[]->NXM_OF_ARP_TPA[]' |
| # set target mac as hex |
| cmd += ',load:0x%s->NXM_NX_ARP_SHA[]' % "".join(target_mac.split(':')) |
| # set target ip as hex |
| octets = target_ip.split('.') |
| dst_ip_hex = '{:02X}{:02X}{:02X}{:02X}'.format(*map(int, octets)) |
| cmd += ',load:0x%s->NXM_OF_ARP_SPA[]' % dst_ip_hex |
| # output to incoming port remember the closing " |
| cmd += ',IN_PORT"' |
| self.net[switch].dpctl(main_cmd, cmd) |
| logging.debug( |
| "Set up ARP reply at %s port %s." % (switch, port_nr)) |
| |
| def delete_flow_by_cookie(self, cookie): |
| """ |
| Removes a flow identified by the cookie |
| |
| :param cookie: The cookie for the specified flow |
| :type cookie: ``int`` |
| :return: True if successful, else false |
| :rtype: ``bool`` |
| """ |
| if not cookie: |
| return False |
| logging.debug("Deleting flow by cookie %d" % (cookie)) |
| flows = list() |
| # we have to call delete-group for each switch |
| for node in self.net.switches: |
| flow = dict() |
| flow["dpid"] = int(node.dpid, 16) |
| flow["cookie"] = cookie |
| flow['cookie_mask'] = int('0xffffffffffffffff', 16) |
| |
| flows.append(flow) |
| for flow in flows: |
| logging.debug("Deleting flowentry with cookie %d" % ( |
| flow["cookie"])) |
| if self.net.controller == RemoteController: |
| self.net.ryu_REST('stats/flowentry/delete', data=flow) |
| |
| self.cookies.remove(cookie) |
| return True |
| |
| def delete_chain_by_intf(self, src_vnf_name, src_vnf_intf, dst_vnf_name, dst_vnf_intf): |
| """ |
| Removes a flow identified by the vnf_name/vnf_intf pairs |
| |
| :param src_vnf_name: The vnf name for the specified flow |
| :type src_vnf_name: ``str`` |
| :param src_vnf_intf: The interface name for the specified flow |
| :type src_vnf_intf: ``str`` |
| :param dst_vnf_name: The vnf name for the specified flow |
| :type dst_vnf_name: ``str`` |
| :param dst_vnf_intf: The interface name for the specified flow |
| :type dst_vnf_intf: ``str`` |
| :return: True if successful, else false |
| :rtype: ``bool`` |
| """ |
| logging.debug("Deleting flow for vnf/intf pair %s %s" % (src_vnf_name, src_vnf_intf)) |
| if not self.check_vnf_intf_pair(src_vnf_name, src_vnf_intf): |
| return False |
| if not self.check_vnf_intf_pair(dst_vnf_name, dst_vnf_intf): |
| return False |
| target_flow = (src_vnf_name, src_vnf_intf, dst_vnf_name, dst_vnf_intf) |
| if not target_flow in self.chain_flow_cookies: |
| return False |
| |
| success = self.delete_flow_by_cookie(self.chain_flow_cookies[target_flow]) |
| |
| if success: |
| del self.chain_flow_cookies[target_flow] |
| del self.full_chain_data[target_flow] |
| return True |
| return False |
| |
| def delete_loadbalancer(self, vnf_src_name, vnf_src_interface): |
| ''' |
| Removes a loadbalancer that is configured for the node and interface |
| |
| :param src_vnf_name: Name of the source VNF |
| :param src_vnf_interface: Name of the destination VNF |
| ''' |
| flows = list() |
| # we have to call delete-group for each switch |
| delete_group = list() |
| group_id = self.get_flow_group(vnf_src_name, vnf_src_interface) |
| for node in self.net.switches: |
| for cookie in self.lb_flow_cookies[(vnf_src_name, vnf_src_interface)]: |
| flow = dict() |
| flow["dpid"] = int(node.dpid, 16) |
| flow["cookie"] = cookie |
| flow['cookie_mask'] = int('0xffffffffffffffff', 16) |
| |
| flows.append(flow) |
| group_del = dict() |
| group_del["dpid"] = int(node.dpid, 16) |
| group_del["group_id"] = group_id |
| delete_group.append(group_del) |
| |
| for flow in flows: |
| logging.debug("Deleting flowentry with cookie %d belonging to lb at %s:%s" % ( |
| flow["cookie"], vnf_src_name, vnf_src_interface)) |
| if self.net.controller == RemoteController: |
| self.net.ryu_REST('stats/flowentry/delete', data=flow) |
| |
| logging.debug("Deleting group with id %s" % group_id) |
| for switch_del_group in delete_group: |
| if self.net.controller == RemoteController: |
| self.net.ryu_REST("stats/groupentry/delete", data=switch_del_group) |
| |
| # unmap groupid from the interface |
| target_pair = (vnf_src_name, vnf_src_interface) |
| if target_pair in self.flow_groups: |
| del self.flow_groups[target_pair] |
| if target_pair in self.full_lb_data: |
| del self.full_lb_data[target_pair] |
| |
| def delete_floating_lb(self, cookie): |
| """ |
| Delete a floating loadbalancer. |
| Floating loadbalancers are different from normal ones as there are multiple ones on the same interface. |
| :param cookie: The cookie of the loadbalancer |
| :type cookie: ``int`` |
| """ |
| cookie = int(cookie) |
| if cookie not in self.floating_cookies: |
| raise Exception("Can not delete floating loadbalancer as the flowcookie is not known") |
| |
| self.delete_flow_by_cookie(cookie) |
| floating_ip = self.floating_cookies[cookie] |
| self.floating_network.withdraw_ip_address(floating_ip) |
| |
| def set_arp_entry(self, vnf_name, vnf_interface, ip, mac): |
| """ |
| Sets an arp entry on the specified VNF. This is done on the node directly and not by open vswitch! |
| :param vnf_name: Name of the VNF |
| :type vnf_name: ``str`` |
| :param vnf_interface: Name of the interface |
| :type vnf_interface: ``str`` |
| :param ip: IP to reply to |
| :type ip: ``str`` |
| :param mac: Answer with this MAC |
| :type mac: ``str`` |
| """ |
| node = self.net.getNodeByName(vnf_name) |
| node.cmd("arp -i %s -s %s %s" % (vnf_interface, ip, mac)) |