blob: 246b19632ff69e4d660bb97d98568aef365a4279 [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,
Your Name44af3412017-04-13 03:11:59 +020063 'bts[].trx_list[].hw_addr': schema.HWADDR,
64 'bts[].trx_list[].net_device': schema.STR,
Pau Espin Pedrolb26f32a2017-09-14 13:52:28 +020065 'bts[].trx_list[].nominal_power': schema.UINT,
66 'bts[].trx_list[].max_power_red': schema.UINT,
Pau Espin Pedrolc9b63762018-05-07 01:57:01 +020067 'bts[].trx_list[].timeslot_list[].phys_chan_config': schema.PHY_CHAN,
Pau Espin Pedrolf6a07122018-07-27 16:24:29 +020068 'bts[].trx_list[].power_supply.type': schema.STR,
69 'bts[].trx_list[].power_supply.device': schema.STR,
70 'bts[].trx_list[].power_supply.port': schema.STR,
Pau Espin Pedrol0d455042018-08-27 17:07:41 +020071 'bts[].osmo_trx.launch_trx': schema.BOOL_STR,
72 'bts[].osmo_trx.type': schema.STR,
73 'bts[].osmo_trx.clock_reference': schema.OSMO_TRX_CLOCK_REF,
74 'bts[].osmo_trx.trx_ip': schema.IPV4,
Pau Espin Pedrola9006df2018-10-01 12:26:39 +020075 'bts[].osmo_trx.remote_user': schema.STR,
Pau Espin Pedrol8cfa10f2018-11-06 12:05:19 +010076 'bts[].osmo_trx.dev_args': schema.STR,
Pau Espin Pedrol94eab262018-09-17 20:25:55 +020077 'bts[].osmo_trx.multi_arfcn': schema.BOOL_STR,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020078 'arfcn[].arfcn': schema.INT,
79 'arfcn[].band': schema.BAND,
Holger Hans Peter Freyther93a89422019-02-27 06:36:36 +000080 'modem[].type': schema.STR,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020081 'modem[].label': schema.STR,
82 'modem[].path': schema.STR,
83 'modem[].imsi': schema.IMSI,
84 'modem[].ki': schema.KI,
Pau Espin Pedrol713ce2c2017-08-24 16:57:17 +020085 'modem[].auth_algo': schema.AUTH_ALGO,
Pau Espin Pedrol57497a62017-08-28 14:21:15 +020086 'modem[].ciphers[]': schema.CIPHER,
Pau Espin Pedrolac18fd32017-08-31 18:49:47 +020087 'modem[].features[]': schema.MODEM_FEATURE,
Pau Espin Pedrolbc1ed882018-05-17 16:59:58 +020088 'osmocon_phone[].serial_device': schema.STR,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020089 }
90
91WANT_SCHEMA = util.dict_add(
Pau Espin Pedrolead79ac2017-09-12 15:19:18 +020092 dict([('%s[].times' % r, schema.TIMES) for r in R_ALL]),
Neels Hofmeyr3531a192017-03-28 14:30:28 +020093 RESOURCES_SCHEMA)
94
Pau Espin Pedrol0b302792017-09-10 16:33:10 +020095CONF_SCHEMA = util.dict_add(
Pau Espin Pedrol5dc24592018-08-27 12:53:41 +020096 { 'defaults.timeout': schema.STR,
97 'config.bsc.net.codec_list[]': schema.CODEC },
Pau Espin Pedrolaab56922018-08-21 14:58:29 +020098 dict([('resources.%s' % key, val) for key, val in WANT_SCHEMA.items()]),
99 dict([('modifiers.%s' % key, val) for key, val in WANT_SCHEMA.items()]))
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200100
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200101KNOWN_BTS_TYPES = {
Your Name44af3412017-04-13 03:11:59 +0200102 'osmo-bts-sysmo': bts_sysmo.SysmoBts,
103 'osmo-bts-trx': bts_osmotrx.OsmoBtsTrx,
Pau Espin Pedrolfeb66e72019-03-04 18:37:04 +0100104 'osmo-bts-oc2g': bts_oc2g.OsmoBtsOC2G,
Pau Espin Pedroldaed4472017-09-15 14:11:35 +0200105 'osmo-bts-octphy': bts_octphy.OsmoBtsOctphy,
Holger Hans Peter Freytherd2db10d2018-12-06 18:34:53 +0000106 'osmo-bts-virtual': bts_osmovirtual.OsmoBtsVirtual,
Pau Espin Pedrol1b28a582018-03-08 21:01:26 +0100107 'nanobts': bts_nanobts.NanoBts,
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200108 }
109
Holger Hans Peter Freyther74243012019-02-27 08:18:38 +0000110
111KNOWN_MS_TYPES = {
112 # Map None to ofono for forward compability
113 None: modem.Modem,
114 'ofono': modem.Modem,
Holger Hans Peter Freyther10501cc2019-02-25 03:20:16 +0000115 'osmo-mobile': ms_osmo_mobile.MSOsmoMobile,
Holger Hans Peter Freyther74243012019-02-27 08:18:38 +0000116}
117
118
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200119def register_bts_type(name, clazz):
120 KNOWN_BTS_TYPES[name] = clazz
121
122class ResourcesPool(log.Origin):
123 _remember_to_free = None
124 _registered_exit_handler = False
125
126 def __init__(self):
127 self.config_path = config.get_config_file(RESOURCES_CONF)
128 self.state_dir = config.get_state_dir()
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200129 super().__init__(log.C_CNF, conf=self.config_path, state=self.state_dir.path)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200130 self.read_conf()
131
132 def read_conf(self):
133 self.all_resources = Resources(config.read(self.config_path, RESOURCES_SCHEMA))
134 self.all_resources.set_hashes()
135
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200136 def reserve(self, origin, want, modifiers):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200137 '''
138 attempt to reserve the resources specified in the dict 'want' for
139 'origin'. Obtain a lock on the resources lock dir, verify that all
140 wanted resources are available, and if yes mark them as reserved.
141
142 On success, return a reservation object which can be used to release
143 the reservation. The reservation will be freed automatically on program
144 exit, if not yet done manually.
145
146 'origin' should be an Origin() instance.
147
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200148 'want' is a dict matching RESOURCES_SCHEMA, used to specify what to
149 reserve.
150
151 'modifiers' is a dict matching RESOURCES_SCHEMA, it is overlaid on top
152 of 'want'.
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200153
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200154 If an entry has no attribute set, any of the resources may be
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200155 reserved without further limitations.
156
157 ResourcesPool may also be selected with narrowed down constraints.
Neels Hofmeyr76d81032017-05-18 18:35:32 +0200158 This would reserve one IP address, two modems, one BTS of type
Neels Hofmeyr391afe32017-05-18 19:22:12 +0200159 sysmo and one of type trx, plus 2 ARFCNs in the 1800 band:
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200160
161 {
Neels Hofmeyr76d81032017-05-18 18:35:32 +0200162 'ip_address': [ {} ],
Neels Hofmeyr391afe32017-05-18 19:22:12 +0200163 'bts': [ { 'type': 'sysmo' }, { 'type': 'trx' } ],
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200164 'arfcn': [ { 'band': 'GSM-1800' }, { 'band': 'GSM-1800' } ],
165 'modem': [ {}, {} ],
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200166 }
167 '''
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200168 schema.validate(want, RESOURCES_SCHEMA)
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200169 schema.validate(modifiers, RESOURCES_SCHEMA)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200170
171 origin_id = origin.origin_id()
172
173 with self.state_dir.lock(origin_id):
174 rrfile_path = self.state_dir.mk_parentdir(RESERVED_RESOURCES_FILE)
175 reserved = Resources(config.read(rrfile_path, if_missing_return={}))
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200176 to_be_reserved = self.all_resources.without(reserved).find(origin, want)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200177
178 to_be_reserved.mark_reserved_by(origin_id)
179
180 reserved.add(to_be_reserved)
181 config.write(rrfile_path, reserved)
182
183 self.remember_to_free(to_be_reserved)
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200184 return ReservedResources(self, origin, to_be_reserved, modifiers)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200185
186 def free(self, origin, to_be_freed):
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200187 log.ctx(origin)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200188 with self.state_dir.lock(origin.origin_id()):
189 rrfile_path = self.state_dir.mk_parentdir(RESERVED_RESOURCES_FILE)
190 reserved = Resources(config.read(rrfile_path, if_missing_return={}))
191 reserved.drop(to_be_freed)
192 config.write(rrfile_path, reserved)
193 self.forget_freed(to_be_freed)
194
195 def register_exit_handler(self):
196 if self._registered_exit_handler:
197 return
198 atexit.register(self.clean_up_registered_resources)
199 self._registered_exit_handler = True
200
201 def unregister_exit_handler(self):
202 if not self._registered_exit_handler:
203 return
204 atexit.unregister(self.clean_up_registered_resources)
205 self._registered_exit_handler = False
206
207 def clean_up_registered_resources(self):
208 if not self._remember_to_free:
209 return
210 self.free(log.Origin('atexit.clean_up_registered_resources()'),
211 self._remember_to_free)
212
213 def remember_to_free(self, to_be_reserved):
214 self.register_exit_handler()
215 if not self._remember_to_free:
216 self._remember_to_free = Resources()
217 self._remember_to_free.add(to_be_reserved)
218
219 def forget_freed(self, freed):
220 if freed is self._remember_to_free:
221 self._remember_to_free.clear()
222 else:
223 self._remember_to_free.drop(freed)
224 if not self._remember_to_free:
225 self.unregister_exit_handler()
226
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100227 def next_persistent_value(self, token, first_val, validate_func, inc_func, origin):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200228 origin_id = origin.origin_id()
229
230 with self.state_dir.lock(origin_id):
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100231 token_path = self.state_dir.child('last_used_%s.state' % token)
232 log.ctx(token_path)
233 last_value = first_val
234 if os.path.exists(token_path):
235 if not os.path.isfile(token_path):
236 raise RuntimeError('path should be a file but is not: %r' % token_path)
237 with open(token_path, 'r') as f:
238 last_value = f.read().strip()
239 validate_func(last_value)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200240
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100241 next_value = inc_func(last_value)
242 with open(token_path, 'w') as f:
243 f.write(next_value)
244 return next_value
245
246 def next_msisdn(self, origin):
247 return self.next_persistent_value('msisdn', '1000', schema.msisdn, util.msisdn_inc, origin)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200248
Pau Espin Pedrol5e0c2512017-11-06 18:40:23 +0100249 def next_lac(self, origin):
Pau Espin Pedrolf7f06362017-11-28 12:56:35 +0100250 # LAC=0 has special meaning (MS detached), avoid it
251 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 +0200252
Pau Espin Pedrol8a3a7b52017-11-28 15:50:02 +0100253 def next_rac(self, origin):
254 return self.next_persistent_value('rac', '1', schema.uint8, lambda x: str((int(x)+1) % pow(2,8) or 1), origin)
255
Pau Espin Pedrol4ccce7c2017-11-07 11:13:20 +0100256 def next_cellid(self, origin):
257 return self.next_persistent_value('cellid', '1', schema.uint16, lambda x: str((int(x)+1) % pow(2,16)), origin)
258
Pau Espin Pedrol8a3a7b52017-11-28 15:50:02 +0100259 def next_bvci(self, origin):
260 # BVCI=0 and =1 are reserved, avoid them.
261 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)
262
Pau Espin Pedrol4676cbd2017-09-14 17:35:03 +0200263class NoResourceExn(log.Error):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200264 pass
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200265
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200266class Resources(dict):
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200267
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200268 def __init__(self, all_resources={}, do_copy=True):
269 if do_copy:
270 all_resources = copy.deepcopy(all_resources)
271 self.update(all_resources)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200272
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200273 def drop(self, reserved, fail_if_not_found=True):
274 # protect from modifying reserved because we're the same object
275 if reserved is self:
276 raise RuntimeError('Refusing to drop a list of resources from itself.'
277 ' This is probably a bug where a list of Resources()'
278 ' should have been copied but is passed as-is.'
279 ' use Resources.clear() instead.')
280
281 for key, reserved_list in reserved.items():
282 my_list = self.get(key) or []
283
284 if my_list is reserved_list:
285 self.pop(key)
286 continue
287
288 for reserved_item in reserved_list:
289 found = False
290 reserved_hash = reserved_item.get(HASH_KEY)
291 if not reserved_hash:
292 raise RuntimeError('Resources.drop() only works with hashed items')
293
294 for i in range(len(my_list)):
295 my_item = my_list[i]
296 my_hash = my_item.get(HASH_KEY)
297 if not my_hash:
298 raise RuntimeError('Resources.drop() only works with hashed items')
299 if my_hash == reserved_hash:
300 found = True
301 my_list.pop(i)
302 break
303
304 if fail_if_not_found and not found:
305 raise RuntimeError('Asked to drop resource from a pool, but the'
306 ' resource was not found: %s = %r' % (key, reserved_item))
307
308 if not my_list:
309 self.pop(key)
310 return self
311
312 def without(self, reserved):
313 return Resources(self).drop(reserved)
314
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200315 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 +0200316 '''
317 Pass a dict of resource requirements, e.g.:
318 want = {
319 'bts': [ {'type': 'osmo-bts-sysmo',}, {} ],
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200320 'modem': [ {}, {}, {} ]
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200321 }
322 This function tries to find a combination from the available resources that
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200323 matches these requirements. The return value is a dict (wrapped in a Resources class)
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200324 that contains the matching resources in the order of 'want' dict: in above
325 example, the returned dict would have a 'bts' list with the first item being
326 a sysmoBTS, the second item being any other available BTS.
327
328 If skip_if_marked is passed, any resource that contains this key is skipped.
329 E.g. if a BTS has the USED_KEY set like
330 reserved_resources = { 'bts' : {..., '_used': True} }
331 then this may be skipped by passing skip_if_marked='_used'
332 (or rather skip_if_marked=USED_KEY).
333
334 If do_copy is True, the returned dict is a deep copy and does not share
335 lists with any other Resources dict.
336
337 If raise_if_missing is False, this will return an empty item for any
338 resource that had no match, instead of immediately raising an exception.
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200339
340 This function expects input dictionaries whose contents have already
341 been replicated based on its the 'times' attributes. See
342 config.replicate_times() for more details.
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200343 '''
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200344 matches = {}
Neels Hofmeyr17c139e2017-04-12 02:42:02 +0200345 for key, want_list in sorted(want.items()): # sorted for deterministic test results
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200346 # here we have a resource of a given type, e.g. 'bts', with a list
347 # containing as many BTSes as the caller wants to reserve/use. Each
348 # list item contains specifics for the particular BTS.
Neels Hofmeyr2a1a1fa2017-05-29 01:36:21 +0200349 my_list = self.get(key, [])
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200350
Neels Hofmeyrf9e86932017-06-06 19:47:40 +0200351 if log_label:
352 for_origin.log(log_label, len(want_list), 'x', key, '(candidates: %d)'%len(my_list))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200353
354 # Try to avoid a less constrained item snatching away a resource
355 # from a more detailed constrained requirement.
356
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200357 # first record all matches, so that each requested item has a list
358 # of all available resources that match it. Some resources may
359 # appear for multiple requested items. Store matching indexes.
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200360 all_matches = []
361 for want_item in want_list:
362 item_match_list = []
363 for i in range(len(my_list)):
364 my_item = my_list[i]
365 if skip_if_marked and my_item.get(skip_if_marked):
366 continue
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200367 if item_matches(my_item, want_item):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200368 item_match_list.append(i)
369 if not item_match_list:
Neels Hofmeyr9b907702017-05-06 23:20:33 +0200370 if raise_if_missing:
371 raise NoResourceExn('No matching resource available for %s = %r'
372 % (key, want_item))
373 else:
374 # this one failed... see below
375 all_matches = []
376 break
377
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200378 all_matches.append( item_match_list )
379
380 if not all_matches:
Neels Hofmeyr9b907702017-05-06 23:20:33 +0200381 # ...this one failed. Makes no sense to solve resource
382 # allocations, return an empty list for this key to mark
383 # failure.
384 matches[key] = []
385 continue
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200386
387 # figure out who gets what
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200388 try:
389 solution = solve(all_matches)
390 except NotSolvable:
391 # instead of a cryptic error message, raise an exception that
392 # conveys meaning to the user.
393 raise NoResourceExn('Could not resolve request to reserve resources: '
394 '%d x %s with requirements: %r' % (len(want_list), key, want_list))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200395 picked = [ my_list[i] for i in solution if i is not None ]
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200396 for_origin.dbg('Picked', config.tostr(picked))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200397 matches[key] = picked
398
399 return Resources(matches, do_copy=do_copy)
400
401 def set_hashes(self):
402 for key, item_list in self.items():
403 for item in item_list:
404 item[HASH_KEY] = util.hash_obj(item, HASH_KEY, RESERVED_KEY, USED_KEY)
405
406 def add(self, more):
407 if more is self:
408 raise RuntimeError('adding a list of resources to itself?')
409 config.add(self, copy.deepcopy(more))
410
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200411 def mark_reserved_by(self, origin_id):
412 for key, item_list in self.items():
413 for item in item_list:
414 item[RESERVED_KEY] = origin_id
415
416
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200417class NotSolvable(Exception):
418 pass
419
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200420def solve(all_matches):
421 '''
422 all_matches shall be a list of index-lists.
423 all_matches[i] is the list of indexes that item i can use.
424 Return a solution so that each i gets a different index.
425 solve([ [0, 1, 2],
426 [0],
427 [0, 2] ]) == [1, 0, 2]
428 '''
429
430 def all_differ(l):
431 return len(set(l)) == len(l)
432
433 def search_in_permutations(fixed=[]):
434 idx = len(fixed)
435 for i in range(len(all_matches[idx])):
436 val = all_matches[idx][i]
437 # don't add a val that's already in the list
438 if val in fixed:
439 continue
440 l = list(fixed)
441 l.append(val)
442 if len(l) == len(all_matches):
443 # found a solution
444 return l
445 # not at the end yet, add next digit
446 r = search_in_permutations(l)
447 if r:
448 # nested search_in_permutations() call found a solution
449 return r
450 # this entire branch yielded no solution
451 return None
452
453 if not all_matches:
454 raise RuntimeError('Cannot solve: no candidates')
455
456 solution = search_in_permutations()
457 if not solution:
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200458 raise NotSolvable('The requested resource requirements are not solvable %r'
459 % all_matches)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200460 return solution
461
462
463def contains_hash(list_of_dicts, a_hash):
464 for d in list_of_dicts:
465 if d.get(HASH_KEY) == a_hash:
466 return True
467 return False
468
469def item_matches(item, wanted_item, ignore_keys=None):
470 if is_dict(wanted_item):
471 # match up two dicts
472 if not isinstance(item, dict):
473 return False
474 for key, wanted_val in wanted_item.items():
475 if ignore_keys and key in ignore_keys:
476 continue
477 if not item_matches(item.get(key), wanted_val, ignore_keys=ignore_keys):
478 return False
479 return True
480
481 if is_list(wanted_item):
Pau Espin Pedrol4e36f7c2017-08-28 13:29:28 +0200482 if not is_list(item):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200483 return False
Pau Espin Pedrol58475512017-09-14 15:33:15 +0200484 # Validate that all elements in both lists are of the same type:
485 t = util.list_validate_same_elem_type(wanted_item + item)
486 if t is None:
487 return True # both lists are empty, return
488 # For lists of complex objects, we expect them to be sorted lists:
489 if t in (dict, list, tuple):
490 for i in range(max(len(wanted_item), len(item))):
491 log.ctx(idx=i)
492 subitem = item[i] if i < len(item) else util.empty_instance_type(t)
493 wanted_subitem = wanted_item[i] if i < len(wanted_item) else util.empty_instance_type(t)
494 if not item_matches(subitem, wanted_subitem, ignore_keys=ignore_keys):
495 return False
496 else: # for lists of basic elements, we handle them as unsorted sets:
497 for val in wanted_item:
498 if val not in item:
499 return False
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200500 return True
501
502 return item == wanted_item
503
504
505class ReservedResources(log.Origin):
506 '''
507 After all resources have been figured out, this is the API that a test case
508 gets to interact with resources. From those resources that have been
509 reserved for it, it can pick some to mark them as currently in use.
510 Functions like nitb() provide a resource by automatically picking its
511 dependencies from so far unused (but reserved) resource.
512 '''
513
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200514 def __init__(self, resources_pool, origin, reserved, modifiers):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200515 self.resources_pool = resources_pool
516 self.origin = origin
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200517 self.reserved_original = reserved
518 self.reserved = copy.deepcopy(self.reserved_original)
519 config.overlay(self.reserved, modifiers)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200520
521 def __repr__(self):
522 return 'resources(%s)=%s' % (self.origin.name(), pprint.pformat(self.reserved))
523
524 def get(self, kind, specifics=None):
525 if specifics is None:
526 specifics = {}
527 self.dbg('requesting use of', kind, specifics=specifics)
528 want = { kind: [specifics] }
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200529 available_dict = self.reserved.find(self.origin, want, skip_if_marked=USED_KEY,
530 do_copy=False, raise_if_missing=False,
531 log_label='Using')
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200532 available = available_dict.get(kind)
533 self.dbg(available=len(available))
534 if not available:
Neels Hofmeyrf9e86932017-06-06 19:47:40 +0200535 # cook up a detailed error message for the current situation
536 kind_reserved = self.reserved.get(kind, [])
537 used_count = len([r for r in kind_reserved if USED_KEY in r])
538 matching = self.reserved.find(self.origin, want, raise_if_missing=False, log_label=None).get(kind, [])
539 if not matching:
540 msg = 'none of the reserved resources matches requirements %r' % specifics
541 elif not (used_count < len(kind_reserved)):
542 msg = 'suite.conf reserved only %d x %r.' % (len(kind_reserved), kind)
543 else:
544 msg = ('No unused resource left that matches the requirements;'
545 ' Of reserved %d x %r, %d match the requirements, but all are already in use;'
546 ' Requirements: %r'
547 % (len(kind_reserved), kind, len(matching), specifics))
548 raise NoResourceExn('When trying to use instance nr %d of %r: %s' % (used_count + 1, kind, msg))
549
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200550 pick = available[0]
551 self.dbg(using=pick)
552 assert not pick.get(USED_KEY)
553 pick[USED_KEY] = True
554 return copy.deepcopy(pick)
555
556 def put(self, item):
557 if not item.get(USED_KEY):
558 raise RuntimeError('Can only put() a resource that is used: %r' % item)
559 hash_to_put = item.get(HASH_KEY)
560 if not hash_to_put:
561 raise RuntimeError('Can only put() a resource that has a hash marker: %r' % item)
562 for key, item_list in self.reserved.items():
563 my_list = self.get(key)
564 for my_item in my_list:
565 if hash_to_put == my_item.get(HASH_KEY):
566 my_item.pop(USED_KEY)
567
568 def put_all(self):
Pau Espin Pedrol1dd29552017-06-13 18:07:57 +0200569 if not self.reserved:
570 return
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200571 for key, item_list in self.reserved.items():
Pau Espin Pedrol1dd29552017-06-13 18:07:57 +0200572 for item in item_list:
573 item.pop(USED_KEY, None)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200574
575 def free(self):
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200576 if self.reserved_original:
577 self.resources_pool.free(self.origin, self.reserved_original)
578 self.reserved_original = None
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200579
Neels Hofmeyr2d1d5612017-05-22 20:02:41 +0200580 def counts(self):
581 counts = {}
582 for key in self.reserved.keys():
583 counts[key] = self.count(key)
584 return counts
585
586 def count(self, key):
587 return len(self.reserved.get(key) or [])
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200588
589# vim: expandtab tabstop=4 shiftwidth=4