merge master and fix SDN chaining unit test
[osm/vim-emu.git] / src / emuvim / dcemulator / resourcemodel / upb / simple.py
old mode 100644 (file)
new mode 100755 (executable)
index 503e35c..beb3fbe
 """
 Playground for resource models created by University of Paderborn.
 """
 """
 Playground for resource models created by University of Paderborn.
 """
+import time
+import json
 import logging
 import logging
-from emuvim.dcemulator.resourcemodel import BaseResourceModel
+from emuvim.dcemulator.resourcemodel import BaseResourceModel, NotEnoughResourcesAvailable
 
 LOG = logging.getLogger("rm.upb.simple")
 LOG.setLevel(logging.DEBUG)
 
 
 LOG = logging.getLogger("rm.upb.simple")
 LOG.setLevel(logging.DEBUG)
 
+CPU_PERIOD = 1000000
 
 
-class UpbSimpleCloudDcApproxRM(BaseResourceModel):
+
+class UpbSimpleCloudDcRM(BaseResourceModel):
+    """
+    This will be an example resource model that limits the overall
+    resources that can be deployed per data center.
+    No over provisioning. Resources are fixed throughout entire container
+    lifetime.
+    """
+
+    def __init__(self, max_cu=32, max_mu=1024,
+                 deactivate_cpu_limit=False,
+                 deactivate_mem_limit=False):
+        """
+        Initialize model.
+        :param max_cu: Maximum number of compute units available in this DC.
+        :param max_mu: Maximum memory of entire dc.
+        :return:
+        """
+        self.dc_max_cu = max_cu
+        self.dc_max_mu = max_mu
+        self.dc_alloc_cu = 0
+        self.dc_alloc_mu = 0
+        self.deactivate_cpu_limit = deactivate_cpu_limit
+        self.deactivate_mem_limit = deactivate_mem_limit
+        self.single_cu = 0
+        self.single_mu = 0
+        self.cpu_op_factor = 1.0  # over provisioning factor
+        self.mem_op_factor = 1.0
+        self.raise_no_cpu_resources_left = True
+        self.raise_no_mem_resources_left = True
+        super(UpbSimpleCloudDcRM, self).__init__()
+
+    def allocate(self, d):
+        """
+        Allocate resources for the given container.
+        Defined by d.flavor_name
+        :param d: container
+        :return:
+        """
+        self._allocated_compute_instances[d.name] = d
+        if not self.deactivate_cpu_limit:
+            self._allocate_cpu(d)
+        if not self.deactivate_mem_limit:
+            self._allocate_mem(d)
+        self._apply_limits()
+
+    def _allocate_cpu(self, d):
+        """
+        Actually allocate (bookkeeping)
+        :param d: container
+        :return:
+        """
+        fl_cu = self._get_flavor(d).get("compute")
+        # check for over provisioning
+        if self.dc_alloc_cu + fl_cu > self.dc_max_cu and self.raise_no_cpu_resources_left:
+            raise NotEnoughResourcesAvailable("Not enough compute resources left.")
+        self.dc_alloc_cu += fl_cu
+
+    def _allocate_mem(self, d):
+        """
+        Actually allocate (bookkeeping)
+        :param d: container
+        :return:
+        """
+        fl_mu = self._get_flavor(d).get("memory")
+        # check for over provisioning
+        if self.dc_alloc_mu + fl_mu > self.dc_max_mu and self.raise_no_mem_resources_left:
+            raise NotEnoughResourcesAvailable("Not enough memory resources left.")
+        self.dc_alloc_mu += fl_mu
+
+    def free(self, d):
+        """
+        Free resources allocated to the given container.
+        :param d: container
+        :return:
+        """
+        del self._allocated_compute_instances[d.name]
+        if not self.deactivate_cpu_limit:
+            self._free_cpu(d)
+        if not self.deactivate_mem_limit:
+            self._free_mem(d)
+        self._apply_limits()
+
+    def _free_cpu(self, d):
+        """
+        Free resources.
+        :param d: container
+        :return:
+        """
+        self.dc_alloc_cu -= self._get_flavor(d).get("compute")
+
+    def _free_mem(self, d):
+        """
+        Free resources.
+        :param d: container
+        :return:
+        """
+        self.dc_alloc_mu -= self._get_flavor(d).get("memory")
+
+    def _apply_limits(self):
+        """
+        Recalculate real resource limits for all allocated containers and apply them
+        to their cgroups.
+        We have to recalculate for all containers to allow e.g. over provisioning models.
+        :return:
+        """
+        for d in self._allocated_compute_instances.itervalues():
+            if not self.deactivate_cpu_limit:
+                self._apply_cpu_limits(d)
+            if not self.deactivate_mem_limit:
+                self._apply_mem_limits(d)
+
+    def _apply_cpu_limits(self, d):
+        """
+        Calculate real CPU limit (CFS bandwidth) and apply.
+        :param d: container
+        :return:
+        """
+        number_cu = self._get_flavor(d).get("compute")
+        # calculate cpu time fraction of a single compute unit
+        self.single_cu = self._compute_single_cu()
+        # calculate cpu time fraction for container with given flavor
+        cpu_time_percentage = self.single_cu * number_cu
+        # calculate input values for CFS scheduler bandwidth limitation
+        cpu_period, cpu_quota = self._calculate_cpu_cfs_values(cpu_time_percentage)
+        # apply limits to container if changed
+        if d.cpu_period != cpu_period or d.cpu_quota != cpu_quota:
+            LOG.debug("Setting CPU limit for %r: cpu_quota = cpu_period * limit = %f * %f = %f (op_factor=%f)" % (
+                      d.name, cpu_period, cpu_time_percentage, cpu_quota, self.cpu_op_factor))
+            d.updateCpuLimit(cpu_period=int(cpu_period), cpu_quota=int(cpu_quota))
+
+    def _compute_single_cu(self):
+        """
+        Calculate percentage of CPU time of a singe CU unit.
+        :return:
+        """
+        # get cpu time fraction for entire emulation
+        e_cpu = self.registrar.e_cpu
+        # calculate
+        return float(e_cpu) / sum([rm.dc_max_cu for rm in list(self.registrar.resource_models)])
+
+    def _calculate_cpu_cfs_values(self, cpu_time_percentage):
+        """
+        Calculate cpu period and quota for CFS
+        :param cpu_time_percentage: percentage of overall CPU to be used
+        :return: cpu_period, cpu_quota
+        """
+        # (see: https://www.kernel.org/doc/Documentation/scheduler/sched-bwc.txt)
+        # Attention minimum cpu_quota is 1ms (micro)
+        cpu_period = CPU_PERIOD  # lets consider a fixed period of 1000000 microseconds for now
+        cpu_quota = cpu_period * cpu_time_percentage  # calculate the fraction of cpu time for this container
+        # ATTENTION >= 1000 to avoid a invalid argument system error ... no idea why
+        if cpu_quota < 1000:
+            cpu_quota = 1000
+            LOG.warning("Increased CPU quota to avoid system error.")
+        return cpu_period, cpu_quota
+
+    def _apply_mem_limits(self, d):
+        """
+        Calculate real mem limit and apply.
+        :param d: container
+        :return:
+        """
+        number_mu = self._get_flavor(d).get("memory")
+        # get memory amount for entire emulation
+        e_mem = self.registrar.e_mem
+        # calculate amount of memory for a single mu
+        self.single_mu = float(e_mem) / sum([rm.dc_max_mu for rm in list(self.registrar.resource_models)])
+        # calculate mem for given flavor
+        mem_limit = self.single_mu * number_mu
+        mem_limit = self._calculate_mem_limit_value(mem_limit)
+        # apply to container if changed
+        if d.mem_limit != mem_limit:
+            LOG.debug("Setting MEM limit for %r: mem_limit = %f MB (op_factor=%f)" %
+                      (d.name, mem_limit/1024/1024, self.mem_op_factor))
+            d.updateMemoryLimit(mem_limit=mem_limit)
+
+    def _calculate_mem_limit_value(self, mem_limit):
+        """
+        Calculate actual mem limit as input for cgroup API
+        :param mem_limit: abstract mem limit
+        :return: concrete mem limit
+        """
+        # ATTENTION minimum mem_limit per container is 4MB
+        if mem_limit < 4:
+            mem_limit = 4
+            LOG.warning("Increased MEM limit because it was less than 4.0 MB.")
+        # to byte!
+        return int(mem_limit*1024*1024)
+
+    def get_state_dict(self):
+        """
+        Return the state of the resource model as simple dict.
+        Helper method for logging functionality.
+        :return:
+        """
+        # collect info about all allocated instances
+        allocation_state = dict()
+        for k, d in self._allocated_compute_instances.iteritems():
+            s = dict()
+            s["cpu_period"] = d.cpu_period
+            s["cpu_quota"] = d.cpu_quota
+            s["cpu_shares"] = d.cpu_shares
+            s["mem_limit"] = d.mem_limit
+            s["memswap_limit"] = d.memswap_limit
+            allocation_state[k] = s
+        # final result
+        r = dict()
+        r["e_cpu"] = self.registrar.e_cpu
+        r["e_mem"] = self.registrar.e_mem
+        r["dc_max_cu"] = self.dc_max_cu
+        r["dc_max_mu"] = self.dc_max_mu
+        r["dc_alloc_cu"] = self.dc_alloc_cu
+        r["dc_alloc_mu"] = self.dc_alloc_mu
+        r["single_cu_percentage"] = self.single_cu
+        r["single_mu_percentage"] = self.single_mu
+        r["cpu_op_factor"] = self.cpu_op_factor
+        r["mem_op_factor"] = self.mem_op_factor
+        r["allocation_state"] = allocation_state
+        return r
+
+    def _get_flavor(self, d):
+        """
+        Get flavor assigned to given container.
+        Identified by d.flavor_name.
+        :param d: container
+        :return:
+        """
+        if d.flavor_name not in self._flavors:
+            raise Exception("Flavor %r does not exist" % d.flavor_name)
+        return self._flavors.get(d.flavor_name)
+
+    def _write_log(self, d, path, action):
+        """
+        Helper to log RM info for experiments.
+        :param d: container
+        :param path: log path
+        :param action: allocate or free
+        :return:
+        """
+        if path is None:
+            return
+        # we have a path: write out RM info
+        l = dict()
+        l["t"] = time.time()
+        l["container_state"] = d.getStatus()
+        l["action"] = action
+        l["rm_state"] = self.get_state_dict()
+        # append to logfile
+        with open(path, "a") as f:
+            f.write("%s\n" % json.dumps(l))
+
+
+class UpbOverprovisioningCloudDcRM(UpbSimpleCloudDcRM):
     """
     This will be an example resource model that limits the overall
     resources that can be deployed per data center.
     """
     This will be an example resource model that limits the overall
     resources that can be deployed per data center.
+    Allows over provisioning. Might result in reducing resources of single
+    containers whenever a data-center is over provisioned.
+    """
+    # TODO add parts for memory
+    def __init__(self, *args, **kvargs):
+        super(UpbOverprovisioningCloudDcRM, self).__init__(*args, **kvargs)
+        self.raise_no_cpu_resources_left = False
+
+    def _compute_single_cu(self):
+        """
+        Calculate percentage of CPU time of a singe CU unit.
+        Take scale-down facte for over provisioning into account.
+        :return:
+        """
+        # get cpu time fraction for entire emulation
+        e_cpu = self.registrar.e_cpu
+        # calculate over provisioning scale factor
+        self.cpu_op_factor = float(self.dc_max_cu) / (max(self.dc_max_cu, self.dc_alloc_cu))
+        # calculate
+        return float(e_cpu) / sum([rm.dc_max_cu for rm in list(self.registrar.resource_models)]) * self.cpu_op_factor
+
+
+class UpbDummyRM(UpbSimpleCloudDcRM):
+    """
+    No limits. But log allocations.
     """
     """
-    # TODO Implement resource model issue #12
+    def __init__(self, *args, **kvargs):
+        super(UpbDummyRM, self).__init__(*args, **kvargs)
+        self.raise_no_cpu_resources_left = False
 
 
-    def __init__(self, max_cu=32, max_mu=1024):
-        self._max_cu = max_cu
-        self._max_mu = max_mu
-        super(self.__class__, self).__init__()
+    def _apply_limits(self):
+        # do nothing here
+        pass