blob: 7e55129ea42c0ce30437d346927b9afe59ade692 [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,
Pau Espin Pedrol57497a62017-08-28 14:21:15 +020059 'bts[].ciphers[]': schema.CIPHER,
Your Name44af3412017-04-13 03:11:59 +020060 'bts[].trx_list[].hw_addr': schema.HWADDR,
61 'bts[].trx_list[].net_device': schema.STR,
Pau Espin Pedrolb26f32a2017-09-14 13:52:28 +020062 'bts[].trx_list[].nominal_power': schema.UINT,
63 'bts[].trx_list[].max_power_red': schema.UINT,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020064 'arfcn[].arfcn': schema.INT,
65 'arfcn[].band': schema.BAND,
66 'modem[].label': schema.STR,
67 'modem[].path': schema.STR,
68 'modem[].imsi': schema.IMSI,
69 'modem[].ki': schema.KI,
Pau Espin Pedrol713ce2c2017-08-24 16:57:17 +020070 'modem[].auth_algo': schema.AUTH_ALGO,
Pau Espin Pedrol57497a62017-08-28 14:21:15 +020071 'modem[].ciphers[]': schema.CIPHER,
Pau Espin Pedrolac18fd32017-08-31 18:49:47 +020072 'modem[].features[]': schema.MODEM_FEATURE,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020073 }
74
75WANT_SCHEMA = util.dict_add(
Pau Espin Pedrolead79ac2017-09-12 15:19:18 +020076 dict([('%s[].times' % r, schema.TIMES) for r in R_ALL]),
Neels Hofmeyr3531a192017-03-28 14:30:28 +020077 RESOURCES_SCHEMA)
78
Pau Espin Pedrol0b302792017-09-10 16:33:10 +020079CONF_SCHEMA = util.dict_add(
80 { 'defaults.timeout': schema.STR },
81 dict([('resources.%s' % key, val) for key, val in WANT_SCHEMA.items()]))
82
Neels Hofmeyr3531a192017-03-28 14:30:28 +020083KNOWN_BTS_TYPES = {
Your Name44af3412017-04-13 03:11:59 +020084 'osmo-bts-sysmo': bts_sysmo.SysmoBts,
85 'osmo-bts-trx': bts_osmotrx.OsmoBtsTrx,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020086 }
87
88def register_bts_type(name, clazz):
89 KNOWN_BTS_TYPES[name] = clazz
90
91class ResourcesPool(log.Origin):
92 _remember_to_free = None
93 _registered_exit_handler = False
94
95 def __init__(self):
96 self.config_path = config.get_config_file(RESOURCES_CONF)
97 self.state_dir = config.get_state_dir()
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +020098 super().__init__(log.C_CNF, conf=self.config_path, state=self.state_dir.path)
Neels Hofmeyr3531a192017-03-28 14:30:28 +020099 self.read_conf()
100
101 def read_conf(self):
102 self.all_resources = Resources(config.read(self.config_path, RESOURCES_SCHEMA))
103 self.all_resources.set_hashes()
104
105 def reserve(self, origin, want):
106 '''
107 attempt to reserve the resources specified in the dict 'want' for
108 'origin'. Obtain a lock on the resources lock dir, verify that all
109 wanted resources are available, and if yes mark them as reserved.
110
111 On success, return a reservation object which can be used to release
112 the reservation. The reservation will be freed automatically on program
113 exit, if not yet done manually.
114
115 'origin' should be an Origin() instance.
116
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200117 'want' is a dict matching RESOURCES_SCHEMA.
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200118
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200119 If an entry has no attribute set, any of the resources may be
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200120 reserved without further limitations.
121
122 ResourcesPool may also be selected with narrowed down constraints.
Neels Hofmeyr76d81032017-05-18 18:35:32 +0200123 This would reserve one IP address, two modems, one BTS of type
Neels Hofmeyr391afe32017-05-18 19:22:12 +0200124 sysmo and one of type trx, plus 2 ARFCNs in the 1800 band:
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200125
126 {
Neels Hofmeyr76d81032017-05-18 18:35:32 +0200127 'ip_address': [ {} ],
Neels Hofmeyr391afe32017-05-18 19:22:12 +0200128 'bts': [ { 'type': 'sysmo' }, { 'type': 'trx' } ],
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200129 'arfcn': [ { 'band': 'GSM-1800' }, { 'band': 'GSM-1800' } ],
130 'modem': [ {}, {} ],
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200131 }
132 '''
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200133 schema.validate(want, RESOURCES_SCHEMA)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200134
135 origin_id = origin.origin_id()
136
137 with self.state_dir.lock(origin_id):
138 rrfile_path = self.state_dir.mk_parentdir(RESERVED_RESOURCES_FILE)
139 reserved = Resources(config.read(rrfile_path, if_missing_return={}))
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200140 to_be_reserved = self.all_resources.without(reserved).find(origin, want)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200141
142 to_be_reserved.mark_reserved_by(origin_id)
143
144 reserved.add(to_be_reserved)
145 config.write(rrfile_path, reserved)
146
147 self.remember_to_free(to_be_reserved)
148 return ReservedResources(self, origin, to_be_reserved)
149
150 def free(self, origin, to_be_freed):
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200151 log.ctx(origin)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200152 with self.state_dir.lock(origin.origin_id()):
153 rrfile_path = self.state_dir.mk_parentdir(RESERVED_RESOURCES_FILE)
154 reserved = Resources(config.read(rrfile_path, if_missing_return={}))
155 reserved.drop(to_be_freed)
156 config.write(rrfile_path, reserved)
157 self.forget_freed(to_be_freed)
158
159 def register_exit_handler(self):
160 if self._registered_exit_handler:
161 return
162 atexit.register(self.clean_up_registered_resources)
163 self._registered_exit_handler = True
164
165 def unregister_exit_handler(self):
166 if not self._registered_exit_handler:
167 return
168 atexit.unregister(self.clean_up_registered_resources)
169 self._registered_exit_handler = False
170
171 def clean_up_registered_resources(self):
172 if not self._remember_to_free:
173 return
174 self.free(log.Origin('atexit.clean_up_registered_resources()'),
175 self._remember_to_free)
176
177 def remember_to_free(self, to_be_reserved):
178 self.register_exit_handler()
179 if not self._remember_to_free:
180 self._remember_to_free = Resources()
181 self._remember_to_free.add(to_be_reserved)
182
183 def forget_freed(self, freed):
184 if freed is self._remember_to_free:
185 self._remember_to_free.clear()
186 else:
187 self._remember_to_free.drop(freed)
188 if not self._remember_to_free:
189 self.unregister_exit_handler()
190
191 def next_msisdn(self, origin):
192 origin_id = origin.origin_id()
193
194 with self.state_dir.lock(origin_id):
195 msisdn_path = self.state_dir.child(LAST_USED_MSISDN_FILE)
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200196 log.ctx(msisdn_path)
197 last_msisdn = '1000'
198 if os.path.exists(msisdn_path):
199 if not os.path.isfile(msisdn_path):
200 raise RuntimeError('path should be a file but is not: %r' % msisdn_path)
201 with open(msisdn_path, 'r') as f:
202 last_msisdn = f.read().strip()
203 schema.msisdn(last_msisdn)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200204
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200205 next_msisdn = util.msisdn_inc(last_msisdn)
206 with open(msisdn_path, 'w') as f:
207 f.write(next_msisdn)
208 return next_msisdn
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200209
210
Pau Espin Pedrol4676cbd2017-09-14 17:35:03 +0200211class NoResourceExn(log.Error):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200212 pass
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200213
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200214class Resources(dict):
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200215
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200216 def __init__(self, all_resources={}, do_copy=True):
217 if do_copy:
218 all_resources = copy.deepcopy(all_resources)
219 self.update(all_resources)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200220
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200221 def drop(self, reserved, fail_if_not_found=True):
222 # protect from modifying reserved because we're the same object
223 if reserved is self:
224 raise RuntimeError('Refusing to drop a list of resources from itself.'
225 ' This is probably a bug where a list of Resources()'
226 ' should have been copied but is passed as-is.'
227 ' use Resources.clear() instead.')
228
229 for key, reserved_list in reserved.items():
230 my_list = self.get(key) or []
231
232 if my_list is reserved_list:
233 self.pop(key)
234 continue
235
236 for reserved_item in reserved_list:
237 found = False
238 reserved_hash = reserved_item.get(HASH_KEY)
239 if not reserved_hash:
240 raise RuntimeError('Resources.drop() only works with hashed items')
241
242 for i in range(len(my_list)):
243 my_item = my_list[i]
244 my_hash = my_item.get(HASH_KEY)
245 if not my_hash:
246 raise RuntimeError('Resources.drop() only works with hashed items')
247 if my_hash == reserved_hash:
248 found = True
249 my_list.pop(i)
250 break
251
252 if fail_if_not_found and not found:
253 raise RuntimeError('Asked to drop resource from a pool, but the'
254 ' resource was not found: %s = %r' % (key, reserved_item))
255
256 if not my_list:
257 self.pop(key)
258 return self
259
260 def without(self, reserved):
261 return Resources(self).drop(reserved)
262
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200263 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 +0200264 '''
265 Pass a dict of resource requirements, e.g.:
266 want = {
267 'bts': [ {'type': 'osmo-bts-sysmo',}, {} ],
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200268 'modem': [ {}, {}, {} ]
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200269 }
270 This function tries to find a combination from the available resources that
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200271 matches these requirements. The return value is a dict (wrapped in a Resources class)
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200272 that contains the matching resources in the order of 'want' dict: in above
273 example, the returned dict would have a 'bts' list with the first item being
274 a sysmoBTS, the second item being any other available BTS.
275
276 If skip_if_marked is passed, any resource that contains this key is skipped.
277 E.g. if a BTS has the USED_KEY set like
278 reserved_resources = { 'bts' : {..., '_used': True} }
279 then this may be skipped by passing skip_if_marked='_used'
280 (or rather skip_if_marked=USED_KEY).
281
282 If do_copy is True, the returned dict is a deep copy and does not share
283 lists with any other Resources dict.
284
285 If raise_if_missing is False, this will return an empty item for any
286 resource that had no match, instead of immediately raising an exception.
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200287
288 This function expects input dictionaries whose contents have already
289 been replicated based on its the 'times' attributes. See
290 config.replicate_times() for more details.
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200291 '''
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
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200315 if item_matches(my_item, want_item):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200316 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 Pedrol58475512017-09-14 15:33:15 +0200437 # Validate that all elements in both lists are of the same type:
438 t = util.list_validate_same_elem_type(wanted_item + item)
439 if t is None:
440 return True # both lists are empty, return
441 # For lists of complex objects, we expect them to be sorted lists:
442 if t in (dict, list, tuple):
443 for i in range(max(len(wanted_item), len(item))):
444 log.ctx(idx=i)
445 subitem = item[i] if i < len(item) else util.empty_instance_type(t)
446 wanted_subitem = wanted_item[i] if i < len(wanted_item) else util.empty_instance_type(t)
447 if not item_matches(subitem, wanted_subitem, ignore_keys=ignore_keys):
448 return False
449 else: # for lists of basic elements, we handle them as unsorted sets:
450 for val in wanted_item:
451 if val not in item:
452 return False
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200453 return True
454
455 return item == wanted_item
456
457
458class ReservedResources(log.Origin):
459 '''
460 After all resources have been figured out, this is the API that a test case
461 gets to interact with resources. From those resources that have been
462 reserved for it, it can pick some to mark them as currently in use.
463 Functions like nitb() provide a resource by automatically picking its
464 dependencies from so far unused (but reserved) resource.
465 '''
466
467 def __init__(self, resources_pool, origin, reserved):
468 self.resources_pool = resources_pool
469 self.origin = origin
470 self.reserved = reserved
471
472 def __repr__(self):
473 return 'resources(%s)=%s' % (self.origin.name(), pprint.pformat(self.reserved))
474
475 def get(self, kind, specifics=None):
476 if specifics is None:
477 specifics = {}
478 self.dbg('requesting use of', kind, specifics=specifics)
479 want = { kind: [specifics] }
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200480 available_dict = self.reserved.find(self.origin, want, skip_if_marked=USED_KEY,
481 do_copy=False, raise_if_missing=False,
482 log_label='Using')
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200483 available = available_dict.get(kind)
484 self.dbg(available=len(available))
485 if not available:
Neels Hofmeyrf9e86932017-06-06 19:47:40 +0200486 # cook up a detailed error message for the current situation
487 kind_reserved = self.reserved.get(kind, [])
488 used_count = len([r for r in kind_reserved if USED_KEY in r])
489 matching = self.reserved.find(self.origin, want, raise_if_missing=False, log_label=None).get(kind, [])
490 if not matching:
491 msg = 'none of the reserved resources matches requirements %r' % specifics
492 elif not (used_count < len(kind_reserved)):
493 msg = 'suite.conf reserved only %d x %r.' % (len(kind_reserved), kind)
494 else:
495 msg = ('No unused resource left that matches the requirements;'
496 ' Of reserved %d x %r, %d match the requirements, but all are already in use;'
497 ' Requirements: %r'
498 % (len(kind_reserved), kind, len(matching), specifics))
499 raise NoResourceExn('When trying to use instance nr %d of %r: %s' % (used_count + 1, kind, msg))
500
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200501 pick = available[0]
502 self.dbg(using=pick)
503 assert not pick.get(USED_KEY)
504 pick[USED_KEY] = True
505 return copy.deepcopy(pick)
506
507 def put(self, item):
508 if not item.get(USED_KEY):
509 raise RuntimeError('Can only put() a resource that is used: %r' % item)
510 hash_to_put = item.get(HASH_KEY)
511 if not hash_to_put:
512 raise RuntimeError('Can only put() a resource that has a hash marker: %r' % item)
513 for key, item_list in self.reserved.items():
514 my_list = self.get(key)
515 for my_item in my_list:
516 if hash_to_put == my_item.get(HASH_KEY):
517 my_item.pop(USED_KEY)
518
519 def put_all(self):
Pau Espin Pedrol1dd29552017-06-13 18:07:57 +0200520 if not self.reserved:
521 return
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200522 for key, item_list in self.reserved.items():
Pau Espin Pedrol1dd29552017-06-13 18:07:57 +0200523 for item in item_list:
524 item.pop(USED_KEY, None)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200525
526 def free(self):
Neels Hofmeyred4e5282017-05-29 02:53:54 +0200527 if self.reserved:
528 self.resources_pool.free(self.origin, self.reserved)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200529 self.reserved = None
530
Neels Hofmeyr2d1d5612017-05-22 20:02:41 +0200531 def counts(self):
532 counts = {}
533 for key in self.reserved.keys():
534 counts[key] = self.count(key)
535 return counts
536
537 def count(self, key):
538 return len(self.reserved.get(key) or [])
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200539
540# vim: expandtab tabstop=4 shiftwidth=4