blob: 9470f48754c41725f38d2a5aacda4fc1bf8409b2 [file] [log] [blame]
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +02001# osmo_gsm_tester: manage resources
2#
3# Copyright (C) 2016-2017 by sysmocom - s.f.m.c. GmbH
4#
5# Author: Neels Hofmeyr <neels@hofmeyr.de>
6#
7# This program is free software: you can redistribute it and/or modify
Harald Welte27205342017-06-03 09:51:45 +02008# it under the terms of the GNU General Public License as
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +02009# published by the Free Software Foundation, either version 3 of the
10# License, or (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
Harald Welte27205342017-06-03 09:51:45 +020015# GNU General Public License for more details.
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +020016#
Harald Welte27205342017-06-03 09:51:45 +020017# You should have received a copy of the GNU General Public License
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +020018# along with this program. If not, see <http://www.gnu.org/licenses/>.
19
20import os
Neels Hofmeyr3531a192017-03-28 14:30:28 +020021import time
22import copy
23import atexit
24import pprint
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +020025
26from . import log
27from . import config
Neels Hofmeyr3531a192017-03-28 14:30:28 +020028from . import util
29from . import schema
30from . import ofono_client
31from . import osmo_nitb
Neels Hofmeyr391afe32017-05-18 19:22:12 +020032from . import bts_sysmo, bts_osmotrx
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +020033
Neels Hofmeyr3531a192017-03-28 14:30:28 +020034from .util import is_dict, is_list
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +020035
Neels Hofmeyr3531a192017-03-28 14:30:28 +020036HASH_KEY = '_hash'
37RESERVED_KEY = '_reserved_by'
38USED_KEY = '_used'
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +020039
Neels Hofmeyr3531a192017-03-28 14:30:28 +020040RESOURCES_CONF = 'resources.conf'
41LAST_USED_MSISDN_FILE = 'last_used_msisdn.state'
42RESERVED_RESOURCES_FILE = 'reserved_resources.state'
43
Neels Hofmeyr76d81032017-05-18 18:35:32 +020044R_IP_ADDRESS = 'ip_address'
Neels Hofmeyr3531a192017-03-28 14:30:28 +020045R_BTS = 'bts'
46R_ARFCN = 'arfcn'
47R_MODEM = 'modem'
Neels Hofmeyr76d81032017-05-18 18:35:32 +020048R_ALL = (R_IP_ADDRESS, R_BTS, R_ARFCN, R_MODEM)
Neels Hofmeyr3531a192017-03-28 14:30:28 +020049
50RESOURCES_SCHEMA = {
Neels Hofmeyr76d81032017-05-18 18:35:32 +020051 'ip_address[].addr': schema.IPV4,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020052 'bts[].label': schema.STR,
53 'bts[].type': schema.STR,
Neels Hofmeyr17c139e2017-04-12 02:42:02 +020054 'bts[].ipa_unit_id': schema.INT,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020055 'bts[].addr': schema.IPV4,
56 'bts[].band': schema.BAND,
Pau Espin Pedrol404e1502017-08-22 11:17:43 +020057 'bts[].trx_remote_ip': schema.IPV4,
58 'bts[].launch_trx': schema.BOOL_STR,
Your Name44af3412017-04-13 03:11:59 +020059 'bts[].trx_list[].hw_addr': schema.HWADDR,
60 'bts[].trx_list[].net_device': schema.STR,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020061 'arfcn[].arfcn': schema.INT,
62 'arfcn[].band': schema.BAND,
63 'modem[].label': schema.STR,
64 'modem[].path': schema.STR,
65 'modem[].imsi': schema.IMSI,
66 'modem[].ki': schema.KI,
67 }
68
69WANT_SCHEMA = util.dict_add(
70 dict([('%s[].times' % r, schema.INT) for r in R_ALL]),
71 RESOURCES_SCHEMA)
72
73KNOWN_BTS_TYPES = {
Your Name44af3412017-04-13 03:11:59 +020074 'osmo-bts-sysmo': bts_sysmo.SysmoBts,
75 'osmo-bts-trx': bts_osmotrx.OsmoBtsTrx,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020076 }
77
78def register_bts_type(name, clazz):
79 KNOWN_BTS_TYPES[name] = clazz
80
81class ResourcesPool(log.Origin):
82 _remember_to_free = None
83 _registered_exit_handler = False
84
85 def __init__(self):
86 self.config_path = config.get_config_file(RESOURCES_CONF)
87 self.state_dir = config.get_state_dir()
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +020088 super().__init__(log.C_CNF, conf=self.config_path, state=self.state_dir.path)
Neels Hofmeyr3531a192017-03-28 14:30:28 +020089 self.read_conf()
90
91 def read_conf(self):
92 self.all_resources = Resources(config.read(self.config_path, RESOURCES_SCHEMA))
93 self.all_resources.set_hashes()
94
95 def reserve(self, origin, want):
96 '''
97 attempt to reserve the resources specified in the dict 'want' for
98 'origin'. Obtain a lock on the resources lock dir, verify that all
99 wanted resources are available, and if yes mark them as reserved.
100
101 On success, return a reservation object which can be used to release
102 the reservation. The reservation will be freed automatically on program
103 exit, if not yet done manually.
104
105 'origin' should be an Origin() instance.
106
107 'want' is a dict matching WANT_SCHEMA, which is the same as
108 the RESOURCES_SCHEMA, except each entity that can be reserved has a 'times'
109 field added, to indicate how many of those should be reserved.
110
111 If an entry has only a 'times' set, any of the resources may be
112 reserved without further limitations.
113
114 ResourcesPool may also be selected with narrowed down constraints.
Neels Hofmeyr76d81032017-05-18 18:35:32 +0200115 This would reserve one IP address, two modems, one BTS of type
Neels Hofmeyr391afe32017-05-18 19:22:12 +0200116 sysmo and one of type trx, plus 2 ARFCNs in the 1800 band:
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200117
118 {
Neels Hofmeyr76d81032017-05-18 18:35:32 +0200119 'ip_address': [ { 'times': 1 } ],
Neels Hofmeyr391afe32017-05-18 19:22:12 +0200120 'bts': [ { 'type': 'sysmo', 'times': 1 }, { 'type': 'trx', 'times': 1 } ],
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200121 'arfcn': [ { 'band': 'GSM-1800', 'times': 2 } ],
122 'modem': [ { 'times': 2 } ],
123 }
124
125 A times=1 value is implicit, so the above is equivalent to:
126
127 {
Neels Hofmeyr76d81032017-05-18 18:35:32 +0200128 'ip_address': [ {} ],
Neels Hofmeyr391afe32017-05-18 19:22:12 +0200129 'bts': [ { 'type': 'sysmo' }, { 'type': 'trx' } ],
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200130 'arfcn': [ { 'band': 'GSM-1800', 'times': 2 } ],
131 'modem': [ { 'times': 2 } ],
132 }
133 '''
134 schema.validate(want, WANT_SCHEMA)
135
136 # replicate items that have a 'times' > 1
137 want = copy.deepcopy(want)
138 for key, item_list in want.items():
139 more_items = []
140 for item in item_list:
141 times = int(item.pop('times'))
142 if times and times > 1:
143 for i in range(times - 1):
144 more_items.append(copy.deepcopy(item))
145 item_list.extend(more_items)
146
147 origin_id = origin.origin_id()
148
149 with self.state_dir.lock(origin_id):
150 rrfile_path = self.state_dir.mk_parentdir(RESERVED_RESOURCES_FILE)
151 reserved = Resources(config.read(rrfile_path, if_missing_return={}))
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200152 to_be_reserved = self.all_resources.without(reserved).find(origin, want)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200153
154 to_be_reserved.mark_reserved_by(origin_id)
155
156 reserved.add(to_be_reserved)
157 config.write(rrfile_path, reserved)
158
159 self.remember_to_free(to_be_reserved)
160 return ReservedResources(self, origin, to_be_reserved)
161
162 def free(self, origin, to_be_freed):
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200163 log.ctx(origin)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200164 with self.state_dir.lock(origin.origin_id()):
165 rrfile_path = self.state_dir.mk_parentdir(RESERVED_RESOURCES_FILE)
166 reserved = Resources(config.read(rrfile_path, if_missing_return={}))
167 reserved.drop(to_be_freed)
168 config.write(rrfile_path, reserved)
169 self.forget_freed(to_be_freed)
170
171 def register_exit_handler(self):
172 if self._registered_exit_handler:
173 return
174 atexit.register(self.clean_up_registered_resources)
175 self._registered_exit_handler = True
176
177 def unregister_exit_handler(self):
178 if not self._registered_exit_handler:
179 return
180 atexit.unregister(self.clean_up_registered_resources)
181 self._registered_exit_handler = False
182
183 def clean_up_registered_resources(self):
184 if not self._remember_to_free:
185 return
186 self.free(log.Origin('atexit.clean_up_registered_resources()'),
187 self._remember_to_free)
188
189 def remember_to_free(self, to_be_reserved):
190 self.register_exit_handler()
191 if not self._remember_to_free:
192 self._remember_to_free = Resources()
193 self._remember_to_free.add(to_be_reserved)
194
195 def forget_freed(self, freed):
196 if freed is self._remember_to_free:
197 self._remember_to_free.clear()
198 else:
199 self._remember_to_free.drop(freed)
200 if not self._remember_to_free:
201 self.unregister_exit_handler()
202
203 def next_msisdn(self, origin):
204 origin_id = origin.origin_id()
205
206 with self.state_dir.lock(origin_id):
207 msisdn_path = self.state_dir.child(LAST_USED_MSISDN_FILE)
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200208 log.ctx(msisdn_path)
209 last_msisdn = '1000'
210 if os.path.exists(msisdn_path):
211 if not os.path.isfile(msisdn_path):
212 raise RuntimeError('path should be a file but is not: %r' % msisdn_path)
213 with open(msisdn_path, 'r') as f:
214 last_msisdn = f.read().strip()
215 schema.msisdn(last_msisdn)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200216
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200217 next_msisdn = util.msisdn_inc(last_msisdn)
218 with open(msisdn_path, 'w') as f:
219 f.write(next_msisdn)
220 return next_msisdn
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200221
222
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200223class NoResourceExn(Exception):
224 pass
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200225
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200226class Resources(dict):
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200227
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200228 def __init__(self, all_resources={}, do_copy=True):
229 if do_copy:
230 all_resources = copy.deepcopy(all_resources)
231 self.update(all_resources)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200232
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200233 def drop(self, reserved, fail_if_not_found=True):
234 # protect from modifying reserved because we're the same object
235 if reserved is self:
236 raise RuntimeError('Refusing to drop a list of resources from itself.'
237 ' This is probably a bug where a list of Resources()'
238 ' should have been copied but is passed as-is.'
239 ' use Resources.clear() instead.')
240
241 for key, reserved_list in reserved.items():
242 my_list = self.get(key) or []
243
244 if my_list is reserved_list:
245 self.pop(key)
246 continue
247
248 for reserved_item in reserved_list:
249 found = False
250 reserved_hash = reserved_item.get(HASH_KEY)
251 if not reserved_hash:
252 raise RuntimeError('Resources.drop() only works with hashed items')
253
254 for i in range(len(my_list)):
255 my_item = my_list[i]
256 my_hash = my_item.get(HASH_KEY)
257 if not my_hash:
258 raise RuntimeError('Resources.drop() only works with hashed items')
259 if my_hash == reserved_hash:
260 found = True
261 my_list.pop(i)
262 break
263
264 if fail_if_not_found and not found:
265 raise RuntimeError('Asked to drop resource from a pool, but the'
266 ' resource was not found: %s = %r' % (key, reserved_item))
267
268 if not my_list:
269 self.pop(key)
270 return self
271
272 def without(self, reserved):
273 return Resources(self).drop(reserved)
274
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200275 def find(self, for_origin, want, skip_if_marked=None, do_copy=True, raise_if_missing=True, log_label='Reserving'):
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200276 '''
277 Pass a dict of resource requirements, e.g.:
278 want = {
279 'bts': [ {'type': 'osmo-bts-sysmo',}, {} ],
280 'modem': [ {'times': 3} ]
281 }
282 This function tries to find a combination from the available resources that
283 matches these requiremens. The returnvalue is a dict (wrapped in a Resources class)
284 that contains the matching resources in the order of 'want' dict: in above
285 example, the returned dict would have a 'bts' list with the first item being
286 a sysmoBTS, the second item being any other available BTS.
287
288 If skip_if_marked is passed, any resource that contains this key is skipped.
289 E.g. if a BTS has the USED_KEY set like
290 reserved_resources = { 'bts' : {..., '_used': True} }
291 then this may be skipped by passing skip_if_marked='_used'
292 (or rather skip_if_marked=USED_KEY).
293
294 If do_copy is True, the returned dict is a deep copy and does not share
295 lists with any other Resources dict.
296
297 If raise_if_missing is False, this will return an empty item for any
298 resource that had no match, instead of immediately raising an exception.
299 '''
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200300 matches = {}
Neels Hofmeyr17c139e2017-04-12 02:42:02 +0200301 for key, want_list in sorted(want.items()): # sorted for deterministic test results
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200302 # here we have a resource of a given type, e.g. 'bts', with a list
303 # containing as many BTSes as the caller wants to reserve/use. Each
304 # list item contains specifics for the particular BTS.
Neels Hofmeyr2a1a1fa2017-05-29 01:36:21 +0200305 my_list = self.get(key, [])
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200306
Neels Hofmeyrf9e86932017-06-06 19:47:40 +0200307 if log_label:
308 for_origin.log(log_label, len(want_list), 'x', key, '(candidates: %d)'%len(my_list))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200309
310 # Try to avoid a less constrained item snatching away a resource
311 # from a more detailed constrained requirement.
312
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200313 # first record all matches, so that each requested item has a list
314 # of all available resources that match it. Some resources may
315 # appear for multiple requested items. Store matching indexes.
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200316 all_matches = []
317 for want_item in want_list:
318 item_match_list = []
319 for i in range(len(my_list)):
320 my_item = my_list[i]
321 if skip_if_marked and my_item.get(skip_if_marked):
322 continue
323 if item_matches(my_item, want_item, ignore_keys=('times',)):
324 item_match_list.append(i)
325 if not item_match_list:
Neels Hofmeyr9b907702017-05-06 23:20:33 +0200326 if raise_if_missing:
327 raise NoResourceExn('No matching resource available for %s = %r'
328 % (key, want_item))
329 else:
330 # this one failed... see below
331 all_matches = []
332 break
333
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200334 all_matches.append( item_match_list )
335
336 if not all_matches:
Neels Hofmeyr9b907702017-05-06 23:20:33 +0200337 # ...this one failed. Makes no sense to solve resource
338 # allocations, return an empty list for this key to mark
339 # failure.
340 matches[key] = []
341 continue
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200342
343 # figure out who gets what
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200344 try:
345 solution = solve(all_matches)
346 except NotSolvable:
347 # instead of a cryptic error message, raise an exception that
348 # conveys meaning to the user.
349 raise NoResourceExn('Could not resolve request to reserve resources: '
350 '%d x %s with requirements: %r' % (len(want_list), key, want_list))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200351 picked = [ my_list[i] for i in solution if i is not None ]
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200352 for_origin.dbg('Picked', config.tostr(picked))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200353 matches[key] = picked
354
355 return Resources(matches, do_copy=do_copy)
356
357 def set_hashes(self):
358 for key, item_list in self.items():
359 for item in item_list:
360 item[HASH_KEY] = util.hash_obj(item, HASH_KEY, RESERVED_KEY, USED_KEY)
361
362 def add(self, more):
363 if more is self:
364 raise RuntimeError('adding a list of resources to itself?')
365 config.add(self, copy.deepcopy(more))
366
367 def combine(self, more_rules):
368 if more_rules is self:
369 raise RuntimeError('combining a list of resource rules with itself?')
370 config.combine(self, copy.deepcopy(more))
371
372 def mark_reserved_by(self, origin_id):
373 for key, item_list in self.items():
374 for item in item_list:
375 item[RESERVED_KEY] = origin_id
376
377
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200378class NotSolvable(Exception):
379 pass
380
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200381def solve(all_matches):
382 '''
383 all_matches shall be a list of index-lists.
384 all_matches[i] is the list of indexes that item i can use.
385 Return a solution so that each i gets a different index.
386 solve([ [0, 1, 2],
387 [0],
388 [0, 2] ]) == [1, 0, 2]
389 '''
390
391 def all_differ(l):
392 return len(set(l)) == len(l)
393
394 def search_in_permutations(fixed=[]):
395 idx = len(fixed)
396 for i in range(len(all_matches[idx])):
397 val = all_matches[idx][i]
398 # don't add a val that's already in the list
399 if val in fixed:
400 continue
401 l = list(fixed)
402 l.append(val)
403 if len(l) == len(all_matches):
404 # found a solution
405 return l
406 # not at the end yet, add next digit
407 r = search_in_permutations(l)
408 if r:
409 # nested search_in_permutations() call found a solution
410 return r
411 # this entire branch yielded no solution
412 return None
413
414 if not all_matches:
415 raise RuntimeError('Cannot solve: no candidates')
416
417 solution = search_in_permutations()
418 if not solution:
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200419 raise NotSolvable('The requested resource requirements are not solvable %r'
420 % all_matches)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200421 return solution
422
423
424def contains_hash(list_of_dicts, a_hash):
425 for d in list_of_dicts:
426 if d.get(HASH_KEY) == a_hash:
427 return True
428 return False
429
430def item_matches(item, wanted_item, ignore_keys=None):
431 if is_dict(wanted_item):
432 # match up two dicts
433 if not isinstance(item, dict):
434 return False
435 for key, wanted_val in wanted_item.items():
436 if ignore_keys and key in ignore_keys:
437 continue
438 if not item_matches(item.get(key), wanted_val, ignore_keys=ignore_keys):
439 return False
440 return True
441
442 if is_list(wanted_item):
443 # multiple possible values
444 if item not in wanted_item:
445 return False
446 return True
447
448 return item == wanted_item
449
450
451class ReservedResources(log.Origin):
452 '''
453 After all resources have been figured out, this is the API that a test case
454 gets to interact with resources. From those resources that have been
455 reserved for it, it can pick some to mark them as currently in use.
456 Functions like nitb() provide a resource by automatically picking its
457 dependencies from so far unused (but reserved) resource.
458 '''
459
460 def __init__(self, resources_pool, origin, reserved):
461 self.resources_pool = resources_pool
462 self.origin = origin
463 self.reserved = reserved
464
465 def __repr__(self):
466 return 'resources(%s)=%s' % (self.origin.name(), pprint.pformat(self.reserved))
467
468 def get(self, kind, specifics=None):
469 if specifics is None:
470 specifics = {}
471 self.dbg('requesting use of', kind, specifics=specifics)
472 want = { kind: [specifics] }
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200473 available_dict = self.reserved.find(self.origin, want, skip_if_marked=USED_KEY,
474 do_copy=False, raise_if_missing=False,
475 log_label='Using')
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200476 available = available_dict.get(kind)
477 self.dbg(available=len(available))
478 if not available:
Neels Hofmeyrf9e86932017-06-06 19:47:40 +0200479 # cook up a detailed error message for the current situation
480 kind_reserved = self.reserved.get(kind, [])
481 used_count = len([r for r in kind_reserved if USED_KEY in r])
482 matching = self.reserved.find(self.origin, want, raise_if_missing=False, log_label=None).get(kind, [])
483 if not matching:
484 msg = 'none of the reserved resources matches requirements %r' % specifics
485 elif not (used_count < len(kind_reserved)):
486 msg = 'suite.conf reserved only %d x %r.' % (len(kind_reserved), kind)
487 else:
488 msg = ('No unused resource left that matches the requirements;'
489 ' Of reserved %d x %r, %d match the requirements, but all are already in use;'
490 ' Requirements: %r'
491 % (len(kind_reserved), kind, len(matching), specifics))
492 raise NoResourceExn('When trying to use instance nr %d of %r: %s' % (used_count + 1, kind, msg))
493
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200494 pick = available[0]
495 self.dbg(using=pick)
496 assert not pick.get(USED_KEY)
497 pick[USED_KEY] = True
498 return copy.deepcopy(pick)
499
500 def put(self, item):
501 if not item.get(USED_KEY):
502 raise RuntimeError('Can only put() a resource that is used: %r' % item)
503 hash_to_put = item.get(HASH_KEY)
504 if not hash_to_put:
505 raise RuntimeError('Can only put() a resource that has a hash marker: %r' % item)
506 for key, item_list in self.reserved.items():
507 my_list = self.get(key)
508 for my_item in my_list:
509 if hash_to_put == my_item.get(HASH_KEY):
510 my_item.pop(USED_KEY)
511
512 def put_all(self):
Pau Espin Pedrol1dd29552017-06-13 18:07:57 +0200513 if not self.reserved:
514 return
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200515 for key, item_list in self.reserved.items():
Pau Espin Pedrol1dd29552017-06-13 18:07:57 +0200516 for item in item_list:
517 item.pop(USED_KEY, None)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200518
519 def free(self):
Neels Hofmeyred4e5282017-05-29 02:53:54 +0200520 if self.reserved:
521 self.resources_pool.free(self.origin, self.reserved)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200522 self.reserved = None
523
Neels Hofmeyr2d1d5612017-05-22 20:02:41 +0200524 def counts(self):
525 counts = {}
526 for key in self.reserved.keys():
527 counts[key] = self.count(key)
528 return counts
529
530 def count(self, key):
531 return len(self.reserved.get(key) or [])
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200532
533# vim: expandtab tabstop=4 shiftwidth=4