X-Git-Url: https://osm.etsi.org/gitweb/?a=blobdiff_plain;f=src%2Femuvim%2Fapi%2Ftango%2Fllcm.py;h=0a624858ef9c784ddce70a090bdc39553f6e728f;hb=8246f98e0c9e694a06661d3f9d67cfe8de1dfff3;hp=eaa0b5ee48c2d26bd93d2499b6484682ddff325c;hpb=f8f135c271341479200232641823520a541f33b0;p=osm%2Fvim-emu.git diff --git a/src/emuvim/api/tango/llcm.py b/src/emuvim/api/tango/llcm.py index eaa0b5e..0a62485 100755 --- a/src/emuvim/api/tango/llcm.py +++ b/src/emuvim/api/tango/llcm.py @@ -1,3 +1,4 @@ + # Copyright (c) 2018 SONATA-NFV, 5GTANGO and Paderborn University # ALL RIGHTS RESERVED. # @@ -36,6 +37,7 @@ import hashlib import zipfile import yaml import threading +import datetime from docker import DockerClient from flask import Flask, request import flask_restful as fr @@ -86,6 +88,11 @@ ELINE_SUBNETS = None # Time in seconds to wait for vnf stop scripts to execute fully VNF_STOP_WAIT_TIME = 5 +# If services are instantiated multiple times, the public port +# mappings need to be adapted to avoid colisions. We use this +# offset for this: NEW_PORT (SSIID * OFFSET) + ORIGINAL_PORT +MULTI_INSTANCE_PORT_OFFSET = 1000 + class OnBoardingException(BaseException): pass @@ -135,6 +142,8 @@ class Service(object): self.local_docker_files = dict() self.remote_docker_image_urls = dict() self.instances = dict() + self._instance_counter = 0 + self.created_at = str(datetime.datetime.now()) def onboard(self): """ @@ -158,7 +167,14 @@ class Service(object): else: self._load_docker_urls() self._pull_predefined_dockerimages() - LOG.info("On-boarded service: %r" % self.manifest.get("name")) + # 4. reserve subnets + eline_fwd_links, elan_fwd_links = self._get_elines_and_elans() + self.eline_subnets = [ELINE_SUBNETS.pop(0) for _ in eline_fwd_links] + self.elan_subnets = [ELAN_SUBNETS.pop(0) for _ in elan_fwd_links] + LOG.debug("Reserved subnets for service '{}': E-Line: {} / E-LAN: {}" + .format(self.manifest.get("name"), + self.eline_subnets, self.elan_subnets)) + LOG.info("On-boarded service: {}".format(self.manifest.get("name"))) def start_service(self): """ @@ -168,13 +184,21 @@ class Service(object): by the placement algorithm. :return: """ - LOG.info("Starting service %r" % self.uuid) + LOG.info("Starting service {} ({})" + .format(get_triple_id(self.nsd), self.uuid)) # 1. each service instance gets a new uuid to identify it instance_uuid = str(uuid.uuid4()) # build a instances dict (a bit like a NSR :)) self.instances[instance_uuid] = dict() + self.instances[instance_uuid]["uuid"] = self.uuid + # SSIID = short service instance ID (to postfix Container names) + self.instances[instance_uuid]["ssiid"] = self._instance_counter + self.instances[instance_uuid]["name"] = get_triple_id(self.nsd) self.instances[instance_uuid]["vnf_instances"] = list() + self.instances[instance_uuid]["created_at"] = str(datetime.datetime.now()) + # increase for next instance + self._instance_counter += 1 # 2. compute placement of this service instance (adds DC names to # VNFDs) @@ -184,35 +208,32 @@ class Service(object): for vnf_id in self.vnfds: vnfd = self.vnfds[vnf_id] # attention: returns a list of started deployment units - vnfis = self._start_vnfd(vnfd, vnf_id) + vnfis = self._start_vnfd( + vnfd, vnf_id, self.instances[instance_uuid]["ssiid"]) # add list of VNFIs to total VNFI list self.instances[instance_uuid]["vnf_instances"].extend(vnfis) # 4. Deploy E-Line, E-Tree and E-LAN links # Attention: Only done if ""forwarding_graphs" section in NSD exists, # even if "forwarding_graphs" are not used directly. - if "virtual_links" in self.nsd and "forwarding_graphs" in self.nsd: - vlinks = self.nsd["virtual_links"] - # constituent virtual links are not checked - eline_fwd_links = [l for l in vlinks if ( - l["connectivity_type"] == "E-Line")] - elan_fwd_links = [l for l in vlinks if ( - l["connectivity_type"] == "E-LAN" or - l["connectivity_type"] == "E-Tree")] # Treat E-Tree as E-LAN - - # 5a. deploy E-Line links - GK.net.deployed_elines.extend(eline_fwd_links) # bookkeeping - self._connect_elines(eline_fwd_links, instance_uuid) - # 5b. deploy E-Tree/E-LAN links - GK.net.deployed_elans.extend(elan_fwd_links) # bookkeeping - self._connect_elans(elan_fwd_links, instance_uuid) + # Attention2: Do a copy of *_subnets with list() is important here! + eline_fwd_links, elan_fwd_links = self._get_elines_and_elans() + # 5a. deploy E-Line links + GK.net.deployed_elines.extend(eline_fwd_links) # bookkeeping + self._connect_elines(eline_fwd_links, instance_uuid, list(self.eline_subnets)) + # 5b. deploy E-Tree/E-LAN links + GK.net.deployed_elans.extend(elan_fwd_links) # bookkeeping + self._connect_elans(elan_fwd_links, instance_uuid, list(self.elan_subnets)) # 6. 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"]) # done - LOG.info("Service started. Instance id: %r" % instance_uuid) + LOG.info("Service '{}' started. Instance id: {} SSIID: {}" + .format(self.instances[instance_uuid]["name"], + instance_uuid, + self.instances[instance_uuid]["ssiid"])) return instance_uuid def stop_service(self, instance_uuid): @@ -236,6 +257,24 @@ class Service(object): # last step: remove the instance from the list of all instances del self.instances[instance_uuid] + def _get_elines_and_elans(self): + """ + Get the E-Line, E-LAN, E-Tree links from the NSD. + """ + # Attention: Only done if ""forwarding_graphs" section in NSD exists, + # even if "forwarding_graphs" are not used directly. + eline_fwd_links = list() + elan_fwd_links = list() + if "virtual_links" in self.nsd and "forwarding_graphs" in self.nsd: + vlinks = self.nsd["virtual_links"] + # constituent virtual links are not checked + eline_fwd_links = [l for l in vlinks if ( + l["connectivity_type"] == "E-Line")] + elan_fwd_links = [l for l in vlinks if ( + l["connectivity_type"] == "E-LAN" or + l["connectivity_type"] == "E-Tree")] # Treat E-Tree as E-LAN + return eline_fwd_links, elan_fwd_links + def _get_resource_limits(self, deployment_unit): """ Extract resource limits from deployment units. @@ -270,7 +309,7 @@ class Service(object): mem_limit = mem_limit * 1024 return cpu_list, cpu_period, cpu_quota, mem_limit - def _start_vnfd(self, vnfd, vnf_id, **kwargs): + def _start_vnfd(self, vnfd, vnf_id, ssiid, **kwargs): """ Start a single VNFD of this service :param vnfd: vnfd descriptor dict @@ -287,6 +326,7 @@ class Service(object): for u in deployment_units: # 0. vnf_container_name = vnf_id.vdu_id vnf_container_name = get_container_name(vnf_id, u.get("id")) + vnf_container_instance_name = get_container_name(vnf_id, u.get("id"), ssiid) # 1. get the name of the docker image to star if vnf_container_name not in self.remote_docker_image_urls: raise Exception("No image name for %r found. Abort." % vnf_container_name) @@ -317,18 +357,20 @@ class Service(object): for i in intfs: if i.get("port"): if not isinstance(i.get("port"), int): - LOG.error("Field 'port' is no int CP: {}".format(i)) + LOG.info("Field 'port' is no int CP: {}".format(i)) else: ports.append(i.get("port")) if i.get("publish"): if not isinstance(i.get("publish"), dict): - LOG.error("Field 'publish' is no dict CP: {}".format(i)) + LOG.info("Field 'publish' is no dict CP: {}".format(i)) else: port_bindings.update(i.get("publish")) + # update port mapping for cases where service is deployed > 1 times + port_bindings = update_port_mapping_multi_instance(ssiid, port_bindings) if len(ports) > 0: - LOG.info("{} exposes ports: {}".format(vnf_container_name, ports)) + LOG.info("{} exposes ports: {}".format(vnf_container_instance_name, ports)) if len(port_bindings) > 0: - LOG.info("{} publishes ports: {}".format(vnf_container_name, port_bindings)) + LOG.info("{} publishes ports: {}".format(vnf_container_instance_name, port_bindings)) # 5. collect additional information to start container volumes = list() @@ -347,11 +389,11 @@ class Service(object): # 6. Start the container LOG.info("Starting %r as %r in DC %r" % - (vnf_name, vnf_container_name, vnfd.get("dc"))) + (vnf_name, vnf_container_instance_name, vnfd.get("dc"))) LOG.debug("Interfaces for %r: %r" % (vnf_id, intfs)) # start the container vnfi = target_dc.startCompute( - vnf_container_name, + vnf_container_instance_name, network=intfs, image=docker_image_name, cpu_quota=cpu_quota, @@ -362,11 +404,15 @@ class Service(object): properties=cenv, # environment ports=ports, port_bindings=port_bindings, + # only publish if explicitly stated in descriptor + publish_all_ports=False, type=kwargs.get('type', 'docker')) # add vnfd reference to vnfi vnfi.vnfd = vnfd # add container name vnfi.vnf_container_name = vnf_container_name + vnfi.vnf_container_instance_name = vnf_container_instance_name + vnfi.ssiid = ssiid # store vnfi vnfis.append(vnfi) return vnfis @@ -539,12 +585,13 @@ class Service(object): LOG.debug("Loaded VNFD: {0} id: {1}" .format(v.get("vnf_name"), v.get("vnf_id"))) - def _connect_elines(self, eline_fwd_links, instance_uuid): + def _connect_elines(self, eline_fwd_links, instance_uuid, subnets): """ Connect all E-LINE links in the NSD Attention: This method DOES NOT support multi V/CDU VNFs! :param eline_fwd_links: list of E-LINE links in the NSD :param: instance_uuid of the service + :param: subnets list of subnets to be used :return: """ # cookie is used as identifier for the flowrules installed by the dummygatekeeper @@ -583,7 +630,7 @@ class Service(object): setChaining = True # re-configure the VNFs IP assignment and ensure that a new # subnet is used for each E-Link - eline_net = ELINE_SUBNETS.pop(0) + eline_net = subnets.pop(0) ip1 = "{0}/{1}".format(str(eline_net[1]), eline_net.prefixlen) ip2 = "{0}/{1}".format(str(eline_net[2]), @@ -619,19 +666,20 @@ class Service(object): if cp.get("id") == ifname: return cp - def _connect_elans(self, elan_fwd_links, instance_uuid): + def _connect_elans(self, elan_fwd_links, instance_uuid, subnets): """ Connect all E-LAN/E-Tree links in the NSD This method supports multi-V/CDU VNFs if the connection point names of the DUs are the same as the ones in the NSD. :param elan_fwd_links: list of E-LAN links in the NSD :param: instance_uuid of the service + :param: subnets list of subnets to be used :return: """ for link in elan_fwd_links: # a new E-LAN/E-Tree elan_vnf_list = [] - lan_net = ELAN_SUBNETS.pop(0) + lan_net = subnets.pop(0) lan_hosts = list(lan_net.hosts()) # generate lan ip address for all interfaces (of all involved (V/CDUs)) @@ -908,11 +956,44 @@ class Packages(fr.Resource): def get(self): """ - Return a list of UUID's of uploaded service packages. - :return: dict/list + Return a list of package descriptor headers. + Fakes the behavior of 5GTANGO's GK API to be + compatible with tng-cli. + :return: list """ LOG.info("GET /packages") - return {"service_uuid_list": list(GK.services.iterkeys())} + result = list() + for suuid, sobj in GK.services.iteritems(): + pkg = dict() + pkg["pd"] = dict() + pkg["uuid"] = suuid + pkg["pd"]["name"] = sobj.manifest.get("name") + pkg["pd"]["version"] = sobj.manifest.get("version") + pkg["created_at"] = sobj.created_at + result.append(pkg) + return result, 200 + + +class Services(fr.Resource): + + def get(self): + """ + Return a list of services. + Fakes the behavior of 5GTANGO's GK API to be + compatible with tng-cli. + :return: list + """ + LOG.info("GET /services") + result = list() + for suuid, sobj in GK.services.iteritems(): + service = dict() + service["nsd"] = dict() + service["uuid"] = suuid + service["nsd"]["name"] = sobj.nsd.get("name") + service["nsd"]["version"] = sobj.nsd.get("version") + service["created_at"] = sobj.created_at + result.append(service) + return result, 200 class Instantiations(fr.Resource): @@ -928,12 +1009,14 @@ class Instantiations(fr.Resource): json_data = request.get_json(force=True) service_uuid = json_data.get("service_uuid") service_name = json_data.get("service_name") - + if service_name is None: + # lets be fuzzy + service_name = service_uuid # first try to find by service_name if service_name is not None: for s_uuid, s in GK.services.iteritems(): if s.manifest.get("name") == service_name: - LOG.info("Found service: {} with UUID: {}" + LOG.info("Searched for: {}. Found service w. UUID: {}" .format(service_name, s_uuid)) service_uuid = s_uuid # lets be a bit fuzzy here to make testing easier @@ -946,7 +1029,10 @@ class Instantiations(fr.Resource): # ok, we have a service uuid, lets start the service service_instance_uuid = GK.services.get( service_uuid).start_service() - return {"service_instance_uuid": service_instance_uuid}, 201 + # multiple ID fields to be compatible with tng-bench and tng-cli + return ({"service_instance_uuid": service_instance_uuid, + "id": service_instance_uuid}, 201) + LOG.error("Service not found: {}/{}".format(service_uuid, service_name)) return "Service not found", 404 def get(self): @@ -954,9 +1040,20 @@ class Instantiations(fr.Resource): Returns a list of UUIDs containing all running services. :return: dict / list """ - LOG.info("GET /instantiations") - return {"service_instantiations_list": [ - list(s.instances.iterkeys()) for s in GK.services.itervalues()]} + LOG.info("GET /instantiations or /api/v3/records/services") + # return {"service_instantiations_list": [ + # list(s.instances.iterkeys()) for s in GK.services.itervalues()]} + result = list() + for suuid, sobj in GK.services.iteritems(): + for iuuid, iobj in sobj.instances.iteritems(): + inst = dict() + inst["uuid"] = iobj.get("uuid") + inst["instance_name"] = "{}-inst.{}".format( + iobj.get("name"), iobj.get("ssiid")) + inst["status"] = "running" + inst["created_at"] = iobj.get("created_at") + result.append(inst) + return result, 200 def delete(self): """ @@ -1034,9 +1131,11 @@ app = Flask(__name__) app.config['MAX_CONTENT_LENGTH'] = 512 * 1024 * 1024 # 512 MB max upload api = fr.Api(app) # define endpoints -api.add_resource(Packages, '/packages', '/api/v2/packages') +api.add_resource(Packages, '/packages', '/api/v2/packages', '/api/v3/packages') +api.add_resource(Services, '/services', '/api/v2/services', '/api/v3/services') api.add_resource(Instantiations, '/instantiations', - '/api/v2/instantiations', '/api/v2/requests') + '/api/v2/instantiations', '/api/v2/requests', '/api/v3/requests', + '/api/v3/records/services') api.add_resource(Exit, '/emulator/exit') @@ -1106,10 +1205,32 @@ def parse_interface(interface_name): return vnf_id, vnf_interface -def get_container_name(vnf_id, vdu_id): +def get_container_name(vnf_id, vdu_id, ssiid=None): + if ssiid is not None: + return "{}.{}.{}".format(vnf_id, vdu_id, ssiid) return "{}.{}".format(vnf_id, vdu_id) +def get_triple_id(descr): + return "{}.{}.{}".format( + descr.get("vendor"), descr.get("name"), descr.get("version")) + + +def update_port_mapping_multi_instance(ssiid, port_bindings): + """ + Port_bindings are used to expose ports of the deployed containers. + They would collide if we deploy multiple service instances. + This function adds a offset to them which is based on the + short service instance id (SSIID). + MULTI_INSTANCE_PORT_OFFSET + """ + def _offset(p): + return p + MULTI_INSTANCE_PORT_OFFSET * ssiid + + port_bindings = {k: _offset(v) for k, v in port_bindings.iteritems()} + return port_bindings + + if __name__ == '__main__': """ Lets allow to run the API in standalone mode.