blob: da543f76250f5ed7a53c5520a4de621e309a2c4d [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,
Pau Espin Pedrol713ce2c2017-08-24 16:57:17 +020067 'modem[].auth_algo': schema.AUTH_ALGO,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020068 }
69
70WANT_SCHEMA = util.dict_add(
71 dict([('%s[].times' % r, schema.INT) for r in R_ALL]),
72 RESOURCES_SCHEMA)
73
74KNOWN_BTS_TYPES = {
Your Name44af3412017-04-13 03:11:59 +020075 'osmo-bts-sysmo': bts_sysmo.SysmoBts,
76 'osmo-bts-trx': bts_osmotrx.OsmoBtsTrx,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020077 }
78
79def register_bts_type(name, clazz):
80 KNOWN_BTS_TYPES[name] = clazz
81
82class ResourcesPool(log.Origin):
83 _remember_to_free = None
84 _registered_exit_handler = False
85
86 def __init__(self):
87 self.config_path = config.get_config_file(RESOURCES_CONF)
88 self.state_dir = config.get_state_dir()
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +020089 super().__init__(log.C_CNF, conf=self.config_path, state=self.state_dir.path)
Neels Hofmeyr3531a192017-03-28 14:30:28 +020090 self.read_conf()
91
92 def read_conf(self):
93 self.all_resources = Resources(config.read(self.config_path, RESOURCES_SCHEMA))
94 self.all_resources.set_hashes()
95
96 def reserve(self, origin, want):
97 '''
98 attempt to reserve the resources specified in the dict 'want' for
99 'origin'. Obtain a lock on the resources lock dir, verify that all
100 wanted resources are available, and if yes mark them as reserved.
101
102 On success, return a reservation object which can be used to release
103 the reservation. The reservation will be freed automatically on program
104 exit, if not yet done manually.
105
106 'origin' should be an Origin() instance.
107
108 'want' is a dict matching WANT_SCHEMA, which is the same as
109 the RESOURCES_SCHEMA, except each entity that can be reserved has a 'times'
110 field added, to indicate how many of those should be reserved.
111
112 If an entry has only a 'times' set, any of the resources may be
113 reserved without further limitations.
114
115 ResourcesPool may also be selected with narrowed down constraints.
Neels Hofmeyr76d81032017-05-18 18:35:32 +0200116 This would reserve one IP address, two modems, one BTS of type
Neels Hofmeyr391afe32017-05-18 19:22:12 +0200117 sysmo and one of type trx, plus 2 ARFCNs in the 1800 band:
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200118
119 {
Neels Hofmeyr76d81032017-05-18 18:35:32 +0200120 'ip_address': [ { 'times': 1 } ],
Neels Hofmeyr391afe32017-05-18 19:22:12 +0200121 'bts': [ { 'type': 'sysmo', 'times': 1 }, { 'type': 'trx', 'times': 1 } ],
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200122 'arfcn': [ { 'band': 'GSM-1800', 'times': 2 } ],
123 'modem': [ { 'times': 2 } ],
124 }
125
126 A times=1 value is implicit, so the above is equivalent to:
127
128 {
Neels Hofmeyr76d81032017-05-18 18:35:32 +0200129 'ip_address': [ {} ],
Neels Hofmeyr391afe32017-05-18 19:22:12 +0200130 'bts': [ { 'type': 'sysmo' }, { 'type': 'trx' } ],
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200131 'arfcn': [ { 'band': 'GSM-1800', 'times': 2 } ],
132 'modem': [ { 'times': 2 } ],
133 }
134 '''
135 schema.validate(want, WANT_SCHEMA)
136
137 # replicate items that have a 'times' > 1
138 want = copy.deepcopy(want)
139 for key, item_list in want.items():
140 more_items = []
141 for item in item_list:
142 times = int(item.pop('times'))
143 if times and times > 1:
144 for i in range(times - 1):
145 more_items.append(copy.deepcopy(item))
146 item_list.extend(more_items)
147
148 origin_id = origin.origin_id()
149
150 with self.state_dir.lock(origin_id):
151 rrfile_path = self.state_dir.mk_parentdir(RESERVED_RESOURCES_FILE)
152 reserved = Resources(config.read(rrfile_path, if_missing_return={}))
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200153 to_be_reserved = self.all_resources.without(reserved).find(origin, want)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200154
155 to_be_reserved.mark_reserved_by(origin_id)
156
157 reserved.add(to_be_reserved)
158 config.write(rrfile_path, reserved)
159
160 self.remember_to_free(to_be_reserved)
161 return ReservedResources(self, origin, to_be_reserved)
162
163 def free(self, origin, to_be_freed):
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200164 log.ctx(origin)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200165 with self.state_dir.lock(origin.origin_id()):
166 rrfile_path = self.state_dir.mk_parentdir(RESERVED_RESOURCES_FILE)
167 reserved = Resources(config.read(rrfile_path, if_missing_return={}))
168 reserved.drop(to_be_freed)
169 config.write(rrfile_path, reserved)
170 self.forget_freed(to_be_freed)
171
172 def register_exit_handler(self):
173 if self._registered_exit_handler:
174 return
175 atexit.register(self.clean_up_registered_resources)
176 self._registered_exit_handler = True
177
178 def unregister_exit_handler(self):
179 if not self._registered_exit_handler:
180 return
181 atexit.unregister(self.clean_up_registered_resources)
182 self._registered_exit_handler = False
183
184 def clean_up_registered_resources(self):
185 if not self._remember_to_free:
186 return
187 self.free(log.Origin('atexit.clean_up_registered_resources()'),
188 self._remember_to_free)
189
190 def remember_to_free(self, to_be_reserved):
191 self.register_exit_handler()
192 if not self._remember_to_free:
193 self._remember_to_free = Resources()
194 self._remember_to_free.add(to_be_reserved)
195
196 def forget_freed(self, freed):
197 if freed is self._remember_to_free:
198 self._remember_to_free.clear()
199 else:
200 self._remember_to_free.drop(freed)
201 if not self._remember_to_free:
202 self.unregister_exit_handler()
203
204 def next_msisdn(self, origin):
205 origin_id = origin.origin_id()
206
207 with self.state_dir.lock(origin_id):
208 msisdn_path = self.state_dir.child(LAST_USED_MSISDN_FILE)
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200209 log.ctx(msisdn_path)
210 last_msisdn = '1000'
211 if os.path.exists(msisdn_path):
212 if not os.path.isfile(msisdn_path):
213 raise RuntimeError('path should be a file but is not: %r' % msisdn_path)
214 with open(msisdn_path, 'r') as f:
215 last_msisdn = f.read().strip()
216 schema.msisdn(last_msisdn)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200217
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200218 next_msisdn = util.msisdn_inc(last_msisdn)
219 with open(msisdn_path, 'w') as f:
220 f.write(next_msisdn)
221 return next_msisdn
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200222
223
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200224class NoResourceExn(Exception):
225 pass
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200226
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200227class Resources(dict):
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200228
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200229 def __init__(self, all_resources={}, do_copy=True):
230 if do_copy:
231 all_resources = copy.deepcopy(all_resources)
232 self.update(all_resources)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200233
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200234 def drop(self, reserved, fail_if_not_found=True):
235 # protect from modifying reserved because we're the same object
236 if reserved is self:
237 raise RuntimeError('Refusing to drop a list of resources from itself.'
238 ' This is probably a bug where a list of Resources()'
239 ' should have been copied but is passed as-is.'
240 ' use Resources.clear() instead.')
241
242 for key, reserved_list in reserved.items():
243 my_list = self.get(key) or []
244
245 if my_list is reserved_list:
246 self.pop(key)
247 continue
248
249 for reserved_item in reserved_list:
250 found = False
251 reserved_hash = reserved_item.get(HASH_KEY)
252 if not reserved_hash:
253 raise RuntimeError('Resources.drop() only works with hashed items')
254
255 for i in range(len(my_list)):
256 my_item = my_list[i]
257 my_hash = my_item.get(HASH_KEY)
258 if not my_hash:
259 raise RuntimeError('Resources.drop() only works with hashed items')
260 if my_hash == reserved_hash:
261 found = True
262 my_list.pop(i)
263 break
264
265 if fail_if_not_found and not found:
266 raise RuntimeError('Asked to drop resource from a pool, but the'
267 ' resource was not found: %s = %r' % (key, reserved_item))
268
269 if not my_list:
270 self.pop(key)
271 return self
272
273 def without(self, reserved):
274 return Resources(self).drop(reserved)
275
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200276 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 +0200277 '''
278 Pass a dict of resource requirements, e.g.:
279 want = {
280 'bts': [ {'type': 'osmo-bts-sysmo',}, {} ],
281 'modem': [ {'times': 3} ]
282 }
283 This function tries to find a combination from the available resources that
284 matches these requiremens. The returnvalue is a dict (wrapped in a Resources class)
285 that contains the matching resources in the order of 'want' dict: in above
286 example, the returned dict would have a 'bts' list with the first item being
287 a sysmoBTS, the second item being any other available BTS.
288
289 If skip_if_marked is passed, any resource that contains this key is skipped.
290 E.g. if a BTS has the USED_KEY set like
291 reserved_resources = { 'bts' : {..., '_used': True} }
292 then this may be skipped by passing skip_if_marked='_used'
293 (or rather skip_if_marked=USED_KEY).
294
295 If do_copy is True, the returned dict is a deep copy and does not share
296 lists with any other Resources dict.
297
298 If raise_if_missing is False, this will return an empty item for any
299 resource that had no match, instead of immediately raising an exception.
300 '''
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200301 matches = {}
Neels Hofmeyr17c139e2017-04-12 02:42:02 +0200302 for key, want_list in sorted(want.items()): # sorted for deterministic test results
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200303 # here we have a resource of a given type, e.g. 'bts', with a list
304 # containing as many BTSes as the caller wants to reserve/use. Each
305 # list item contains specifics for the particular BTS.
Neels Hofmeyr2a1a1fa2017-05-29 01:36:21 +0200306 my_list = self.get(key, [])
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200307
Neels Hofmeyrf9e86932017-06-06 19:47:40 +0200308 if log_label:
309 for_origin.log(log_label, len(want_list), 'x', key, '(candidates: %d)'%len(my_list))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200310
311 # Try to avoid a less constrained item snatching away a resource
312 # from a more detailed constrained requirement.
313
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200314 # first record all matches, so that each requested item has a list
315 # of all available resources that match it. Some resources may
316 # appear for multiple requested items. Store matching indexes.
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200317 all_matches = []
318 for want_item in want_list:
319 item_match_list = []
320 for i in range(len(my_list)):
321 my_item = my_list[i]
322 if skip_if_marked and my_item.get(skip_if_marked):
323 continue
324 if item_matches(my_item, want_item, ignore_keys=('times',)):
325 item_match_list.append(i)
326 if not item_match_list:
Neels Hofmeyr9b907702017-05-06 23:20:33 +0200327 if raise_if_missing:
328 raise NoResourceExn('No matching resource available for %s = %r'
329 % (key, want_item))
330 else:
331 # this one failed... see below
332 all_matches = []
333 break
334
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200335 all_matches.append( item_match_list )
336
337 if not all_matches:
Neels Hofmeyr9b907702017-05-06 23:20:33 +0200338 # ...this one failed. Makes no sense to solve resource
339 # allocations, return an empty list for this key to mark
340 # failure.
341 matches[key] = []
342 continue
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200343
344 # figure out who gets what
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200345 try:
346 solution = solve(all_matches)
347 except NotSolvable:
348 # instead of a cryptic error message, raise an exception that
349 # conveys meaning to the user.
350 raise NoResourceExn('Could not resolve request to reserve resources: '
351 '%d x %s with requirements: %r' % (len(want_list), key, want_list))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200352 picked = [ my_list[i] for i in solution if i is not None ]
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200353 for_origin.dbg('Picked', config.tostr(picked))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200354 matches[key] = picked
355
356 return Resources(matches, do_copy=do_copy)
357
358 def set_hashes(self):
359 for key, item_list in self.items():
360 for item in item_list:
361 item[HASH_KEY] = util.hash_obj(item, HASH_KEY, RESERVED_KEY, USED_KEY)
362
363 def add(self, more):
364 if more is self:
365 raise RuntimeError('adding a list of resources to itself?')
366 config.add(self, copy.deepcopy(more))
367
368 def combine(self, more_rules):
369 if more_rules is self:
370 raise RuntimeError('combining a list of resource rules with itself?')
371 config.combine(self, copy.deepcopy(more))
372
373 def mark_reserved_by(self, origin_id):
374 for key, item_list in self.items():
375 for item in item_list:
376 item[RESERVED_KEY] = origin_id
377
378
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200379class NotSolvable(Exception):
380 pass
381
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200382def solve(all_matches):
383 '''
384 all_matches shall be a list of index-lists.
385 all_matches[i] is the list of indexes that item i can use.
386 Return a solution so that each i gets a different index.
387 solve([ [0, 1, 2],
388 [0],
389 [0, 2] ]) == [1, 0, 2]
390 '''
391
392 def all_differ(l):
393 return len(set(l)) == len(l)
394
395 def search_in_permutations(fixed=[]):
396 idx = len(fixed)
397 for i in range(len(all_matches[idx])):
398 val = all_matches[idx][i]
399 # don't add a val that's already in the list
400 if val in fixed:
401 continue
402 l = list(fixed)
403 l.append(val)
404 if len(l) == len(all_matches):
405 # found a solution
406 return l
407 # not at the end yet, add next digit
408 r = search_in_permutations(l)
409 if r:
410 # nested search_in_permutations() call found a solution
411 return r
412 # this entire branch yielded no solution
413 return None
414
415 if not all_matches:
416 raise RuntimeError('Cannot solve: no candidates')
417
418 solution = search_in_permutations()
419 if not solution:
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200420 raise NotSolvable('The requested resource requirements are not solvable %r'
421 % all_matches)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200422 return solution
423
424
425def contains_hash(list_of_dicts, a_hash):
426 for d in list_of_dicts:
427 if d.get(HASH_KEY) == a_hash:
428 return True
429 return False
430
431def item_matches(item, wanted_item, ignore_keys=None):
432 if is_dict(wanted_item):
433 # match up two dicts
434 if not isinstance(item, dict):
435 return False
436 for key, wanted_val in wanted_item.items():
437 if ignore_keys and key in ignore_keys:
438 continue
439 if not item_matches(item.get(key), wanted_val, ignore_keys=ignore_keys):
440 return False
441 return True
442
443 if is_list(wanted_item):
Pau Espin Pedrol4e36f7c2017-08-28 13:29:28 +0200444 if not is_list(item):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200445 return False
Pau Espin Pedrol4e36f7c2017-08-28 13:29:28 +0200446 # multiple possible values
447 for val in wanted_item:
448 if val not in item:
449 return False
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200450 return True
451
452 return item == wanted_item
453
454
455class ReservedResources(log.Origin):
456 '''
457 After all resources have been figured out, this is the API that a test case
458 gets to interact with resources. From those resources that have been
459 reserved for it, it can pick some to mark them as currently in use.
460 Functions like nitb() provide a resource by automatically picking its
461 dependencies from so far unused (but reserved) resource.
462 '''
463
464 def __init__(self, resources_pool, origin, reserved):
465 self.resources_pool = resources_pool
466 self.origin = origin
467 self.reserved = reserved
468
469 def __repr__(self):
470 return 'resources(%s)=%s' % (self.origin.name(), pprint.pformat(self.reserved))
471
472 def get(self, kind, specifics=None):
473 if specifics is None:
474 specifics = {}
475 self.dbg('requesting use of', kind, specifics=specifics)
476 want = { kind: [specifics] }
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200477 available_dict = self.reserved.find(self.origin, want, skip_if_marked=USED_KEY,
478 do_copy=False, raise_if_missing=False,
479 log_label='Using')
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200480 available = available_dict.get(kind)
481 self.dbg(available=len(available))
482 if not available:
Neels Hofmeyrf9e86932017-06-06 19:47:40 +0200483 # cook up a detailed error message for the current situation
484 kind_reserved = self.reserved.get(kind, [])
485 used_count = len([r for r in kind_reserved if USED_KEY in r])
486 matching = self.reserved.find(self.origin, want, raise_if_missing=False, log_label=None).get(kind, [])
487 if not matching:
488 msg = 'none of the reserved resources matches requirements %r' % specifics
489 elif not (used_count < len(kind_reserved)):
490 msg = 'suite.conf reserved only %d x %r.' % (len(kind_reserved), kind)
491 else:
492 msg = ('No unused resource left that matches the requirements;'
493 ' Of reserved %d x %r, %d match the requirements, but all are already in use;'
494 ' Requirements: %r'
495 % (len(kind_reserved), kind, len(matching), specifics))
496 raise NoResourceExn('When trying to use instance nr %d of %r: %s' % (used_count + 1, kind, msg))
497
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200498 pick = available[0]
499 self.dbg(using=pick)
500 assert not pick.get(USED_KEY)
501 pick[USED_KEY] = True
502 return copy.deepcopy(pick)
503
504 def put(self, item):
505 if not item.get(USED_KEY):
506 raise RuntimeError('Can only put() a resource that is used: %r' % item)
507 hash_to_put = item.get(HASH_KEY)
508 if not hash_to_put:
509 raise RuntimeError('Can only put() a resource that has a hash marker: %r' % item)
510 for key, item_list in self.reserved.items():
511 my_list = self.get(key)
512 for my_item in my_list:
513 if hash_to_put == my_item.get(HASH_KEY):
514 my_item.pop(USED_KEY)
515
516 def put_all(self):
Pau Espin Pedrol1dd29552017-06-13 18:07:57 +0200517 if not self.reserved:
518 return
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200519 for key, item_list in self.reserved.items():
Pau Espin Pedrol1dd29552017-06-13 18:07:57 +0200520 for item in item_list:
521 item.pop(USED_KEY, None)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200522
523 def free(self):
Neels Hofmeyred4e5282017-05-29 02:53:54 +0200524 if self.reserved:
525 self.resources_pool.free(self.origin, self.reserved)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200526 self.reserved = None
527
Neels Hofmeyr2d1d5612017-05-22 20:02:41 +0200528 def counts(self):
529 counts = {}
530 for key in self.reserved.keys():
531 counts[key] = self.count(key)
532 return counts
533
534 def count(self, key):
535 return len(self.reserved.get(key) or [])
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200536
537# vim: expandtab tabstop=4 shiftwidth=4