blob: 25bb00fa6472cb86e36d84b09a6da8a2d188027a [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
Pau Espin Pedroldaed4472017-09-15 14:11:35 +020032from . import bts_sysmo, bts_osmotrx, bts_octphy
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,
Pau Espin Pedroldaed4472017-09-15 14:11:35 +020086 'osmo-bts-octphy': bts_octphy.OsmoBtsOctphy,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020087 }
88
89def register_bts_type(name, clazz):
90 KNOWN_BTS_TYPES[name] = clazz
91
92class ResourcesPool(log.Origin):
93 _remember_to_free = None
94 _registered_exit_handler = False
95
96 def __init__(self):
97 self.config_path = config.get_config_file(RESOURCES_CONF)
98 self.state_dir = config.get_state_dir()
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +020099 super().__init__(log.C_CNF, conf=self.config_path, state=self.state_dir.path)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200100 self.read_conf()
101
102 def read_conf(self):
103 self.all_resources = Resources(config.read(self.config_path, RESOURCES_SCHEMA))
104 self.all_resources.set_hashes()
105
106 def reserve(self, origin, want):
107 '''
108 attempt to reserve the resources specified in the dict 'want' for
109 'origin'. Obtain a lock on the resources lock dir, verify that all
110 wanted resources are available, and if yes mark them as reserved.
111
112 On success, return a reservation object which can be used to release
113 the reservation. The reservation will be freed automatically on program
114 exit, if not yet done manually.
115
116 'origin' should be an Origin() instance.
117
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200118 'want' is a dict matching RESOURCES_SCHEMA.
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200119
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200120 If an entry has no attribute set, any of the resources may be
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200121 reserved without further limitations.
122
123 ResourcesPool may also be selected with narrowed down constraints.
Neels Hofmeyr76d81032017-05-18 18:35:32 +0200124 This would reserve one IP address, two modems, one BTS of type
Neels Hofmeyr391afe32017-05-18 19:22:12 +0200125 sysmo and one of type trx, plus 2 ARFCNs in the 1800 band:
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200126
127 {
Neels Hofmeyr76d81032017-05-18 18:35:32 +0200128 'ip_address': [ {} ],
Neels Hofmeyr391afe32017-05-18 19:22:12 +0200129 'bts': [ { 'type': 'sysmo' }, { 'type': 'trx' } ],
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200130 'arfcn': [ { 'band': 'GSM-1800' }, { 'band': 'GSM-1800' } ],
131 'modem': [ {}, {} ],
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200132 }
133 '''
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200134 schema.validate(want, RESOURCES_SCHEMA)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200135
136 origin_id = origin.origin_id()
137
138 with self.state_dir.lock(origin_id):
139 rrfile_path = self.state_dir.mk_parentdir(RESERVED_RESOURCES_FILE)
140 reserved = Resources(config.read(rrfile_path, if_missing_return={}))
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200141 to_be_reserved = self.all_resources.without(reserved).find(origin, want)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200142
143 to_be_reserved.mark_reserved_by(origin_id)
144
145 reserved.add(to_be_reserved)
146 config.write(rrfile_path, reserved)
147
148 self.remember_to_free(to_be_reserved)
149 return ReservedResources(self, origin, to_be_reserved)
150
151 def free(self, origin, to_be_freed):
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200152 log.ctx(origin)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200153 with self.state_dir.lock(origin.origin_id()):
154 rrfile_path = self.state_dir.mk_parentdir(RESERVED_RESOURCES_FILE)
155 reserved = Resources(config.read(rrfile_path, if_missing_return={}))
156 reserved.drop(to_be_freed)
157 config.write(rrfile_path, reserved)
158 self.forget_freed(to_be_freed)
159
160 def register_exit_handler(self):
161 if self._registered_exit_handler:
162 return
163 atexit.register(self.clean_up_registered_resources)
164 self._registered_exit_handler = True
165
166 def unregister_exit_handler(self):
167 if not self._registered_exit_handler:
168 return
169 atexit.unregister(self.clean_up_registered_resources)
170 self._registered_exit_handler = False
171
172 def clean_up_registered_resources(self):
173 if not self._remember_to_free:
174 return
175 self.free(log.Origin('atexit.clean_up_registered_resources()'),
176 self._remember_to_free)
177
178 def remember_to_free(self, to_be_reserved):
179 self.register_exit_handler()
180 if not self._remember_to_free:
181 self._remember_to_free = Resources()
182 self._remember_to_free.add(to_be_reserved)
183
184 def forget_freed(self, freed):
185 if freed is self._remember_to_free:
186 self._remember_to_free.clear()
187 else:
188 self._remember_to_free.drop(freed)
189 if not self._remember_to_free:
190 self.unregister_exit_handler()
191
192 def next_msisdn(self, origin):
193 origin_id = origin.origin_id()
194
195 with self.state_dir.lock(origin_id):
196 msisdn_path = self.state_dir.child(LAST_USED_MSISDN_FILE)
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200197 log.ctx(msisdn_path)
198 last_msisdn = '1000'
199 if os.path.exists(msisdn_path):
200 if not os.path.isfile(msisdn_path):
201 raise RuntimeError('path should be a file but is not: %r' % msisdn_path)
202 with open(msisdn_path, 'r') as f:
203 last_msisdn = f.read().strip()
204 schema.msisdn(last_msisdn)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200205
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200206 next_msisdn = util.msisdn_inc(last_msisdn)
207 with open(msisdn_path, 'w') as f:
208 f.write(next_msisdn)
209 return next_msisdn
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200210
211
Pau Espin Pedrol4676cbd2017-09-14 17:35:03 +0200212class NoResourceExn(log.Error):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200213 pass
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200214
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200215class Resources(dict):
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200216
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200217 def __init__(self, all_resources={}, do_copy=True):
218 if do_copy:
219 all_resources = copy.deepcopy(all_resources)
220 self.update(all_resources)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200221
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200222 def drop(self, reserved, fail_if_not_found=True):
223 # protect from modifying reserved because we're the same object
224 if reserved is self:
225 raise RuntimeError('Refusing to drop a list of resources from itself.'
226 ' This is probably a bug where a list of Resources()'
227 ' should have been copied but is passed as-is.'
228 ' use Resources.clear() instead.')
229
230 for key, reserved_list in reserved.items():
231 my_list = self.get(key) or []
232
233 if my_list is reserved_list:
234 self.pop(key)
235 continue
236
237 for reserved_item in reserved_list:
238 found = False
239 reserved_hash = reserved_item.get(HASH_KEY)
240 if not reserved_hash:
241 raise RuntimeError('Resources.drop() only works with hashed items')
242
243 for i in range(len(my_list)):
244 my_item = my_list[i]
245 my_hash = my_item.get(HASH_KEY)
246 if not my_hash:
247 raise RuntimeError('Resources.drop() only works with hashed items')
248 if my_hash == reserved_hash:
249 found = True
250 my_list.pop(i)
251 break
252
253 if fail_if_not_found and not found:
254 raise RuntimeError('Asked to drop resource from a pool, but the'
255 ' resource was not found: %s = %r' % (key, reserved_item))
256
257 if not my_list:
258 self.pop(key)
259 return self
260
261 def without(self, reserved):
262 return Resources(self).drop(reserved)
263
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200264 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 +0200265 '''
266 Pass a dict of resource requirements, e.g.:
267 want = {
268 'bts': [ {'type': 'osmo-bts-sysmo',}, {} ],
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200269 'modem': [ {}, {}, {} ]
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200270 }
271 This function tries to find a combination from the available resources that
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200272 matches these requirements. The return value is a dict (wrapped in a Resources class)
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200273 that contains the matching resources in the order of 'want' dict: in above
274 example, the returned dict would have a 'bts' list with the first item being
275 a sysmoBTS, the second item being any other available BTS.
276
277 If skip_if_marked is passed, any resource that contains this key is skipped.
278 E.g. if a BTS has the USED_KEY set like
279 reserved_resources = { 'bts' : {..., '_used': True} }
280 then this may be skipped by passing skip_if_marked='_used'
281 (or rather skip_if_marked=USED_KEY).
282
283 If do_copy is True, the returned dict is a deep copy and does not share
284 lists with any other Resources dict.
285
286 If raise_if_missing is False, this will return an empty item for any
287 resource that had no match, instead of immediately raising an exception.
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200288
289 This function expects input dictionaries whose contents have already
290 been replicated based on its the 'times' attributes. See
291 config.replicate_times() for more details.
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200292 '''
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200293 matches = {}
Neels Hofmeyr17c139e2017-04-12 02:42:02 +0200294 for key, want_list in sorted(want.items()): # sorted for deterministic test results
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200295 # here we have a resource of a given type, e.g. 'bts', with a list
296 # containing as many BTSes as the caller wants to reserve/use. Each
297 # list item contains specifics for the particular BTS.
Neels Hofmeyr2a1a1fa2017-05-29 01:36:21 +0200298 my_list = self.get(key, [])
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200299
Neels Hofmeyrf9e86932017-06-06 19:47:40 +0200300 if log_label:
301 for_origin.log(log_label, len(want_list), 'x', key, '(candidates: %d)'%len(my_list))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200302
303 # Try to avoid a less constrained item snatching away a resource
304 # from a more detailed constrained requirement.
305
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200306 # first record all matches, so that each requested item has a list
307 # of all available resources that match it. Some resources may
308 # appear for multiple requested items. Store matching indexes.
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200309 all_matches = []
310 for want_item in want_list:
311 item_match_list = []
312 for i in range(len(my_list)):
313 my_item = my_list[i]
314 if skip_if_marked and my_item.get(skip_if_marked):
315 continue
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200316 if item_matches(my_item, want_item):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200317 item_match_list.append(i)
318 if not item_match_list:
Neels Hofmeyr9b907702017-05-06 23:20:33 +0200319 if raise_if_missing:
320 raise NoResourceExn('No matching resource available for %s = %r'
321 % (key, want_item))
322 else:
323 # this one failed... see below
324 all_matches = []
325 break
326
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200327 all_matches.append( item_match_list )
328
329 if not all_matches:
Neels Hofmeyr9b907702017-05-06 23:20:33 +0200330 # ...this one failed. Makes no sense to solve resource
331 # allocations, return an empty list for this key to mark
332 # failure.
333 matches[key] = []
334 continue
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200335
336 # figure out who gets what
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200337 try:
338 solution = solve(all_matches)
339 except NotSolvable:
340 # instead of a cryptic error message, raise an exception that
341 # conveys meaning to the user.
342 raise NoResourceExn('Could not resolve request to reserve resources: '
343 '%d x %s with requirements: %r' % (len(want_list), key, want_list))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200344 picked = [ my_list[i] for i in solution if i is not None ]
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200345 for_origin.dbg('Picked', config.tostr(picked))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200346 matches[key] = picked
347
348 return Resources(matches, do_copy=do_copy)
349
350 def set_hashes(self):
351 for key, item_list in self.items():
352 for item in item_list:
353 item[HASH_KEY] = util.hash_obj(item, HASH_KEY, RESERVED_KEY, USED_KEY)
354
355 def add(self, more):
356 if more is self:
357 raise RuntimeError('adding a list of resources to itself?')
358 config.add(self, copy.deepcopy(more))
359
360 def combine(self, more_rules):
361 if more_rules is self:
362 raise RuntimeError('combining a list of resource rules with itself?')
363 config.combine(self, copy.deepcopy(more))
364
365 def mark_reserved_by(self, origin_id):
366 for key, item_list in self.items():
367 for item in item_list:
368 item[RESERVED_KEY] = origin_id
369
370
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200371class NotSolvable(Exception):
372 pass
373
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200374def solve(all_matches):
375 '''
376 all_matches shall be a list of index-lists.
377 all_matches[i] is the list of indexes that item i can use.
378 Return a solution so that each i gets a different index.
379 solve([ [0, 1, 2],
380 [0],
381 [0, 2] ]) == [1, 0, 2]
382 '''
383
384 def all_differ(l):
385 return len(set(l)) == len(l)
386
387 def search_in_permutations(fixed=[]):
388 idx = len(fixed)
389 for i in range(len(all_matches[idx])):
390 val = all_matches[idx][i]
391 # don't add a val that's already in the list
392 if val in fixed:
393 continue
394 l = list(fixed)
395 l.append(val)
396 if len(l) == len(all_matches):
397 # found a solution
398 return l
399 # not at the end yet, add next digit
400 r = search_in_permutations(l)
401 if r:
402 # nested search_in_permutations() call found a solution
403 return r
404 # this entire branch yielded no solution
405 return None
406
407 if not all_matches:
408 raise RuntimeError('Cannot solve: no candidates')
409
410 solution = search_in_permutations()
411 if not solution:
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200412 raise NotSolvable('The requested resource requirements are not solvable %r'
413 % all_matches)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200414 return solution
415
416
417def contains_hash(list_of_dicts, a_hash):
418 for d in list_of_dicts:
419 if d.get(HASH_KEY) == a_hash:
420 return True
421 return False
422
423def item_matches(item, wanted_item, ignore_keys=None):
424 if is_dict(wanted_item):
425 # match up two dicts
426 if not isinstance(item, dict):
427 return False
428 for key, wanted_val in wanted_item.items():
429 if ignore_keys and key in ignore_keys:
430 continue
431 if not item_matches(item.get(key), wanted_val, ignore_keys=ignore_keys):
432 return False
433 return True
434
435 if is_list(wanted_item):
Pau Espin Pedrol4e36f7c2017-08-28 13:29:28 +0200436 if not is_list(item):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200437 return False
Pau Espin Pedrol58475512017-09-14 15:33:15 +0200438 # Validate that all elements in both lists are of the same type:
439 t = util.list_validate_same_elem_type(wanted_item + item)
440 if t is None:
441 return True # both lists are empty, return
442 # For lists of complex objects, we expect them to be sorted lists:
443 if t in (dict, list, tuple):
444 for i in range(max(len(wanted_item), len(item))):
445 log.ctx(idx=i)
446 subitem = item[i] if i < len(item) else util.empty_instance_type(t)
447 wanted_subitem = wanted_item[i] if i < len(wanted_item) else util.empty_instance_type(t)
448 if not item_matches(subitem, wanted_subitem, ignore_keys=ignore_keys):
449 return False
450 else: # for lists of basic elements, we handle them as unsorted sets:
451 for val in wanted_item:
452 if val not in item:
453 return False
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200454 return True
455
456 return item == wanted_item
457
458
459class ReservedResources(log.Origin):
460 '''
461 After all resources have been figured out, this is the API that a test case
462 gets to interact with resources. From those resources that have been
463 reserved for it, it can pick some to mark them as currently in use.
464 Functions like nitb() provide a resource by automatically picking its
465 dependencies from so far unused (but reserved) resource.
466 '''
467
468 def __init__(self, resources_pool, origin, reserved):
469 self.resources_pool = resources_pool
470 self.origin = origin
471 self.reserved = reserved
472
473 def __repr__(self):
474 return 'resources(%s)=%s' % (self.origin.name(), pprint.pformat(self.reserved))
475
476 def get(self, kind, specifics=None):
477 if specifics is None:
478 specifics = {}
479 self.dbg('requesting use of', kind, specifics=specifics)
480 want = { kind: [specifics] }
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200481 available_dict = self.reserved.find(self.origin, want, skip_if_marked=USED_KEY,
482 do_copy=False, raise_if_missing=False,
483 log_label='Using')
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200484 available = available_dict.get(kind)
485 self.dbg(available=len(available))
486 if not available:
Neels Hofmeyrf9e86932017-06-06 19:47:40 +0200487 # cook up a detailed error message for the current situation
488 kind_reserved = self.reserved.get(kind, [])
489 used_count = len([r for r in kind_reserved if USED_KEY in r])
490 matching = self.reserved.find(self.origin, want, raise_if_missing=False, log_label=None).get(kind, [])
491 if not matching:
492 msg = 'none of the reserved resources matches requirements %r' % specifics
493 elif not (used_count < len(kind_reserved)):
494 msg = 'suite.conf reserved only %d x %r.' % (len(kind_reserved), kind)
495 else:
496 msg = ('No unused resource left that matches the requirements;'
497 ' Of reserved %d x %r, %d match the requirements, but all are already in use;'
498 ' Requirements: %r'
499 % (len(kind_reserved), kind, len(matching), specifics))
500 raise NoResourceExn('When trying to use instance nr %d of %r: %s' % (used_count + 1, kind, msg))
501
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200502 pick = available[0]
503 self.dbg(using=pick)
504 assert not pick.get(USED_KEY)
505 pick[USED_KEY] = True
506 return copy.deepcopy(pick)
507
508 def put(self, item):
509 if not item.get(USED_KEY):
510 raise RuntimeError('Can only put() a resource that is used: %r' % item)
511 hash_to_put = item.get(HASH_KEY)
512 if not hash_to_put:
513 raise RuntimeError('Can only put() a resource that has a hash marker: %r' % item)
514 for key, item_list in self.reserved.items():
515 my_list = self.get(key)
516 for my_item in my_list:
517 if hash_to_put == my_item.get(HASH_KEY):
518 my_item.pop(USED_KEY)
519
520 def put_all(self):
Pau Espin Pedrol1dd29552017-06-13 18:07:57 +0200521 if not self.reserved:
522 return
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200523 for key, item_list in self.reserved.items():
Pau Espin Pedrol1dd29552017-06-13 18:07:57 +0200524 for item in item_list:
525 item.pop(USED_KEY, None)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200526
527 def free(self):
Neels Hofmeyred4e5282017-05-29 02:53:54 +0200528 if self.reserved:
529 self.resources_pool.free(self.origin, self.reserved)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200530 self.reserved = None
531
Neels Hofmeyr2d1d5612017-05-22 20:02:41 +0200532 def counts(self):
533 counts = {}
534 for key in self.reserved.keys():
535 counts[key] = self.count(key)
536 return counts
537
538 def count(self, key):
539 return len(self.reserved.get(key) or [])
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200540
541# vim: expandtab tabstop=4 shiftwidth=4