Fix: Improved performance of REST getStatus endpoint.
[osm/vim-emu.git] / src / emuvim / dcemulator / node.py
index 7590973..87085d8 100755 (executable)
@@ -1,24 +1,46 @@
-"""
-Distributed Cloud Emulator (dcemulator)
-(c) 2015 by Manuel Peuster <manuel.peuster@upb.de>
-"""
+# Copyright (c) 2015 SONATA-NFV and Paderborn University
+# ALL RIGHTS RESERVED.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Neither the name of the SONATA-NFV, Paderborn University
+# nor the names of its contributors may be used to endorse or promote
+# products derived from this software without specific prior written
+# permission.
+#
+# This work has been performed in the framework of the SONATA project,
+# funded by the European Commission under Grant number 671517 through
+# the Horizon 2020 and 5G-PPP programmes. The authors would like to
+# acknowledge the contributions of their colleagues of the SONATA
+# partner consortium (www.sonata-nfv.eu).
 from mininet.node import Docker
 from mininet.link import Link
 from mininet.node import Docker
 from mininet.link import Link
+from emuvim.dcemulator.resourcemodel import NotEnoughResourcesAvailable
 import logging
 import logging
-import time
-import json
 
 
-LOG = logging.getLogger("dcemulator")
+
+LOG = logging.getLogger("dcemulator.node")
 LOG.setLevel(logging.DEBUG)
 
 
 DCDPID_BASE = 1000  # start of switch dpid's used for data center switches
 LOG.setLevel(logging.DEBUG)
 
 
 DCDPID_BASE = 1000  # start of switch dpid's used for data center switches
+EXTSAPDPID_BASE = 2000  # start of switch dpid's used for external SAP switches
 
 
 class EmulatorCompute(Docker):
     """
     Emulator specific compute node class.
 
 
 class EmulatorCompute(Docker):
     """
     Emulator specific compute node class.
-    Inherits from Dockernet's Docker host class.
+    Inherits from Containernet's Docker host class.
     Represents a single container connected to a (logical)
     data center.
     We can add emulator specific helper functions to it.
     Represents a single container connected to a (logical)
     data center.
     We can add emulator specific helper functions to it.
@@ -28,7 +50,9 @@ class EmulatorCompute(Docker):
             self, name, dimage, **kwargs):
         self.datacenter = kwargs.get("datacenter")  # pointer to current DC
         self.flavor_name = kwargs.get("flavor_name")
             self, name, dimage, **kwargs):
         self.datacenter = kwargs.get("datacenter")  # pointer to current DC
         self.flavor_name = kwargs.get("flavor_name")
-        LOG.debug("Starting compute instance %r in data center %r" % (name, str(self.datacenter)))
+        self._network_state_cache = None
+        LOG.debug("Starting compute instance %r in data center %r" %
+                  (name, str(self.datacenter)))
         # call original Docker.__init__
         Docker.__init__(self, name, dimage, **kwargs)
 
         # call original Docker.__init__
         Docker.__init__(self, name, dimage, **kwargs)
 
@@ -37,31 +61,112 @@ class EmulatorCompute(Docker):
         Helper method to receive information about the virtual networks
         this compute instance is connected to.
         """
         Helper method to receive information about the virtual networks
         this compute instance is connected to.
         """
-        # format list of tuples (name, Ip, MAC, isUp, status)
-        return [(str(i), i.IP(), i.MAC(), i.isUp(), i.status())
-                for i in self.intfList()]
+        # get all links and find dc switch interface
+        networkStatusList = []
+        for i in self.intfList():
+            vnf_name = self.name
+            vnf_interface = str(i)
+            dc_port_name = self.datacenter.net.find_connected_dc_interface(
+                vnf_name, vnf_interface)
+            # format list of tuples (name, Ip, MAC, isUp, status, dc_portname)
+            intf_dict = {'intf_name': str(i), 'ip': "{0}/{1}".format(i.IP(), i.prefixLen), 'netmask': i.prefixLen,
+                         'mac': i.MAC(), 'up': i.isUp(), 'status': i.status(), 'dc_portname': dc_port_name}
+            networkStatusList.append(intf_dict)
+        return networkStatusList
 
     def getStatus(self):
         """
         Helper method to receive information about this compute instance.
         """
 
     def getStatus(self):
         """
         Helper method to receive information about this compute instance.
         """
+        # inspect container
+        cinspect = self.dcli.inspect_container(self.dc)
+        # inspect networking (slow, so do only once)
+        if self._network_state_cache is None:
+            self._network_state_cache = self.getNetworkStatus()
+        # build status
         status = {}
         status["name"] = self.name
         status = {}
         status["name"] = self.name
-        status["network"] = self.getNetworkStatus()
+        status["network"] = self._network_state_cache
+        status["docker_network"] = self.dcinfo['NetworkSettings']['IPAddress']
         status["image"] = self.dimage
         status["image"] = self.dimage
-        status["cpu_quota"] = self.cpu_quota
-        status["cpu_period"] = self.cpu_period
-        status["cpu_shares"] = self.cpu_shares
-        status["cpuset"] = self.cpuset
-        status["mem_limit"] = self.mem_limit
-        status["memswap_limit"] = self.memswap_limit
-        status["state"] = self.dcli.inspect_container(self.dc)["State"]
-        status["id"] = self.dcli.inspect_container(self.dc)["Id"]
+        status["flavor_name"] = self.flavor_name
+        status["cpu_quota"] = self.resources.get('cpu_quota')
+        status["cpu_period"] = self.resources.get('cpu_period')
+        status["cpu_shares"] = self.resources.get('cpu_shares')
+        status["cpuset"] = self.resources.get('cpuset_cpus')
+        status["mem_limit"] = self.resources.get('mem_limit')
+        status["memswap_limit"] = self.resources.get('memswap_limit')
+        status["state"] = cinspect["State"]
+        status["id"] = cinspect["Id"]
+        status["short_id"] = cinspect["Id"][:12]
+        status["hostname"] = cinspect["Config"]['Hostname']
         status["datacenter"] = (None if self.datacenter is None
                                 else self.datacenter.label)
         status["datacenter"] = (None if self.datacenter is None
                                 else self.datacenter.label)
+
         return status
 
 
         return status
 
 
+class EmulatorExtSAP(object):
+    """
+    Emulator specific class that defines an external service access point (SAP) for the service.
+    Inherits from Containernet's OVSBridge class.
+    Represents a single OVS switch connected to a (logical)
+    data center.
+    We can add emulator specific helper functions to it.
+    """
+
+    def __init__(self, sap_name, sap_net, datacenter, **kwargs):
+
+        self.datacenter = datacenter  # pointer to current DC
+        self.net = self.datacenter.net
+        self.name = sap_name
+
+        LOG.debug("Starting ext SAP instance %r in data center %r" %
+                  (sap_name, str(self.datacenter)))
+
+        # create SAP as separate OVS switch with an assigned ip address
+        self.ip = str(sap_net[1]) + '/' + str(sap_net.prefixlen)
+        self.subnet = sap_net
+        # allow connection to the external internet through the host
+        params = dict(NAT=True)
+        self.switch = self.net.addExtSAP(sap_name, self.ip, dpid=hex(
+            self._get_next_extSAP_dpid())[2:], **params)
+        self.switch.start()
+
+    def _get_next_extSAP_dpid(self):
+        global EXTSAPDPID_BASE
+        EXTSAPDPID_BASE += 1
+        return EXTSAPDPID_BASE
+
+    def getNetworkStatus(self):
+        """
+        Helper method to receive information about the virtual networks
+        this compute instance is connected to.
+        """
+        # get all links and find dc switch interface
+        networkStatusList = []
+        for i in self.switch.intfList():
+            vnf_name = self.name
+            vnf_interface = str(i)
+            if vnf_interface == 'lo':
+                continue
+            dc_port_name = self.datacenter.net.find_connected_dc_interface(
+                vnf_name, vnf_interface)
+            # format list of tuples (name, Ip, MAC, isUp, status, dc_portname)
+            intf_dict = {'intf_name': str(i), 'ip': self.ip, 'netmask': i.prefixLen, 'mac': i.MAC(
+            ), 'up': i.isUp(), 'status': i.status(), 'dc_portname': dc_port_name}
+            networkStatusList.append(intf_dict)
+
+        return networkStatusList
+
+    def getStatus(self):
+        return {
+            "name": self.switch.name,
+            "datacenter": self.datacenter.name,
+            "network": self.getNetworkStatus()
+        }
+
+
 class Datacenter(object):
     """
     Represents a logical data center to which compute resources
 class Datacenter(object):
     """
     Represents a logical data center to which compute resources
@@ -79,15 +184,18 @@ class Datacenter(object):
         self.name = "dc%d" % Datacenter.DC_COUNTER
         Datacenter.DC_COUNTER += 1
         # use this for user defined names that can be longer than self.name
         self.name = "dc%d" % Datacenter.DC_COUNTER
         Datacenter.DC_COUNTER += 1
         # use this for user defined names that can be longer than self.name
-        self.label = label  
+        self.label = label
         # dict to store arbitrary metadata (e.g. latitude and longitude)
         self.metadata = metadata
         # dict to store arbitrary metadata (e.g. latitude and longitude)
         self.metadata = metadata
-        # path to which resource information should be logged (e.g. for experiments). None = no logging
+        # path to which resource information should be logged (e.g. for
+        # experiments). None = no logging
         self.resource_log_path = resource_log_path
         # first prototype assumes one "bigswitch" per DC
         self.switch = None
         # keep track of running containers
         self.containers = {}
         self.resource_log_path = resource_log_path
         # first prototype assumes one "bigswitch" per DC
         self.switch = None
         # keep track of running containers
         self.containers = {}
+        # keep track of attached external access points
+        self.extSAPs = {}
         # pointer to assigned resource model
         self._resource_model = None
 
         # pointer to assigned resource model
         self._resource_model = None
 
@@ -114,7 +222,8 @@ class Datacenter(object):
     def start(self):
         pass
 
     def start(self):
         pass
 
-    def startCompute(self, name, image=None, command=None, network=None, flavor_name="tiny"):
+    def startCompute(self, name, image=None, command=None, network=None,
+                     flavor_name="tiny", properties=dict(), **params):
         """
         Create a new container as compute resource and connect it to this
         data center.
         """
         Create a new container as compute resource and connect it to this
         data center.
@@ -123,88 +232,77 @@ class Datacenter(object):
         :param command: command (string)
         :param network: networks list({"ip": "10.0.0.254/8"}, {"ip": "11.0.0.254/24"})
         :param flavor_name: name of the flavor for this compute container
         :param command: command (string)
         :param network: networks list({"ip": "10.0.0.254/8"}, {"ip": "11.0.0.254/24"})
         :param flavor_name: name of the flavor for this compute container
+        :param properties: dictionary of properties (key-value) that will be passed as environment variables
         :return:
         """
         assert name is not None
         :return:
         """
         assert name is not None
+        default_net = {"id": "emu0"}
         # no duplications
         if name in [c.name for c in self.net.getAllContainers()]:
             raise Exception("Container with name %s already exists." % name)
         # set default parameter
         if image is None:
         # no duplications
         if name in [c.name for c in self.net.getAllContainers()]:
             raise Exception("Container with name %s already exists." % name)
         # set default parameter
         if image is None:
-            image = "ubuntu"
+            image = "ubuntu:trusty"
         if network is None:
         if network is None:
-            network = {}  # {"ip": "10.0.0.254/8"}
+            network = {}
         if isinstance(network, dict):
         if isinstance(network, dict):
-            network = [network]  # if we have only one network, put it in a list
+            if len(network) < 1:
+                # create at least one default interface
+                network = default_net
+            # if we have only one network, put it in a list
+            network = [network]
         if isinstance(network, list):
             if len(network) < 1:
         if isinstance(network, list):
             if len(network) < 1:
-                network.append({})
+                # create at least one default interface
+                network.append(default_net)
 
 
-        # allocate in resource resource model and compute resource limits for new container
-        cpu_limit = mem_limit = disk_limit = -1
-        cpu_period = cpu_quota = None
-        if self._resource_model is not None:
-            # call allocate in resource model to calculate resource limit for this container
-            (cpu_limit, mem_limit, disk_limit) = alloc = self._resource_model.allocate(name, flavor_name)
-            LOG.debug("Allocation result: %r" % str(alloc))
-            # check if we have a cpu_limit given by the used resource model
-            if cpu_limit > 0:
-                # calculate cpu period and quota for CFS
-                # (see: https://www.kernel.org/doc/Documentation/scheduler/sched-bwc.txt)
-                # TODO consider multi core machines etc! non trivial!
-                # Attention minimum cpu_quota is 1ms (micro)
-                cpu_period = 100000  # lets consider a fixed period of 100000 microseconds for now
-                cpu_quota = cpu_period * cpu_limit  # calculate the fraction of cpu time for this container
-                LOG.debug(
-                    "CPU limit: cpu_quota = cpu_period * cpu_limit = %f * %f = %f" % (cpu_period, cpu_limit, cpu_quota))
-                # ATTENTION >= 1000 to avoid a invalid argument system error ... no idea why
-                if cpu_quota < 1000:
-                    cpu_quota = 1000
-                    LOG.warning("Increased CPU quota for %r to avoid system error." % name)
-            # check if we have a mem_limit given by the used resource model
-            if mem_limit > 0:
-                LOG.debug(
-                    "MEM limit: mem_limit = %f MB" % mem_limit)
-                # ATTENTION minimum mem_limit per container is 4MB
-                if mem_limit < 4:
-                    mem_limit = 4
-                    LOG.warning("Increased MEM limit for %r because it was less than 4.0 MB." % name)
+        # apply hard-set resource limits=0
+        cpu_percentage = params.get('cpu_percent')
+        if cpu_percentage:
+            params['cpu_period'] = self.net.cpu_period
+            params['cpu_quota'] = self.net.cpu_period * float(cpu_percentage)
+
+        env = properties
+        properties['VNF_NAME'] = name
         # create the container
         d = self.net.addDocker(
         # create the container
         d = self.net.addDocker(
-            "%s" % (name),
+            str(name),
             dimage=image,
             dcmd=command,
             datacenter=self,
             flavor_name=flavor_name,
             dimage=image,
             dcmd=command,
             datacenter=self,
             flavor_name=flavor_name,
-            cpu_period=int(cpu_period) if cpu_limit > 0 else None,  # set cpu limits if needed
-            cpu_quota=int(cpu_quota) if cpu_limit > 0 else None,
-            mem_limit="%dm" % int(mem_limit) if mem_limit > 0 else None,  # set mem limits if needed
-            memswap_limit="%dm" % int(mem_limit) if mem_limit > 0 else None  # lets set swap to mem limit for now
+            environment=env,
+            **params
         )
         )
+
+        # apply resource limits to container if a resource model is defined
+        if self._resource_model is not None:
+            try:
+                self._resource_model.allocate(d)
+                self._resource_model.write_allocation_log(
+                    d, self.resource_log_path)
+            except NotEnoughResourcesAvailable as ex:
+                LOG.warning(
+                    "Allocation of container %r was blocked by resource model." % name)
+                LOG.info(ex.message)
+                # ensure that we remove the container
+                self.net.removeDocker(name)
+                return None
+
         # connect all given networks
         # 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:
         # connect all given networks
         # 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:
-            # TODO we cannot use TCLink here (see: https://github.com/mpeuster/dockernet/issues/3)
-            self.net.addLink(d, self.switch, params1=nw, cls=Link)
+            # 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
         self.containers[name] = d
         # do bookkeeping
         self.containers[name] = d
-
-        # write resource log if a path is given
-        if self.resource_log_path is not None:
-            l = dict()
-            l["t"] = time.time()
-            l["name"] = name
-            l["compute"] = d.getStatus()
-            l["flavor_name"] = flavor_name
-            l["action"] = "allocate"
-            l["cpu_limit"] = cpu_limit
-            l["mem_limit"] = mem_limit
-            l["disk_limit"] = disk_limit
-            l["rm_state"] = None if self._resource_model is None else self._resource_model.get_state_dict()
-            # append to logfile
-            with open(self.resource_log_path, "a") as f:
-                f.write("%s\n" % json.dumps(l))
         return d  # we might use UUIDs for naming later on
 
     def stopCompute(self, name):
         return d  # we might use UUIDs for naming later on
 
     def stopCompute(self, name):
@@ -214,31 +312,43 @@ class Datacenter(object):
         assert name is not None
         if name not in self.containers:
             raise Exception("Container with name %s not found." % name)
         assert name is not None
         if name not in self.containers:
             raise Exception("Container with name %s not found." % name)
-        LOG.debug("Stopping compute instance %r in data center %r" % (name, str(self)))
+        LOG.debug("Stopping compute instance %r in data center %r" %
+                  (name, str(self)))
+
+        #  stop the monitored metrics
+        if self.net.monitor_agent is not None:
+            self.net.monitor_agent.stop_metric(name)
+
+        # call resource model and free resources
+        if self._resource_model is not None:
+            self._resource_model.free(self.containers[name])
+            self._resource_model.write_free_log(
+                self.containers[name], self.resource_log_path)
+
+        # remove links
         self.net.removeLink(
             link=None, node1=self.containers[name], node2=self.switch)
         self.net.removeLink(
             link=None, node1=self.containers[name], node2=self.switch)
+
+        # remove container
         self.net.removeDocker("%s" % (name))
         del self.containers[name]
         self.net.removeDocker("%s" % (name))
         del self.containers[name]
-        # call resource model and free resources
-        if self._resource_model is not None:
-            self._resource_model.free(name)
-
-        # write resource log if a path is given
-        if self.resource_log_path is not None:
-            l = dict()
-            l["t"] = time.time()
-            l["name"] = name
-            l["flavor_name"] = None
-            l["action"] = "free"
-            l["cpu_limit"] = -1
-            l["mem_limit"] = -1
-            l["disk_limit"] = -1
-            l["rm_state"] = None if self._resource_model is None else self._resource_model.get_state_dict()
-            # append to logfile
-            with open(self.resource_log_path, "a") as f:
-                f.write("%s\n" % json.dumps(l))
+
         return True
 
         return True
 
+    def attachExternalSAP(self, sap_name, sap_net, **params):
+        extSAP = EmulatorExtSAP(sap_name, sap_net, self, **params)
+        # link SAP to the DC switch
+        self.net.addLink(extSAP.switch, self.switch, cls=Link)
+        self.extSAPs[sap_name] = extSAP
+
+    def removeExternalSAP(self, sap_name):
+        sap_switch = self.extSAPs[sap_name].switch
+        # sap_switch = self.net.getNodeByName(sap_name)
+        # remove link of SAP to the DC switch
+        self.net.removeLink(link=None, node1=sap_switch, node2=self.switch)
+        self.net.removeExtSAP(sap_name)
+        del self.extSAPs[sap_name]
+
     def listCompute(self):
         """
         Return a list of all running containers assigned to this
     def listCompute(self):
         """
         Return a list of all running containers assigned to this
@@ -246,16 +356,27 @@ class Datacenter(object):
         """
         return list(self.containers.itervalues())
 
         """
         return list(self.containers.itervalues())
 
+    def listExtSAPs(self):
+        """
+        Return a list of all external SAPs assigned to this
+        data center.
+        """
+        return list(self.extSAPs.itervalues())
+
     def getStatus(self):
         """
         Return a dict with status information about this DC.
         """
     def getStatus(self):
         """
         Return a dict with status information about this DC.
         """
+        container_list = [name for name in self.containers]
+        ext_saplist = [sap_name for sap_name in self.extSAPs]
         return {
             "label": self.label,
             "internalname": self.name,
             "switch": self.switch.name,
             "n_running_containers": len(self.containers),
         return {
             "label": self.label,
             "internalname": self.name,
             "switch": self.switch.name,
             "n_running_containers": len(self.containers),
-            "metadata": self.metadata
+            "metadata": self.metadata,
+            "vnf_list": container_list,
+            "ext SAP list": ext_saplist
         }
 
     def assignResourceModel(self, rm):
         }
 
     def assignResourceModel(self, rm):
@@ -265,8 +386,24 @@ class Datacenter(object):
         :return:
         """
         if self._resource_model is not None:
         :return:
         """
         if self._resource_model is not None:
-            raise Exception("There is already an resource model assigned to this DC.")
+            raise Exception(
+                "There is already an resource model assigned to this DC.")
         self._resource_model = rm
         self.net.rm_registrar.register(self, rm)
         LOG.info("Assigned RM: %r to DC: %r" % (rm, self))
 
         self._resource_model = rm
         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