blob: 992734d699a02b1a183aa65b67c5ee1a1d039e6a [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,
Pau Espin Pedrol0cde25f2019-07-24 19:55:08 +020078 'bts[].osmo_trx.max_trxd_version': schema.UINT,
Pau Espin Pedrolc18c5b82019-11-26 14:24:24 +010079 'bts[].osmo_trx.channels[].rx_path': schema.STR,
80 'bts[].osmo_trx.channels[].tx_path': schema.STR,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020081 'arfcn[].arfcn': schema.INT,
82 'arfcn[].band': schema.BAND,
Holger Hans Peter Freyther93a89422019-02-27 06:36:36 +000083 'modem[].type': schema.STR,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020084 'modem[].label': schema.STR,
85 'modem[].path': schema.STR,
86 'modem[].imsi': schema.IMSI,
87 'modem[].ki': schema.KI,
Pau Espin Pedrol713ce2c2017-08-24 16:57:17 +020088 'modem[].auth_algo': schema.AUTH_ALGO,
Pau Espin Pedrol57497a62017-08-28 14:21:15 +020089 'modem[].ciphers[]': schema.CIPHER,
Pau Espin Pedrolac18fd32017-08-31 18:49:47 +020090 'modem[].features[]': schema.MODEM_FEATURE,
Pau Espin Pedrolbc1ed882018-05-17 16:59:58 +020091 'osmocon_phone[].serial_device': schema.STR,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020092 }
93
94WANT_SCHEMA = util.dict_add(
Pau Espin Pedrolead79ac2017-09-12 15:19:18 +020095 dict([('%s[].times' % r, schema.TIMES) for r in R_ALL]),
Neels Hofmeyr3531a192017-03-28 14:30:28 +020096 RESOURCES_SCHEMA)
97
Pau Espin Pedrol0b302792017-09-10 16:33:10 +020098CONF_SCHEMA = util.dict_add(
Pau Espin Pedrol5dc24592018-08-27 12:53:41 +020099 { 'defaults.timeout': schema.STR,
100 'config.bsc.net.codec_list[]': schema.CODEC },
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200101 dict([('resources.%s' % key, val) for key, val in WANT_SCHEMA.items()]),
102 dict([('modifiers.%s' % key, val) for key, val in WANT_SCHEMA.items()]))
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200103
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200104KNOWN_BTS_TYPES = {
Your Name44af3412017-04-13 03:11:59 +0200105 'osmo-bts-sysmo': bts_sysmo.SysmoBts,
106 'osmo-bts-trx': bts_osmotrx.OsmoBtsTrx,
Pau Espin Pedrolfeb66e72019-03-04 18:37:04 +0100107 'osmo-bts-oc2g': bts_oc2g.OsmoBtsOC2G,
Pau Espin Pedroldaed4472017-09-15 14:11:35 +0200108 'osmo-bts-octphy': bts_octphy.OsmoBtsOctphy,
Holger Hans Peter Freytherd2db10d2018-12-06 18:34:53 +0000109 'osmo-bts-virtual': bts_osmovirtual.OsmoBtsVirtual,
Pau Espin Pedrol1b28a582018-03-08 21:01:26 +0100110 'nanobts': bts_nanobts.NanoBts,
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200111 }
112
Holger Hans Peter Freyther74243012019-02-27 08:18:38 +0000113
114KNOWN_MS_TYPES = {
115 # Map None to ofono for forward compability
116 None: modem.Modem,
117 'ofono': modem.Modem,
Holger Hans Peter Freyther10501cc2019-02-25 03:20:16 +0000118 'osmo-mobile': ms_osmo_mobile.MSOsmoMobile,
Holger Hans Peter Freyther74243012019-02-27 08:18:38 +0000119}
120
121
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200122def register_bts_type(name, clazz):
123 KNOWN_BTS_TYPES[name] = clazz
124
125class ResourcesPool(log.Origin):
126 _remember_to_free = None
127 _registered_exit_handler = False
128
129 def __init__(self):
130 self.config_path = config.get_config_file(RESOURCES_CONF)
131 self.state_dir = config.get_state_dir()
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200132 super().__init__(log.C_CNF, conf=self.config_path, state=self.state_dir.path)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200133 self.read_conf()
134
135 def read_conf(self):
136 self.all_resources = Resources(config.read(self.config_path, RESOURCES_SCHEMA))
137 self.all_resources.set_hashes()
138
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200139 def reserve(self, origin, want, modifiers):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200140 '''
141 attempt to reserve the resources specified in the dict 'want' for
142 'origin'. Obtain a lock on the resources lock dir, verify that all
143 wanted resources are available, and if yes mark them as reserved.
144
145 On success, return a reservation object which can be used to release
146 the reservation. The reservation will be freed automatically on program
147 exit, if not yet done manually.
148
149 'origin' should be an Origin() instance.
150
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200151 'want' is a dict matching RESOURCES_SCHEMA, used to specify what to
152 reserve.
153
154 'modifiers' is a dict matching RESOURCES_SCHEMA, it is overlaid on top
155 of 'want'.
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200156
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200157 If an entry has no attribute set, any of the resources may be
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200158 reserved without further limitations.
159
160 ResourcesPool may also be selected with narrowed down constraints.
Neels Hofmeyr76d81032017-05-18 18:35:32 +0200161 This would reserve one IP address, two modems, one BTS of type
Neels Hofmeyr391afe32017-05-18 19:22:12 +0200162 sysmo and one of type trx, plus 2 ARFCNs in the 1800 band:
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200163
164 {
Neels Hofmeyr76d81032017-05-18 18:35:32 +0200165 'ip_address': [ {} ],
Neels Hofmeyr391afe32017-05-18 19:22:12 +0200166 'bts': [ { 'type': 'sysmo' }, { 'type': 'trx' } ],
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200167 'arfcn': [ { 'band': 'GSM-1800' }, { 'band': 'GSM-1800' } ],
168 'modem': [ {}, {} ],
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200169 }
170 '''
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200171 schema.validate(want, RESOURCES_SCHEMA)
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200172 schema.validate(modifiers, RESOURCES_SCHEMA)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200173
174 origin_id = origin.origin_id()
175
176 with self.state_dir.lock(origin_id):
177 rrfile_path = self.state_dir.mk_parentdir(RESERVED_RESOURCES_FILE)
178 reserved = Resources(config.read(rrfile_path, if_missing_return={}))
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200179 to_be_reserved = self.all_resources.without(reserved).find(origin, want)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200180
181 to_be_reserved.mark_reserved_by(origin_id)
182
183 reserved.add(to_be_reserved)
184 config.write(rrfile_path, reserved)
185
186 self.remember_to_free(to_be_reserved)
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200187 return ReservedResources(self, origin, to_be_reserved, modifiers)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200188
189 def free(self, origin, to_be_freed):
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200190 log.ctx(origin)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200191 with self.state_dir.lock(origin.origin_id()):
192 rrfile_path = self.state_dir.mk_parentdir(RESERVED_RESOURCES_FILE)
193 reserved = Resources(config.read(rrfile_path, if_missing_return={}))
194 reserved.drop(to_be_freed)
195 config.write(rrfile_path, reserved)
196 self.forget_freed(to_be_freed)
197
198 def register_exit_handler(self):
199 if self._registered_exit_handler:
200 return
201 atexit.register(self.clean_up_registered_resources)
202 self._registered_exit_handler = True
203
204 def unregister_exit_handler(self):
205 if not self._registered_exit_handler:
206 return
207 atexit.unregister(self.clean_up_registered_resources)
208 self._registered_exit_handler = False
209
210 def clean_up_registered_resources(self):
211 if not self._remember_to_free:
212 return
213 self.free(log.Origin('atexit.clean_up_registered_resources()'),
214 self._remember_to_free)
215
216 def remember_to_free(self, to_be_reserved):
217 self.register_exit_handler()
218 if not self._remember_to_free:
219 self._remember_to_free = Resources()
220 self._remember_to_free.add(to_be_reserved)
221
222 def forget_freed(self, freed):
223 if freed is self._remember_to_free:
224 self._remember_to_free.clear()
225 else:
226 self._remember_to_free.drop(freed)
227 if not self._remember_to_free:
228 self.unregister_exit_handler()
229
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100230 def next_persistent_value(self, token, first_val, validate_func, inc_func, origin):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200231 origin_id = origin.origin_id()
232
233 with self.state_dir.lock(origin_id):
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100234 token_path = self.state_dir.child('last_used_%s.state' % token)
235 log.ctx(token_path)
236 last_value = first_val
237 if os.path.exists(token_path):
238 if not os.path.isfile(token_path):
239 raise RuntimeError('path should be a file but is not: %r' % token_path)
240 with open(token_path, 'r') as f:
241 last_value = f.read().strip()
242 validate_func(last_value)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200243
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100244 next_value = inc_func(last_value)
245 with open(token_path, 'w') as f:
246 f.write(next_value)
247 return next_value
248
249 def next_msisdn(self, origin):
250 return self.next_persistent_value('msisdn', '1000', schema.msisdn, util.msisdn_inc, origin)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200251
Pau Espin Pedrol5e0c2512017-11-06 18:40:23 +0100252 def next_lac(self, origin):
Pau Espin Pedrolf7f06362017-11-28 12:56:35 +0100253 # LAC=0 has special meaning (MS detached), avoid it
254 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 +0200255
Pau Espin Pedrol8a3a7b52017-11-28 15:50:02 +0100256 def next_rac(self, origin):
257 return self.next_persistent_value('rac', '1', schema.uint8, lambda x: str((int(x)+1) % pow(2,8) or 1), origin)
258
Pau Espin Pedrol4ccce7c2017-11-07 11:13:20 +0100259 def next_cellid(self, origin):
260 return self.next_persistent_value('cellid', '1', schema.uint16, lambda x: str((int(x)+1) % pow(2,16)), origin)
261
Pau Espin Pedrol8a3a7b52017-11-28 15:50:02 +0100262 def next_bvci(self, origin):
263 # BVCI=0 and =1 are reserved, avoid them.
264 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)
265
Pau Espin Pedrol4676cbd2017-09-14 17:35:03 +0200266class NoResourceExn(log.Error):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200267 pass
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200268
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200269class Resources(dict):
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200270
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200271 def __init__(self, all_resources={}, do_copy=True):
272 if do_copy:
273 all_resources = copy.deepcopy(all_resources)
274 self.update(all_resources)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200275
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200276 def drop(self, reserved, fail_if_not_found=True):
277 # protect from modifying reserved because we're the same object
278 if reserved is self:
279 raise RuntimeError('Refusing to drop a list of resources from itself.'
280 ' This is probably a bug where a list of Resources()'
281 ' should have been copied but is passed as-is.'
282 ' use Resources.clear() instead.')
283
284 for key, reserved_list in reserved.items():
285 my_list = self.get(key) or []
286
287 if my_list is reserved_list:
288 self.pop(key)
289 continue
290
291 for reserved_item in reserved_list:
292 found = False
293 reserved_hash = reserved_item.get(HASH_KEY)
294 if not reserved_hash:
295 raise RuntimeError('Resources.drop() only works with hashed items')
296
297 for i in range(len(my_list)):
298 my_item = my_list[i]
299 my_hash = my_item.get(HASH_KEY)
300 if not my_hash:
301 raise RuntimeError('Resources.drop() only works with hashed items')
302 if my_hash == reserved_hash:
303 found = True
304 my_list.pop(i)
305 break
306
307 if fail_if_not_found and not found:
308 raise RuntimeError('Asked to drop resource from a pool, but the'
309 ' resource was not found: %s = %r' % (key, reserved_item))
310
311 if not my_list:
312 self.pop(key)
313 return self
314
315 def without(self, reserved):
316 return Resources(self).drop(reserved)
317
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200318 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 +0200319 '''
320 Pass a dict of resource requirements, e.g.:
321 want = {
322 'bts': [ {'type': 'osmo-bts-sysmo',}, {} ],
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200323 'modem': [ {}, {}, {} ]
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200324 }
325 This function tries to find a combination from the available resources that
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200326 matches these requirements. The return value is a dict (wrapped in a Resources class)
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200327 that contains the matching resources in the order of 'want' dict: in above
328 example, the returned dict would have a 'bts' list with the first item being
329 a sysmoBTS, the second item being any other available BTS.
330
331 If skip_if_marked is passed, any resource that contains this key is skipped.
332 E.g. if a BTS has the USED_KEY set like
333 reserved_resources = { 'bts' : {..., '_used': True} }
334 then this may be skipped by passing skip_if_marked='_used'
335 (or rather skip_if_marked=USED_KEY).
336
337 If do_copy is True, the returned dict is a deep copy and does not share
338 lists with any other Resources dict.
339
340 If raise_if_missing is False, this will return an empty item for any
341 resource that had no match, instead of immediately raising an exception.
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200342
343 This function expects input dictionaries whose contents have already
344 been replicated based on its the 'times' attributes. See
345 config.replicate_times() for more details.
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200346 '''
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200347 matches = {}
Neels Hofmeyr17c139e2017-04-12 02:42:02 +0200348 for key, want_list in sorted(want.items()): # sorted for deterministic test results
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200349 # here we have a resource of a given type, e.g. 'bts', with a list
350 # containing as many BTSes as the caller wants to reserve/use. Each
351 # list item contains specifics for the particular BTS.
Neels Hofmeyr2a1a1fa2017-05-29 01:36:21 +0200352 my_list = self.get(key, [])
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200353
Neels Hofmeyrf9e86932017-06-06 19:47:40 +0200354 if log_label:
355 for_origin.log(log_label, len(want_list), 'x', key, '(candidates: %d)'%len(my_list))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200356
357 # Try to avoid a less constrained item snatching away a resource
358 # from a more detailed constrained requirement.
359
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200360 # first record all matches, so that each requested item has a list
361 # of all available resources that match it. Some resources may
362 # appear for multiple requested items. Store matching indexes.
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200363 all_matches = []
364 for want_item in want_list:
365 item_match_list = []
366 for i in range(len(my_list)):
367 my_item = my_list[i]
368 if skip_if_marked and my_item.get(skip_if_marked):
369 continue
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200370 if item_matches(my_item, want_item):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200371 item_match_list.append(i)
372 if not item_match_list:
Neels Hofmeyr9b907702017-05-06 23:20:33 +0200373 if raise_if_missing:
374 raise NoResourceExn('No matching resource available for %s = %r'
375 % (key, want_item))
376 else:
377 # this one failed... see below
378 all_matches = []
379 break
380
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200381 all_matches.append( item_match_list )
382
383 if not all_matches:
Neels Hofmeyr9b907702017-05-06 23:20:33 +0200384 # ...this one failed. Makes no sense to solve resource
385 # allocations, return an empty list for this key to mark
386 # failure.
387 matches[key] = []
388 continue
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200389
390 # figure out who gets what
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200391 try:
392 solution = solve(all_matches)
393 except NotSolvable:
394 # instead of a cryptic error message, raise an exception that
395 # conveys meaning to the user.
396 raise NoResourceExn('Could not resolve request to reserve resources: '
397 '%d x %s with requirements: %r' % (len(want_list), key, want_list))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200398 picked = [ my_list[i] for i in solution if i is not None ]
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200399 for_origin.dbg('Picked', config.tostr(picked))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200400 matches[key] = picked
401
402 return Resources(matches, do_copy=do_copy)
403
404 def set_hashes(self):
405 for key, item_list in self.items():
406 for item in item_list:
407 item[HASH_KEY] = util.hash_obj(item, HASH_KEY, RESERVED_KEY, USED_KEY)
408
409 def add(self, more):
410 if more is self:
411 raise RuntimeError('adding a list of resources to itself?')
412 config.add(self, copy.deepcopy(more))
413
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200414 def mark_reserved_by(self, origin_id):
415 for key, item_list in self.items():
416 for item in item_list:
417 item[RESERVED_KEY] = origin_id
418
419
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200420class NotSolvable(Exception):
421 pass
422
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200423def solve(all_matches):
424 '''
425 all_matches shall be a list of index-lists.
426 all_matches[i] is the list of indexes that item i can use.
427 Return a solution so that each i gets a different index.
428 solve([ [0, 1, 2],
429 [0],
430 [0, 2] ]) == [1, 0, 2]
431 '''
432
433 def all_differ(l):
434 return len(set(l)) == len(l)
435
436 def search_in_permutations(fixed=[]):
437 idx = len(fixed)
438 for i in range(len(all_matches[idx])):
439 val = all_matches[idx][i]
440 # don't add a val that's already in the list
441 if val in fixed:
442 continue
443 l = list(fixed)
444 l.append(val)
445 if len(l) == len(all_matches):
446 # found a solution
447 return l
448 # not at the end yet, add next digit
449 r = search_in_permutations(l)
450 if r:
451 # nested search_in_permutations() call found a solution
452 return r
453 # this entire branch yielded no solution
454 return None
455
456 if not all_matches:
457 raise RuntimeError('Cannot solve: no candidates')
458
459 solution = search_in_permutations()
460 if not solution:
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200461 raise NotSolvable('The requested resource requirements are not solvable %r'
462 % all_matches)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200463 return solution
464
465
466def contains_hash(list_of_dicts, a_hash):
467 for d in list_of_dicts:
468 if d.get(HASH_KEY) == a_hash:
469 return True
470 return False
471
472def item_matches(item, wanted_item, ignore_keys=None):
473 if is_dict(wanted_item):
474 # match up two dicts
475 if not isinstance(item, dict):
476 return False
477 for key, wanted_val in wanted_item.items():
478 if ignore_keys and key in ignore_keys:
479 continue
480 if not item_matches(item.get(key), wanted_val, ignore_keys=ignore_keys):
481 return False
482 return True
483
484 if is_list(wanted_item):
Pau Espin Pedrol4e36f7c2017-08-28 13:29:28 +0200485 if not is_list(item):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200486 return False
Pau Espin Pedrol58475512017-09-14 15:33:15 +0200487 # Validate that all elements in both lists are of the same type:
488 t = util.list_validate_same_elem_type(wanted_item + item)
489 if t is None:
490 return True # both lists are empty, return
491 # For lists of complex objects, we expect them to be sorted lists:
492 if t in (dict, list, tuple):
493 for i in range(max(len(wanted_item), len(item))):
494 log.ctx(idx=i)
495 subitem = item[i] if i < len(item) else util.empty_instance_type(t)
496 wanted_subitem = wanted_item[i] if i < len(wanted_item) else util.empty_instance_type(t)
497 if not item_matches(subitem, wanted_subitem, ignore_keys=ignore_keys):
498 return False
499 else: # for lists of basic elements, we handle them as unsorted sets:
500 for val in wanted_item:
501 if val not in item:
502 return False
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200503 return True
504
505 return item == wanted_item
506
507
508class ReservedResources(log.Origin):
509 '''
510 After all resources have been figured out, this is the API that a test case
511 gets to interact with resources. From those resources that have been
512 reserved for it, it can pick some to mark them as currently in use.
513 Functions like nitb() provide a resource by automatically picking its
514 dependencies from so far unused (but reserved) resource.
515 '''
516
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200517 def __init__(self, resources_pool, origin, reserved, modifiers):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200518 self.resources_pool = resources_pool
519 self.origin = origin
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200520 self.reserved_original = reserved
521 self.reserved = copy.deepcopy(self.reserved_original)
522 config.overlay(self.reserved, modifiers)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200523
524 def __repr__(self):
525 return 'resources(%s)=%s' % (self.origin.name(), pprint.pformat(self.reserved))
526
527 def get(self, kind, specifics=None):
528 if specifics is None:
529 specifics = {}
530 self.dbg('requesting use of', kind, specifics=specifics)
531 want = { kind: [specifics] }
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200532 available_dict = self.reserved.find(self.origin, want, skip_if_marked=USED_KEY,
533 do_copy=False, raise_if_missing=False,
534 log_label='Using')
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200535 available = available_dict.get(kind)
536 self.dbg(available=len(available))
537 if not available:
Neels Hofmeyrf9e86932017-06-06 19:47:40 +0200538 # cook up a detailed error message for the current situation
539 kind_reserved = self.reserved.get(kind, [])
540 used_count = len([r for r in kind_reserved if USED_KEY in r])
541 matching = self.reserved.find(self.origin, want, raise_if_missing=False, log_label=None).get(kind, [])
542 if not matching:
543 msg = 'none of the reserved resources matches requirements %r' % specifics
544 elif not (used_count < len(kind_reserved)):
545 msg = 'suite.conf reserved only %d x %r.' % (len(kind_reserved), kind)
546 else:
547 msg = ('No unused resource left that matches the requirements;'
548 ' Of reserved %d x %r, %d match the requirements, but all are already in use;'
549 ' Requirements: %r'
550 % (len(kind_reserved), kind, len(matching), specifics))
551 raise NoResourceExn('When trying to use instance nr %d of %r: %s' % (used_count + 1, kind, msg))
552
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200553 pick = available[0]
554 self.dbg(using=pick)
555 assert not pick.get(USED_KEY)
556 pick[USED_KEY] = True
557 return copy.deepcopy(pick)
558
559 def put(self, item):
560 if not item.get(USED_KEY):
561 raise RuntimeError('Can only put() a resource that is used: %r' % item)
562 hash_to_put = item.get(HASH_KEY)
563 if not hash_to_put:
564 raise RuntimeError('Can only put() a resource that has a hash marker: %r' % item)
565 for key, item_list in self.reserved.items():
566 my_list = self.get(key)
567 for my_item in my_list:
568 if hash_to_put == my_item.get(HASH_KEY):
569 my_item.pop(USED_KEY)
570
571 def put_all(self):
Pau Espin Pedrol1dd29552017-06-13 18:07:57 +0200572 if not self.reserved:
573 return
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200574 for key, item_list in self.reserved.items():
Pau Espin Pedrol1dd29552017-06-13 18:07:57 +0200575 for item in item_list:
576 item.pop(USED_KEY, None)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200577
578 def free(self):
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200579 if self.reserved_original:
580 self.resources_pool.free(self.origin, self.reserved_original)
581 self.reserved_original = None
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200582
Neels Hofmeyr2d1d5612017-05-22 20:02:41 +0200583 def counts(self):
584 counts = {}
585 for key in self.reserved.keys():
586 counts[key] = self.count(key)
587 return counts
588
589 def count(self, key):
590 return len(self.reserved.get(key) or [])
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200591
592# vim: expandtab tabstop=4 shiftwidth=4