Fix: Correctly handle cap_add in 5GTANGO LLCM
[osm/vim-emu.git] / src / emuvim / api / tango / llcm.py
index 0a62485..bd3e1f9 100755 (executable)
@@ -52,6 +52,10 @@ LOG = logging.getLogger("5gtango.llcm")
 LOG.setLevel(logging.INFO)
 
 
+CORS_HEADER = {'Access-Control-Allow-Origin': '*',
+               'Access-Control-Allow-Methods': 'GET,OPTIONS'}
+
+
 GK_STORAGE = "/tmp/vim-emu-tango-llcm/"
 UPLOAD_FOLDER = os.path.join(GK_STORAGE, "uploads/")
 CATALOG_FOLDER = os.path.join(GK_STORAGE, "catalog/")
@@ -93,6 +97,15 @@ VNF_STOP_WAIT_TIME = 5
 # offset for this: NEW_PORT (SSIID * OFFSET) + ORIGINAL_PORT
 MULTI_INSTANCE_PORT_OFFSET = 1000
 
+# Selected Placement Algorithm: Points to the class of the selected
+# placement algorithm.
+PLACEMENT_ALGORITHM_OBJ = None
+
+# Path to folder with <container_name>.env.yml files that contain
+# environment variables injected into the specific container
+# when it is started.
+PER_INSTANCE_ENV_CONFIGURATION_FOLDER = None
+
 
 class OnBoardingException(BaseException):
     pass
@@ -200,10 +213,6 @@ class Service(object):
         # 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]
@@ -332,7 +341,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)
@@ -348,6 +357,8 @@ class Service(object):
             # do some re-naming of fields to be compatible to containernet
             for i in intfs:
                 if i.get("address"):
+                    LOG.info("Found static address for {}: {}"
+                             .format(i.get("id"), i.get("address")))
                     i["ip"] = i.get("address")
 
             # get ports and port_bindings from the port and publish fields of CNFD
@@ -355,11 +366,30 @@ class Service(object):
             ports = list()  # Containernet naming
             port_bindings = dict()
             for i in intfs:
-                if i.get("port"):
+                if i.get("port"):  # field with a single port
                     if not isinstance(i.get("port"), int):
                         LOG.info("Field 'port' is no int CP: {}".format(i))
                     else:
-                        ports.append(i.get("port"))
+                        ports.append(i.get("port"))  # collect all ports
+                if i.get("ports"):  # list with multiple ports
+                    if not isinstance(i.get("ports"), list):
+                        LOG.info("Field 'port' is no list CP: {}".format(i))
+                    else:
+                        for p in i.get("ports"):
+                            if not isinstance(p, int):
+                                # do some parsing
+                                try:
+                                    if "/udp" in p:
+                                        p = tuple(p.split("/"))
+                                    else:
+                                        p = int(p)
+                                    ports.append(p)  # collect all ports
+                                except BaseException as ex:
+                                    LOG.error(
+                                        "Could not parse ports list: {}".format(p))
+                                    LOG.error(ex)
+                            else:
+                                ports.append(p)  # collect all ports
                 if i.get("publish"):
                     if not isinstance(i.get("publish"), dict):
                         LOG.info("Field 'publish' is no dict CP: {}".format(i))
@@ -387,9 +417,20 @@ class Service(object):
                          " Overwriting SON_EMU_CMD_STOP.")
                 cenv["SON_EMU_CMD_STOP"] = VNFD_CMD_STOP
 
+            # 5.2 inject per instance configurations based on envs
+            conf_envs = self._load_instance_conf_envs(vnf_container_instance_name)
+            cenv.update(conf_envs)
+
+            # 5.3 handle optional ipc_mode setting
+            ipc_mode = u.get("ipc_mode", None)
+            # 5.4 handle optional devices setting
+            devices = u.get("devices", [])
+            # 5.5 handle optional cap_add setting
+            cap_add = u.get("cap_add", [])
+
             # 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(
@@ -406,6 +447,9 @@ class Service(object):
                 port_bindings=port_bindings,
                 # only publish if explicitly stated in descriptor
                 publish_all_ports=False,
+                ipc_mode=ipc_mode,
+                devices=devices,
+                cap_add=cap_add,
                 type=kwargs.get('type', 'docker'))
             # add vnfd reference to vnfi
             vnfi.vnfd = vnfd
@@ -520,6 +564,26 @@ class Service(object):
                     t.start()
                     break  # only execute one command
 
+    def _load_instance_conf_envs(self, cname):
+        """
+        Try to load an instance-specific env file. If not found,
+        just return an empty dict.
+        """
+        if PER_INSTANCE_ENV_CONFIGURATION_FOLDER is None:
+            return dict()
+        try:
+            path = os.path.expanduser(PER_INSTANCE_ENV_CONFIGURATION_FOLDER)
+            path = os.path.join(path, "{}.env.yml".format(cname))
+            res = load_yaml(path)
+            LOG.info("Loaded instance-specific env file for '{}': {}"
+                     .format(cname, res))
+            return res
+        except BaseException as ex:
+            LOG.info("No instance-specific env file found for: {}"
+                     .format(cname))
+            del ex
+        return dict()
+
     def _unpack_service_package(self):
         """
         unzip *.son file and store contents in CATALOG_FOLDER/services/<service_uuid>/
@@ -683,8 +747,8 @@ class Service(object):
             lan_hosts = list(lan_net.hosts())
 
             # generate lan ip address for all interfaces (of all involved (V/CDUs))
-            for intf in link["connection_points_reference"]:
-                vnf_id, intf_name = parse_interface(intf)
+            for intf_ref in link["connection_points_reference"]:
+                vnf_id, intf_name = parse_interface(intf_ref)
                 if vnf_id is None:
                     continue  # skip references to NS connection points
                 units = self._get_vnf_instance_units(instance_uuid, vnf_id)
@@ -695,10 +759,20 @@ class Service(object):
                     # Attention: we apply a simplification for multi DU VNFs here:
                     # the connection points of all involved DUs have to have the same
                     # name as the connection points of the surrounding VNF to be mapped.
-                    # This is because we do not consider links specified in the VNFds
+                    # This is because we do not consider links specified in the VNFDs
                     container_name = uvnfi.name
-                    ip_address = "{0}/{1}".format(str(lan_hosts.pop(0)),
-                                                  lan_net.prefixlen)
+
+                    ip_address = None
+                    # get the interface of the unit
+                    intf = self._get_vnfd_cp_from_vnfi(uvnfi, intf_name)
+                    # check if there is a manually assigned address
+                    if intf is not None:
+                        if intf.get("address"):
+                            ip_address = intf.get("address")
+                    if ip_address is None:
+                        # automatically asign an IP from our pool
+                        ip_address = "{0}/{1}".format(str(lan_hosts.pop(0)),
+                                                      lan_net.prefixlen)
                     LOG.debug(
                         "Setting up E-LAN/E-Tree interface. (%s:%s) -> %s" % (
                             container_name, intf_name, ip_address))
@@ -810,21 +884,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):
         """
@@ -864,9 +939,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):
@@ -874,12 +948,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)
 
 
 """
@@ -971,7 +1083,7 @@ class Packages(fr.Resource):
             pkg["pd"]["version"] = sobj.manifest.get("version")
             pkg["created_at"] = sobj.created_at
             result.append(pkg)
-        return result, 200
+        return result, 200, CORS_HEADER
 
 
 class Services(fr.Resource):
@@ -993,7 +1105,7 @@ class Services(fr.Resource):
             service["nsd"]["version"] = sobj.nsd.get("version")
             service["created_at"] = sobj.created_at
             result.append(service)
-        return result, 200
+        return result, 200, CORS_HEADER
 
 
 class Instantiations(fr.Resource):
@@ -1040,7 +1152,7 @@ class Instantiations(fr.Resource):
         Returns a list of UUIDs containing all running services.
         :return: dict / list
         """
-        LOG.info("GET /instantiations or /api/v3/records/services")
+        LOG.debug("GET /instantiations or /api/v3/records/services")
         # return {"service_instantiations_list": [
         #    list(s.instances.iterkeys()) for s in GK.services.itervalues()]}
         result = list()
@@ -1053,7 +1165,7 @@ class Instantiations(fr.Resource):
                 inst["status"] = "running"
                 inst["created_at"] = iobj.get("created_at")
                 result.append(inst)
-        return result, 200
+        return result, 200, CORS_HEADER
 
     def delete(self):
         """