blob: af05a51313ebc9f356462570d96c2094086c76e3 [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 copy
22import atexit
23import pprint
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +020024
Pau Espin Pedrol06cb5362020-05-04 18:58:53 +020025from . import log
26from . import config
27from . import util
28from . import schema
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +020029
Pau Espin Pedrol06cb5362020-05-04 18:58:53 +020030from .util import is_dict, is_list
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +020031
Neels Hofmeyr3531a192017-03-28 14:30:28 +020032HASH_KEY = '_hash'
33RESERVED_KEY = '_reserved_by'
34USED_KEY = '_used'
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +020035
Neels Hofmeyr3531a192017-03-28 14:30:28 +020036RESERVED_RESOURCES_FILE = 'reserved_resources.state'
37
Neels Hofmeyr76d81032017-05-18 18:35:32 +020038R_IP_ADDRESS = 'ip_address'
Pau Espin Pedrol116a2c42020-02-11 17:41:13 +010039R_RUN_NODE = 'run_node'
Neels Hofmeyr3531a192017-03-28 14:30:28 +020040R_BTS = 'bts'
41R_ARFCN = 'arfcn'
42R_MODEM = 'modem'
Pau Espin Pedrolbc1ed882018-05-17 16:59:58 +020043R_OSMOCON = 'osmocon_phone'
Pau Espin Pedrolc8b0f932020-02-11 17:45:26 +010044R_ENB = 'enb'
Pau Espin Pedrol0b302792017-09-10 16:33:10 +020045
Neels Hofmeyr3531a192017-03-28 14:30:28 +020046class ResourcesPool(log.Origin):
47 _remember_to_free = None
48 _registered_exit_handler = False
49
50 def __init__(self):
Pau Espin Pedrol6c6c0e82020-05-11 18:30:58 +020051 self.config_path = config.get_main_config_value(config.CFG_RESOURCES_CONF)
Neels Hofmeyr3531a192017-03-28 14:30:28 +020052 self.state_dir = config.get_state_dir()
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +020053 super().__init__(log.C_CNF, conf=self.config_path, state=self.state_dir.path)
Neels Hofmeyr3531a192017-03-28 14:30:28 +020054 self.read_conf()
55
56 def read_conf(self):
Pau Espin Pedrolea8c3d42020-05-04 12:05:05 +020057 self.all_resources = Resources(config.read(self.config_path, schema.get_resources_schema()))
Neels Hofmeyr3531a192017-03-28 14:30:28 +020058 self.all_resources.set_hashes()
59
Pau Espin Pedrolaab56922018-08-21 14:58:29 +020060 def reserve(self, origin, want, modifiers):
Neels Hofmeyr3531a192017-03-28 14:30:28 +020061 '''
62 attempt to reserve the resources specified in the dict 'want' for
63 'origin'. Obtain a lock on the resources lock dir, verify that all
64 wanted resources are available, and if yes mark them as reserved.
65
66 On success, return a reservation object which can be used to release
67 the reservation. The reservation will be freed automatically on program
68 exit, if not yet done manually.
69
70 'origin' should be an Origin() instance.
71
Pau Espin Pedrolaab56922018-08-21 14:58:29 +020072 'want' is a dict matching RESOURCES_SCHEMA, used to specify what to
73 reserve.
74
75 'modifiers' is a dict matching RESOURCES_SCHEMA, it is overlaid on top
76 of 'want'.
Neels Hofmeyr3531a192017-03-28 14:30:28 +020077
Pau Espin Pedrol0b302792017-09-10 16:33:10 +020078 If an entry has no attribute set, any of the resources may be
Neels Hofmeyr3531a192017-03-28 14:30:28 +020079 reserved without further limitations.
80
81 ResourcesPool may also be selected with narrowed down constraints.
Neels Hofmeyr76d81032017-05-18 18:35:32 +020082 This would reserve one IP address, two modems, one BTS of type
Neels Hofmeyr391afe32017-05-18 19:22:12 +020083 sysmo and one of type trx, plus 2 ARFCNs in the 1800 band:
Neels Hofmeyr3531a192017-03-28 14:30:28 +020084
85 {
Neels Hofmeyr76d81032017-05-18 18:35:32 +020086 'ip_address': [ {} ],
Neels Hofmeyr391afe32017-05-18 19:22:12 +020087 'bts': [ { 'type': 'sysmo' }, { 'type': 'trx' } ],
Pau Espin Pedrol0b302792017-09-10 16:33:10 +020088 'arfcn': [ { 'band': 'GSM-1800' }, { 'band': 'GSM-1800' } ],
89 'modem': [ {}, {} ],
Neels Hofmeyr3531a192017-03-28 14:30:28 +020090 }
91 '''
Pau Espin Pedrolea8c3d42020-05-04 12:05:05 +020092 schema.validate(want, schema.get_resources_schema())
93 schema.validate(modifiers, schema.get_resources_schema())
Neels Hofmeyr3531a192017-03-28 14:30:28 +020094
95 origin_id = origin.origin_id()
96
97 with self.state_dir.lock(origin_id):
98 rrfile_path = self.state_dir.mk_parentdir(RESERVED_RESOURCES_FILE)
99 reserved = Resources(config.read(rrfile_path, if_missing_return={}))
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200100 to_be_reserved = self.all_resources.without(reserved).find(origin, want)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200101
102 to_be_reserved.mark_reserved_by(origin_id)
103
104 reserved.add(to_be_reserved)
105 config.write(rrfile_path, reserved)
106
107 self.remember_to_free(to_be_reserved)
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200108 return ReservedResources(self, origin, to_be_reserved, modifiers)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200109
110 def free(self, origin, to_be_freed):
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200111 log.ctx(origin)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200112 with self.state_dir.lock(origin.origin_id()):
113 rrfile_path = self.state_dir.mk_parentdir(RESERVED_RESOURCES_FILE)
114 reserved = Resources(config.read(rrfile_path, if_missing_return={}))
115 reserved.drop(to_be_freed)
116 config.write(rrfile_path, reserved)
117 self.forget_freed(to_be_freed)
118
119 def register_exit_handler(self):
120 if self._registered_exit_handler:
121 return
122 atexit.register(self.clean_up_registered_resources)
123 self._registered_exit_handler = True
124
125 def unregister_exit_handler(self):
126 if not self._registered_exit_handler:
127 return
128 atexit.unregister(self.clean_up_registered_resources)
129 self._registered_exit_handler = False
130
131 def clean_up_registered_resources(self):
132 if not self._remember_to_free:
133 return
134 self.free(log.Origin('atexit.clean_up_registered_resources()'),
135 self._remember_to_free)
136
137 def remember_to_free(self, to_be_reserved):
138 self.register_exit_handler()
139 if not self._remember_to_free:
140 self._remember_to_free = Resources()
141 self._remember_to_free.add(to_be_reserved)
142
143 def forget_freed(self, freed):
144 if freed is self._remember_to_free:
145 self._remember_to_free.clear()
146 else:
147 self._remember_to_free.drop(freed)
148 if not self._remember_to_free:
149 self.unregister_exit_handler()
150
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100151 def next_persistent_value(self, token, first_val, validate_func, inc_func, origin):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200152 origin_id = origin.origin_id()
153
154 with self.state_dir.lock(origin_id):
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100155 token_path = self.state_dir.child('last_used_%s.state' % token)
156 log.ctx(token_path)
157 last_value = first_val
158 if os.path.exists(token_path):
159 if not os.path.isfile(token_path):
160 raise RuntimeError('path should be a file but is not: %r' % token_path)
161 with open(token_path, 'r') as f:
162 last_value = f.read().strip()
163 validate_func(last_value)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200164
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100165 next_value = inc_func(last_value)
166 with open(token_path, 'w') as f:
167 f.write(next_value)
168 return next_value
169
170 def next_msisdn(self, origin):
171 return self.next_persistent_value('msisdn', '1000', schema.msisdn, util.msisdn_inc, origin)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200172
Pau Espin Pedrol5e0c2512017-11-06 18:40:23 +0100173 def next_lac(self, origin):
Pau Espin Pedrolf7f06362017-11-28 12:56:35 +0100174 # LAC=0 has special meaning (MS detached), avoid it
175 return self.next_persistent_value('lac', '1', schema.uint16, lambda x: str(((int(x)+1) % pow(2,16)) or 1), origin)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200176
Pau Espin Pedrol8a3a7b52017-11-28 15:50:02 +0100177 def next_rac(self, origin):
178 return self.next_persistent_value('rac', '1', schema.uint8, lambda x: str((int(x)+1) % pow(2,8) or 1), origin)
179
Pau Espin Pedrol4ccce7c2017-11-07 11:13:20 +0100180 def next_cellid(self, origin):
181 return self.next_persistent_value('cellid', '1', schema.uint16, lambda x: str((int(x)+1) % pow(2,16)), origin)
182
Pau Espin Pedrol8a3a7b52017-11-28 15:50:02 +0100183 def next_bvci(self, origin):
184 # BVCI=0 and =1 are reserved, avoid them.
185 return self.next_persistent_value('bvci', '2', schema.uint16, lambda x: str(int(x)+1) if int(x) < pow(2,16) - 1 else '2', origin)
186
Pau Espin Pedrol4676cbd2017-09-14 17:35:03 +0200187class NoResourceExn(log.Error):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200188 pass
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200189
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200190class Resources(dict):
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200191
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200192 def __init__(self, all_resources={}, do_copy=True):
193 if do_copy:
194 all_resources = copy.deepcopy(all_resources)
195 self.update(all_resources)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200196
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200197 def drop(self, reserved, fail_if_not_found=True):
198 # protect from modifying reserved because we're the same object
199 if reserved is self:
200 raise RuntimeError('Refusing to drop a list of resources from itself.'
201 ' This is probably a bug where a list of Resources()'
202 ' should have been copied but is passed as-is.'
203 ' use Resources.clear() instead.')
204
205 for key, reserved_list in reserved.items():
206 my_list = self.get(key) or []
207
208 if my_list is reserved_list:
209 self.pop(key)
210 continue
211
212 for reserved_item in reserved_list:
213 found = False
214 reserved_hash = reserved_item.get(HASH_KEY)
215 if not reserved_hash:
216 raise RuntimeError('Resources.drop() only works with hashed items')
217
218 for i in range(len(my_list)):
219 my_item = my_list[i]
220 my_hash = my_item.get(HASH_KEY)
221 if not my_hash:
222 raise RuntimeError('Resources.drop() only works with hashed items')
223 if my_hash == reserved_hash:
224 found = True
225 my_list.pop(i)
226 break
227
228 if fail_if_not_found and not found:
229 raise RuntimeError('Asked to drop resource from a pool, but the'
230 ' resource was not found: %s = %r' % (key, reserved_item))
231
232 if not my_list:
233 self.pop(key)
234 return self
235
236 def without(self, reserved):
237 return Resources(self).drop(reserved)
238
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200239 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 +0200240 '''
241 Pass a dict of resource requirements, e.g.:
242 want = {
243 'bts': [ {'type': 'osmo-bts-sysmo',}, {} ],
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200244 'modem': [ {}, {}, {} ]
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200245 }
246 This function tries to find a combination from the available resources that
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200247 matches these requirements. The return value is a dict (wrapped in a Resources class)
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200248 that contains the matching resources in the order of 'want' dict: in above
249 example, the returned dict would have a 'bts' list with the first item being
250 a sysmoBTS, the second item being any other available BTS.
251
252 If skip_if_marked is passed, any resource that contains this key is skipped.
253 E.g. if a BTS has the USED_KEY set like
254 reserved_resources = { 'bts' : {..., '_used': True} }
255 then this may be skipped by passing skip_if_marked='_used'
256 (or rather skip_if_marked=USED_KEY).
257
258 If do_copy is True, the returned dict is a deep copy and does not share
259 lists with any other Resources dict.
260
261 If raise_if_missing is False, this will return an empty item for any
262 resource that had no match, instead of immediately raising an exception.
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200263
264 This function expects input dictionaries whose contents have already
265 been replicated based on its the 'times' attributes. See
266 config.replicate_times() for more details.
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200267 '''
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200268 matches = {}
Neels Hofmeyr17c139e2017-04-12 02:42:02 +0200269 for key, want_list in sorted(want.items()): # sorted for deterministic test results
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200270 # here we have a resource of a given type, e.g. 'bts', with a list
271 # containing as many BTSes as the caller wants to reserve/use. Each
272 # list item contains specifics for the particular BTS.
Neels Hofmeyr2a1a1fa2017-05-29 01:36:21 +0200273 my_list = self.get(key, [])
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200274
Neels Hofmeyrf9e86932017-06-06 19:47:40 +0200275 if log_label:
276 for_origin.log(log_label, len(want_list), 'x', key, '(candidates: %d)'%len(my_list))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200277
278 # Try to avoid a less constrained item snatching away a resource
279 # from a more detailed constrained requirement.
280
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200281 # first record all matches, so that each requested item has a list
282 # of all available resources that match it. Some resources may
283 # appear for multiple requested items. Store matching indexes.
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200284 all_matches = []
285 for want_item in want_list:
286 item_match_list = []
287 for i in range(len(my_list)):
288 my_item = my_list[i]
289 if skip_if_marked and my_item.get(skip_if_marked):
290 continue
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200291 if item_matches(my_item, want_item):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200292 item_match_list.append(i)
293 if not item_match_list:
Neels Hofmeyr9b907702017-05-06 23:20:33 +0200294 if raise_if_missing:
295 raise NoResourceExn('No matching resource available for %s = %r'
296 % (key, want_item))
297 else:
298 # this one failed... see below
299 all_matches = []
300 break
301
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200302 all_matches.append( item_match_list )
303
304 if not all_matches:
Neels Hofmeyr9b907702017-05-06 23:20:33 +0200305 # ...this one failed. Makes no sense to solve resource
306 # allocations, return an empty list for this key to mark
307 # failure.
308 matches[key] = []
309 continue
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200310
311 # figure out who gets what
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200312 try:
313 solution = solve(all_matches)
314 except NotSolvable:
315 # instead of a cryptic error message, raise an exception that
316 # conveys meaning to the user.
317 raise NoResourceExn('Could not resolve request to reserve resources: '
318 '%d x %s with requirements: %r' % (len(want_list), key, want_list))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200319 picked = [ my_list[i] for i in solution if i is not None ]
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200320 for_origin.dbg('Picked', config.tostr(picked))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200321 matches[key] = picked
322
323 return Resources(matches, do_copy=do_copy)
324
325 def set_hashes(self):
326 for key, item_list in self.items():
327 for item in item_list:
328 item[HASH_KEY] = util.hash_obj(item, HASH_KEY, RESERVED_KEY, USED_KEY)
329
330 def add(self, more):
331 if more is self:
332 raise RuntimeError('adding a list of resources to itself?')
Pau Espin Pedrolea8c3d42020-05-04 12:05:05 +0200333 schema.add(self, copy.deepcopy(more))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200334
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200335 def mark_reserved_by(self, origin_id):
336 for key, item_list in self.items():
337 for item in item_list:
338 item[RESERVED_KEY] = origin_id
339
340
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200341class NotSolvable(Exception):
342 pass
343
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200344def solve(all_matches):
345 '''
346 all_matches shall be a list of index-lists.
347 all_matches[i] is the list of indexes that item i can use.
348 Return a solution so that each i gets a different index.
349 solve([ [0, 1, 2],
350 [0],
351 [0, 2] ]) == [1, 0, 2]
352 '''
353
354 def all_differ(l):
355 return len(set(l)) == len(l)
356
357 def search_in_permutations(fixed=[]):
358 idx = len(fixed)
359 for i in range(len(all_matches[idx])):
360 val = all_matches[idx][i]
361 # don't add a val that's already in the list
362 if val in fixed:
363 continue
364 l = list(fixed)
365 l.append(val)
366 if len(l) == len(all_matches):
367 # found a solution
368 return l
369 # not at the end yet, add next digit
370 r = search_in_permutations(l)
371 if r:
372 # nested search_in_permutations() call found a solution
373 return r
374 # this entire branch yielded no solution
375 return None
376
377 if not all_matches:
378 raise RuntimeError('Cannot solve: no candidates')
379
380 solution = search_in_permutations()
381 if not solution:
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200382 raise NotSolvable('The requested resource requirements are not solvable %r'
383 % all_matches)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200384 return solution
385
386
387def contains_hash(list_of_dicts, a_hash):
388 for d in list_of_dicts:
389 if d.get(HASH_KEY) == a_hash:
390 return True
391 return False
392
393def item_matches(item, wanted_item, ignore_keys=None):
394 if is_dict(wanted_item):
395 # match up two dicts
396 if not isinstance(item, dict):
397 return False
398 for key, wanted_val in wanted_item.items():
399 if ignore_keys and key in ignore_keys:
400 continue
401 if not item_matches(item.get(key), wanted_val, ignore_keys=ignore_keys):
402 return False
403 return True
404
405 if is_list(wanted_item):
Pau Espin Pedrol4e36f7c2017-08-28 13:29:28 +0200406 if not is_list(item):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200407 return False
Pau Espin Pedrol58475512017-09-14 15:33:15 +0200408 # Validate that all elements in both lists are of the same type:
409 t = util.list_validate_same_elem_type(wanted_item + item)
410 if t is None:
411 return True # both lists are empty, return
412 # For lists of complex objects, we expect them to be sorted lists:
413 if t in (dict, list, tuple):
414 for i in range(max(len(wanted_item), len(item))):
415 log.ctx(idx=i)
416 subitem = item[i] if i < len(item) else util.empty_instance_type(t)
417 wanted_subitem = wanted_item[i] if i < len(wanted_item) else util.empty_instance_type(t)
418 if not item_matches(subitem, wanted_subitem, ignore_keys=ignore_keys):
419 return False
420 else: # for lists of basic elements, we handle them as unsorted sets:
421 for val in wanted_item:
422 if val not in item:
423 return False
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200424 return True
425
426 return item == wanted_item
427
428
429class ReservedResources(log.Origin):
430 '''
431 After all resources have been figured out, this is the API that a test case
432 gets to interact with resources. From those resources that have been
433 reserved for it, it can pick some to mark them as currently in use.
434 Functions like nitb() provide a resource by automatically picking its
435 dependencies from so far unused (but reserved) resource.
436 '''
437
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200438 def __init__(self, resources_pool, origin, reserved, modifiers):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200439 self.resources_pool = resources_pool
440 self.origin = origin
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200441 self.reserved_original = reserved
442 self.reserved = copy.deepcopy(self.reserved_original)
443 config.overlay(self.reserved, modifiers)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200444
445 def __repr__(self):
446 return 'resources(%s)=%s' % (self.origin.name(), pprint.pformat(self.reserved))
447
448 def get(self, kind, specifics=None):
449 if specifics is None:
450 specifics = {}
451 self.dbg('requesting use of', kind, specifics=specifics)
452 want = { kind: [specifics] }
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200453 available_dict = self.reserved.find(self.origin, want, skip_if_marked=USED_KEY,
454 do_copy=False, raise_if_missing=False,
455 log_label='Using')
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200456 available = available_dict.get(kind)
457 self.dbg(available=len(available))
458 if not available:
Neels Hofmeyrf9e86932017-06-06 19:47:40 +0200459 # cook up a detailed error message for the current situation
460 kind_reserved = self.reserved.get(kind, [])
461 used_count = len([r for r in kind_reserved if USED_KEY in r])
462 matching = self.reserved.find(self.origin, want, raise_if_missing=False, log_label=None).get(kind, [])
463 if not matching:
464 msg = 'none of the reserved resources matches requirements %r' % specifics
465 elif not (used_count < len(kind_reserved)):
466 msg = 'suite.conf reserved only %d x %r.' % (len(kind_reserved), kind)
467 else:
468 msg = ('No unused resource left that matches the requirements;'
469 ' Of reserved %d x %r, %d match the requirements, but all are already in use;'
470 ' Requirements: %r'
471 % (len(kind_reserved), kind, len(matching), specifics))
472 raise NoResourceExn('When trying to use instance nr %d of %r: %s' % (used_count + 1, kind, msg))
473
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200474 pick = available[0]
475 self.dbg(using=pick)
476 assert not pick.get(USED_KEY)
477 pick[USED_KEY] = True
478 return copy.deepcopy(pick)
479
480 def put(self, item):
481 if not item.get(USED_KEY):
482 raise RuntimeError('Can only put() a resource that is used: %r' % item)
483 hash_to_put = item.get(HASH_KEY)
484 if not hash_to_put:
485 raise RuntimeError('Can only put() a resource that has a hash marker: %r' % item)
486 for key, item_list in self.reserved.items():
487 my_list = self.get(key)
488 for my_item in my_list:
489 if hash_to_put == my_item.get(HASH_KEY):
490 my_item.pop(USED_KEY)
491
492 def put_all(self):
Pau Espin Pedrol1dd29552017-06-13 18:07:57 +0200493 if not self.reserved:
494 return
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200495 for key, item_list in self.reserved.items():
Pau Espin Pedrol1dd29552017-06-13 18:07:57 +0200496 for item in item_list:
497 item.pop(USED_KEY, None)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200498
499 def free(self):
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200500 if self.reserved_original:
501 self.resources_pool.free(self.origin, self.reserved_original)
502 self.reserved_original = None
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200503
Neels Hofmeyr2d1d5612017-05-22 20:02:41 +0200504 def counts(self):
505 counts = {}
506 for key in self.reserved.keys():
507 counts[key] = self.count(key)
508 return counts
509
510 def count(self, key):
511 return len(self.reserved.get(key) or [])
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200512
513# vim: expandtab tabstop=4 shiftwidth=4