blob: 621522b1ae86080d9cb8d553b2ae620c9432333b [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
Pau Espin Pedrol600c7992020-11-09 21:17:51 +010029from .event_loop import MainLoop
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +020030
Pau Espin Pedrol06cb5362020-05-04 18:58:53 +020031from .util import is_dict, is_list
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +020032
Neels Hofmeyr3531a192017-03-28 14:30:28 +020033HASH_KEY = '_hash'
34RESERVED_KEY = '_reserved_by'
35USED_KEY = '_used'
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +020036
Neels Hofmeyr3531a192017-03-28 14:30:28 +020037RESERVED_RESOURCES_FILE = 'reserved_resources.state'
38
Neels Hofmeyr76d81032017-05-18 18:35:32 +020039R_IP_ADDRESS = 'ip_address'
Pau Espin Pedrol116a2c42020-02-11 17:41:13 +010040R_RUN_NODE = 'run_node'
Neels Hofmeyr3531a192017-03-28 14:30:28 +020041R_BTS = 'bts'
42R_ARFCN = 'arfcn'
43R_MODEM = 'modem'
Pau Espin Pedrolbc1ed882018-05-17 16:59:58 +020044R_OSMOCON = 'osmocon_phone'
Pau Espin Pedrolc8b0f932020-02-11 17:45:26 +010045R_ENB = 'enb'
Pau Espin Pedrol0b302792017-09-10 16:33:10 +020046
Neels Hofmeyr3531a192017-03-28 14:30:28 +020047class ResourcesPool(log.Origin):
48 _remember_to_free = None
49 _registered_exit_handler = False
50
51 def __init__(self):
Pau Espin Pedrol600c7992020-11-09 21:17:51 +010052 self.reserved_modified = False
Pau Espin Pedrol6c6c0e82020-05-11 18:30:58 +020053 self.config_path = config.get_main_config_value(config.CFG_RESOURCES_CONF)
Neels Hofmeyr3531a192017-03-28 14:30:28 +020054 self.state_dir = config.get_state_dir()
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +020055 super().__init__(log.C_CNF, conf=self.config_path, state=self.state_dir.path)
Neels Hofmeyr3531a192017-03-28 14:30:28 +020056 self.read_conf()
57
58 def read_conf(self):
Pau Espin Pedrole66e3ae2020-06-15 13:57:54 +020059 self.all_resources = Resources(config.read(self.config_path, schema.get_resources_schema()) or {})
Neels Hofmeyr3531a192017-03-28 14:30:28 +020060 self.all_resources.set_hashes()
61
Pau Espin Pedrol600c7992020-11-09 21:17:51 +010062 # Used by FileWatch in reserve() method below
63 def reserve_resources_fw_cb(self, event):
64 if event.event_type == 'modified':
65 self.reserved_modified = True
66
Pau Espin Pedrolaab56922018-08-21 14:58:29 +020067 def reserve(self, origin, want, modifiers):
Neels Hofmeyr3531a192017-03-28 14:30:28 +020068 '''
69 attempt to reserve the resources specified in the dict 'want' for
70 'origin'. Obtain a lock on the resources lock dir, verify that all
71 wanted resources are available, and if yes mark them as reserved.
72
73 On success, return a reservation object which can be used to release
74 the reservation. The reservation will be freed automatically on program
75 exit, if not yet done manually.
76
77 'origin' should be an Origin() instance.
78
Pau Espin Pedrolaab56922018-08-21 14:58:29 +020079 'want' is a dict matching RESOURCES_SCHEMA, used to specify what to
80 reserve.
81
82 'modifiers' is a dict matching RESOURCES_SCHEMA, it is overlaid on top
83 of 'want'.
Neels Hofmeyr3531a192017-03-28 14:30:28 +020084
Pau Espin Pedrol0b302792017-09-10 16:33:10 +020085 If an entry has no attribute set, any of the resources may be
Neels Hofmeyr3531a192017-03-28 14:30:28 +020086 reserved without further limitations.
87
88 ResourcesPool may also be selected with narrowed down constraints.
Neels Hofmeyr76d81032017-05-18 18:35:32 +020089 This would reserve one IP address, two modems, one BTS of type
Neels Hofmeyr391afe32017-05-18 19:22:12 +020090 sysmo and one of type trx, plus 2 ARFCNs in the 1800 band:
Neels Hofmeyr3531a192017-03-28 14:30:28 +020091
92 {
Neels Hofmeyr76d81032017-05-18 18:35:32 +020093 'ip_address': [ {} ],
Neels Hofmeyr391afe32017-05-18 19:22:12 +020094 'bts': [ { 'type': 'sysmo' }, { 'type': 'trx' } ],
Pau Espin Pedrol0b302792017-09-10 16:33:10 +020095 'arfcn': [ { 'band': 'GSM-1800' }, { 'band': 'GSM-1800' } ],
96 'modem': [ {}, {} ],
Neels Hofmeyr3531a192017-03-28 14:30:28 +020097 }
98 '''
Pau Espin Pedrolea8c3d42020-05-04 12:05:05 +020099 schema.validate(want, schema.get_resources_schema())
100 schema.validate(modifiers, schema.get_resources_schema())
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200101
102 origin_id = origin.origin_id()
103
Pau Espin Pedrol600c7992020-11-09 21:17:51 +0100104 # Make sure wanted resources can ever be reserved, even if all
105 # resources are unallocated. It will throw an exception if not
106 # possible:
107 self.all_resources.find(origin, want, None, False, True, 'Verifying')
108 self.reserved_modified = True # go through on first attempt
109 rrfile_path = self.state_dir.mk_parentdir(RESERVED_RESOURCES_FILE)
110 fw = util.FileWatch(origin, rrfile_path, self.reserve_resources_fw_cb)
111 fw.start()
112 while True:
113 # First, figure out if RESERVED_RESOURCES_FILE was modified since last time we checked:
114 modified = False
115 try:
116 fw.get_lock().acquire()
117 if self.reserved_modified:
118 modified = True
119 self.reserved_modified = False
120 finally:
121 fw.get_lock().release()
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200122
Pau Espin Pedrol600c7992020-11-09 21:17:51 +0100123 if modified: # file was modified, attempt to reserve resources
124 # It should be possible at some point to reserve the wanted
125 # resources, so try and wait for some to be released if it's not
126 # possible to allocate them now:
127 try:
128 with self.state_dir.lock(origin_id):
129 reserved = Resources(config.read(rrfile_path, if_missing_return={}))
130 to_be_reserved = self.all_resources.without(reserved).find(origin, want)
131 to_be_reserved.mark_reserved_by(origin_id)
132 reserved.add(to_be_reserved)
133 fw.stop()
134 config.write(rrfile_path, reserved)
135 self.remember_to_free(to_be_reserved)
136 return ReservedResources(self, origin, to_be_reserved, modifiers)
137 except NoResourceExn:
138 origin.log('Unable to reserve resources, too many currently reserved. Waiting until some are available again')
139 MainLoop.sleep(1)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200140
141 def free(self, origin, to_be_freed):
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200142 log.ctx(origin)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200143 with self.state_dir.lock(origin.origin_id()):
144 rrfile_path = self.state_dir.mk_parentdir(RESERVED_RESOURCES_FILE)
145 reserved = Resources(config.read(rrfile_path, if_missing_return={}))
146 reserved.drop(to_be_freed)
147 config.write(rrfile_path, reserved)
148 self.forget_freed(to_be_freed)
149
150 def register_exit_handler(self):
151 if self._registered_exit_handler:
152 return
153 atexit.register(self.clean_up_registered_resources)
154 self._registered_exit_handler = True
155
156 def unregister_exit_handler(self):
157 if not self._registered_exit_handler:
158 return
159 atexit.unregister(self.clean_up_registered_resources)
160 self._registered_exit_handler = False
161
162 def clean_up_registered_resources(self):
163 if not self._remember_to_free:
164 return
165 self.free(log.Origin('atexit.clean_up_registered_resources()'),
166 self._remember_to_free)
167
168 def remember_to_free(self, to_be_reserved):
169 self.register_exit_handler()
170 if not self._remember_to_free:
171 self._remember_to_free = Resources()
172 self._remember_to_free.add(to_be_reserved)
173
174 def forget_freed(self, freed):
175 if freed is self._remember_to_free:
176 self._remember_to_free.clear()
177 else:
178 self._remember_to_free.drop(freed)
179 if not self._remember_to_free:
180 self.unregister_exit_handler()
181
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100182 def next_persistent_value(self, token, first_val, validate_func, inc_func, origin):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200183 origin_id = origin.origin_id()
184
185 with self.state_dir.lock(origin_id):
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100186 token_path = self.state_dir.child('last_used_%s.state' % token)
187 log.ctx(token_path)
188 last_value = first_val
189 if os.path.exists(token_path):
190 if not os.path.isfile(token_path):
191 raise RuntimeError('path should be a file but is not: %r' % token_path)
192 with open(token_path, 'r') as f:
193 last_value = f.read().strip()
194 validate_func(last_value)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200195
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100196 next_value = inc_func(last_value)
197 with open(token_path, 'w') as f:
198 f.write(next_value)
199 return next_value
200
201 def next_msisdn(self, origin):
202 return self.next_persistent_value('msisdn', '1000', schema.msisdn, util.msisdn_inc, origin)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200203
Pau Espin Pedrol5e0c2512017-11-06 18:40:23 +0100204 def next_lac(self, origin):
Pau Espin Pedrolf7f06362017-11-28 12:56:35 +0100205 # LAC=0 has special meaning (MS detached), avoid it
206 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 +0200207
Pau Espin Pedrol8a3a7b52017-11-28 15:50:02 +0100208 def next_rac(self, origin):
209 return self.next_persistent_value('rac', '1', schema.uint8, lambda x: str((int(x)+1) % pow(2,8) or 1), origin)
210
Pau Espin Pedrol4ccce7c2017-11-07 11:13:20 +0100211 def next_cellid(self, origin):
212 return self.next_persistent_value('cellid', '1', schema.uint16, lambda x: str((int(x)+1) % pow(2,16)), origin)
213
Pau Espin Pedrol8a3a7b52017-11-28 15:50:02 +0100214 def next_bvci(self, origin):
215 # BVCI=0 and =1 are reserved, avoid them.
216 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)
217
Pau Espin Pedrol92a29d62020-10-05 17:25:16 +0200218 def next_zmq_port_range(self, origin, num_ports):
219 # Allocate continuous num_ports port between 2000 and 2200. returns base port.
220 # Assumption: base port is always an odd number.
221 num_ports = num_ports if num_ports % 2 == 0 else num_ports + 1
222 base_port = self.next_persistent_value('bvci', '2000', schema.uint16,
223 lambda x: str(int(x)+num_ports) if int(x) < 2200 - num_ports else '2000', origin)
224 return int(base_port)
225
226
Pau Espin Pedrol4676cbd2017-09-14 17:35:03 +0200227class NoResourceExn(log.Error):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200228 pass
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200229
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200230class Resources(dict):
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200231
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200232 def __init__(self, all_resources={}, do_copy=True):
233 if do_copy:
234 all_resources = copy.deepcopy(all_resources)
235 self.update(all_resources)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200236
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200237 def drop(self, reserved, fail_if_not_found=True):
238 # protect from modifying reserved because we're the same object
239 if reserved is self:
240 raise RuntimeError('Refusing to drop a list of resources from itself.'
241 ' This is probably a bug where a list of Resources()'
242 ' should have been copied but is passed as-is.'
243 ' use Resources.clear() instead.')
244
245 for key, reserved_list in reserved.items():
246 my_list = self.get(key) or []
247
248 if my_list is reserved_list:
249 self.pop(key)
250 continue
251
252 for reserved_item in reserved_list:
253 found = False
254 reserved_hash = reserved_item.get(HASH_KEY)
255 if not reserved_hash:
256 raise RuntimeError('Resources.drop() only works with hashed items')
257
258 for i in range(len(my_list)):
259 my_item = my_list[i]
260 my_hash = my_item.get(HASH_KEY)
261 if not my_hash:
262 raise RuntimeError('Resources.drop() only works with hashed items')
263 if my_hash == reserved_hash:
264 found = True
265 my_list.pop(i)
266 break
267
268 if fail_if_not_found and not found:
269 raise RuntimeError('Asked to drop resource from a pool, but the'
270 ' resource was not found: %s = %r' % (key, reserved_item))
271
272 if not my_list:
273 self.pop(key)
274 return self
275
276 def without(self, reserved):
277 return Resources(self).drop(reserved)
278
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200279 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 +0200280 '''
281 Pass a dict of resource requirements, e.g.:
282 want = {
283 'bts': [ {'type': 'osmo-bts-sysmo',}, {} ],
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200284 'modem': [ {}, {}, {} ]
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200285 }
286 This function tries to find a combination from the available resources that
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200287 matches these requirements. The return value is a dict (wrapped in a Resources class)
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200288 that contains the matching resources in the order of 'want' dict: in above
289 example, the returned dict would have a 'bts' list with the first item being
290 a sysmoBTS, the second item being any other available BTS.
291
292 If skip_if_marked is passed, any resource that contains this key is skipped.
293 E.g. if a BTS has the USED_KEY set like
294 reserved_resources = { 'bts' : {..., '_used': True} }
295 then this may be skipped by passing skip_if_marked='_used'
296 (or rather skip_if_marked=USED_KEY).
297
298 If do_copy is True, the returned dict is a deep copy and does not share
299 lists with any other Resources dict.
300
301 If raise_if_missing is False, this will return an empty item for any
302 resource that had no match, instead of immediately raising an exception.
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200303
304 This function expects input dictionaries whose contents have already
305 been replicated based on its the 'times' attributes. See
306 config.replicate_times() for more details.
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200307 '''
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200308 matches = {}
Neels Hofmeyr17c139e2017-04-12 02:42:02 +0200309 for key, want_list in sorted(want.items()): # sorted for deterministic test results
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200310 # here we have a resource of a given type, e.g. 'bts', with a list
311 # containing as many BTSes as the caller wants to reserve/use. Each
312 # list item contains specifics for the particular BTS.
Neels Hofmeyr2a1a1fa2017-05-29 01:36:21 +0200313 my_list = self.get(key, [])
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200314
Neels Hofmeyrf9e86932017-06-06 19:47:40 +0200315 if log_label:
316 for_origin.log(log_label, len(want_list), 'x', key, '(candidates: %d)'%len(my_list))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200317
318 # Try to avoid a less constrained item snatching away a resource
319 # from a more detailed constrained requirement.
320
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200321 # first record all matches, so that each requested item has a list
322 # of all available resources that match it. Some resources may
323 # appear for multiple requested items. Store matching indexes.
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200324 all_matches = []
325 for want_item in want_list:
326 item_match_list = []
327 for i in range(len(my_list)):
328 my_item = my_list[i]
329 if skip_if_marked and my_item.get(skip_if_marked):
330 continue
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200331 if item_matches(my_item, want_item):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200332 item_match_list.append(i)
333 if not item_match_list:
Neels Hofmeyr9b907702017-05-06 23:20:33 +0200334 if raise_if_missing:
335 raise NoResourceExn('No matching resource available for %s = %r'
336 % (key, want_item))
337 else:
338 # this one failed... see below
339 all_matches = []
340 break
341
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200342 all_matches.append( item_match_list )
343
344 if not all_matches:
Neels Hofmeyr9b907702017-05-06 23:20:33 +0200345 # ...this one failed. Makes no sense to solve resource
346 # allocations, return an empty list for this key to mark
347 # failure.
348 matches[key] = []
349 continue
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200350
351 # figure out who gets what
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200352 try:
353 solution = solve(all_matches)
354 except NotSolvable:
355 # instead of a cryptic error message, raise an exception that
356 # conveys meaning to the user.
357 raise NoResourceExn('Could not resolve request to reserve resources: '
358 '%d x %s with requirements: %r' % (len(want_list), key, want_list))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200359 picked = [ my_list[i] for i in solution if i is not None ]
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200360 for_origin.dbg('Picked', config.tostr(picked))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200361 matches[key] = picked
362
363 return Resources(matches, do_copy=do_copy)
364
365 def set_hashes(self):
366 for key, item_list in self.items():
367 for item in item_list:
368 item[HASH_KEY] = util.hash_obj(item, HASH_KEY, RESERVED_KEY, USED_KEY)
369
370 def add(self, more):
371 if more is self:
372 raise RuntimeError('adding a list of resources to itself?')
Pau Espin Pedrolea8c3d42020-05-04 12:05:05 +0200373 schema.add(self, copy.deepcopy(more))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200374
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200375 def mark_reserved_by(self, origin_id):
376 for key, item_list in self.items():
377 for item in item_list:
378 item[RESERVED_KEY] = origin_id
379
380
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200381class NotSolvable(Exception):
382 pass
383
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200384def solve(all_matches):
385 '''
386 all_matches shall be a list of index-lists.
387 all_matches[i] is the list of indexes that item i can use.
388 Return a solution so that each i gets a different index.
389 solve([ [0, 1, 2],
390 [0],
391 [0, 2] ]) == [1, 0, 2]
392 '''
393
394 def all_differ(l):
395 return len(set(l)) == len(l)
396
397 def search_in_permutations(fixed=[]):
398 idx = len(fixed)
399 for i in range(len(all_matches[idx])):
400 val = all_matches[idx][i]
401 # don't add a val that's already in the list
402 if val in fixed:
403 continue
404 l = list(fixed)
405 l.append(val)
406 if len(l) == len(all_matches):
407 # found a solution
408 return l
409 # not at the end yet, add next digit
410 r = search_in_permutations(l)
411 if r:
412 # nested search_in_permutations() call found a solution
413 return r
414 # this entire branch yielded no solution
415 return None
416
417 if not all_matches:
418 raise RuntimeError('Cannot solve: no candidates')
419
420 solution = search_in_permutations()
421 if not solution:
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200422 raise NotSolvable('The requested resource requirements are not solvable %r'
423 % all_matches)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200424 return solution
425
426
427def contains_hash(list_of_dicts, a_hash):
428 for d in list_of_dicts:
429 if d.get(HASH_KEY) == a_hash:
430 return True
431 return False
432
433def item_matches(item, wanted_item, ignore_keys=None):
434 if is_dict(wanted_item):
435 # match up two dicts
436 if not isinstance(item, dict):
437 return False
438 for key, wanted_val in wanted_item.items():
439 if ignore_keys and key in ignore_keys:
440 continue
441 if not item_matches(item.get(key), wanted_val, ignore_keys=ignore_keys):
442 return False
443 return True
444
445 if is_list(wanted_item):
Pau Espin Pedrol4e36f7c2017-08-28 13:29:28 +0200446 if not is_list(item):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200447 return False
Pau Espin Pedrol58475512017-09-14 15:33:15 +0200448 # Validate that all elements in both lists are of the same type:
449 t = util.list_validate_same_elem_type(wanted_item + item)
450 if t is None:
451 return True # both lists are empty, return
452 # For lists of complex objects, we expect them to be sorted lists:
453 if t in (dict, list, tuple):
454 for i in range(max(len(wanted_item), len(item))):
455 log.ctx(idx=i)
456 subitem = item[i] if i < len(item) else util.empty_instance_type(t)
457 wanted_subitem = wanted_item[i] if i < len(wanted_item) else util.empty_instance_type(t)
458 if not item_matches(subitem, wanted_subitem, ignore_keys=ignore_keys):
459 return False
460 else: # for lists of basic elements, we handle them as unsorted sets:
461 for val in wanted_item:
462 if val not in item:
463 return False
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200464 return True
465
466 return item == wanted_item
467
468
469class ReservedResources(log.Origin):
470 '''
471 After all resources have been figured out, this is the API that a test case
472 gets to interact with resources. From those resources that have been
473 reserved for it, it can pick some to mark them as currently in use.
474 Functions like nitb() provide a resource by automatically picking its
475 dependencies from so far unused (but reserved) resource.
476 '''
477
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200478 def __init__(self, resources_pool, origin, reserved, modifiers):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200479 self.resources_pool = resources_pool
480 self.origin = origin
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200481 self.reserved_original = reserved
482 self.reserved = copy.deepcopy(self.reserved_original)
483 config.overlay(self.reserved, modifiers)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200484
485 def __repr__(self):
486 return 'resources(%s)=%s' % (self.origin.name(), pprint.pformat(self.reserved))
487
488 def get(self, kind, specifics=None):
489 if specifics is None:
490 specifics = {}
491 self.dbg('requesting use of', kind, specifics=specifics)
492 want = { kind: [specifics] }
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200493 available_dict = self.reserved.find(self.origin, want, skip_if_marked=USED_KEY,
494 do_copy=False, raise_if_missing=False,
495 log_label='Using')
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200496 available = available_dict.get(kind)
497 self.dbg(available=len(available))
498 if not available:
Neels Hofmeyrf9e86932017-06-06 19:47:40 +0200499 # cook up a detailed error message for the current situation
500 kind_reserved = self.reserved.get(kind, [])
501 used_count = len([r for r in kind_reserved if USED_KEY in r])
502 matching = self.reserved.find(self.origin, want, raise_if_missing=False, log_label=None).get(kind, [])
503 if not matching:
504 msg = 'none of the reserved resources matches requirements %r' % specifics
505 elif not (used_count < len(kind_reserved)):
506 msg = 'suite.conf reserved only %d x %r.' % (len(kind_reserved), kind)
507 else:
508 msg = ('No unused resource left that matches the requirements;'
509 ' Of reserved %d x %r, %d match the requirements, but all are already in use;'
510 ' Requirements: %r'
511 % (len(kind_reserved), kind, len(matching), specifics))
512 raise NoResourceExn('When trying to use instance nr %d of %r: %s' % (used_count + 1, kind, msg))
513
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200514 pick = available[0]
515 self.dbg(using=pick)
516 assert not pick.get(USED_KEY)
517 pick[USED_KEY] = True
518 return copy.deepcopy(pick)
519
520 def put(self, item):
521 if not item.get(USED_KEY):
522 raise RuntimeError('Can only put() a resource that is used: %r' % item)
523 hash_to_put = item.get(HASH_KEY)
524 if not hash_to_put:
525 raise RuntimeError('Can only put() a resource that has a hash marker: %r' % item)
526 for key, item_list in self.reserved.items():
527 my_list = self.get(key)
528 for my_item in my_list:
529 if hash_to_put == my_item.get(HASH_KEY):
530 my_item.pop(USED_KEY)
531
532 def put_all(self):
Pau Espin Pedrol1dd29552017-06-13 18:07:57 +0200533 if not self.reserved:
534 return
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200535 for key, item_list in self.reserved.items():
Pau Espin Pedrol1dd29552017-06-13 18:07:57 +0200536 for item in item_list:
537 item.pop(USED_KEY, None)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200538
539 def free(self):
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200540 if self.reserved_original:
541 self.resources_pool.free(self.origin, self.reserved_original)
542 self.reserved_original = None
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200543
Neels Hofmeyr2d1d5612017-05-22 20:02:41 +0200544 def counts(self):
545 counts = {}
546 for key in self.reserved.keys():
547 counts[key] = self.count(key)
548 return counts
549
550 def count(self, key):
551 return len(self.reserved.get(key) or [])
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200552
553# vim: expandtab tabstop=4 shiftwidth=4