blob: 8f34bdb0cee6566db07496484cf0a1fd621a8edf [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,
Pau Espin Pedrolfa9a6d32017-09-12 15:13:21 +020054 'bts[].ipa_unit_id': schema.UINT,
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(
Pau Espin Pedrolead79ac2017-09-12 15:19:18 +020071 dict([('%s[].times' % r, schema.TIMES) for r in R_ALL]),
Neels Hofmeyr3531a192017-03-28 14:30:28 +020072 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
Pau Espin Pedrol802dfe52017-09-12 13:43:40 +0200137 want = config.replicate_times(want)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200138
139 origin_id = origin.origin_id()
140
141 with self.state_dir.lock(origin_id):
142 rrfile_path = self.state_dir.mk_parentdir(RESERVED_RESOURCES_FILE)
143 reserved = Resources(config.read(rrfile_path, if_missing_return={}))
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200144 to_be_reserved = self.all_resources.without(reserved).find(origin, want)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200145
146 to_be_reserved.mark_reserved_by(origin_id)
147
148 reserved.add(to_be_reserved)
149 config.write(rrfile_path, reserved)
150
151 self.remember_to_free(to_be_reserved)
152 return ReservedResources(self, origin, to_be_reserved)
153
154 def free(self, origin, to_be_freed):
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200155 log.ctx(origin)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200156 with self.state_dir.lock(origin.origin_id()):
157 rrfile_path = self.state_dir.mk_parentdir(RESERVED_RESOURCES_FILE)
158 reserved = Resources(config.read(rrfile_path, if_missing_return={}))
159 reserved.drop(to_be_freed)
160 config.write(rrfile_path, reserved)
161 self.forget_freed(to_be_freed)
162
163 def register_exit_handler(self):
164 if self._registered_exit_handler:
165 return
166 atexit.register(self.clean_up_registered_resources)
167 self._registered_exit_handler = True
168
169 def unregister_exit_handler(self):
170 if not self._registered_exit_handler:
171 return
172 atexit.unregister(self.clean_up_registered_resources)
173 self._registered_exit_handler = False
174
175 def clean_up_registered_resources(self):
176 if not self._remember_to_free:
177 return
178 self.free(log.Origin('atexit.clean_up_registered_resources()'),
179 self._remember_to_free)
180
181 def remember_to_free(self, to_be_reserved):
182 self.register_exit_handler()
183 if not self._remember_to_free:
184 self._remember_to_free = Resources()
185 self._remember_to_free.add(to_be_reserved)
186
187 def forget_freed(self, freed):
188 if freed is self._remember_to_free:
189 self._remember_to_free.clear()
190 else:
191 self._remember_to_free.drop(freed)
192 if not self._remember_to_free:
193 self.unregister_exit_handler()
194
195 def next_msisdn(self, origin):
196 origin_id = origin.origin_id()
197
198 with self.state_dir.lock(origin_id):
199 msisdn_path = self.state_dir.child(LAST_USED_MSISDN_FILE)
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200200 log.ctx(msisdn_path)
201 last_msisdn = '1000'
202 if os.path.exists(msisdn_path):
203 if not os.path.isfile(msisdn_path):
204 raise RuntimeError('path should be a file but is not: %r' % msisdn_path)
205 with open(msisdn_path, 'r') as f:
206 last_msisdn = f.read().strip()
207 schema.msisdn(last_msisdn)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200208
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200209 next_msisdn = util.msisdn_inc(last_msisdn)
210 with open(msisdn_path, 'w') as f:
211 f.write(next_msisdn)
212 return next_msisdn
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200213
214
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200215class NoResourceExn(Exception):
216 pass
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200217
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200218class Resources(dict):
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200219
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200220 def __init__(self, all_resources={}, do_copy=True):
221 if do_copy:
222 all_resources = copy.deepcopy(all_resources)
223 self.update(all_resources)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200224
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200225 def drop(self, reserved, fail_if_not_found=True):
226 # protect from modifying reserved because we're the same object
227 if reserved is self:
228 raise RuntimeError('Refusing to drop a list of resources from itself.'
229 ' This is probably a bug where a list of Resources()'
230 ' should have been copied but is passed as-is.'
231 ' use Resources.clear() instead.')
232
233 for key, reserved_list in reserved.items():
234 my_list = self.get(key) or []
235
236 if my_list is reserved_list:
237 self.pop(key)
238 continue
239
240 for reserved_item in reserved_list:
241 found = False
242 reserved_hash = reserved_item.get(HASH_KEY)
243 if not reserved_hash:
244 raise RuntimeError('Resources.drop() only works with hashed items')
245
246 for i in range(len(my_list)):
247 my_item = my_list[i]
248 my_hash = my_item.get(HASH_KEY)
249 if not my_hash:
250 raise RuntimeError('Resources.drop() only works with hashed items')
251 if my_hash == reserved_hash:
252 found = True
253 my_list.pop(i)
254 break
255
256 if fail_if_not_found and not found:
257 raise RuntimeError('Asked to drop resource from a pool, but the'
258 ' resource was not found: %s = %r' % (key, reserved_item))
259
260 if not my_list:
261 self.pop(key)
262 return self
263
264 def without(self, reserved):
265 return Resources(self).drop(reserved)
266
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200267 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 +0200268 '''
269 Pass a dict of resource requirements, e.g.:
270 want = {
271 'bts': [ {'type': 'osmo-bts-sysmo',}, {} ],
272 'modem': [ {'times': 3} ]
273 }
274 This function tries to find a combination from the available resources that
275 matches these requiremens. The returnvalue is a dict (wrapped in a Resources class)
276 that contains the matching resources in the order of 'want' dict: in above
277 example, the returned dict would have a 'bts' list with the first item being
278 a sysmoBTS, the second item being any other available BTS.
279
280 If skip_if_marked is passed, any resource that contains this key is skipped.
281 E.g. if a BTS has the USED_KEY set like
282 reserved_resources = { 'bts' : {..., '_used': True} }
283 then this may be skipped by passing skip_if_marked='_used'
284 (or rather skip_if_marked=USED_KEY).
285
286 If do_copy is True, the returned dict is a deep copy and does not share
287 lists with any other Resources dict.
288
289 If raise_if_missing is False, this will return an empty item for any
290 resource that had no match, instead of immediately raising an exception.
291 '''
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200292 matches = {}
Neels Hofmeyr17c139e2017-04-12 02:42:02 +0200293 for key, want_list in sorted(want.items()): # sorted for deterministic test results
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200294 # here we have a resource of a given type, e.g. 'bts', with a list
295 # containing as many BTSes as the caller wants to reserve/use. Each
296 # list item contains specifics for the particular BTS.
Neels Hofmeyr2a1a1fa2017-05-29 01:36:21 +0200297 my_list = self.get(key, [])
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200298
Neels Hofmeyrf9e86932017-06-06 19:47:40 +0200299 if log_label:
300 for_origin.log(log_label, len(want_list), 'x', key, '(candidates: %d)'%len(my_list))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200301
302 # Try to avoid a less constrained item snatching away a resource
303 # from a more detailed constrained requirement.
304
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200305 # first record all matches, so that each requested item has a list
306 # of all available resources that match it. Some resources may
307 # appear for multiple requested items. Store matching indexes.
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200308 all_matches = []
309 for want_item in want_list:
310 item_match_list = []
311 for i in range(len(my_list)):
312 my_item = my_list[i]
313 if skip_if_marked and my_item.get(skip_if_marked):
314 continue
315 if item_matches(my_item, want_item, ignore_keys=('times',)):
316 item_match_list.append(i)
317 if not item_match_list:
Neels Hofmeyr9b907702017-05-06 23:20:33 +0200318 if raise_if_missing:
319 raise NoResourceExn('No matching resource available for %s = %r'
320 % (key, want_item))
321 else:
322 # this one failed... see below
323 all_matches = []
324 break
325
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200326 all_matches.append( item_match_list )
327
328 if not all_matches:
Neels Hofmeyr9b907702017-05-06 23:20:33 +0200329 # ...this one failed. Makes no sense to solve resource
330 # allocations, return an empty list for this key to mark
331 # failure.
332 matches[key] = []
333 continue
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200334
335 # figure out who gets what
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200336 try:
337 solution = solve(all_matches)
338 except NotSolvable:
339 # instead of a cryptic error message, raise an exception that
340 # conveys meaning to the user.
341 raise NoResourceExn('Could not resolve request to reserve resources: '
342 '%d x %s with requirements: %r' % (len(want_list), key, want_list))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200343 picked = [ my_list[i] for i in solution if i is not None ]
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200344 for_origin.dbg('Picked', config.tostr(picked))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200345 matches[key] = picked
346
347 return Resources(matches, do_copy=do_copy)
348
349 def set_hashes(self):
350 for key, item_list in self.items():
351 for item in item_list:
352 item[HASH_KEY] = util.hash_obj(item, HASH_KEY, RESERVED_KEY, USED_KEY)
353
354 def add(self, more):
355 if more is self:
356 raise RuntimeError('adding a list of resources to itself?')
357 config.add(self, copy.deepcopy(more))
358
359 def combine(self, more_rules):
360 if more_rules is self:
361 raise RuntimeError('combining a list of resource rules with itself?')
362 config.combine(self, copy.deepcopy(more))
363
364 def mark_reserved_by(self, origin_id):
365 for key, item_list in self.items():
366 for item in item_list:
367 item[RESERVED_KEY] = origin_id
368
369
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200370class NotSolvable(Exception):
371 pass
372
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200373def solve(all_matches):
374 '''
375 all_matches shall be a list of index-lists.
376 all_matches[i] is the list of indexes that item i can use.
377 Return a solution so that each i gets a different index.
378 solve([ [0, 1, 2],
379 [0],
380 [0, 2] ]) == [1, 0, 2]
381 '''
382
383 def all_differ(l):
384 return len(set(l)) == len(l)
385
386 def search_in_permutations(fixed=[]):
387 idx = len(fixed)
388 for i in range(len(all_matches[idx])):
389 val = all_matches[idx][i]
390 # don't add a val that's already in the list
391 if val in fixed:
392 continue
393 l = list(fixed)
394 l.append(val)
395 if len(l) == len(all_matches):
396 # found a solution
397 return l
398 # not at the end yet, add next digit
399 r = search_in_permutations(l)
400 if r:
401 # nested search_in_permutations() call found a solution
402 return r
403 # this entire branch yielded no solution
404 return None
405
406 if not all_matches:
407 raise RuntimeError('Cannot solve: no candidates')
408
409 solution = search_in_permutations()
410 if not solution:
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200411 raise NotSolvable('The requested resource requirements are not solvable %r'
412 % all_matches)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200413 return solution
414
415
416def contains_hash(list_of_dicts, a_hash):
417 for d in list_of_dicts:
418 if d.get(HASH_KEY) == a_hash:
419 return True
420 return False
421
422def item_matches(item, wanted_item, ignore_keys=None):
423 if is_dict(wanted_item):
424 # match up two dicts
425 if not isinstance(item, dict):
426 return False
427 for key, wanted_val in wanted_item.items():
428 if ignore_keys and key in ignore_keys:
429 continue
430 if not item_matches(item.get(key), wanted_val, ignore_keys=ignore_keys):
431 return False
432 return True
433
434 if is_list(wanted_item):
Pau Espin Pedrol4e36f7c2017-08-28 13:29:28 +0200435 if not is_list(item):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200436 return False
Pau Espin Pedrol4e36f7c2017-08-28 13:29:28 +0200437 # multiple possible values
438 for val in wanted_item:
439 if val not in item:
440 return False
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200441 return True
442
443 return item == wanted_item
444
445
446class ReservedResources(log.Origin):
447 '''
448 After all resources have been figured out, this is the API that a test case
449 gets to interact with resources. From those resources that have been
450 reserved for it, it can pick some to mark them as currently in use.
451 Functions like nitb() provide a resource by automatically picking its
452 dependencies from so far unused (but reserved) resource.
453 '''
454
455 def __init__(self, resources_pool, origin, reserved):
456 self.resources_pool = resources_pool
457 self.origin = origin
458 self.reserved = reserved
459
460 def __repr__(self):
461 return 'resources(%s)=%s' % (self.origin.name(), pprint.pformat(self.reserved))
462
463 def get(self, kind, specifics=None):
464 if specifics is None:
465 specifics = {}
466 self.dbg('requesting use of', kind, specifics=specifics)
467 want = { kind: [specifics] }
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200468 available_dict = self.reserved.find(self.origin, want, skip_if_marked=USED_KEY,
469 do_copy=False, raise_if_missing=False,
470 log_label='Using')
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200471 available = available_dict.get(kind)
472 self.dbg(available=len(available))
473 if not available:
Neels Hofmeyrf9e86932017-06-06 19:47:40 +0200474 # cook up a detailed error message for the current situation
475 kind_reserved = self.reserved.get(kind, [])
476 used_count = len([r for r in kind_reserved if USED_KEY in r])
477 matching = self.reserved.find(self.origin, want, raise_if_missing=False, log_label=None).get(kind, [])
478 if not matching:
479 msg = 'none of the reserved resources matches requirements %r' % specifics
480 elif not (used_count < len(kind_reserved)):
481 msg = 'suite.conf reserved only %d x %r.' % (len(kind_reserved), kind)
482 else:
483 msg = ('No unused resource left that matches the requirements;'
484 ' Of reserved %d x %r, %d match the requirements, but all are already in use;'
485 ' Requirements: %r'
486 % (len(kind_reserved), kind, len(matching), specifics))
487 raise NoResourceExn('When trying to use instance nr %d of %r: %s' % (used_count + 1, kind, msg))
488
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200489 pick = available[0]
490 self.dbg(using=pick)
491 assert not pick.get(USED_KEY)
492 pick[USED_KEY] = True
493 return copy.deepcopy(pick)
494
495 def put(self, item):
496 if not item.get(USED_KEY):
497 raise RuntimeError('Can only put() a resource that is used: %r' % item)
498 hash_to_put = item.get(HASH_KEY)
499 if not hash_to_put:
500 raise RuntimeError('Can only put() a resource that has a hash marker: %r' % item)
501 for key, item_list in self.reserved.items():
502 my_list = self.get(key)
503 for my_item in my_list:
504 if hash_to_put == my_item.get(HASH_KEY):
505 my_item.pop(USED_KEY)
506
507 def put_all(self):
Pau Espin Pedrol1dd29552017-06-13 18:07:57 +0200508 if not self.reserved:
509 return
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200510 for key, item_list in self.reserved.items():
Pau Espin Pedrol1dd29552017-06-13 18:07:57 +0200511 for item in item_list:
512 item.pop(USED_KEY, None)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200513
514 def free(self):
Neels Hofmeyred4e5282017-05-29 02:53:54 +0200515 if self.reserved:
516 self.resources_pool.free(self.origin, self.reserved)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200517 self.reserved = None
518
Neels Hofmeyr2d1d5612017-05-22 20:02:41 +0200519 def counts(self):
520 counts = {}
521 for key in self.reserved.keys():
522 counts[key] = self.count(key)
523 return counts
524
525 def count(self, key):
526 return len(self.reserved.get(key) or [])
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200527
528# vim: expandtab tabstop=4 shiftwidth=4