From: stevenvanrossem Date: Thu, 4 Aug 2016 11:30:38 +0000 (+0200) Subject: Merge remote-tracking branch 'upstream/master' X-Git-Tag: v3.1~90 X-Git-Url: https://osm.etsi.org/gitweb/?a=commitdiff_plain;h=3989dcc6e2c8564fc2d8c9057d669bb56e4298b3;hp=404f26f2896b92af8ddb1bec28edcc6813819e4d;p=osm%2Fvim-emu.git Merge remote-tracking branch 'upstream/master' --- diff --git a/README.md b/README.md index a6b1de5..31ba421 100755 --- a/README.md +++ b/README.md @@ -140,8 +140,9 @@ This example shows how to deploy a SONATA example package in the emulator using Note: The [son-push](https://github.com/mpeuster/son-cli) tool can be used instead of CURL. -### CLI Commands +### Further Documentation * [Full CLI command documentation](https://github.com/sonata-nfv/son-emu/wiki/CLI-Command-Overview) +* [Requirements for Docker containers executed by the emulator](https://github.com/sonata-nfv/son-emu/wiki/Container-Requirements) ## License diff --git a/src/emuvim/api/sonata/dummygatekeeper.py b/src/emuvim/api/sonata/dummygatekeeper.py index 20150d0..432c134 100755 --- a/src/emuvim/api/sonata/dummygatekeeper.py +++ b/src/emuvim/api/sonata/dummygatekeeper.py @@ -105,7 +105,10 @@ class Service(object): self.local_docker_files = dict() self.remote_docker_image_urls = dict() self.instances = dict() - self.vnfname2num = dict() + self.vnf_name2docker_name = dict() + # lets generate a set of subnet configurations used for e-line chaining setup + self.eline_subnets_src = generate_subnet_strings(50, start=200, subnet_size=24, ip=1) + self.eline_subnets_dst = generate_subnet_strings(50, start=200, subnet_size=24, ip=2) def onboard(self): """ @@ -154,31 +157,46 @@ class Service(object): self.instances[instance_uuid]["vnf_instances"].append(vnfi) # 3. Configure the chaining of the network functions (currently only E-Line links supported) - nfid2name = defaultdict(lambda :"NotExistingNode", - reduce(lambda x,y: dict(x, **y), - map(lambda d:{d["vnf_id"]:d["vnf_name"]}, + vnf_id2vnf_name = defaultdict(lambda: "NotExistingNode", + reduce(lambda x, y: dict(x, **y), + map(lambda d: {d["vnf_id"]: d["vnf_name"]}, self.nsd["network_functions"]))) vlinks = self.nsd["virtual_links"] fwd_links = self.nsd["forwarding_graphs"][0]["constituent_virtual_links"] eline_fwd_links = [l for l in vlinks if (l["id"] in fwd_links) and (l["connectivity_type"] == "E-Line")] - cookie = 1 # not clear why this is needed - to check with Steven + cookie = 1 # not clear why this is needed - to check with Steven for link in eline_fwd_links: - src_node, src_port = link["connection_points_reference"][0].split(":") - dst_node, dst_port = link["connection_points_reference"][1].split(":") - - srcname = nfid2name[src_node] - dstname = nfid2name[dst_node] - LOG.debug("src name: "+srcname+" dst name: "+dstname) - - if (srcname in self.vnfds) and (dstname in self.vnfds) : - network = self.vnfds[srcname].get("dc").net # there should be a cleaner way to find the DCNetwork - src_vnf = self.vnfname2num[srcname] - dst_vnf = self.vnfname2num[dstname] - ret = network.setChain(src_vnf, dst_vnf, vnf_src_interface=src_port, vnf_dst_interface=dst_port, bidirectional = True, cmd="add-flow", cookie = cookie) + src_id, src_if_name = link["connection_points_reference"][0].split(":") + dst_id, dst_if_name = link["connection_points_reference"][1].split(":") + + src_name = vnf_id2vnf_name[src_id] + dst_name = vnf_id2vnf_name[dst_id] + + LOG.debug( + "Setting up E-Line link. %s(%s:%s) -> %s(%s:%s)" % ( + src_name, src_id, src_if_name, dst_name, dst_id, dst_if_name)) + + if (src_name in self.vnfds) and (dst_name in self.vnfds): + network = self.vnfds[src_name].get("dc").net # there should be a cleaner way to find the DCNetwork + src_docker_name = self.vnf_name2docker_name[src_name] + dst_docker_name = self.vnf_name2docker_name[dst_name] + LOG.debug(src_docker_name) + ret = network.setChain( + src_docker_name, dst_docker_name, + vnf_src_interface=src_if_name, vnf_dst_interface=dst_if_name, + bidirectional=True, cmd="add-flow", cookie=cookie) cookie += 1 + # re-configure the VNFs IP assignment and ensure that a new subnet is used for each E-Link + src_vnfi = self._get_vnf_instance(instance_uuid, src_name) + if src_vnfi is not None: + self._vnf_reconfigure_network(src_vnfi, src_if_name, self.eline_subnets_src.pop(0)) + dst_vnfi = self._get_vnf_instance(instance_uuid, dst_name) + if dst_vnfi is not None: + self._vnf_reconfigure_network(dst_vnfi, dst_if_name, self.eline_subnets_dst.pop(0)) + # 4. run the emulator specific entrypoint scripts in the VNFIs of this service instance self._trigger_emulator_start_scripts_in_vnfis(self.instances[instance_uuid]["vnf_instances"]) @@ -207,11 +225,46 @@ class Service(object): # 3. do the dc.startCompute(name="foobar") call to run the container # TODO consider flavors, and other annotations intfs = vnfd.get("connection_points") - self.vnfname2num[vnf_name] = GK.get_next_vnf_name() - LOG.info("VNF "+vnf_name+" mapped to "+self.vnfname2num[vnf_name]+" on dc "+str(vnfd.get("dc"))) - vnfi = target_dc.startCompute(self.vnfname2num[vnf_name], network=intfs, image=docker_name, flavor_name="small") + self.vnf_name2docker_name[vnf_name] = GK.get_next_vnf_name() + LOG.info("Starting %r as %r in DC %r" % (vnf_name, self.vnf_name2docker_name[vnf_name], vnfd.get("dc"))) + LOG.debug("Interfaces for %r: %r" % (vnf_name, intfs)) + vnfi = target_dc.startCompute(self.vnf_name2docker_name[vnf_name], network=intfs, image=docker_name, flavor_name="small") return vnfi + def _get_vnf_instance(self, instance_uuid, name): + """ + Returns the Docker object for the given VNF name (or Docker name). + :param instance_uuid: UUID of the service instance to search in. + :param name: VNF name or Docker name. We are fuzzy here. + :return: + """ + dn = name + if name in self.vnf_name2docker_name: + dn = self.vnf_name2docker_name[name] + for vnfi in self.instances[instance_uuid]["vnf_instances"]: + if vnfi.name == dn: + return vnfi + LOG.warning("No container with name: %r found.") + return None + + @staticmethod + def _vnf_reconfigure_network(vnfi, if_name, net_str): + """ + Reconfigure the network configuration of a specific interface + of a running container. + :param vnfi: container instacne + :param if_name: interface name + :param net_str: network configuration string, e.g., 1.2.3.4/24 + :return: + """ + intf = vnfi.intf(intf=if_name) + if intf is not None: + intf.setIP(net_str) + LOG.debug("Reconfigured network of %s:%s to %r" % (vnfi.name, if_name, net_str)) + else: + LOG.warning("Interface not found: %s:%s. Network reconfiguration skipped." % (vnfi.name, if_name)) + + def _trigger_emulator_start_scripts_in_vnfis(self, vnfi_list): for vnfi in vnfi_list: config = vnfi.dcinfo.get("Config", dict()) @@ -409,6 +462,7 @@ class Packages(fr.Resource): Return a list of UUID's of uploaded service packages. :return: dict/list """ + LOG.info("GET /packages") return {"service_uuid_list": list(GK.services.iterkeys())} @@ -440,7 +494,8 @@ class Instantiations(fr.Resource): Returns a list of UUIDs containing all running services. :return: dict / list """ - return {"service_instance_list": [ + LOG.info("GET /instantiations") + return {"service_instantiations_list": [ list(s.instances.iterkeys()) for s in GK.services.itervalues()]} @@ -488,6 +543,16 @@ def make_relative_path(path): return path +def generate_subnet_strings(n, start=1, subnet_size=24, ip=0): + """ + Helper to generate different network configuration strings. + """ + r = list() + for i in range(start, start + n): + r.append("%d.0.0.%d/%d" % (i, ip, subnet_size)) + return r + + if __name__ == '__main__': """ Lets allow to run the API in standalone mode. diff --git a/src/emuvim/cli/rest/network.py b/src/emuvim/cli/rest/network.py index 86ed92b..4f6a32d 100755 --- a/src/emuvim/cli/rest/network.py +++ b/src/emuvim/cli/rest/network.py @@ -117,7 +117,7 @@ parser.add_argument( "--match", "-m", dest="match", help="string holding extra matches for the flow entries") parser.add_argument( - "--bidirectional", "-b", dest="bidirectional", + "--bidirectional", "-b", dest="bidirectional", action='store_true', help="add/remove the flow entries from src to dst and back") parser.add_argument( "--cookie", "-c", dest="cookie", diff --git a/src/emuvim/dcemulator/net.py b/src/emuvim/dcemulator/net.py index 2c5ce14..cbe668f 100755 --- a/src/emuvim/dcemulator/net.py +++ b/src/emuvim/dcemulator/net.py @@ -39,11 +39,15 @@ from mininet.net import Containernet from mininet.node import Controller, DefaultController, OVSSwitch, OVSKernelSwitch, Docker, RemoteController from mininet.cli import CLI from mininet.link import TCLink +from mininet.clean import cleanup import networkx as nx from emuvim.dcemulator.monitoring import DCNetworkMonitor from emuvim.dcemulator.node import Datacenter, EmulatorCompute from emuvim.dcemulator.resourcemodel import ResourceModelRegistrar +LOG = logging.getLogger("dcemulator.net") +LOG.setLevel(logging.DEBUG) + class DCNetwork(Containernet): """ Wraps the original Mininet/Containernet class and provides @@ -63,20 +67,19 @@ class DCNetwork(Containernet): :param kwargs: path through for Mininet parameters :return: """ + # members self.dcs = {} + self.ryu_process = None - # make sure any remaining Ryu processes are killed + # always cleanup environment before we start the emulator self.killRyu() - # make sure no containers are left over from a previous emulator run. - self.removeLeftoverContainers() + cleanup() # call original Docker.__init__ and setup default controller Containernet.__init__( self, switch=OVSKernelSwitch, controller=controller, **kwargs) - # Ryu management - self.ryu_process = None if controller == RemoteController: # start Ryu controller self.startRyu(learning_switch=enable_learning) @@ -115,7 +118,7 @@ class DCNetwork(Containernet): dc.net = self # set reference to network self.dcs[label] = dc dc.create() # finally create the data center in our Mininet instance - logging.info("added data center: %s" % label) + LOG.info("added data center: %s" % label) return dc def addLink(self, node1, node2, **params): @@ -125,7 +128,7 @@ class DCNetwork(Containernet): """ assert node1 is not None assert node2 is not None - logging.debug("addLink: n1=%s n2=%s" % (str(node1), str(node2))) + LOG.debug("addLink: n1=%s n2=%s" % (str(node1), str(node2))) # ensure type of node1 if isinstance( node1, basestring ): if node1 in self.dcs: @@ -252,7 +255,7 @@ class DCNetwork(Containernet): Containernet.stop(self) # stop Ryu controller - self.stopRyu() + self.killRyu() def CLI(self): @@ -279,6 +282,14 @@ class DCNetwork(Containernet): def _chainAddFlow(self, vnf_src_name, vnf_dst_name, vnf_src_interface=None, vnf_dst_interface=None, **kwargs): + src_sw = None + dst_sw = None + src_sw_inport_nr = 0 + dst_sw_outport_nr = 0 + + LOG.debug("call chainAddFlow vnf_src_name=%r, vnf_src_interface=%r, vnf_dst_name=%r, vnf_dst_interface=%r", + vnf_src_name, vnf_src_interface, vnf_dst_name, vnf_dst_interface) + #check if port is specified (vnf:port) if vnf_src_interface is None: # take first interface by default @@ -289,10 +300,10 @@ class DCNetwork(Containernet): for connected_sw in self.DCNetwork_graph.neighbors(vnf_src_name): link_dict = self.DCNetwork_graph[vnf_src_name][connected_sw] for link in link_dict: - if link_dict[link]['src_port_id'] == vnf_src_interface: + if (link_dict[link]['src_port_id'] == vnf_src_interface or + link_dict[link]['src_port_name'] == vnf_src_interface): # Fix: we might also get interface names, e.g, from a son-emu-cli call # found the right link and connected switch src_sw = connected_sw - src_sw_inport_nr = link_dict[link]['dst_port_nr'] break @@ -306,7 +317,8 @@ class DCNetwork(Containernet): for connected_sw in self.DCNetwork_graph.neighbors(vnf_dst_name): link_dict = self.DCNetwork_graph[connected_sw][vnf_dst_name] for link in link_dict: - if link_dict[link]['dst_port_id'] == vnf_dst_interface: + if link_dict[link]['dst_port_id'] == vnf_dst_interface or \ + link_dict[link]['dst_port_name'] == vnf_dst_interface: # Fix: we might also get interface names, e.g, from a son-emu-cli call # found the right link and connected switch dst_sw = connected_sw dst_sw_outport_nr = link_dict[link]['src_port_nr'] @@ -319,10 +331,15 @@ class DCNetwork(Containernet): # if all shortest paths are wanted, use: all_shortest_paths path = nx.shortest_path(self.DCNetwork_graph, src_sw, dst_sw, weight=kwargs.get('weight')) except: - logging.info("No path could be found between {0} and {1}".format(vnf_src_name, vnf_dst_name)) + LOG.exception("No path could be found between {0} and {1} using src_sw={2} and dst_sw={3}".format( + vnf_src_name, vnf_dst_name, src_sw, dst_sw)) + LOG.debug("Graph nodes: %r" % self.DCNetwork_graph.nodes()) + LOG.debug("Graph edges: %r" % self.DCNetwork_graph.edges()) + for e, v in self.DCNetwork_graph.edges(): + LOG.debug("%r" % self.DCNetwork_graph[e][v]) return "No path could be found between {0} and {1}".format(vnf_src_name, vnf_dst_name) - logging.info("Path between {0} and {1}: {2}".format(vnf_src_name, vnf_dst_name, path)) + LOG.info("Path between {0} and {1}: {2}".format(vnf_src_name, vnf_dst_name, path)) current_hop = src_sw switch_inport_nr = src_sw_inport_nr @@ -347,9 +364,9 @@ class DCNetwork(Containernet): if next_hop == vnf_dst_name: switch_outport_nr = dst_sw_outport_nr - logging.info("end node reached: {0}".format(vnf_dst_name)) + LOG.info("end node reached: {0}".format(vnf_dst_name)) elif not isinstance( next_node, OVSSwitch ): - logging.info("Next node: {0} is not a switch".format(next_hop)) + LOG.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 @@ -479,7 +496,7 @@ class DCNetwork(Containernet): ofcmd = '' node.dpctl(cmd, ofcmd) - logging.info("{3} in switch: {0} in_port: {1} out_port: {2}".format(node.name, switch_inport_nr, + LOG.info("{3} in switch: {0} in_port: {1} out_port: {2}".format(node.name, switch_inport_nr, switch_outport_nr, cmd)) # start Ryu Openflow controller as Remote Controller for the DCNetwork @@ -501,19 +518,16 @@ class DCNetwork(Containernet): self.ryu_process = Popen([ryu_cmd, ryu_path2, ryu_option, ryu_of_port], stdout=FNULL, stderr=FNULL) time.sleep(1) - def stopRyu(self): + def killRyu(self): + """ + Stop the Ryu controller that might be started by son-emu. + :return: + """ + # try it nicely if self.ryu_process is not None: self.ryu_process.terminate() self.ryu_process.kill() - self.killRyu() - - @staticmethod - def removeLeftoverContainers(): - # TODO can be more python-based using eg. docker-py? - Popen('docker ps -a -q --filter="name=mn.*" | xargs -r docker rm -f', shell=True) - - @staticmethod - def killRyu(): + # ensure its death ;-) Popen(['pkill', '-f', 'ryu-manager']) def ryu_REST(self, prefix, dpid=None, data=None): @@ -523,7 +537,7 @@ class DCNetwork(Containernet): else: url = self.ryu_REST_api + '/' + str(prefix) if data: - #logging.info('POST: {0}'.format(str(data))) + #LOG.info('POST: {0}'.format(str(data))) req = urllib2.Request(url, str(data)) else: req = urllib2.Request(url) @@ -531,8 +545,8 @@ class DCNetwork(Containernet): ret = urllib2.urlopen(req).read() return ret except: - logging.info('error url: {0}'.format(str(url))) - if data: logging.info('error POST: {0}'.format(str(data))) + LOG.info('error url: {0}'.format(str(url))) + if data: LOG.info('error POST: {0}'.format(str(data))) # need to respect that some match fields must be integers # http://ryu.readthedocs.io/en/latest/app/ofctl_rest.html#description-of-match-and-actions diff --git a/src/emuvim/dcemulator/node.py b/src/emuvim/dcemulator/node.py index b29d634..b88913b 100755 --- a/src/emuvim/dcemulator/node.py +++ b/src/emuvim/dcemulator/node.py @@ -32,7 +32,7 @@ import logging import time import json -LOG = logging.getLogger("dcemulator") +LOG = logging.getLogger("dcemulator.node") LOG.setLevel(logging.DEBUG) @@ -191,6 +191,9 @@ class Datacenter(object): # if no --net option is given, network = [{}], so 1 empty dict in the list # this results in 1 default interface with a default ip address for nw in network: + # clean up network configuration (e.g. RTNETLINK does not allow ':' in intf names + if nw.get("id") is not None: + nw["id"] = self._clean_ifname(nw["id"]) # TODO we cannot use TCLink here (see: https://github.com/mpeuster/containernet/issues/3) self.net.addLink(d, self.switch, params1=nw, cls=Link, intfName1=nw.get('id')) # do bookkeeping @@ -256,3 +259,19 @@ class Datacenter(object): self.net.rm_registrar.register(self, rm) LOG.info("Assigned RM: %r to DC: %r" % (rm, self)) + @staticmethod + def _clean_ifname(name): + """ + Cleans up given string to be a + RTNETLINK compatible interface name. + :param name: string + :return: string + """ + if name is None: + return "if0" + name = name.replace(":", "-") + name = name.replace(" ", "-") + name = name.replace(".", "-") + name = name.replace("_", "-") + return name + diff --git a/src/emuvim/examples/sonata_y1_demo_topology_1.py b/src/emuvim/examples/sonata_y1_demo_topology_1.py index c5c9611..1792f88 100755 --- a/src/emuvim/examples/sonata_y1_demo_topology_1.py +++ b/src/emuvim/examples/sonata_y1_demo_topology_1.py @@ -43,7 +43,7 @@ logging.basicConfig(level=logging.INFO) def create_topology1(): # create topology - net = DCNetwork(controller=RemoteController, monitor=False, enable_learning = False) + net = DCNetwork(controller=RemoteController, monitor=False, enable_learning=False) dc1 = net.addDatacenter("dc1") dc2 = net.addDatacenter("dc2") s1 = net.addSwitch("s1") @@ -52,6 +52,7 @@ def create_topology1(): # add the command line interface endpoint to each DC (REST API) rapi1 = RestApiEndpoint("0.0.0.0", 5001) + rapi1.connectDCNetwork(net) rapi1.connectDatacenter(dc1) rapi1.connectDatacenter(dc2) # run API endpoint server (in another thread, don't block)