Refactoring: Made complete codebase PEP8 compatible.
[osm/vim-emu.git] / src / emuvim / dcemulator / resourcemodel / upb / simple.py
1 # Copyright (c) 2015 SONATA-NFV and Paderborn University
2 # ALL RIGHTS RESERVED.
3 #
4 # Licensed under the Apache License, Version 2.0 (the "License");
5 # you may not use this file except in compliance with the License.
6 # You may obtain a copy of the License at
7 #
8 # http://www.apache.org/licenses/LICENSE-2.0
9 #
10 # Unless required by applicable law or agreed to in writing, software
11 # distributed under the License is distributed on an "AS IS" BASIS,
12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # See the License for the specific language governing permissions and
14 # limitations under the License.
15 #
16 # Neither the name of the SONATA-NFV, Paderborn University
17 # nor the names of its contributors may be used to endorse or promote
18 # products derived from this software without specific prior written
19 # permission.
20 #
21 # This work has been performed in the framework of the SONATA project,
22 # funded by the European Commission under Grant number 671517 through
23 # the Horizon 2020 and 5G-PPP programmes. The authors would like to
24 # acknowledge the contributions of their colleagues of the SONATA
25 # partner consortium (www.sonata-nfv.eu).
26 import time
27 import json
28 import logging
29 from emuvim.dcemulator.resourcemodel import BaseResourceModel, NotEnoughResourcesAvailable
30
31 LOG = logging.getLogger("rm.upb.simple")
32 LOG.setLevel(logging.DEBUG)
33
34 CPU_PERIOD = 1000000
35
36
37 class UpbSimpleCloudDcRM(BaseResourceModel):
38 """
39 This will be an example resource model that limits the overall
40 resources that can be deployed per data center.
41 No over provisioning. Resources are fixed throughout entire container
42 lifetime.
43 """
44
45 def __init__(self, max_cu=32, max_mu=1024,
46 deactivate_cpu_limit=False,
47 deactivate_mem_limit=False):
48 """
49 Initialize model.
50 :param max_cu: Maximum number of compute units available in this DC.
51 :param max_mu: Maximum memory of entire dc.
52 :return:
53 """
54 self.dc_max_cu = max_cu
55 self.dc_max_mu = max_mu
56 self.dc_alloc_cu = 0
57 self.dc_alloc_mu = 0
58 self.deactivate_cpu_limit = deactivate_cpu_limit
59 self.deactivate_mem_limit = deactivate_mem_limit
60 self.single_cu = 0
61 self.single_mu = 0
62 self.cpu_op_factor = 1.0 # over provisioning factor
63 self.mem_op_factor = 1.0
64 self.raise_no_cpu_resources_left = True
65 self.raise_no_mem_resources_left = True
66 super(UpbSimpleCloudDcRM, self).__init__()
67
68 def allocate(self, d):
69 """
70 Allocate resources for the given container.
71 Defined by d.flavor_name
72 :param d: container
73 :return:
74 """
75 self._allocated_compute_instances[d.name] = d
76 if not self.deactivate_cpu_limit:
77 self._allocate_cpu(d)
78 if not self.deactivate_mem_limit:
79 self._allocate_mem(d)
80 self._apply_limits()
81
82 def _allocate_cpu(self, d):
83 """
84 Actually allocate (bookkeeping)
85 :param d: container
86 :return:
87 """
88 fl_cu = self._get_flavor(d).get("compute")
89 # check for over provisioning
90 if self.dc_alloc_cu + fl_cu > self.dc_max_cu and self.raise_no_cpu_resources_left:
91 raise NotEnoughResourcesAvailable(
92 "Not enough compute resources left.")
93 self.dc_alloc_cu += fl_cu
94
95 def _allocate_mem(self, d):
96 """
97 Actually allocate (bookkeeping)
98 :param d: container
99 :return:
100 """
101 fl_mu = self._get_flavor(d).get("memory")
102 # check for over provisioning
103 if self.dc_alloc_mu + fl_mu > self.dc_max_mu and self.raise_no_mem_resources_left:
104 raise NotEnoughResourcesAvailable(
105 "Not enough memory resources left.")
106 self.dc_alloc_mu += fl_mu
107
108 def free(self, d):
109 """
110 Free resources allocated to the given container.
111 :param d: container
112 :return:
113 """
114 del self._allocated_compute_instances[d.name]
115 if not self.deactivate_cpu_limit:
116 self._free_cpu(d)
117 if not self.deactivate_mem_limit:
118 self._free_mem(d)
119 self._apply_limits()
120
121 def _free_cpu(self, d):
122 """
123 Free resources.
124 :param d: container
125 :return:
126 """
127 self.dc_alloc_cu -= self._get_flavor(d).get("compute")
128
129 def _free_mem(self, d):
130 """
131 Free resources.
132 :param d: container
133 :return:
134 """
135 self.dc_alloc_mu -= self._get_flavor(d).get("memory")
136
137 def _apply_limits(self):
138 """
139 Recalculate real resource limits for all allocated containers and apply them
140 to their cgroups.
141 We have to recalculate for all containers to allow e.g. over provisioning models.
142 :return:
143 """
144 for d in self._allocated_compute_instances.itervalues():
145 if not self.deactivate_cpu_limit:
146 self._apply_cpu_limits(d)
147 if not self.deactivate_mem_limit:
148 self._apply_mem_limits(d)
149
150 def _apply_cpu_limits(self, d):
151 """
152 Calculate real CPU limit (CFS bandwidth) and apply.
153 :param d: container
154 :return:
155 """
156 number_cu = self._get_flavor(d).get("compute")
157 # calculate cpu time fraction of a single compute unit
158 self.single_cu = self._compute_single_cu()
159 # calculate cpu time fraction for container with given flavor
160 cpu_time_percentage = self.single_cu * number_cu
161 # calculate input values for CFS scheduler bandwidth limitation
162 cpu_period, cpu_quota = self._calculate_cpu_cfs_values(
163 cpu_time_percentage)
164 # apply limits to container if changed
165 if d.resources['cpu_period'] != cpu_period or d.resources['cpu_quota'] != cpu_quota:
166 LOG.debug("Setting CPU limit for %r: cpu_quota = cpu_period * limit = %f * %f = %f (op_factor=%f)" % (
167 d.name, cpu_period, cpu_time_percentage, cpu_quota, self.cpu_op_factor))
168 d.updateCpuLimit(cpu_period=int(cpu_period),
169 cpu_quota=int(cpu_quota))
170
171 def _compute_single_cu(self):
172 """
173 Calculate percentage of CPU time of a singe CU unit.
174 :return:
175 """
176 # get cpu time fraction for entire emulation
177 e_cpu = self.registrar.e_cpu
178 # calculate
179 return float(
180 e_cpu) / sum([rm.dc_max_cu for rm in list(self.registrar.resource_models)])
181
182 def _calculate_cpu_cfs_values(self, cpu_time_percentage):
183 """
184 Calculate cpu period and quota for CFS
185 :param cpu_time_percentage: percentage of overall CPU to be used
186 :return: cpu_period, cpu_quota
187 """
188 # (see: https://www.kernel.org/doc/Documentation/scheduler/sched-bwc.txt)
189 # Attention minimum cpu_quota is 1ms (micro)
190 cpu_period = CPU_PERIOD # lets consider a fixed period of 1000000 microseconds for now
191 # calculate the fraction of cpu time for this container
192 cpu_quota = cpu_period * cpu_time_percentage
193 # ATTENTION >= 1000 to avoid a invalid argument system error ... no
194 # idea why
195 if cpu_quota < 1000:
196 cpu_quota = 1000
197 LOG.warning("Increased CPU quota to avoid system error.")
198 return cpu_period, cpu_quota
199
200 def _apply_mem_limits(self, d):
201 """
202 Calculate real mem limit and apply.
203 :param d: container
204 :return:
205 """
206 number_mu = self._get_flavor(d).get("memory")
207 # get memory amount for entire emulation
208 e_mem = self.registrar.e_mem
209 # calculate amount of memory for a single mu
210 self.single_mu = float(
211 e_mem) / sum([rm.dc_max_mu for rm in list(self.registrar.resource_models)])
212 # calculate mem for given flavor
213 mem_limit = self.single_mu * number_mu
214 mem_limit = self._calculate_mem_limit_value(mem_limit)
215 # apply to container if changed
216 if d.resources['mem_limit'] != mem_limit:
217 LOG.debug("Setting MEM limit for %r: mem_limit = %f MB (op_factor=%f)" %
218 (d.name, mem_limit / 1024 / 1024, self.mem_op_factor))
219 d.updateMemoryLimit(mem_limit=mem_limit)
220
221 def _calculate_mem_limit_value(self, mem_limit):
222 """
223 Calculate actual mem limit as input for cgroup API
224 :param mem_limit: abstract mem limit
225 :return: concrete mem limit
226 """
227 # ATTENTION minimum mem_limit per container is 4MB
228 if mem_limit < 4:
229 mem_limit = 4
230 LOG.warning("Increased MEM limit because it was less than 4.0 MB.")
231 # to byte!
232 return int(mem_limit * 1024 * 1024)
233
234 def get_state_dict(self):
235 """
236 Return the state of the resource model as simple dict.
237 Helper method for logging functionality.
238 :return:
239 """
240 # collect info about all allocated instances
241 allocation_state = dict()
242 for k, d in self._allocated_compute_instances.iteritems():
243 s = dict()
244 s["cpu_period"] = d.cpu_period
245 s["cpu_quota"] = d.cpu_quota
246 s["cpu_shares"] = d.cpu_shares
247 s["mem_limit"] = d.mem_limit
248 s["memswap_limit"] = d.memswap_limit
249 allocation_state[k] = s
250 # final result
251 r = dict()
252 r["e_cpu"] = self.registrar.e_cpu
253 r["e_mem"] = self.registrar.e_mem
254 r["dc_max_cu"] = self.dc_max_cu
255 r["dc_max_mu"] = self.dc_max_mu
256 r["dc_alloc_cu"] = self.dc_alloc_cu
257 r["dc_alloc_mu"] = self.dc_alloc_mu
258 r["single_cu_percentage"] = self.single_cu
259 r["single_mu_percentage"] = self.single_mu
260 r["cpu_op_factor"] = self.cpu_op_factor
261 r["mem_op_factor"] = self.mem_op_factor
262 r["allocation_state"] = allocation_state
263 return r
264
265 def _get_flavor(self, d):
266 """
267 Get flavor assigned to given container.
268 Identified by d.flavor_name.
269 :param d: container
270 :return:
271 """
272 if d.flavor_name not in self._flavors:
273 raise Exception("Flavor %r does not exist" % d.flavor_name)
274 return self._flavors.get(d.flavor_name)
275
276 def _write_log(self, d, path, action):
277 """
278 Helper to log RM info for experiments.
279 :param d: container
280 :param path: log path
281 :param action: allocate or free
282 :return:
283 """
284 if path is None:
285 return
286 # we have a path: write out RM info
287 logd = dict()
288 logd["t"] = time.time()
289 logd["container_state"] = d.getStatus()
290 logd["action"] = action
291 logd["rm_state"] = self.get_state_dict()
292 # append to logfile
293 with open(path, "a") as f:
294 f.write("%s\n" % json.dumps(logd))
295
296
297 class UpbOverprovisioningCloudDcRM(UpbSimpleCloudDcRM):
298 """
299 This will be an example resource model that limits the overall
300 resources that can be deployed per data center.
301 Allows over provisioning. Might result in reducing resources of single
302 containers whenever a data-center is over provisioned.
303 """
304 # TODO add parts for memory
305
306 def __init__(self, *args, **kvargs):
307 super(UpbOverprovisioningCloudDcRM, self).__init__(*args, **kvargs)
308 self.raise_no_cpu_resources_left = False
309
310 def _compute_single_cu(self):
311 """
312 Calculate percentage of CPU time of a singe CU unit.
313 Take scale-down facte for over provisioning into account.
314 :return:
315 """
316 # get cpu time fraction for entire emulation
317 e_cpu = self.registrar.e_cpu
318 # calculate over provisioning scale factor
319 self.cpu_op_factor = float(self.dc_max_cu) / \
320 (max(self.dc_max_cu, self.dc_alloc_cu))
321 # calculate
322 return float(e_cpu) / sum([rm.dc_max_cu for rm in list(
323 self.registrar.resource_models)]) * self.cpu_op_factor
324
325
326 class UpbDummyRM(UpbSimpleCloudDcRM):
327 """
328 No limits. But log allocations.
329 """
330
331 def __init__(self, *args, **kvargs):
332 super(UpbDummyRM, self).__init__(*args, **kvargs)
333 self.raise_no_cpu_resources_left = False
334
335 def _apply_limits(self):
336 # do nothing here
337 pass