5GTANGO LLCM: New placement algo. integration.
[osm/vim-emu.git] / src / emuvim / api / tango / llcm.py
index f0caf56..1d55d11 100755 (executable)
@@ -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
@@ -92,6 +94,11 @@ VNF_STOP_WAIT_TIME = 5
 MULTI_INSTANCE_PORT_OFFSET = 1000
 
 
+# Selected Placement Algorithm: Points to the class of the selected
+# placement algorithm.
+PLACEMENT_ALGORITHM_OBJ = None
+
+
 class OnBoardingException(BaseException):
     pass
 
@@ -141,6 +148,7 @@ class Service(object):
         self.remote_docker_image_urls = dict()
         self.instances = dict()
         self._instance_counter = 0
+        self.created_at = str(datetime.datetime.now())
 
     def onboard(self):
         """
@@ -193,13 +201,10 @@ class Service(object):
         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)
-        # self._calculate_placement(FirstDcPlacement)
-        self._calculate_placement(RoundRobinDcPlacement)
         # 3. start all vnfds that we have in the service
         for vnf_id in self.vnfds:
             vnfd = self.vnfds[vnf_id]
@@ -328,7 +333,7 @@ class Service(object):
                 raise Exception("No image name for %r found. Abort." % vnf_container_name)
             docker_image_name = self.remote_docker_image_urls.get(vnf_container_name)
             # 2. select datacenter to start the VNF in
-            target_dc = vnfd.get("dc")
+            target_dc = self._place(vnfd, vnf_id, u, ssiid)
             # 3. perform some checks to ensure we can start the container
             assert(docker_image_name is not None)
             assert(target_dc is not None)
@@ -385,7 +390,7 @@ class Service(object):
 
             # 6. Start the container
             LOG.info("Starting %r as %r in DC %r" %
-                     (vnf_name, vnf_container_instance_name, vnfd.get("dc")))
+                     (vnf_name, vnf_container_instance_name, target_dc))
             LOG.debug("Interfaces for %r: %r" % (vnf_id, intfs))
             # start the container
             vnfi = target_dc.startCompute(
@@ -806,21 +811,22 @@ class Service(object):
         """
         return len(DockerClient().images.list(name=image_name)) > 0
 
-    def _calculate_placement(self, algorithm):
+    def _place(self, vnfd, vnfid, vdu, ssiid):
         """
-        Do placement by adding the a field "dc" to
-        each VNFD that points to one of our
-        data center objects known to the gatekeeper.
+        Do placement. Return the name of the DC to place
+        the given VDU.
         """
         assert(len(self.vnfds) > 0)
         assert(len(GK.dcs) > 0)
-        # instantiate algorithm an place
-        p = algorithm()
-        p.place(self.nsd, self.vnfds, GK.dcs)
-        LOG.info("Using placement algorithm: %r" % p.__class__.__name__)
-        # lets print the placement result
-        for name, vnfd in self.vnfds.iteritems():
-            LOG.info("Placed VNF %r on DC %r" % (name, str(vnfd.get("dc"))))
+        if PLACEMENT_ALGORITHM_OBJ is None:
+            LOG.error("No placement algorithm given. Using FirstDcPlacement!")
+            p = FirstDcPlacement()
+        else:
+            p = PLACEMENT_ALGORITHM_OBJ
+        cname = get_container_name(vnfid, vdu.get("id"), ssiid)
+        rdc = p.place(GK.dcs, vnfd, vnfid, vdu, ssiid, cname)
+        LOG.info("Placement: '{}' --> '{}'".format(cname, rdc))
+        return rdc
 
     def _calculate_cpu_cfs_values(self, cpu_time_percentage):
         """
@@ -860,9 +866,8 @@ class FirstDcPlacement(object):
     Placement: Always use one and the same data center from the GK.dcs dict.
     """
 
-    def place(self, nsd, vnfds, dcs):
-        for id, vnfd in vnfds.iteritems():
-            vnfd["dc"] = list(dcs.itervalues())[0]
+    def place(self, dcs, vnfd, vnfid, vdu, ssiid, cname):
+        return list(dcs.itervalues())[0]
 
 
 class RoundRobinDcPlacement(object):
@@ -870,12 +875,50 @@ class RoundRobinDcPlacement(object):
     Placement: Distribute VNFs across all available DCs in a round robin fashion.
     """
 
-    def place(self, nsd, vnfds, dcs):
-        c = 0
+    def __init__(self):
+        self.count = 0
+
+    def place(self, dcs, vnfd, vnfid, vdu, ssiid, cname):
         dcs_list = list(dcs.itervalues())
-        for id, vnfd in vnfds.iteritems():
-            vnfd["dc"] = dcs_list[c % len(dcs_list)]
-            c += 1  # inc. c to use next DC
+        rdc = dcs_list[self.count % len(dcs_list)]
+        self.count += 1  # inc. count to use next DC
+        return rdc
+
+
+class StaticConfigPlacement(object):
+    """
+    Placement: Fixed assignment based on config file.
+    """
+
+    def __init__(self, path=None):
+        if path is None:
+            path = "static_placement.yml"
+        path = os.path.expanduser(path)
+        self.static_placement = dict()
+        try:
+            self.static_placement = load_yaml(path)
+        except BaseException as ex:
+            LOG.error(ex)
+            LOG.error("Couldn't load placement from {}"
+                      .format(path))
+        LOG.info("Loaded static placement: {}"
+                 .format(self.static_placement))
+
+    def place(self, dcs, vnfd, vnfid, vdu, ssiid, cname):
+        # check for container name entry
+        if cname not in self.static_placement:
+            LOG.error("Coudn't find {} in placement".format(cname))
+            LOG.error("Using first DC as fallback!")
+            return list(dcs.itervalues())[0]
+        # lookup
+        candidate_dc = self.static_placement.get(cname)
+        # check if DC exsits
+        if candidate_dc not in dcs:
+            LOG.error("Coudn't find DC {}".format(candidate_dc))
+            LOG.error("Using first DC as fallback!")
+            return list(dcs.itervalues())[0]
+        # return correct DC
+        return dcs.get(candidate_dc)
 
 
 """
@@ -952,11 +995,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):
@@ -972,7 +1048,9 @@ 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():
@@ -990,7 +1068,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):
@@ -998,9 +1079,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):
         """
@@ -1078,9 +1170,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')