blob: 7ba6e72b7b728da24549755e4788b01e347a1a1c [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,
Pau Espin Pedrolb26f32a2017-09-14 13:52:28 +020061 'bts[].trx_list[].nominal_power': schema.UINT,
62 'bts[].trx_list[].max_power_red': schema.UINT,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020063 'arfcn[].arfcn': schema.INT,
64 'arfcn[].band': schema.BAND,
65 'modem[].label': schema.STR,
66 'modem[].path': schema.STR,
67 'modem[].imsi': schema.IMSI,
68 'modem[].ki': schema.KI,
Pau Espin Pedrol713ce2c2017-08-24 16:57:17 +020069 'modem[].auth_algo': schema.AUTH_ALGO,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020070 }
71
72WANT_SCHEMA = util.dict_add(
Pau Espin Pedrolead79ac2017-09-12 15:19:18 +020073 dict([('%s[].times' % r, schema.TIMES) for r in R_ALL]),
Neels Hofmeyr3531a192017-03-28 14:30:28 +020074 RESOURCES_SCHEMA)
75
76KNOWN_BTS_TYPES = {
Your Name44af3412017-04-13 03:11:59 +020077 'osmo-bts-sysmo': bts_sysmo.SysmoBts,
78 'osmo-bts-trx': bts_osmotrx.OsmoBtsTrx,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020079 }
80
81def register_bts_type(name, clazz):
82 KNOWN_BTS_TYPES[name] = clazz
83
84class ResourcesPool(log.Origin):
85 _remember_to_free = None
86 _registered_exit_handler = False
87
88 def __init__(self):
89 self.config_path = config.get_config_file(RESOURCES_CONF)
90 self.state_dir = config.get_state_dir()
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +020091 super().__init__(log.C_CNF, conf=self.config_path, state=self.state_dir.path)
Neels Hofmeyr3531a192017-03-28 14:30:28 +020092 self.read_conf()
93
94 def read_conf(self):
95 self.all_resources = Resources(config.read(self.config_path, RESOURCES_SCHEMA))
96 self.all_resources.set_hashes()
97
98 def reserve(self, origin, want):
99 '''
100 attempt to reserve the resources specified in the dict 'want' for
101 'origin'. Obtain a lock on the resources lock dir, verify that all
102 wanted resources are available, and if yes mark them as reserved.
103
104 On success, return a reservation object which can be used to release
105 the reservation. The reservation will be freed automatically on program
106 exit, if not yet done manually.
107
108 'origin' should be an Origin() instance.
109
110 'want' is a dict matching WANT_SCHEMA, which is the same as
111 the RESOURCES_SCHEMA, except each entity that can be reserved has a 'times'
112 field added, to indicate how many of those should be reserved.
113
114 If an entry has only a 'times' set, any of the resources may be
115 reserved without further limitations.
116
117 ResourcesPool may also be selected with narrowed down constraints.
Neels Hofmeyr76d81032017-05-18 18:35:32 +0200118 This would reserve one IP address, two modems, one BTS of type
Neels Hofmeyr391afe32017-05-18 19:22:12 +0200119 sysmo and one of type trx, plus 2 ARFCNs in the 1800 band:
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200120
121 {
Neels Hofmeyr76d81032017-05-18 18:35:32 +0200122 'ip_address': [ { 'times': 1 } ],
Neels Hofmeyr391afe32017-05-18 19:22:12 +0200123 'bts': [ { 'type': 'sysmo', 'times': 1 }, { 'type': 'trx', 'times': 1 } ],
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200124 'arfcn': [ { 'band': 'GSM-1800', 'times': 2 } ],
125 'modem': [ { 'times': 2 } ],
126 }
127
128 A times=1 value is implicit, so the above is equivalent to:
129
130 {
Neels Hofmeyr76d81032017-05-18 18:35:32 +0200131 'ip_address': [ {} ],
Neels Hofmeyr391afe32017-05-18 19:22:12 +0200132 'bts': [ { 'type': 'sysmo' }, { 'type': 'trx' } ],
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200133 'arfcn': [ { 'band': 'GSM-1800', 'times': 2 } ],
134 'modem': [ { 'times': 2 } ],
135 }
136 '''
137 schema.validate(want, WANT_SCHEMA)
138
Pau Espin Pedrol802dfe52017-09-12 13:43:40 +0200139 want = config.replicate_times(want)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200140
141 origin_id = origin.origin_id()
142
143 with self.state_dir.lock(origin_id):
144 rrfile_path = self.state_dir.mk_parentdir(RESERVED_RESOURCES_FILE)
145 reserved = Resources(config.read(rrfile_path, if_missing_return={}))
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200146 to_be_reserved = self.all_resources.without(reserved).find(origin, want)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200147
148 to_be_reserved.mark_reserved_by(origin_id)
149
150 reserved.add(to_be_reserved)
151 config.write(rrfile_path, reserved)
152
153 self.remember_to_free(to_be_reserved)
154 return ReservedResources(self, origin, to_be_reserved)
155
156 def free(self, origin, to_be_freed):
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200157 log.ctx(origin)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200158 with self.state_dir.lock(origin.origin_id()):
159 rrfile_path = self.state_dir.mk_parentdir(RESERVED_RESOURCES_FILE)
160 reserved = Resources(config.read(rrfile_path, if_missing_return={}))
161 reserved.drop(to_be_freed)
162 config.write(rrfile_path, reserved)
163 self.forget_freed(to_be_freed)
164
165 def register_exit_handler(self):
166 if self._registered_exit_handler:
167 return
168 atexit.register(self.clean_up_registered_resources)
169 self._registered_exit_handler = True
170
171 def unregister_exit_handler(self):
172 if not self._registered_exit_handler:
173 return
174 atexit.unregister(self.clean_up_registered_resources)
175 self._registered_exit_handler = False
176
177 def clean_up_registered_resources(self):
178 if not self._remember_to_free:
179 return
180 self.free(log.Origin('atexit.clean_up_registered_resources()'),
181 self._remember_to_free)
182
183 def remember_to_free(self, to_be_reserved):
184 self.register_exit_handler()
185 if not self._remember_to_free:
186 self._remember_to_free = Resources()
187 self._remember_to_free.add(to_be_reserved)
188
189 def forget_freed(self, freed):
190 if freed is self._remember_to_free:
191 self._remember_to_free.clear()
192 else:
193 self._remember_to_free.drop(freed)
194 if not self._remember_to_free:
195 self.unregister_exit_handler()
196
197 def next_msisdn(self, origin):
198 origin_id = origin.origin_id()
199
200 with self.state_dir.lock(origin_id):
201 msisdn_path = self.state_dir.child(LAST_USED_MSISDN_FILE)
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200202 log.ctx(msisdn_path)
203 last_msisdn = '1000'
204 if os.path.exists(msisdn_path):
205 if not os.path.isfile(msisdn_path):
206 raise RuntimeError('path should be a file but is not: %r' % msisdn_path)
207 with open(msisdn_path, 'r') as f:
208 last_msisdn = f.read().strip()
209 schema.msisdn(last_msisdn)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200210
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200211 next_msisdn = util.msisdn_inc(last_msisdn)
212 with open(msisdn_path, 'w') as f:
213 f.write(next_msisdn)
214 return next_msisdn
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200215
216
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200217class NoResourceExn(Exception):
218 pass
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200219
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200220class Resources(dict):
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200221
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200222 def __init__(self, all_resources={}, do_copy=True):
223 if do_copy:
224 all_resources = copy.deepcopy(all_resources)
225 self.update(all_resources)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200226
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200227 def drop(self, reserved, fail_if_not_found=True):
228 # protect from modifying reserved because we're the same object
229 if reserved is self:
230 raise RuntimeError('Refusing to drop a list of resources from itself.'
231 ' This is probably a bug where a list of Resources()'
232 ' should have been copied but is passed as-is.'
233 ' use Resources.clear() instead.')
234
235 for key, reserved_list in reserved.items():
236 my_list = self.get(key) or []
237
238 if my_list is reserved_list:
239 self.pop(key)
240 continue
241
242 for reserved_item in reserved_list:
243 found = False
244 reserved_hash = reserved_item.get(HASH_KEY)
245 if not reserved_hash:
246 raise RuntimeError('Resources.drop() only works with hashed items')
247
248 for i in range(len(my_list)):
249 my_item = my_list[i]
250 my_hash = my_item.get(HASH_KEY)
251 if not my_hash:
252 raise RuntimeError('Resources.drop() only works with hashed items')
253 if my_hash == reserved_hash:
254 found = True
255 my_list.pop(i)
256 break
257
258 if fail_if_not_found and not found:
259 raise RuntimeError('Asked to drop resource from a pool, but the'
260 ' resource was not found: %s = %r' % (key, reserved_item))
261
262 if not my_list:
263 self.pop(key)
264 return self
265
266 def without(self, reserved):
267 return Resources(self).drop(reserved)
268
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200269 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 +0200270 '''
271 Pass a dict of resource requirements, e.g.:
272 want = {
273 'bts': [ {'type': 'osmo-bts-sysmo',}, {} ],
274 'modem': [ {'times': 3} ]
275 }
276 This function tries to find a combination from the available resources that
277 matches these requiremens. The returnvalue is a dict (wrapped in a Resources class)
278 that contains the matching resources in the order of 'want' dict: in above
279 example, the returned dict would have a 'bts' list with the first item being
280 a sysmoBTS, the second item being any other available BTS.
281
282 If skip_if_marked is passed, any resource that contains this key is skipped.
283 E.g. if a BTS has the USED_KEY set like
284 reserved_resources = { 'bts' : {..., '_used': True} }
285 then this may be skipped by passing skip_if_marked='_used'
286 (or rather skip_if_marked=USED_KEY).
287
288 If do_copy is True, the returned dict is a deep copy and does not share
289 lists with any other Resources dict.
290
291 If raise_if_missing is False, this will return an empty item for any
292 resource that had no match, instead of immediately raising an exception.
293 '''
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200294 matches = {}
Neels Hofmeyr17c139e2017-04-12 02:42:02 +0200295 for key, want_list in sorted(want.items()): # sorted for deterministic test results
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200296 # here we have a resource of a given type, e.g. 'bts', with a list
297 # containing as many BTSes as the caller wants to reserve/use. Each
298 # list item contains specifics for the particular BTS.
Neels Hofmeyr2a1a1fa2017-05-29 01:36:21 +0200299 my_list = self.get(key, [])
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200300
Neels Hofmeyrf9e86932017-06-06 19:47:40 +0200301 if log_label:
302 for_origin.log(log_label, len(want_list), 'x', key, '(candidates: %d)'%len(my_list))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200303
304 # Try to avoid a less constrained item snatching away a resource
305 # from a more detailed constrained requirement.
306
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200307 # first record all matches, so that each requested item has a list
308 # of all available resources that match it. Some resources may
309 # appear for multiple requested items. Store matching indexes.
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200310 all_matches = []
311 for want_item in want_list:
312 item_match_list = []
313 for i in range(len(my_list)):
314 my_item = my_list[i]
315 if skip_if_marked and my_item.get(skip_if_marked):
316 continue
317 if item_matches(my_item, want_item, ignore_keys=('times',)):
318 item_match_list.append(i)
319 if not item_match_list:
Neels Hofmeyr9b907702017-05-06 23:20:33 +0200320 if raise_if_missing:
321 raise NoResourceExn('No matching resource available for %s = %r'
322 % (key, want_item))
323 else:
324 # this one failed... see below
325 all_matches = []
326 break
327
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200328 all_matches.append( item_match_list )
329
330 if not all_matches:
Neels Hofmeyr9b907702017-05-06 23:20:33 +0200331 # ...this one failed. Makes no sense to solve resource
332 # allocations, return an empty list for this key to mark
333 # failure.
334 matches[key] = []
335 continue
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200336
337 # figure out who gets what
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200338 try:
339 solution = solve(all_matches)
340 except NotSolvable:
341 # instead of a cryptic error message, raise an exception that
342 # conveys meaning to the user.
343 raise NoResourceExn('Could not resolve request to reserve resources: '
344 '%d x %s with requirements: %r' % (len(want_list), key, want_list))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200345 picked = [ my_list[i] for i in solution if i is not None ]
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200346 for_origin.dbg('Picked', config.tostr(picked))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200347 matches[key] = picked
348
349 return Resources(matches, do_copy=do_copy)
350
351 def set_hashes(self):
352 for key, item_list in self.items():
353 for item in item_list:
354 item[HASH_KEY] = util.hash_obj(item, HASH_KEY, RESERVED_KEY, USED_KEY)
355
356 def add(self, more):
357 if more is self:
358 raise RuntimeError('adding a list of resources to itself?')
359 config.add(self, copy.deepcopy(more))
360
361 def combine(self, more_rules):
362 if more_rules is self:
363 raise RuntimeError('combining a list of resource rules with itself?')
364 config.combine(self, copy.deepcopy(more))
365
366 def mark_reserved_by(self, origin_id):
367 for key, item_list in self.items():
368 for item in item_list:
369 item[RESERVED_KEY] = origin_id
370
371
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200372class NotSolvable(Exception):
373 pass
374
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200375def solve(all_matches):
376 '''
377 all_matches shall be a list of index-lists.
378 all_matches[i] is the list of indexes that item i can use.
379 Return a solution so that each i gets a different index.
380 solve([ [0, 1, 2],
381 [0],
382 [0, 2] ]) == [1, 0, 2]
383 '''
384
385 def all_differ(l):
386 return len(set(l)) == len(l)
387
388 def search_in_permutations(fixed=[]):
389 idx = len(fixed)
390 for i in range(len(all_matches[idx])):
391 val = all_matches[idx][i]
392 # don't add a val that's already in the list
393 if val in fixed:
394 continue
395 l = list(fixed)
396 l.append(val)
397 if len(l) == len(all_matches):
398 # found a solution
399 return l
400 # not at the end yet, add next digit
401 r = search_in_permutations(l)
402 if r:
403 # nested search_in_permutations() call found a solution
404 return r
405 # this entire branch yielded no solution
406 return None
407
408 if not all_matches:
409 raise RuntimeError('Cannot solve: no candidates')
410
411 solution = search_in_permutations()
412 if not solution:
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200413 raise NotSolvable('The requested resource requirements are not solvable %r'
414 % all_matches)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200415 return solution
416
417
418def contains_hash(list_of_dicts, a_hash):
419 for d in list_of_dicts:
420 if d.get(HASH_KEY) == a_hash:
421 return True
422 return False
423
424def item_matches(item, wanted_item, ignore_keys=None):
425 if is_dict(wanted_item):
426 # match up two dicts
427 if not isinstance(item, dict):
428 return False
429 for key, wanted_val in wanted_item.items():
430 if ignore_keys and key in ignore_keys:
431 continue
432 if not item_matches(item.get(key), wanted_val, ignore_keys=ignore_keys):
433 return False
434 return True
435
436 if is_list(wanted_item):
Pau Espin Pedrol4e36f7c2017-08-28 13:29:28 +0200437 if not is_list(item):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200438 return False
Pau Espin Pedrol4e36f7c2017-08-28 13:29:28 +0200439 # multiple possible values
440 for val in wanted_item:
441 if val not in item:
442 return False
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200443 return True
444
445 return item == wanted_item
446
447
448class ReservedResources(log.Origin):
449 '''
450 After all resources have been figured out, this is the API that a test case
451 gets to interact with resources. From those resources that have been
452 reserved for it, it can pick some to mark them as currently in use.
453 Functions like nitb() provide a resource by automatically picking its
454 dependencies from so far unused (but reserved) resource.
455 '''
456
457 def __init__(self, resources_pool, origin, reserved):
458 self.resources_pool = resources_pool
459 self.origin = origin
460 self.reserved = reserved
461
462 def __repr__(self):
463 return 'resources(%s)=%s' % (self.origin.name(), pprint.pformat(self.reserved))
464
465 def get(self, kind, specifics=None):
466 if specifics is None:
467 specifics = {}
468 self.dbg('requesting use of', kind, specifics=specifics)
469 want = { kind: [specifics] }
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200470 available_dict = self.reserved.find(self.origin, want, skip_if_marked=USED_KEY,
471 do_copy=False, raise_if_missing=False,
472 log_label='Using')
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200473 available = available_dict.get(kind)
474 self.dbg(available=len(available))
475 if not available:
Neels Hofmeyrf9e86932017-06-06 19:47:40 +0200476 # cook up a detailed error message for the current situation
477 kind_reserved = self.reserved.get(kind, [])
478 used_count = len([r for r in kind_reserved if USED_KEY in r])
479 matching = self.reserved.find(self.origin, want, raise_if_missing=False, log_label=None).get(kind, [])
480 if not matching:
481 msg = 'none of the reserved resources matches requirements %r' % specifics
482 elif not (used_count < len(kind_reserved)):
483 msg = 'suite.conf reserved only %d x %r.' % (len(kind_reserved), kind)
484 else:
485 msg = ('No unused resource left that matches the requirements;'
486 ' Of reserved %d x %r, %d match the requirements, but all are already in use;'
487 ' Requirements: %r'
488 % (len(kind_reserved), kind, len(matching), specifics))
489 raise NoResourceExn('When trying to use instance nr %d of %r: %s' % (used_count + 1, kind, msg))
490
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200491 pick = available[0]
492 self.dbg(using=pick)
493 assert not pick.get(USED_KEY)
494 pick[USED_KEY] = True
495 return copy.deepcopy(pick)
496
497 def put(self, item):
498 if not item.get(USED_KEY):
499 raise RuntimeError('Can only put() a resource that is used: %r' % item)
500 hash_to_put = item.get(HASH_KEY)
501 if not hash_to_put:
502 raise RuntimeError('Can only put() a resource that has a hash marker: %r' % item)
503 for key, item_list in self.reserved.items():
504 my_list = self.get(key)
505 for my_item in my_list:
506 if hash_to_put == my_item.get(HASH_KEY):
507 my_item.pop(USED_KEY)
508
509 def put_all(self):
Pau Espin Pedrol1dd29552017-06-13 18:07:57 +0200510 if not self.reserved:
511 return
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200512 for key, item_list in self.reserved.items():
Pau Espin Pedrol1dd29552017-06-13 18:07:57 +0200513 for item in item_list:
514 item.pop(USED_KEY, None)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200515
516 def free(self):
Neels Hofmeyred4e5282017-05-29 02:53:54 +0200517 if self.reserved:
518 self.resources_pool.free(self.origin, self.reserved)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200519 self.reserved = None
520
Neels Hofmeyr2d1d5612017-05-22 20:02:41 +0200521 def counts(self):
522 counts = {}
523 for key in self.reserved.keys():
524 counts[key] = self.count(key)
525 return counts
526
527 def count(self, key):
528 return len(self.reserved.get(key) or [])
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200529
530# vim: expandtab tabstop=4 shiftwidth=4