blob: aa41b9842f19481ab23fcf329b55b82ecc6f8378 [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
25from . import log
26from . import config
Neels Hofmeyr3531a192017-03-28 14:30:28 +020027from . import util
28from . import schema
Pau Espin Pedrolfeb66e72019-03-04 18:37:04 +010029from . import bts_sysmo, bts_osmotrx, bts_osmovirtual, bts_octphy, bts_nanobts, bts_oc2g
Holger Hans Peter Freyther74243012019-02-27 08:18:38 +000030from . import modem
Holger Hans Peter Freyther10501cc2019-02-25 03:20:16 +000031from . import ms_osmo_mobile
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +020032
Neels Hofmeyr3531a192017-03-28 14:30:28 +020033from .util import is_dict, is_list
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +020034
Neels Hofmeyr3531a192017-03-28 14:30:28 +020035HASH_KEY = '_hash'
36RESERVED_KEY = '_reserved_by'
37USED_KEY = '_used'
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +020038
Neels Hofmeyr3531a192017-03-28 14:30:28 +020039RESOURCES_CONF = 'resources.conf'
Neels Hofmeyr3531a192017-03-28 14:30:28 +020040RESERVED_RESOURCES_FILE = 'reserved_resources.state'
41
Neels Hofmeyr76d81032017-05-18 18:35:32 +020042R_IP_ADDRESS = 'ip_address'
Neels Hofmeyr3531a192017-03-28 14:30:28 +020043R_BTS = 'bts'
44R_ARFCN = 'arfcn'
45R_MODEM = 'modem'
Pau Espin Pedrolbc1ed882018-05-17 16:59:58 +020046R_OSMOCON = 'osmocon_phone'
47R_ALL = (R_IP_ADDRESS, R_BTS, R_ARFCN, R_MODEM, R_OSMOCON)
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 Pedrolce35d912017-11-23 11:01:24 +010056 'bts[].direct_pcu': schema.BOOL_STR,
Pau Espin Pedrol57497a62017-08-28 14:21:15 +020057 'bts[].ciphers[]': schema.CIPHER,
Pau Espin Pedrol722e94e2018-08-22 11:01:32 +020058 'bts[].channel_allocator': schema.CHAN_ALLOCATOR,
Pau Espin Pedrol4f23ab52018-10-29 11:30:00 +010059 'bts[].gprs_mode': schema.GPRS_MODE,
Pau Espin Pedrol39df7f42018-05-07 13:49:33 +020060 'bts[].num_trx': schema.UINT,
61 'bts[].max_trx': schema.UINT,
Pau Espin Pedrolf6a07122018-07-27 16:24:29 +020062 'bts[].trx_list[].addr': schema.IPV4,
Pau Espin Pedrol91199a32019-07-25 12:59:30 +020063 'bts[].trx_list[].arfcn': schema.INT,
Your Name44af3412017-04-13 03:11:59 +020064 'bts[].trx_list[].hw_addr': schema.HWADDR,
65 'bts[].trx_list[].net_device': schema.STR,
Pau Espin Pedrolb26f32a2017-09-14 13:52:28 +020066 'bts[].trx_list[].nominal_power': schema.UINT,
67 'bts[].trx_list[].max_power_red': schema.UINT,
Pau Espin Pedrolc9b63762018-05-07 01:57:01 +020068 'bts[].trx_list[].timeslot_list[].phys_chan_config': schema.PHY_CHAN,
Pau Espin Pedrolf6a07122018-07-27 16:24:29 +020069 'bts[].trx_list[].power_supply.type': schema.STR,
70 'bts[].trx_list[].power_supply.device': schema.STR,
71 'bts[].trx_list[].power_supply.port': schema.STR,
Pau Espin Pedrol0d455042018-08-27 17:07:41 +020072 'bts[].osmo_trx.launch_trx': schema.BOOL_STR,
73 'bts[].osmo_trx.type': schema.STR,
74 'bts[].osmo_trx.clock_reference': schema.OSMO_TRX_CLOCK_REF,
75 'bts[].osmo_trx.trx_ip': schema.IPV4,
Pau Espin Pedrola9006df2018-10-01 12:26:39 +020076 'bts[].osmo_trx.remote_user': schema.STR,
Pau Espin Pedrol8cfa10f2018-11-06 12:05:19 +010077 'bts[].osmo_trx.dev_args': schema.STR,
Pau Espin Pedrol94eab262018-09-17 20:25:55 +020078 'bts[].osmo_trx.multi_arfcn': schema.BOOL_STR,
Pau Espin Pedrol0cde25f2019-07-24 19:55:08 +020079 'bts[].osmo_trx.max_trxd_version': schema.UINT,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020080 'arfcn[].arfcn': schema.INT,
81 'arfcn[].band': schema.BAND,
Holger Hans Peter Freyther93a89422019-02-27 06:36:36 +000082 'modem[].type': schema.STR,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020083 'modem[].label': schema.STR,
84 'modem[].path': schema.STR,
85 'modem[].imsi': schema.IMSI,
86 'modem[].ki': schema.KI,
Pau Espin Pedrol713ce2c2017-08-24 16:57:17 +020087 'modem[].auth_algo': schema.AUTH_ALGO,
Pau Espin Pedrol57497a62017-08-28 14:21:15 +020088 'modem[].ciphers[]': schema.CIPHER,
Pau Espin Pedrolac18fd32017-08-31 18:49:47 +020089 'modem[].features[]': schema.MODEM_FEATURE,
Pau Espin Pedrolbc1ed882018-05-17 16:59:58 +020090 'osmocon_phone[].serial_device': schema.STR,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020091 }
92
93WANT_SCHEMA = util.dict_add(
Pau Espin Pedrolead79ac2017-09-12 15:19:18 +020094 dict([('%s[].times' % r, schema.TIMES) for r in R_ALL]),
Neels Hofmeyr3531a192017-03-28 14:30:28 +020095 RESOURCES_SCHEMA)
96
Pau Espin Pedrol0b302792017-09-10 16:33:10 +020097CONF_SCHEMA = util.dict_add(
Pau Espin Pedrol5dc24592018-08-27 12:53:41 +020098 { 'defaults.timeout': schema.STR,
99 'config.bsc.net.codec_list[]': schema.CODEC },
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200100 dict([('resources.%s' % key, val) for key, val in WANT_SCHEMA.items()]),
101 dict([('modifiers.%s' % key, val) for key, val in WANT_SCHEMA.items()]))
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200102
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200103KNOWN_BTS_TYPES = {
Your Name44af3412017-04-13 03:11:59 +0200104 'osmo-bts-sysmo': bts_sysmo.SysmoBts,
105 'osmo-bts-trx': bts_osmotrx.OsmoBtsTrx,
Pau Espin Pedrolfeb66e72019-03-04 18:37:04 +0100106 'osmo-bts-oc2g': bts_oc2g.OsmoBtsOC2G,
Pau Espin Pedroldaed4472017-09-15 14:11:35 +0200107 'osmo-bts-octphy': bts_octphy.OsmoBtsOctphy,
Holger Hans Peter Freytherd2db10d2018-12-06 18:34:53 +0000108 'osmo-bts-virtual': bts_osmovirtual.OsmoBtsVirtual,
Pau Espin Pedrol1b28a582018-03-08 21:01:26 +0100109 'nanobts': bts_nanobts.NanoBts,
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200110 }
111
Holger Hans Peter Freyther74243012019-02-27 08:18:38 +0000112
113KNOWN_MS_TYPES = {
114 # Map None to ofono for forward compability
115 None: modem.Modem,
116 'ofono': modem.Modem,
Holger Hans Peter Freyther10501cc2019-02-25 03:20:16 +0000117 'osmo-mobile': ms_osmo_mobile.MSOsmoMobile,
Holger Hans Peter Freyther74243012019-02-27 08:18:38 +0000118}
119
120
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200121def register_bts_type(name, clazz):
122 KNOWN_BTS_TYPES[name] = clazz
123
124class ResourcesPool(log.Origin):
125 _remember_to_free = None
126 _registered_exit_handler = False
127
128 def __init__(self):
129 self.config_path = config.get_config_file(RESOURCES_CONF)
130 self.state_dir = config.get_state_dir()
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200131 super().__init__(log.C_CNF, conf=self.config_path, state=self.state_dir.path)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200132 self.read_conf()
133
134 def read_conf(self):
135 self.all_resources = Resources(config.read(self.config_path, RESOURCES_SCHEMA))
136 self.all_resources.set_hashes()
137
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200138 def reserve(self, origin, want, modifiers):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200139 '''
140 attempt to reserve the resources specified in the dict 'want' for
141 'origin'. Obtain a lock on the resources lock dir, verify that all
142 wanted resources are available, and if yes mark them as reserved.
143
144 On success, return a reservation object which can be used to release
145 the reservation. The reservation will be freed automatically on program
146 exit, if not yet done manually.
147
148 'origin' should be an Origin() instance.
149
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200150 'want' is a dict matching RESOURCES_SCHEMA, used to specify what to
151 reserve.
152
153 'modifiers' is a dict matching RESOURCES_SCHEMA, it is overlaid on top
154 of 'want'.
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200155
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200156 If an entry has no attribute set, any of the resources may be
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200157 reserved without further limitations.
158
159 ResourcesPool may also be selected with narrowed down constraints.
Neels Hofmeyr76d81032017-05-18 18:35:32 +0200160 This would reserve one IP address, two modems, one BTS of type
Neels Hofmeyr391afe32017-05-18 19:22:12 +0200161 sysmo and one of type trx, plus 2 ARFCNs in the 1800 band:
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200162
163 {
Neels Hofmeyr76d81032017-05-18 18:35:32 +0200164 'ip_address': [ {} ],
Neels Hofmeyr391afe32017-05-18 19:22:12 +0200165 'bts': [ { 'type': 'sysmo' }, { 'type': 'trx' } ],
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200166 'arfcn': [ { 'band': 'GSM-1800' }, { 'band': 'GSM-1800' } ],
167 'modem': [ {}, {} ],
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200168 }
169 '''
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200170 schema.validate(want, RESOURCES_SCHEMA)
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200171 schema.validate(modifiers, RESOURCES_SCHEMA)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200172
173 origin_id = origin.origin_id()
174
175 with self.state_dir.lock(origin_id):
176 rrfile_path = self.state_dir.mk_parentdir(RESERVED_RESOURCES_FILE)
177 reserved = Resources(config.read(rrfile_path, if_missing_return={}))
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200178 to_be_reserved = self.all_resources.without(reserved).find(origin, want)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200179
180 to_be_reserved.mark_reserved_by(origin_id)
181
182 reserved.add(to_be_reserved)
183 config.write(rrfile_path, reserved)
184
185 self.remember_to_free(to_be_reserved)
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200186 return ReservedResources(self, origin, to_be_reserved, modifiers)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200187
188 def free(self, origin, to_be_freed):
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200189 log.ctx(origin)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200190 with self.state_dir.lock(origin.origin_id()):
191 rrfile_path = self.state_dir.mk_parentdir(RESERVED_RESOURCES_FILE)
192 reserved = Resources(config.read(rrfile_path, if_missing_return={}))
193 reserved.drop(to_be_freed)
194 config.write(rrfile_path, reserved)
195 self.forget_freed(to_be_freed)
196
197 def register_exit_handler(self):
198 if self._registered_exit_handler:
199 return
200 atexit.register(self.clean_up_registered_resources)
201 self._registered_exit_handler = True
202
203 def unregister_exit_handler(self):
204 if not self._registered_exit_handler:
205 return
206 atexit.unregister(self.clean_up_registered_resources)
207 self._registered_exit_handler = False
208
209 def clean_up_registered_resources(self):
210 if not self._remember_to_free:
211 return
212 self.free(log.Origin('atexit.clean_up_registered_resources()'),
213 self._remember_to_free)
214
215 def remember_to_free(self, to_be_reserved):
216 self.register_exit_handler()
217 if not self._remember_to_free:
218 self._remember_to_free = Resources()
219 self._remember_to_free.add(to_be_reserved)
220
221 def forget_freed(self, freed):
222 if freed is self._remember_to_free:
223 self._remember_to_free.clear()
224 else:
225 self._remember_to_free.drop(freed)
226 if not self._remember_to_free:
227 self.unregister_exit_handler()
228
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100229 def next_persistent_value(self, token, first_val, validate_func, inc_func, origin):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200230 origin_id = origin.origin_id()
231
232 with self.state_dir.lock(origin_id):
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100233 token_path = self.state_dir.child('last_used_%s.state' % token)
234 log.ctx(token_path)
235 last_value = first_val
236 if os.path.exists(token_path):
237 if not os.path.isfile(token_path):
238 raise RuntimeError('path should be a file but is not: %r' % token_path)
239 with open(token_path, 'r') as f:
240 last_value = f.read().strip()
241 validate_func(last_value)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200242
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100243 next_value = inc_func(last_value)
244 with open(token_path, 'w') as f:
245 f.write(next_value)
246 return next_value
247
248 def next_msisdn(self, origin):
249 return self.next_persistent_value('msisdn', '1000', schema.msisdn, util.msisdn_inc, origin)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200250
Pau Espin Pedrol5e0c2512017-11-06 18:40:23 +0100251 def next_lac(self, origin):
Pau Espin Pedrolf7f06362017-11-28 12:56:35 +0100252 # LAC=0 has special meaning (MS detached), avoid it
253 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 +0200254
Pau Espin Pedrol8a3a7b52017-11-28 15:50:02 +0100255 def next_rac(self, origin):
256 return self.next_persistent_value('rac', '1', schema.uint8, lambda x: str((int(x)+1) % pow(2,8) or 1), origin)
257
Pau Espin Pedrol4ccce7c2017-11-07 11:13:20 +0100258 def next_cellid(self, origin):
259 return self.next_persistent_value('cellid', '1', schema.uint16, lambda x: str((int(x)+1) % pow(2,16)), origin)
260
Pau Espin Pedrol8a3a7b52017-11-28 15:50:02 +0100261 def next_bvci(self, origin):
262 # BVCI=0 and =1 are reserved, avoid them.
263 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)
264
Pau Espin Pedrol4676cbd2017-09-14 17:35:03 +0200265class NoResourceExn(log.Error):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200266 pass
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200267
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200268class Resources(dict):
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200269
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200270 def __init__(self, all_resources={}, do_copy=True):
271 if do_copy:
272 all_resources = copy.deepcopy(all_resources)
273 self.update(all_resources)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200274
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200275 def drop(self, reserved, fail_if_not_found=True):
276 # protect from modifying reserved because we're the same object
277 if reserved is self:
278 raise RuntimeError('Refusing to drop a list of resources from itself.'
279 ' This is probably a bug where a list of Resources()'
280 ' should have been copied but is passed as-is.'
281 ' use Resources.clear() instead.')
282
283 for key, reserved_list in reserved.items():
284 my_list = self.get(key) or []
285
286 if my_list is reserved_list:
287 self.pop(key)
288 continue
289
290 for reserved_item in reserved_list:
291 found = False
292 reserved_hash = reserved_item.get(HASH_KEY)
293 if not reserved_hash:
294 raise RuntimeError('Resources.drop() only works with hashed items')
295
296 for i in range(len(my_list)):
297 my_item = my_list[i]
298 my_hash = my_item.get(HASH_KEY)
299 if not my_hash:
300 raise RuntimeError('Resources.drop() only works with hashed items')
301 if my_hash == reserved_hash:
302 found = True
303 my_list.pop(i)
304 break
305
306 if fail_if_not_found and not found:
307 raise RuntimeError('Asked to drop resource from a pool, but the'
308 ' resource was not found: %s = %r' % (key, reserved_item))
309
310 if not my_list:
311 self.pop(key)
312 return self
313
314 def without(self, reserved):
315 return Resources(self).drop(reserved)
316
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200317 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 +0200318 '''
319 Pass a dict of resource requirements, e.g.:
320 want = {
321 'bts': [ {'type': 'osmo-bts-sysmo',}, {} ],
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200322 'modem': [ {}, {}, {} ]
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200323 }
324 This function tries to find a combination from the available resources that
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200325 matches these requirements. The return value is a dict (wrapped in a Resources class)
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200326 that contains the matching resources in the order of 'want' dict: in above
327 example, the returned dict would have a 'bts' list with the first item being
328 a sysmoBTS, the second item being any other available BTS.
329
330 If skip_if_marked is passed, any resource that contains this key is skipped.
331 E.g. if a BTS has the USED_KEY set like
332 reserved_resources = { 'bts' : {..., '_used': True} }
333 then this may be skipped by passing skip_if_marked='_used'
334 (or rather skip_if_marked=USED_KEY).
335
336 If do_copy is True, the returned dict is a deep copy and does not share
337 lists with any other Resources dict.
338
339 If raise_if_missing is False, this will return an empty item for any
340 resource that had no match, instead of immediately raising an exception.
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200341
342 This function expects input dictionaries whose contents have already
343 been replicated based on its the 'times' attributes. See
344 config.replicate_times() for more details.
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200345 '''
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200346 matches = {}
Neels Hofmeyr17c139e2017-04-12 02:42:02 +0200347 for key, want_list in sorted(want.items()): # sorted for deterministic test results
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200348 # here we have a resource of a given type, e.g. 'bts', with a list
349 # containing as many BTSes as the caller wants to reserve/use. Each
350 # list item contains specifics for the particular BTS.
Neels Hofmeyr2a1a1fa2017-05-29 01:36:21 +0200351 my_list = self.get(key, [])
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200352
Neels Hofmeyrf9e86932017-06-06 19:47:40 +0200353 if log_label:
354 for_origin.log(log_label, len(want_list), 'x', key, '(candidates: %d)'%len(my_list))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200355
356 # Try to avoid a less constrained item snatching away a resource
357 # from a more detailed constrained requirement.
358
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200359 # first record all matches, so that each requested item has a list
360 # of all available resources that match it. Some resources may
361 # appear for multiple requested items. Store matching indexes.
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200362 all_matches = []
363 for want_item in want_list:
364 item_match_list = []
365 for i in range(len(my_list)):
366 my_item = my_list[i]
367 if skip_if_marked and my_item.get(skip_if_marked):
368 continue
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200369 if item_matches(my_item, want_item):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200370 item_match_list.append(i)
371 if not item_match_list:
Neels Hofmeyr9b907702017-05-06 23:20:33 +0200372 if raise_if_missing:
373 raise NoResourceExn('No matching resource available for %s = %r'
374 % (key, want_item))
375 else:
376 # this one failed... see below
377 all_matches = []
378 break
379
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200380 all_matches.append( item_match_list )
381
382 if not all_matches:
Neels Hofmeyr9b907702017-05-06 23:20:33 +0200383 # ...this one failed. Makes no sense to solve resource
384 # allocations, return an empty list for this key to mark
385 # failure.
386 matches[key] = []
387 continue
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200388
389 # figure out who gets what
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200390 try:
391 solution = solve(all_matches)
392 except NotSolvable:
393 # instead of a cryptic error message, raise an exception that
394 # conveys meaning to the user.
395 raise NoResourceExn('Could not resolve request to reserve resources: '
396 '%d x %s with requirements: %r' % (len(want_list), key, want_list))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200397 picked = [ my_list[i] for i in solution if i is not None ]
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200398 for_origin.dbg('Picked', config.tostr(picked))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200399 matches[key] = picked
400
401 return Resources(matches, do_copy=do_copy)
402
403 def set_hashes(self):
404 for key, item_list in self.items():
405 for item in item_list:
406 item[HASH_KEY] = util.hash_obj(item, HASH_KEY, RESERVED_KEY, USED_KEY)
407
408 def add(self, more):
409 if more is self:
410 raise RuntimeError('adding a list of resources to itself?')
411 config.add(self, copy.deepcopy(more))
412
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200413 def mark_reserved_by(self, origin_id):
414 for key, item_list in self.items():
415 for item in item_list:
416 item[RESERVED_KEY] = origin_id
417
418
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200419class NotSolvable(Exception):
420 pass
421
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200422def solve(all_matches):
423 '''
424 all_matches shall be a list of index-lists.
425 all_matches[i] is the list of indexes that item i can use.
426 Return a solution so that each i gets a different index.
427 solve([ [0, 1, 2],
428 [0],
429 [0, 2] ]) == [1, 0, 2]
430 '''
431
432 def all_differ(l):
433 return len(set(l)) == len(l)
434
435 def search_in_permutations(fixed=[]):
436 idx = len(fixed)
437 for i in range(len(all_matches[idx])):
438 val = all_matches[idx][i]
439 # don't add a val that's already in the list
440 if val in fixed:
441 continue
442 l = list(fixed)
443 l.append(val)
444 if len(l) == len(all_matches):
445 # found a solution
446 return l
447 # not at the end yet, add next digit
448 r = search_in_permutations(l)
449 if r:
450 # nested search_in_permutations() call found a solution
451 return r
452 # this entire branch yielded no solution
453 return None
454
455 if not all_matches:
456 raise RuntimeError('Cannot solve: no candidates')
457
458 solution = search_in_permutations()
459 if not solution:
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200460 raise NotSolvable('The requested resource requirements are not solvable %r'
461 % all_matches)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200462 return solution
463
464
465def contains_hash(list_of_dicts, a_hash):
466 for d in list_of_dicts:
467 if d.get(HASH_KEY) == a_hash:
468 return True
469 return False
470
471def item_matches(item, wanted_item, ignore_keys=None):
472 if is_dict(wanted_item):
473 # match up two dicts
474 if not isinstance(item, dict):
475 return False
476 for key, wanted_val in wanted_item.items():
477 if ignore_keys and key in ignore_keys:
478 continue
479 if not item_matches(item.get(key), wanted_val, ignore_keys=ignore_keys):
480 return False
481 return True
482
483 if is_list(wanted_item):
Pau Espin Pedrol4e36f7c2017-08-28 13:29:28 +0200484 if not is_list(item):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200485 return False
Pau Espin Pedrol58475512017-09-14 15:33:15 +0200486 # Validate that all elements in both lists are of the same type:
487 t = util.list_validate_same_elem_type(wanted_item + item)
488 if t is None:
489 return True # both lists are empty, return
490 # For lists of complex objects, we expect them to be sorted lists:
491 if t in (dict, list, tuple):
492 for i in range(max(len(wanted_item), len(item))):
493 log.ctx(idx=i)
494 subitem = item[i] if i < len(item) else util.empty_instance_type(t)
495 wanted_subitem = wanted_item[i] if i < len(wanted_item) else util.empty_instance_type(t)
496 if not item_matches(subitem, wanted_subitem, ignore_keys=ignore_keys):
497 return False
498 else: # for lists of basic elements, we handle them as unsorted sets:
499 for val in wanted_item:
500 if val not in item:
501 return False
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200502 return True
503
504 return item == wanted_item
505
506
507class ReservedResources(log.Origin):
508 '''
509 After all resources have been figured out, this is the API that a test case
510 gets to interact with resources. From those resources that have been
511 reserved for it, it can pick some to mark them as currently in use.
512 Functions like nitb() provide a resource by automatically picking its
513 dependencies from so far unused (but reserved) resource.
514 '''
515
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200516 def __init__(self, resources_pool, origin, reserved, modifiers):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200517 self.resources_pool = resources_pool
518 self.origin = origin
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200519 self.reserved_original = reserved
520 self.reserved = copy.deepcopy(self.reserved_original)
521 config.overlay(self.reserved, modifiers)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200522
523 def __repr__(self):
524 return 'resources(%s)=%s' % (self.origin.name(), pprint.pformat(self.reserved))
525
526 def get(self, kind, specifics=None):
527 if specifics is None:
528 specifics = {}
529 self.dbg('requesting use of', kind, specifics=specifics)
530 want = { kind: [specifics] }
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200531 available_dict = self.reserved.find(self.origin, want, skip_if_marked=USED_KEY,
532 do_copy=False, raise_if_missing=False,
533 log_label='Using')
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200534 available = available_dict.get(kind)
535 self.dbg(available=len(available))
536 if not available:
Neels Hofmeyrf9e86932017-06-06 19:47:40 +0200537 # cook up a detailed error message for the current situation
538 kind_reserved = self.reserved.get(kind, [])
539 used_count = len([r for r in kind_reserved if USED_KEY in r])
540 matching = self.reserved.find(self.origin, want, raise_if_missing=False, log_label=None).get(kind, [])
541 if not matching:
542 msg = 'none of the reserved resources matches requirements %r' % specifics
543 elif not (used_count < len(kind_reserved)):
544 msg = 'suite.conf reserved only %d x %r.' % (len(kind_reserved), kind)
545 else:
546 msg = ('No unused resource left that matches the requirements;'
547 ' Of reserved %d x %r, %d match the requirements, but all are already in use;'
548 ' Requirements: %r'
549 % (len(kind_reserved), kind, len(matching), specifics))
550 raise NoResourceExn('When trying to use instance nr %d of %r: %s' % (used_count + 1, kind, msg))
551
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200552 pick = available[0]
553 self.dbg(using=pick)
554 assert not pick.get(USED_KEY)
555 pick[USED_KEY] = True
556 return copy.deepcopy(pick)
557
558 def put(self, item):
559 if not item.get(USED_KEY):
560 raise RuntimeError('Can only put() a resource that is used: %r' % item)
561 hash_to_put = item.get(HASH_KEY)
562 if not hash_to_put:
563 raise RuntimeError('Can only put() a resource that has a hash marker: %r' % item)
564 for key, item_list in self.reserved.items():
565 my_list = self.get(key)
566 for my_item in my_list:
567 if hash_to_put == my_item.get(HASH_KEY):
568 my_item.pop(USED_KEY)
569
570 def put_all(self):
Pau Espin Pedrol1dd29552017-06-13 18:07:57 +0200571 if not self.reserved:
572 return
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200573 for key, item_list in self.reserved.items():
Pau Espin Pedrol1dd29552017-06-13 18:07:57 +0200574 for item in item_list:
575 item.pop(USED_KEY, None)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200576
577 def free(self):
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200578 if self.reserved_original:
579 self.resources_pool.free(self.origin, self.reserved_original)
580 self.reserved_original = None
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200581
Neels Hofmeyr2d1d5612017-05-22 20:02:41 +0200582 def counts(self):
583 counts = {}
584 for key in self.reserved.keys():
585 counts[key] = self.count(key)
586 return counts
587
588 def count(self, key):
589 return len(self.reserved.get(key) or [])
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200590
591# vim: expandtab tabstop=4 shiftwidth=4