blob: 70d6e8af4ad2d3a8f567537192615aaf51e59d57 [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 Pedrol1b28a582018-03-08 21:01:26 +010032from . import bts_sysmo, bts_osmotrx, bts_octphy, bts_nanobts
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 Pedrolce35d912017-11-23 11:01:24 +010058 'bts[].direct_pcu': schema.BOOL_STR,
Pau Espin Pedrol1b28a582018-03-08 21:01:26 +010059 'bts[].power_supply.type': schema.STR,
60 'bts[].power_supply.device': schema.STR,
61 'bts[].power_supply.port': schema.STR,
Pau Espin Pedrol57497a62017-08-28 14:21:15 +020062 'bts[].ciphers[]': schema.CIPHER,
Pau Espin Pedrol39df7f42018-05-07 13:49:33 +020063 'bts[].num_trx': schema.UINT,
64 'bts[].max_trx': schema.UINT,
Your Name44af3412017-04-13 03:11:59 +020065 'bts[].trx_list[].hw_addr': schema.HWADDR,
66 'bts[].trx_list[].net_device': schema.STR,
Pau Espin Pedrolb26f32a2017-09-14 13:52:28 +020067 'bts[].trx_list[].nominal_power': schema.UINT,
68 'bts[].trx_list[].max_power_red': schema.UINT,
Pau Espin Pedrolc9b63762018-05-07 01:57:01 +020069 'bts[].trx_list[].timeslot_list[].phys_chan_config': schema.PHY_CHAN,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020070 'arfcn[].arfcn': schema.INT,
71 'arfcn[].band': schema.BAND,
72 'modem[].label': schema.STR,
73 'modem[].path': schema.STR,
74 'modem[].imsi': schema.IMSI,
75 'modem[].ki': schema.KI,
Pau Espin Pedrol713ce2c2017-08-24 16:57:17 +020076 'modem[].auth_algo': schema.AUTH_ALGO,
Pau Espin Pedrol57497a62017-08-28 14:21:15 +020077 'modem[].ciphers[]': schema.CIPHER,
Pau Espin Pedrolac18fd32017-08-31 18:49:47 +020078 'modem[].features[]': schema.MODEM_FEATURE,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020079 }
80
81WANT_SCHEMA = util.dict_add(
Pau Espin Pedrolead79ac2017-09-12 15:19:18 +020082 dict([('%s[].times' % r, schema.TIMES) for r in R_ALL]),
Neels Hofmeyr3531a192017-03-28 14:30:28 +020083 RESOURCES_SCHEMA)
84
Pau Espin Pedrol0b302792017-09-10 16:33:10 +020085CONF_SCHEMA = util.dict_add(
86 { 'defaults.timeout': schema.STR },
87 dict([('resources.%s' % key, val) for key, val in WANT_SCHEMA.items()]))
88
Neels Hofmeyr3531a192017-03-28 14:30:28 +020089KNOWN_BTS_TYPES = {
Your Name44af3412017-04-13 03:11:59 +020090 'osmo-bts-sysmo': bts_sysmo.SysmoBts,
91 'osmo-bts-trx': bts_osmotrx.OsmoBtsTrx,
Pau Espin Pedroldaed4472017-09-15 14:11:35 +020092 'osmo-bts-octphy': bts_octphy.OsmoBtsOctphy,
Pau Espin Pedrol1b28a582018-03-08 21:01:26 +010093 'nanobts': bts_nanobts.NanoBts,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020094 }
95
96def register_bts_type(name, clazz):
97 KNOWN_BTS_TYPES[name] = clazz
98
99class ResourcesPool(log.Origin):
100 _remember_to_free = None
101 _registered_exit_handler = False
102
103 def __init__(self):
104 self.config_path = config.get_config_file(RESOURCES_CONF)
105 self.state_dir = config.get_state_dir()
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200106 super().__init__(log.C_CNF, conf=self.config_path, state=self.state_dir.path)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200107 self.read_conf()
108
109 def read_conf(self):
110 self.all_resources = Resources(config.read(self.config_path, RESOURCES_SCHEMA))
111 self.all_resources.set_hashes()
112
113 def reserve(self, origin, want):
114 '''
115 attempt to reserve the resources specified in the dict 'want' for
116 'origin'. Obtain a lock on the resources lock dir, verify that all
117 wanted resources are available, and if yes mark them as reserved.
118
119 On success, return a reservation object which can be used to release
120 the reservation. The reservation will be freed automatically on program
121 exit, if not yet done manually.
122
123 'origin' should be an Origin() instance.
124
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200125 'want' is a dict matching RESOURCES_SCHEMA.
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200126
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200127 If an entry has no attribute set, any of the resources may be
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200128 reserved without further limitations.
129
130 ResourcesPool may also be selected with narrowed down constraints.
Neels Hofmeyr76d81032017-05-18 18:35:32 +0200131 This would reserve one IP address, two modems, one BTS of type
Neels Hofmeyr391afe32017-05-18 19:22:12 +0200132 sysmo and one of type trx, plus 2 ARFCNs in the 1800 band:
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200133
134 {
Neels Hofmeyr76d81032017-05-18 18:35:32 +0200135 'ip_address': [ {} ],
Neels Hofmeyr391afe32017-05-18 19:22:12 +0200136 'bts': [ { 'type': 'sysmo' }, { 'type': 'trx' } ],
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200137 'arfcn': [ { 'band': 'GSM-1800' }, { 'band': 'GSM-1800' } ],
138 'modem': [ {}, {} ],
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200139 }
140 '''
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200141 schema.validate(want, RESOURCES_SCHEMA)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200142
143 origin_id = origin.origin_id()
144
145 with self.state_dir.lock(origin_id):
146 rrfile_path = self.state_dir.mk_parentdir(RESERVED_RESOURCES_FILE)
147 reserved = Resources(config.read(rrfile_path, if_missing_return={}))
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200148 to_be_reserved = self.all_resources.without(reserved).find(origin, want)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200149
150 to_be_reserved.mark_reserved_by(origin_id)
151
152 reserved.add(to_be_reserved)
153 config.write(rrfile_path, reserved)
154
155 self.remember_to_free(to_be_reserved)
156 return ReservedResources(self, origin, to_be_reserved)
157
158 def free(self, origin, to_be_freed):
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200159 log.ctx(origin)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200160 with self.state_dir.lock(origin.origin_id()):
161 rrfile_path = self.state_dir.mk_parentdir(RESERVED_RESOURCES_FILE)
162 reserved = Resources(config.read(rrfile_path, if_missing_return={}))
163 reserved.drop(to_be_freed)
164 config.write(rrfile_path, reserved)
165 self.forget_freed(to_be_freed)
166
167 def register_exit_handler(self):
168 if self._registered_exit_handler:
169 return
170 atexit.register(self.clean_up_registered_resources)
171 self._registered_exit_handler = True
172
173 def unregister_exit_handler(self):
174 if not self._registered_exit_handler:
175 return
176 atexit.unregister(self.clean_up_registered_resources)
177 self._registered_exit_handler = False
178
179 def clean_up_registered_resources(self):
180 if not self._remember_to_free:
181 return
182 self.free(log.Origin('atexit.clean_up_registered_resources()'),
183 self._remember_to_free)
184
185 def remember_to_free(self, to_be_reserved):
186 self.register_exit_handler()
187 if not self._remember_to_free:
188 self._remember_to_free = Resources()
189 self._remember_to_free.add(to_be_reserved)
190
191 def forget_freed(self, freed):
192 if freed is self._remember_to_free:
193 self._remember_to_free.clear()
194 else:
195 self._remember_to_free.drop(freed)
196 if not self._remember_to_free:
197 self.unregister_exit_handler()
198
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100199 def next_persistent_value(self, token, first_val, validate_func, inc_func, origin):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200200 origin_id = origin.origin_id()
201
202 with self.state_dir.lock(origin_id):
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100203 token_path = self.state_dir.child('last_used_%s.state' % token)
204 log.ctx(token_path)
205 last_value = first_val
206 if os.path.exists(token_path):
207 if not os.path.isfile(token_path):
208 raise RuntimeError('path should be a file but is not: %r' % token_path)
209 with open(token_path, 'r') as f:
210 last_value = f.read().strip()
211 validate_func(last_value)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200212
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100213 next_value = inc_func(last_value)
214 with open(token_path, 'w') as f:
215 f.write(next_value)
216 return next_value
217
218 def next_msisdn(self, origin):
219 return self.next_persistent_value('msisdn', '1000', schema.msisdn, util.msisdn_inc, origin)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200220
Pau Espin Pedrol5e0c2512017-11-06 18:40:23 +0100221 def next_lac(self, origin):
Pau Espin Pedrolf7f06362017-11-28 12:56:35 +0100222 # LAC=0 has special meaning (MS detached), avoid it
223 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 +0200224
Pau Espin Pedrol8a3a7b52017-11-28 15:50:02 +0100225 def next_rac(self, origin):
226 return self.next_persistent_value('rac', '1', schema.uint8, lambda x: str((int(x)+1) % pow(2,8) or 1), origin)
227
Pau Espin Pedrol4ccce7c2017-11-07 11:13:20 +0100228 def next_cellid(self, origin):
229 return self.next_persistent_value('cellid', '1', schema.uint16, lambda x: str((int(x)+1) % pow(2,16)), origin)
230
Pau Espin Pedrol8a3a7b52017-11-28 15:50:02 +0100231 def next_bvci(self, origin):
232 # BVCI=0 and =1 are reserved, avoid them.
233 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)
234
Pau Espin Pedrol4676cbd2017-09-14 17:35:03 +0200235class NoResourceExn(log.Error):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200236 pass
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200237
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200238class Resources(dict):
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200239
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200240 def __init__(self, all_resources={}, do_copy=True):
241 if do_copy:
242 all_resources = copy.deepcopy(all_resources)
243 self.update(all_resources)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200244
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200245 def drop(self, reserved, fail_if_not_found=True):
246 # protect from modifying reserved because we're the same object
247 if reserved is self:
248 raise RuntimeError('Refusing to drop a list of resources from itself.'
249 ' This is probably a bug where a list of Resources()'
250 ' should have been copied but is passed as-is.'
251 ' use Resources.clear() instead.')
252
253 for key, reserved_list in reserved.items():
254 my_list = self.get(key) or []
255
256 if my_list is reserved_list:
257 self.pop(key)
258 continue
259
260 for reserved_item in reserved_list:
261 found = False
262 reserved_hash = reserved_item.get(HASH_KEY)
263 if not reserved_hash:
264 raise RuntimeError('Resources.drop() only works with hashed items')
265
266 for i in range(len(my_list)):
267 my_item = my_list[i]
268 my_hash = my_item.get(HASH_KEY)
269 if not my_hash:
270 raise RuntimeError('Resources.drop() only works with hashed items')
271 if my_hash == reserved_hash:
272 found = True
273 my_list.pop(i)
274 break
275
276 if fail_if_not_found and not found:
277 raise RuntimeError('Asked to drop resource from a pool, but the'
278 ' resource was not found: %s = %r' % (key, reserved_item))
279
280 if not my_list:
281 self.pop(key)
282 return self
283
284 def without(self, reserved):
285 return Resources(self).drop(reserved)
286
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200287 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 +0200288 '''
289 Pass a dict of resource requirements, e.g.:
290 want = {
291 'bts': [ {'type': 'osmo-bts-sysmo',}, {} ],
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200292 'modem': [ {}, {}, {} ]
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200293 }
294 This function tries to find a combination from the available resources that
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200295 matches these requirements. The return value is a dict (wrapped in a Resources class)
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200296 that contains the matching resources in the order of 'want' dict: in above
297 example, the returned dict would have a 'bts' list with the first item being
298 a sysmoBTS, the second item being any other available BTS.
299
300 If skip_if_marked is passed, any resource that contains this key is skipped.
301 E.g. if a BTS has the USED_KEY set like
302 reserved_resources = { 'bts' : {..., '_used': True} }
303 then this may be skipped by passing skip_if_marked='_used'
304 (or rather skip_if_marked=USED_KEY).
305
306 If do_copy is True, the returned dict is a deep copy and does not share
307 lists with any other Resources dict.
308
309 If raise_if_missing is False, this will return an empty item for any
310 resource that had no match, instead of immediately raising an exception.
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200311
312 This function expects input dictionaries whose contents have already
313 been replicated based on its the 'times' attributes. See
314 config.replicate_times() for more details.
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200315 '''
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200316 matches = {}
Neels Hofmeyr17c139e2017-04-12 02:42:02 +0200317 for key, want_list in sorted(want.items()): # sorted for deterministic test results
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200318 # here we have a resource of a given type, e.g. 'bts', with a list
319 # containing as many BTSes as the caller wants to reserve/use. Each
320 # list item contains specifics for the particular BTS.
Neels Hofmeyr2a1a1fa2017-05-29 01:36:21 +0200321 my_list = self.get(key, [])
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200322
Neels Hofmeyrf9e86932017-06-06 19:47:40 +0200323 if log_label:
324 for_origin.log(log_label, len(want_list), 'x', key, '(candidates: %d)'%len(my_list))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200325
326 # Try to avoid a less constrained item snatching away a resource
327 # from a more detailed constrained requirement.
328
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200329 # first record all matches, so that each requested item has a list
330 # of all available resources that match it. Some resources may
331 # appear for multiple requested items. Store matching indexes.
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200332 all_matches = []
333 for want_item in want_list:
334 item_match_list = []
335 for i in range(len(my_list)):
336 my_item = my_list[i]
337 if skip_if_marked and my_item.get(skip_if_marked):
338 continue
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200339 if item_matches(my_item, want_item):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200340 item_match_list.append(i)
341 if not item_match_list:
Neels Hofmeyr9b907702017-05-06 23:20:33 +0200342 if raise_if_missing:
343 raise NoResourceExn('No matching resource available for %s = %r'
344 % (key, want_item))
345 else:
346 # this one failed... see below
347 all_matches = []
348 break
349
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200350 all_matches.append( item_match_list )
351
352 if not all_matches:
Neels Hofmeyr9b907702017-05-06 23:20:33 +0200353 # ...this one failed. Makes no sense to solve resource
354 # allocations, return an empty list for this key to mark
355 # failure.
356 matches[key] = []
357 continue
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200358
359 # figure out who gets what
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200360 try:
361 solution = solve(all_matches)
362 except NotSolvable:
363 # instead of a cryptic error message, raise an exception that
364 # conveys meaning to the user.
365 raise NoResourceExn('Could not resolve request to reserve resources: '
366 '%d x %s with requirements: %r' % (len(want_list), key, want_list))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200367 picked = [ my_list[i] for i in solution if i is not None ]
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200368 for_origin.dbg('Picked', config.tostr(picked))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200369 matches[key] = picked
370
371 return Resources(matches, do_copy=do_copy)
372
373 def set_hashes(self):
374 for key, item_list in self.items():
375 for item in item_list:
376 item[HASH_KEY] = util.hash_obj(item, HASH_KEY, RESERVED_KEY, USED_KEY)
377
378 def add(self, more):
379 if more is self:
380 raise RuntimeError('adding a list of resources to itself?')
381 config.add(self, copy.deepcopy(more))
382
383 def combine(self, more_rules):
384 if more_rules is self:
385 raise RuntimeError('combining a list of resource rules with itself?')
386 config.combine(self, copy.deepcopy(more))
387
388 def mark_reserved_by(self, origin_id):
389 for key, item_list in self.items():
390 for item in item_list:
391 item[RESERVED_KEY] = origin_id
392
393
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200394class NotSolvable(Exception):
395 pass
396
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200397def solve(all_matches):
398 '''
399 all_matches shall be a list of index-lists.
400 all_matches[i] is the list of indexes that item i can use.
401 Return a solution so that each i gets a different index.
402 solve([ [0, 1, 2],
403 [0],
404 [0, 2] ]) == [1, 0, 2]
405 '''
406
407 def all_differ(l):
408 return len(set(l)) == len(l)
409
410 def search_in_permutations(fixed=[]):
411 idx = len(fixed)
412 for i in range(len(all_matches[idx])):
413 val = all_matches[idx][i]
414 # don't add a val that's already in the list
415 if val in fixed:
416 continue
417 l = list(fixed)
418 l.append(val)
419 if len(l) == len(all_matches):
420 # found a solution
421 return l
422 # not at the end yet, add next digit
423 r = search_in_permutations(l)
424 if r:
425 # nested search_in_permutations() call found a solution
426 return r
427 # this entire branch yielded no solution
428 return None
429
430 if not all_matches:
431 raise RuntimeError('Cannot solve: no candidates')
432
433 solution = search_in_permutations()
434 if not solution:
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200435 raise NotSolvable('The requested resource requirements are not solvable %r'
436 % all_matches)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200437 return solution
438
439
440def contains_hash(list_of_dicts, a_hash):
441 for d in list_of_dicts:
442 if d.get(HASH_KEY) == a_hash:
443 return True
444 return False
445
446def item_matches(item, wanted_item, ignore_keys=None):
447 if is_dict(wanted_item):
448 # match up two dicts
449 if not isinstance(item, dict):
450 return False
451 for key, wanted_val in wanted_item.items():
452 if ignore_keys and key in ignore_keys:
453 continue
454 if not item_matches(item.get(key), wanted_val, ignore_keys=ignore_keys):
455 return False
456 return True
457
458 if is_list(wanted_item):
Pau Espin Pedrol4e36f7c2017-08-28 13:29:28 +0200459 if not is_list(item):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200460 return False
Pau Espin Pedrol58475512017-09-14 15:33:15 +0200461 # Validate that all elements in both lists are of the same type:
462 t = util.list_validate_same_elem_type(wanted_item + item)
463 if t is None:
464 return True # both lists are empty, return
465 # For lists of complex objects, we expect them to be sorted lists:
466 if t in (dict, list, tuple):
467 for i in range(max(len(wanted_item), len(item))):
468 log.ctx(idx=i)
469 subitem = item[i] if i < len(item) else util.empty_instance_type(t)
470 wanted_subitem = wanted_item[i] if i < len(wanted_item) else util.empty_instance_type(t)
471 if not item_matches(subitem, wanted_subitem, ignore_keys=ignore_keys):
472 return False
473 else: # for lists of basic elements, we handle them as unsorted sets:
474 for val in wanted_item:
475 if val not in item:
476 return False
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200477 return True
478
479 return item == wanted_item
480
481
482class ReservedResources(log.Origin):
483 '''
484 After all resources have been figured out, this is the API that a test case
485 gets to interact with resources. From those resources that have been
486 reserved for it, it can pick some to mark them as currently in use.
487 Functions like nitb() provide a resource by automatically picking its
488 dependencies from so far unused (but reserved) resource.
489 '''
490
491 def __init__(self, resources_pool, origin, reserved):
492 self.resources_pool = resources_pool
493 self.origin = origin
494 self.reserved = reserved
495
496 def __repr__(self):
497 return 'resources(%s)=%s' % (self.origin.name(), pprint.pformat(self.reserved))
498
499 def get(self, kind, specifics=None):
500 if specifics is None:
501 specifics = {}
502 self.dbg('requesting use of', kind, specifics=specifics)
503 want = { kind: [specifics] }
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200504 available_dict = self.reserved.find(self.origin, want, skip_if_marked=USED_KEY,
505 do_copy=False, raise_if_missing=False,
506 log_label='Using')
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200507 available = available_dict.get(kind)
508 self.dbg(available=len(available))
509 if not available:
Neels Hofmeyrf9e86932017-06-06 19:47:40 +0200510 # cook up a detailed error message for the current situation
511 kind_reserved = self.reserved.get(kind, [])
512 used_count = len([r for r in kind_reserved if USED_KEY in r])
513 matching = self.reserved.find(self.origin, want, raise_if_missing=False, log_label=None).get(kind, [])
514 if not matching:
515 msg = 'none of the reserved resources matches requirements %r' % specifics
516 elif not (used_count < len(kind_reserved)):
517 msg = 'suite.conf reserved only %d x %r.' % (len(kind_reserved), kind)
518 else:
519 msg = ('No unused resource left that matches the requirements;'
520 ' Of reserved %d x %r, %d match the requirements, but all are already in use;'
521 ' Requirements: %r'
522 % (len(kind_reserved), kind, len(matching), specifics))
523 raise NoResourceExn('When trying to use instance nr %d of %r: %s' % (used_count + 1, kind, msg))
524
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200525 pick = available[0]
526 self.dbg(using=pick)
527 assert not pick.get(USED_KEY)
528 pick[USED_KEY] = True
529 return copy.deepcopy(pick)
530
531 def put(self, item):
532 if not item.get(USED_KEY):
533 raise RuntimeError('Can only put() a resource that is used: %r' % item)
534 hash_to_put = item.get(HASH_KEY)
535 if not hash_to_put:
536 raise RuntimeError('Can only put() a resource that has a hash marker: %r' % item)
537 for key, item_list in self.reserved.items():
538 my_list = self.get(key)
539 for my_item in my_list:
540 if hash_to_put == my_item.get(HASH_KEY):
541 my_item.pop(USED_KEY)
542
543 def put_all(self):
Pau Espin Pedrol1dd29552017-06-13 18:07:57 +0200544 if not self.reserved:
545 return
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200546 for key, item_list in self.reserved.items():
Pau Espin Pedrol1dd29552017-06-13 18:07:57 +0200547 for item in item_list:
548 item.pop(USED_KEY, None)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200549
550 def free(self):
Neels Hofmeyred4e5282017-05-29 02:53:54 +0200551 if self.reserved:
552 self.resources_pool.free(self.origin, self.reserved)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200553 self.reserved = None
554
Neels Hofmeyr2d1d5612017-05-22 20:02:41 +0200555 def counts(self):
556 counts = {}
557 for key in self.reserved.keys():
558 counts[key] = self.count(key)
559 return counts
560
561 def count(self, key):
562 return len(self.reserved.get(key) or [])
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200563
564# vim: expandtab tabstop=4 shiftwidth=4