blob: 61a73aac96004b3d9b328674e894c5a2a4a0da3c [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 Pedrole66e3ae2020-06-15 13:57:54 +020057 self.all_resources = Resources(config.read(self.config_path, schema.get_resources_schema()) or {})
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 Pedrol92a29d62020-10-05 17:25:16 +0200187 def next_zmq_port_range(self, origin, num_ports):
188 # Allocate continuous num_ports port between 2000 and 2200. returns base port.
189 # Assumption: base port is always an odd number.
190 num_ports = num_ports if num_ports % 2 == 0 else num_ports + 1
191 base_port = self.next_persistent_value('bvci', '2000', schema.uint16,
192 lambda x: str(int(x)+num_ports) if int(x) < 2200 - num_ports else '2000', origin)
193 return int(base_port)
194
195
Pau Espin Pedrol4676cbd2017-09-14 17:35:03 +0200196class NoResourceExn(log.Error):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200197 pass
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200198
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200199class Resources(dict):
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200200
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200201 def __init__(self, all_resources={}, do_copy=True):
202 if do_copy:
203 all_resources = copy.deepcopy(all_resources)
204 self.update(all_resources)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200205
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200206 def drop(self, reserved, fail_if_not_found=True):
207 # protect from modifying reserved because we're the same object
208 if reserved is self:
209 raise RuntimeError('Refusing to drop a list of resources from itself.'
210 ' This is probably a bug where a list of Resources()'
211 ' should have been copied but is passed as-is.'
212 ' use Resources.clear() instead.')
213
214 for key, reserved_list in reserved.items():
215 my_list = self.get(key) or []
216
217 if my_list is reserved_list:
218 self.pop(key)
219 continue
220
221 for reserved_item in reserved_list:
222 found = False
223 reserved_hash = reserved_item.get(HASH_KEY)
224 if not reserved_hash:
225 raise RuntimeError('Resources.drop() only works with hashed items')
226
227 for i in range(len(my_list)):
228 my_item = my_list[i]
229 my_hash = my_item.get(HASH_KEY)
230 if not my_hash:
231 raise RuntimeError('Resources.drop() only works with hashed items')
232 if my_hash == reserved_hash:
233 found = True
234 my_list.pop(i)
235 break
236
237 if fail_if_not_found and not found:
238 raise RuntimeError('Asked to drop resource from a pool, but the'
239 ' resource was not found: %s = %r' % (key, reserved_item))
240
241 if not my_list:
242 self.pop(key)
243 return self
244
245 def without(self, reserved):
246 return Resources(self).drop(reserved)
247
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200248 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 +0200249 '''
250 Pass a dict of resource requirements, e.g.:
251 want = {
252 'bts': [ {'type': 'osmo-bts-sysmo',}, {} ],
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200253 'modem': [ {}, {}, {} ]
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200254 }
255 This function tries to find a combination from the available resources that
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200256 matches these requirements. The return value is a dict (wrapped in a Resources class)
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200257 that contains the matching resources in the order of 'want' dict: in above
258 example, the returned dict would have a 'bts' list with the first item being
259 a sysmoBTS, the second item being any other available BTS.
260
261 If skip_if_marked is passed, any resource that contains this key is skipped.
262 E.g. if a BTS has the USED_KEY set like
263 reserved_resources = { 'bts' : {..., '_used': True} }
264 then this may be skipped by passing skip_if_marked='_used'
265 (or rather skip_if_marked=USED_KEY).
266
267 If do_copy is True, the returned dict is a deep copy and does not share
268 lists with any other Resources dict.
269
270 If raise_if_missing is False, this will return an empty item for any
271 resource that had no match, instead of immediately raising an exception.
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200272
273 This function expects input dictionaries whose contents have already
274 been replicated based on its the 'times' attributes. See
275 config.replicate_times() for more details.
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200276 '''
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200277 matches = {}
Neels Hofmeyr17c139e2017-04-12 02:42:02 +0200278 for key, want_list in sorted(want.items()): # sorted for deterministic test results
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200279 # here we have a resource of a given type, e.g. 'bts', with a list
280 # containing as many BTSes as the caller wants to reserve/use. Each
281 # list item contains specifics for the particular BTS.
Neels Hofmeyr2a1a1fa2017-05-29 01:36:21 +0200282 my_list = self.get(key, [])
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200283
Neels Hofmeyrf9e86932017-06-06 19:47:40 +0200284 if log_label:
285 for_origin.log(log_label, len(want_list), 'x', key, '(candidates: %d)'%len(my_list))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200286
287 # Try to avoid a less constrained item snatching away a resource
288 # from a more detailed constrained requirement.
289
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200290 # first record all matches, so that each requested item has a list
291 # of all available resources that match it. Some resources may
292 # appear for multiple requested items. Store matching indexes.
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200293 all_matches = []
294 for want_item in want_list:
295 item_match_list = []
296 for i in range(len(my_list)):
297 my_item = my_list[i]
298 if skip_if_marked and my_item.get(skip_if_marked):
299 continue
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200300 if item_matches(my_item, want_item):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200301 item_match_list.append(i)
302 if not item_match_list:
Neels Hofmeyr9b907702017-05-06 23:20:33 +0200303 if raise_if_missing:
304 raise NoResourceExn('No matching resource available for %s = %r'
305 % (key, want_item))
306 else:
307 # this one failed... see below
308 all_matches = []
309 break
310
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200311 all_matches.append( item_match_list )
312
313 if not all_matches:
Neels Hofmeyr9b907702017-05-06 23:20:33 +0200314 # ...this one failed. Makes no sense to solve resource
315 # allocations, return an empty list for this key to mark
316 # failure.
317 matches[key] = []
318 continue
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200319
320 # figure out who gets what
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200321 try:
322 solution = solve(all_matches)
323 except NotSolvable:
324 # instead of a cryptic error message, raise an exception that
325 # conveys meaning to the user.
326 raise NoResourceExn('Could not resolve request to reserve resources: '
327 '%d x %s with requirements: %r' % (len(want_list), key, want_list))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200328 picked = [ my_list[i] for i in solution if i is not None ]
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200329 for_origin.dbg('Picked', config.tostr(picked))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200330 matches[key] = picked
331
332 return Resources(matches, do_copy=do_copy)
333
334 def set_hashes(self):
335 for key, item_list in self.items():
336 for item in item_list:
337 item[HASH_KEY] = util.hash_obj(item, HASH_KEY, RESERVED_KEY, USED_KEY)
338
339 def add(self, more):
340 if more is self:
341 raise RuntimeError('adding a list of resources to itself?')
Pau Espin Pedrolea8c3d42020-05-04 12:05:05 +0200342 schema.add(self, copy.deepcopy(more))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200343
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200344 def mark_reserved_by(self, origin_id):
345 for key, item_list in self.items():
346 for item in item_list:
347 item[RESERVED_KEY] = origin_id
348
349
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200350class NotSolvable(Exception):
351 pass
352
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200353def solve(all_matches):
354 '''
355 all_matches shall be a list of index-lists.
356 all_matches[i] is the list of indexes that item i can use.
357 Return a solution so that each i gets a different index.
358 solve([ [0, 1, 2],
359 [0],
360 [0, 2] ]) == [1, 0, 2]
361 '''
362
363 def all_differ(l):
364 return len(set(l)) == len(l)
365
366 def search_in_permutations(fixed=[]):
367 idx = len(fixed)
368 for i in range(len(all_matches[idx])):
369 val = all_matches[idx][i]
370 # don't add a val that's already in the list
371 if val in fixed:
372 continue
373 l = list(fixed)
374 l.append(val)
375 if len(l) == len(all_matches):
376 # found a solution
377 return l
378 # not at the end yet, add next digit
379 r = search_in_permutations(l)
380 if r:
381 # nested search_in_permutations() call found a solution
382 return r
383 # this entire branch yielded no solution
384 return None
385
386 if not all_matches:
387 raise RuntimeError('Cannot solve: no candidates')
388
389 solution = search_in_permutations()
390 if not solution:
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200391 raise NotSolvable('The requested resource requirements are not solvable %r'
392 % all_matches)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200393 return solution
394
395
396def contains_hash(list_of_dicts, a_hash):
397 for d in list_of_dicts:
398 if d.get(HASH_KEY) == a_hash:
399 return True
400 return False
401
402def item_matches(item, wanted_item, ignore_keys=None):
403 if is_dict(wanted_item):
404 # match up two dicts
405 if not isinstance(item, dict):
406 return False
407 for key, wanted_val in wanted_item.items():
408 if ignore_keys and key in ignore_keys:
409 continue
410 if not item_matches(item.get(key), wanted_val, ignore_keys=ignore_keys):
411 return False
412 return True
413
414 if is_list(wanted_item):
Pau Espin Pedrol4e36f7c2017-08-28 13:29:28 +0200415 if not is_list(item):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200416 return False
Pau Espin Pedrol58475512017-09-14 15:33:15 +0200417 # Validate that all elements in both lists are of the same type:
418 t = util.list_validate_same_elem_type(wanted_item + item)
419 if t is None:
420 return True # both lists are empty, return
421 # For lists of complex objects, we expect them to be sorted lists:
422 if t in (dict, list, tuple):
423 for i in range(max(len(wanted_item), len(item))):
424 log.ctx(idx=i)
425 subitem = item[i] if i < len(item) else util.empty_instance_type(t)
426 wanted_subitem = wanted_item[i] if i < len(wanted_item) else util.empty_instance_type(t)
427 if not item_matches(subitem, wanted_subitem, ignore_keys=ignore_keys):
428 return False
429 else: # for lists of basic elements, we handle them as unsorted sets:
430 for val in wanted_item:
431 if val not in item:
432 return False
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200433 return True
434
435 return item == wanted_item
436
437
438class ReservedResources(log.Origin):
439 '''
440 After all resources have been figured out, this is the API that a test case
441 gets to interact with resources. From those resources that have been
442 reserved for it, it can pick some to mark them as currently in use.
443 Functions like nitb() provide a resource by automatically picking its
444 dependencies from so far unused (but reserved) resource.
445 '''
446
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200447 def __init__(self, resources_pool, origin, reserved, modifiers):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200448 self.resources_pool = resources_pool
449 self.origin = origin
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200450 self.reserved_original = reserved
451 self.reserved = copy.deepcopy(self.reserved_original)
452 config.overlay(self.reserved, modifiers)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200453
454 def __repr__(self):
455 return 'resources(%s)=%s' % (self.origin.name(), pprint.pformat(self.reserved))
456
457 def get(self, kind, specifics=None):
458 if specifics is None:
459 specifics = {}
460 self.dbg('requesting use of', kind, specifics=specifics)
461 want = { kind: [specifics] }
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200462 available_dict = self.reserved.find(self.origin, want, skip_if_marked=USED_KEY,
463 do_copy=False, raise_if_missing=False,
464 log_label='Using')
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200465 available = available_dict.get(kind)
466 self.dbg(available=len(available))
467 if not available:
Neels Hofmeyrf9e86932017-06-06 19:47:40 +0200468 # cook up a detailed error message for the current situation
469 kind_reserved = self.reserved.get(kind, [])
470 used_count = len([r for r in kind_reserved if USED_KEY in r])
471 matching = self.reserved.find(self.origin, want, raise_if_missing=False, log_label=None).get(kind, [])
472 if not matching:
473 msg = 'none of the reserved resources matches requirements %r' % specifics
474 elif not (used_count < len(kind_reserved)):
475 msg = 'suite.conf reserved only %d x %r.' % (len(kind_reserved), kind)
476 else:
477 msg = ('No unused resource left that matches the requirements;'
478 ' Of reserved %d x %r, %d match the requirements, but all are already in use;'
479 ' Requirements: %r'
480 % (len(kind_reserved), kind, len(matching), specifics))
481 raise NoResourceExn('When trying to use instance nr %d of %r: %s' % (used_count + 1, kind, msg))
482
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200483 pick = available[0]
484 self.dbg(using=pick)
485 assert not pick.get(USED_KEY)
486 pick[USED_KEY] = True
487 return copy.deepcopy(pick)
488
489 def put(self, item):
490 if not item.get(USED_KEY):
491 raise RuntimeError('Can only put() a resource that is used: %r' % item)
492 hash_to_put = item.get(HASH_KEY)
493 if not hash_to_put:
494 raise RuntimeError('Can only put() a resource that has a hash marker: %r' % item)
495 for key, item_list in self.reserved.items():
496 my_list = self.get(key)
497 for my_item in my_list:
498 if hash_to_put == my_item.get(HASH_KEY):
499 my_item.pop(USED_KEY)
500
501 def put_all(self):
Pau Espin Pedrol1dd29552017-06-13 18:07:57 +0200502 if not self.reserved:
503 return
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200504 for key, item_list in self.reserved.items():
Pau Espin Pedrol1dd29552017-06-13 18:07:57 +0200505 for item in item_list:
506 item.pop(USED_KEY, None)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200507
508 def free(self):
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200509 if self.reserved_original:
510 self.resources_pool.free(self.origin, self.reserved_original)
511 self.reserved_original = None
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200512
Neels Hofmeyr2d1d5612017-05-22 20:02:41 +0200513 def counts(self):
514 counts = {}
515 for key in self.reserved.keys():
516 counts[key] = self.count(key)
517 return counts
518
519 def count(self, key):
520 return len(self.reserved.get(key) or [])
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200521
522# vim: expandtab tabstop=4 shiftwidth=4