blob: 8412d6a73ddb2a86154d76b882f76dbe3b9d99ba [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
Pau Espin Pedrol6cdd2fd2017-11-07 11:57:42 +010030from . import modem
Neels Hofmeyr3531a192017-03-28 14:30:28 +020031from . 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'
Neels Hofmeyr3531a192017-03-28 14:30:28 +020041RESERVED_RESOURCES_FILE = 'reserved_resources.state'
42
Neels Hofmeyr76d81032017-05-18 18:35:32 +020043R_IP_ADDRESS = 'ip_address'
Neels Hofmeyr3531a192017-03-28 14:30:28 +020044R_BTS = 'bts'
45R_ARFCN = 'arfcn'
46R_MODEM = 'modem'
Neels Hofmeyr76d81032017-05-18 18:35:32 +020047R_ALL = (R_IP_ADDRESS, R_BTS, R_ARFCN, R_MODEM)
Neels Hofmeyr3531a192017-03-28 14:30:28 +020048
49RESOURCES_SCHEMA = {
Neels Hofmeyr76d81032017-05-18 18:35:32 +020050 'ip_address[].addr': schema.IPV4,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020051 'bts[].label': schema.STR,
52 'bts[].type': schema.STR,
Pau Espin Pedrolfa9a6d32017-09-12 15:13:21 +020053 'bts[].ipa_unit_id': schema.UINT,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020054 'bts[].addr': schema.IPV4,
55 'bts[].band': schema.BAND,
Pau Espin Pedrol404e1502017-08-22 11:17:43 +020056 'bts[].trx_remote_ip': schema.IPV4,
57 'bts[].launch_trx': schema.BOOL_STR,
Pau Espin Pedrol57497a62017-08-28 14:21:15 +020058 'bts[].ciphers[]': schema.CIPHER,
Your Name44af3412017-04-13 03:11:59 +020059 'bts[].trx_list[].hw_addr': schema.HWADDR,
60 'bts[].trx_list[].net_device': schema.STR,
Pau Espin Pedrolb26f32a2017-09-14 13:52:28 +020061 'bts[].trx_list[].nominal_power': schema.UINT,
62 'bts[].trx_list[].max_power_red': schema.UINT,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020063 'arfcn[].arfcn': schema.INT,
64 'arfcn[].band': schema.BAND,
65 'modem[].label': schema.STR,
66 'modem[].path': schema.STR,
67 'modem[].imsi': schema.IMSI,
68 'modem[].ki': schema.KI,
Pau Espin Pedrol713ce2c2017-08-24 16:57:17 +020069 'modem[].auth_algo': schema.AUTH_ALGO,
Pau Espin Pedrol57497a62017-08-28 14:21:15 +020070 'modem[].ciphers[]': schema.CIPHER,
Pau Espin Pedrolac18fd32017-08-31 18:49:47 +020071 'modem[].features[]': schema.MODEM_FEATURE,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020072 }
73
74WANT_SCHEMA = util.dict_add(
Pau Espin Pedrolead79ac2017-09-12 15:19:18 +020075 dict([('%s[].times' % r, schema.TIMES) for r in R_ALL]),
Neels Hofmeyr3531a192017-03-28 14:30:28 +020076 RESOURCES_SCHEMA)
77
Pau Espin Pedrol0b302792017-09-10 16:33:10 +020078CONF_SCHEMA = util.dict_add(
79 { 'defaults.timeout': schema.STR },
80 dict([('resources.%s' % key, val) for key, val in WANT_SCHEMA.items()]))
81
Neels Hofmeyr3531a192017-03-28 14:30:28 +020082KNOWN_BTS_TYPES = {
Your Name44af3412017-04-13 03:11:59 +020083 'osmo-bts-sysmo': bts_sysmo.SysmoBts,
84 'osmo-bts-trx': bts_osmotrx.OsmoBtsTrx,
Pau Espin Pedroldaed4472017-09-15 14:11:35 +020085 'osmo-bts-octphy': bts_octphy.OsmoBtsOctphy,
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
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100191 def next_persistent_value(self, token, first_val, validate_func, inc_func, origin):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200192 origin_id = origin.origin_id()
193
194 with self.state_dir.lock(origin_id):
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100195 token_path = self.state_dir.child('last_used_%s.state' % token)
196 log.ctx(token_path)
197 last_value = first_val
198 if os.path.exists(token_path):
199 if not os.path.isfile(token_path):
200 raise RuntimeError('path should be a file but is not: %r' % token_path)
201 with open(token_path, 'r') as f:
202 last_value = f.read().strip()
203 validate_func(last_value)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200204
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100205 next_value = inc_func(last_value)
206 with open(token_path, 'w') as f:
207 f.write(next_value)
208 return next_value
209
210 def next_msisdn(self, origin):
211 return self.next_persistent_value('msisdn', '1000', schema.msisdn, util.msisdn_inc, origin)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200212
Pau Espin Pedrol5e0c2512017-11-06 18:40:23 +0100213 def next_lac(self, origin):
Pau Espin Pedrolf7f06362017-11-28 12:56:35 +0100214 # LAC=0 has special meaning (MS detached), avoid it
215 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 +0200216
Pau Espin Pedrol4ccce7c2017-11-07 11:13:20 +0100217 def next_cellid(self, origin):
218 return self.next_persistent_value('cellid', '1', schema.uint16, lambda x: str((int(x)+1) % pow(2,16)), origin)
219
Pau Espin Pedrol4676cbd2017-09-14 17:35:03 +0200220class NoResourceExn(log.Error):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200221 pass
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200222
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200223class Resources(dict):
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200224
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200225 def __init__(self, all_resources={}, do_copy=True):
226 if do_copy:
227 all_resources = copy.deepcopy(all_resources)
228 self.update(all_resources)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200229
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200230 def drop(self, reserved, fail_if_not_found=True):
231 # protect from modifying reserved because we're the same object
232 if reserved is self:
233 raise RuntimeError('Refusing to drop a list of resources from itself.'
234 ' This is probably a bug where a list of Resources()'
235 ' should have been copied but is passed as-is.'
236 ' use Resources.clear() instead.')
237
238 for key, reserved_list in reserved.items():
239 my_list = self.get(key) or []
240
241 if my_list is reserved_list:
242 self.pop(key)
243 continue
244
245 for reserved_item in reserved_list:
246 found = False
247 reserved_hash = reserved_item.get(HASH_KEY)
248 if not reserved_hash:
249 raise RuntimeError('Resources.drop() only works with hashed items')
250
251 for i in range(len(my_list)):
252 my_item = my_list[i]
253 my_hash = my_item.get(HASH_KEY)
254 if not my_hash:
255 raise RuntimeError('Resources.drop() only works with hashed items')
256 if my_hash == reserved_hash:
257 found = True
258 my_list.pop(i)
259 break
260
261 if fail_if_not_found and not found:
262 raise RuntimeError('Asked to drop resource from a pool, but the'
263 ' resource was not found: %s = %r' % (key, reserved_item))
264
265 if not my_list:
266 self.pop(key)
267 return self
268
269 def without(self, reserved):
270 return Resources(self).drop(reserved)
271
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200272 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 +0200273 '''
274 Pass a dict of resource requirements, e.g.:
275 want = {
276 'bts': [ {'type': 'osmo-bts-sysmo',}, {} ],
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200277 'modem': [ {}, {}, {} ]
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200278 }
279 This function tries to find a combination from the available resources that
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200280 matches these requirements. The return value is a dict (wrapped in a Resources class)
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200281 that contains the matching resources in the order of 'want' dict: in above
282 example, the returned dict would have a 'bts' list with the first item being
283 a sysmoBTS, the second item being any other available BTS.
284
285 If skip_if_marked is passed, any resource that contains this key is skipped.
286 E.g. if a BTS has the USED_KEY set like
287 reserved_resources = { 'bts' : {..., '_used': True} }
288 then this may be skipped by passing skip_if_marked='_used'
289 (or rather skip_if_marked=USED_KEY).
290
291 If do_copy is True, the returned dict is a deep copy and does not share
292 lists with any other Resources dict.
293
294 If raise_if_missing is False, this will return an empty item for any
295 resource that had no match, instead of immediately raising an exception.
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200296
297 This function expects input dictionaries whose contents have already
298 been replicated based on its the 'times' attributes. See
299 config.replicate_times() for more details.
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200300 '''
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200301 matches = {}
Neels Hofmeyr17c139e2017-04-12 02:42:02 +0200302 for key, want_list in sorted(want.items()): # sorted for deterministic test results
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200303 # here we have a resource of a given type, e.g. 'bts', with a list
304 # containing as many BTSes as the caller wants to reserve/use. Each
305 # list item contains specifics for the particular BTS.
Neels Hofmeyr2a1a1fa2017-05-29 01:36:21 +0200306 my_list = self.get(key, [])
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200307
Neels Hofmeyrf9e86932017-06-06 19:47:40 +0200308 if log_label:
309 for_origin.log(log_label, len(want_list), 'x', key, '(candidates: %d)'%len(my_list))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200310
311 # Try to avoid a less constrained item snatching away a resource
312 # from a more detailed constrained requirement.
313
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200314 # first record all matches, so that each requested item has a list
315 # of all available resources that match it. Some resources may
316 # appear for multiple requested items. Store matching indexes.
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200317 all_matches = []
318 for want_item in want_list:
319 item_match_list = []
320 for i in range(len(my_list)):
321 my_item = my_list[i]
322 if skip_if_marked and my_item.get(skip_if_marked):
323 continue
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200324 if item_matches(my_item, want_item):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200325 item_match_list.append(i)
326 if not item_match_list:
Neels Hofmeyr9b907702017-05-06 23:20:33 +0200327 if raise_if_missing:
328 raise NoResourceExn('No matching resource available for %s = %r'
329 % (key, want_item))
330 else:
331 # this one failed... see below
332 all_matches = []
333 break
334
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200335 all_matches.append( item_match_list )
336
337 if not all_matches:
Neels Hofmeyr9b907702017-05-06 23:20:33 +0200338 # ...this one failed. Makes no sense to solve resource
339 # allocations, return an empty list for this key to mark
340 # failure.
341 matches[key] = []
342 continue
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200343
344 # figure out who gets what
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200345 try:
346 solution = solve(all_matches)
347 except NotSolvable:
348 # instead of a cryptic error message, raise an exception that
349 # conveys meaning to the user.
350 raise NoResourceExn('Could not resolve request to reserve resources: '
351 '%d x %s with requirements: %r' % (len(want_list), key, want_list))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200352 picked = [ my_list[i] for i in solution if i is not None ]
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200353 for_origin.dbg('Picked', config.tostr(picked))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200354 matches[key] = picked
355
356 return Resources(matches, do_copy=do_copy)
357
358 def set_hashes(self):
359 for key, item_list in self.items():
360 for item in item_list:
361 item[HASH_KEY] = util.hash_obj(item, HASH_KEY, RESERVED_KEY, USED_KEY)
362
363 def add(self, more):
364 if more is self:
365 raise RuntimeError('adding a list of resources to itself?')
366 config.add(self, copy.deepcopy(more))
367
368 def combine(self, more_rules):
369 if more_rules is self:
370 raise RuntimeError('combining a list of resource rules with itself?')
371 config.combine(self, copy.deepcopy(more))
372
373 def mark_reserved_by(self, origin_id):
374 for key, item_list in self.items():
375 for item in item_list:
376 item[RESERVED_KEY] = origin_id
377
378
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200379class NotSolvable(Exception):
380 pass
381
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200382def solve(all_matches):
383 '''
384 all_matches shall be a list of index-lists.
385 all_matches[i] is the list of indexes that item i can use.
386 Return a solution so that each i gets a different index.
387 solve([ [0, 1, 2],
388 [0],
389 [0, 2] ]) == [1, 0, 2]
390 '''
391
392 def all_differ(l):
393 return len(set(l)) == len(l)
394
395 def search_in_permutations(fixed=[]):
396 idx = len(fixed)
397 for i in range(len(all_matches[idx])):
398 val = all_matches[idx][i]
399 # don't add a val that's already in the list
400 if val in fixed:
401 continue
402 l = list(fixed)
403 l.append(val)
404 if len(l) == len(all_matches):
405 # found a solution
406 return l
407 # not at the end yet, add next digit
408 r = search_in_permutations(l)
409 if r:
410 # nested search_in_permutations() call found a solution
411 return r
412 # this entire branch yielded no solution
413 return None
414
415 if not all_matches:
416 raise RuntimeError('Cannot solve: no candidates')
417
418 solution = search_in_permutations()
419 if not solution:
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200420 raise NotSolvable('The requested resource requirements are not solvable %r'
421 % all_matches)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200422 return solution
423
424
425def contains_hash(list_of_dicts, a_hash):
426 for d in list_of_dicts:
427 if d.get(HASH_KEY) == a_hash:
428 return True
429 return False
430
431def item_matches(item, wanted_item, ignore_keys=None):
432 if is_dict(wanted_item):
433 # match up two dicts
434 if not isinstance(item, dict):
435 return False
436 for key, wanted_val in wanted_item.items():
437 if ignore_keys and key in ignore_keys:
438 continue
439 if not item_matches(item.get(key), wanted_val, ignore_keys=ignore_keys):
440 return False
441 return True
442
443 if is_list(wanted_item):
Pau Espin Pedrol4e36f7c2017-08-28 13:29:28 +0200444 if not is_list(item):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200445 return False
Pau Espin Pedrol58475512017-09-14 15:33:15 +0200446 # Validate that all elements in both lists are of the same type:
447 t = util.list_validate_same_elem_type(wanted_item + item)
448 if t is None:
449 return True # both lists are empty, return
450 # For lists of complex objects, we expect them to be sorted lists:
451 if t in (dict, list, tuple):
452 for i in range(max(len(wanted_item), len(item))):
453 log.ctx(idx=i)
454 subitem = item[i] if i < len(item) else util.empty_instance_type(t)
455 wanted_subitem = wanted_item[i] if i < len(wanted_item) else util.empty_instance_type(t)
456 if not item_matches(subitem, wanted_subitem, ignore_keys=ignore_keys):
457 return False
458 else: # for lists of basic elements, we handle them as unsorted sets:
459 for val in wanted_item:
460 if val not in item:
461 return False
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200462 return True
463
464 return item == wanted_item
465
466
467class ReservedResources(log.Origin):
468 '''
469 After all resources have been figured out, this is the API that a test case
470 gets to interact with resources. From those resources that have been
471 reserved for it, it can pick some to mark them as currently in use.
472 Functions like nitb() provide a resource by automatically picking its
473 dependencies from so far unused (but reserved) resource.
474 '''
475
476 def __init__(self, resources_pool, origin, reserved):
477 self.resources_pool = resources_pool
478 self.origin = origin
479 self.reserved = reserved
480
481 def __repr__(self):
482 return 'resources(%s)=%s' % (self.origin.name(), pprint.pformat(self.reserved))
483
484 def get(self, kind, specifics=None):
485 if specifics is None:
486 specifics = {}
487 self.dbg('requesting use of', kind, specifics=specifics)
488 want = { kind: [specifics] }
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200489 available_dict = self.reserved.find(self.origin, want, skip_if_marked=USED_KEY,
490 do_copy=False, raise_if_missing=False,
491 log_label='Using')
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200492 available = available_dict.get(kind)
493 self.dbg(available=len(available))
494 if not available:
Neels Hofmeyrf9e86932017-06-06 19:47:40 +0200495 # cook up a detailed error message for the current situation
496 kind_reserved = self.reserved.get(kind, [])
497 used_count = len([r for r in kind_reserved if USED_KEY in r])
498 matching = self.reserved.find(self.origin, want, raise_if_missing=False, log_label=None).get(kind, [])
499 if not matching:
500 msg = 'none of the reserved resources matches requirements %r' % specifics
501 elif not (used_count < len(kind_reserved)):
502 msg = 'suite.conf reserved only %d x %r.' % (len(kind_reserved), kind)
503 else:
504 msg = ('No unused resource left that matches the requirements;'
505 ' Of reserved %d x %r, %d match the requirements, but all are already in use;'
506 ' Requirements: %r'
507 % (len(kind_reserved), kind, len(matching), specifics))
508 raise NoResourceExn('When trying to use instance nr %d of %r: %s' % (used_count + 1, kind, msg))
509
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200510 pick = available[0]
511 self.dbg(using=pick)
512 assert not pick.get(USED_KEY)
513 pick[USED_KEY] = True
514 return copy.deepcopy(pick)
515
516 def put(self, item):
517 if not item.get(USED_KEY):
518 raise RuntimeError('Can only put() a resource that is used: %r' % item)
519 hash_to_put = item.get(HASH_KEY)
520 if not hash_to_put:
521 raise RuntimeError('Can only put() a resource that has a hash marker: %r' % item)
522 for key, item_list in self.reserved.items():
523 my_list = self.get(key)
524 for my_item in my_list:
525 if hash_to_put == my_item.get(HASH_KEY):
526 my_item.pop(USED_KEY)
527
528 def put_all(self):
Pau Espin Pedrol1dd29552017-06-13 18:07:57 +0200529 if not self.reserved:
530 return
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200531 for key, item_list in self.reserved.items():
Pau Espin Pedrol1dd29552017-06-13 18:07:57 +0200532 for item in item_list:
533 item.pop(USED_KEY, None)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200534
535 def free(self):
Neels Hofmeyred4e5282017-05-29 02:53:54 +0200536 if self.reserved:
537 self.resources_pool.free(self.origin, self.reserved)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200538 self.reserved = None
539
Neels Hofmeyr2d1d5612017-05-22 20:02:41 +0200540 def counts(self):
541 counts = {}
542 for key in self.reserved.keys():
543 counts[key] = self.count(key)
544 return counts
545
546 def count(self, key):
547 return len(self.reserved.get(key) or [])
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200548
549# vim: expandtab tabstop=4 shiftwidth=4