blob: 0cdcb8aa553141a97b61dcb47345232c7b85f46e [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'
Pau Espin Pedrol116a2c42020-02-11 17:41:13 +010043R_RUN_NODE = 'run_node'
Neels Hofmeyr3531a192017-03-28 14:30:28 +020044R_BTS = 'bts'
45R_ARFCN = 'arfcn'
46R_MODEM = 'modem'
Pau Espin Pedrolbc1ed882018-05-17 16:59:58 +020047R_OSMOCON = 'osmocon_phone'
Pau Espin Pedrol116a2c42020-02-11 17:41:13 +010048R_ALL = (R_IP_ADDRESS, R_RUN_NODE, R_BTS, R_ARFCN, R_MODEM, R_OSMOCON)
Neels Hofmeyr3531a192017-03-28 14:30:28 +020049
50RESOURCES_SCHEMA = {
Neels Hofmeyr76d81032017-05-18 18:35:32 +020051 'ip_address[].addr': schema.IPV4,
Pau Espin Pedrol116a2c42020-02-11 17:41:13 +010052 'run_node[].run_type': schema.STR,
53 'run_node[].run_addr': schema.IPV4,
54 'run_node[].ssh_user': schema.STR,
55 'run_node[].ssh_addr': schema.IPV4,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020056 'bts[].label': schema.STR,
57 'bts[].type': schema.STR,
Pau Espin Pedrolfa9a6d32017-09-12 15:13:21 +020058 'bts[].ipa_unit_id': schema.UINT,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020059 'bts[].addr': schema.IPV4,
60 'bts[].band': schema.BAND,
Pau Espin Pedrolce35d912017-11-23 11:01:24 +010061 'bts[].direct_pcu': schema.BOOL_STR,
Pau Espin Pedrol57497a62017-08-28 14:21:15 +020062 'bts[].ciphers[]': schema.CIPHER,
Pau Espin Pedrol722e94e2018-08-22 11:01:32 +020063 'bts[].channel_allocator': schema.CHAN_ALLOCATOR,
Pau Espin Pedrol4f23ab52018-10-29 11:30:00 +010064 'bts[].gprs_mode': schema.GPRS_MODE,
Pau Espin Pedrol39df7f42018-05-07 13:49:33 +020065 'bts[].num_trx': schema.UINT,
66 'bts[].max_trx': schema.UINT,
Pau Espin Pedrolf6a07122018-07-27 16:24:29 +020067 'bts[].trx_list[].addr': schema.IPV4,
Your Name44af3412017-04-13 03:11:59 +020068 'bts[].trx_list[].hw_addr': schema.HWADDR,
69 'bts[].trx_list[].net_device': schema.STR,
Pau Espin Pedrolb26f32a2017-09-14 13:52:28 +020070 'bts[].trx_list[].nominal_power': schema.UINT,
71 'bts[].trx_list[].max_power_red': schema.UINT,
Pau Espin Pedrolc9b63762018-05-07 01:57:01 +020072 'bts[].trx_list[].timeslot_list[].phys_chan_config': schema.PHY_CHAN,
Pau Espin Pedrolf6a07122018-07-27 16:24:29 +020073 'bts[].trx_list[].power_supply.type': schema.STR,
74 'bts[].trx_list[].power_supply.device': schema.STR,
75 'bts[].trx_list[].power_supply.port': schema.STR,
Pau Espin Pedrol0d455042018-08-27 17:07:41 +020076 'bts[].osmo_trx.launch_trx': schema.BOOL_STR,
77 'bts[].osmo_trx.type': schema.STR,
78 'bts[].osmo_trx.clock_reference': schema.OSMO_TRX_CLOCK_REF,
79 'bts[].osmo_trx.trx_ip': schema.IPV4,
Pau Espin Pedrola9006df2018-10-01 12:26:39 +020080 'bts[].osmo_trx.remote_user': schema.STR,
Pau Espin Pedrol8cfa10f2018-11-06 12:05:19 +010081 'bts[].osmo_trx.dev_args': schema.STR,
Pau Espin Pedrol94eab262018-09-17 20:25:55 +020082 'bts[].osmo_trx.multi_arfcn': schema.BOOL_STR,
Pau Espin Pedrol0cde25f2019-07-24 19:55:08 +020083 'bts[].osmo_trx.max_trxd_version': schema.UINT,
Pau Espin Pedrolc18c5b82019-11-26 14:24:24 +010084 'bts[].osmo_trx.channels[].rx_path': schema.STR,
85 'bts[].osmo_trx.channels[].tx_path': schema.STR,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020086 'arfcn[].arfcn': schema.INT,
87 'arfcn[].band': schema.BAND,
Holger Hans Peter Freyther93a89422019-02-27 06:36:36 +000088 'modem[].type': schema.STR,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020089 'modem[].label': schema.STR,
90 'modem[].path': schema.STR,
91 'modem[].imsi': schema.IMSI,
92 'modem[].ki': schema.KI,
Pau Espin Pedrol713ce2c2017-08-24 16:57:17 +020093 'modem[].auth_algo': schema.AUTH_ALGO,
Pau Espin Pedrol57497a62017-08-28 14:21:15 +020094 'modem[].ciphers[]': schema.CIPHER,
Pau Espin Pedrolac18fd32017-08-31 18:49:47 +020095 'modem[].features[]': schema.MODEM_FEATURE,
Pau Espin Pedrolbc1ed882018-05-17 16:59:58 +020096 'osmocon_phone[].serial_device': schema.STR,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020097 }
98
99WANT_SCHEMA = util.dict_add(
Pau Espin Pedrolead79ac2017-09-12 15:19:18 +0200100 dict([('%s[].times' % r, schema.TIMES) for r in R_ALL]),
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200101 RESOURCES_SCHEMA)
102
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200103CONF_SCHEMA = util.dict_add(
Pau Espin Pedrol5dc24592018-08-27 12:53:41 +0200104 { 'defaults.timeout': schema.STR,
105 'config.bsc.net.codec_list[]': schema.CODEC },
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200106 dict([('resources.%s' % key, val) for key, val in WANT_SCHEMA.items()]),
107 dict([('modifiers.%s' % key, val) for key, val in WANT_SCHEMA.items()]))
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200108
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200109KNOWN_BTS_TYPES = {
Your Name44af3412017-04-13 03:11:59 +0200110 'osmo-bts-sysmo': bts_sysmo.SysmoBts,
111 'osmo-bts-trx': bts_osmotrx.OsmoBtsTrx,
Pau Espin Pedrolfeb66e72019-03-04 18:37:04 +0100112 'osmo-bts-oc2g': bts_oc2g.OsmoBtsOC2G,
Pau Espin Pedroldaed4472017-09-15 14:11:35 +0200113 'osmo-bts-octphy': bts_octphy.OsmoBtsOctphy,
Holger Hans Peter Freytherd2db10d2018-12-06 18:34:53 +0000114 'osmo-bts-virtual': bts_osmovirtual.OsmoBtsVirtual,
Pau Espin Pedrol1b28a582018-03-08 21:01:26 +0100115 'nanobts': bts_nanobts.NanoBts,
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200116 }
117
Holger Hans Peter Freyther74243012019-02-27 08:18:38 +0000118
119KNOWN_MS_TYPES = {
120 # Map None to ofono for forward compability
121 None: modem.Modem,
122 'ofono': modem.Modem,
Holger Hans Peter Freyther10501cc2019-02-25 03:20:16 +0000123 'osmo-mobile': ms_osmo_mobile.MSOsmoMobile,
Holger Hans Peter Freyther74243012019-02-27 08:18:38 +0000124}
125
126
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200127def register_bts_type(name, clazz):
128 KNOWN_BTS_TYPES[name] = clazz
129
130class ResourcesPool(log.Origin):
131 _remember_to_free = None
132 _registered_exit_handler = False
133
134 def __init__(self):
135 self.config_path = config.get_config_file(RESOURCES_CONF)
136 self.state_dir = config.get_state_dir()
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200137 super().__init__(log.C_CNF, conf=self.config_path, state=self.state_dir.path)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200138 self.read_conf()
139
140 def read_conf(self):
141 self.all_resources = Resources(config.read(self.config_path, RESOURCES_SCHEMA))
142 self.all_resources.set_hashes()
143
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200144 def reserve(self, origin, want, modifiers):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200145 '''
146 attempt to reserve the resources specified in the dict 'want' for
147 'origin'. Obtain a lock on the resources lock dir, verify that all
148 wanted resources are available, and if yes mark them as reserved.
149
150 On success, return a reservation object which can be used to release
151 the reservation. The reservation will be freed automatically on program
152 exit, if not yet done manually.
153
154 'origin' should be an Origin() instance.
155
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200156 'want' is a dict matching RESOURCES_SCHEMA, used to specify what to
157 reserve.
158
159 'modifiers' is a dict matching RESOURCES_SCHEMA, it is overlaid on top
160 of 'want'.
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200161
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200162 If an entry has no attribute set, any of the resources may be
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200163 reserved without further limitations.
164
165 ResourcesPool may also be selected with narrowed down constraints.
Neels Hofmeyr76d81032017-05-18 18:35:32 +0200166 This would reserve one IP address, two modems, one BTS of type
Neels Hofmeyr391afe32017-05-18 19:22:12 +0200167 sysmo and one of type trx, plus 2 ARFCNs in the 1800 band:
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200168
169 {
Neels Hofmeyr76d81032017-05-18 18:35:32 +0200170 'ip_address': [ {} ],
Neels Hofmeyr391afe32017-05-18 19:22:12 +0200171 'bts': [ { 'type': 'sysmo' }, { 'type': 'trx' } ],
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200172 'arfcn': [ { 'band': 'GSM-1800' }, { 'band': 'GSM-1800' } ],
173 'modem': [ {}, {} ],
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200174 }
175 '''
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200176 schema.validate(want, RESOURCES_SCHEMA)
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200177 schema.validate(modifiers, RESOURCES_SCHEMA)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200178
179 origin_id = origin.origin_id()
180
181 with self.state_dir.lock(origin_id):
182 rrfile_path = self.state_dir.mk_parentdir(RESERVED_RESOURCES_FILE)
183 reserved = Resources(config.read(rrfile_path, if_missing_return={}))
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200184 to_be_reserved = self.all_resources.without(reserved).find(origin, want)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200185
186 to_be_reserved.mark_reserved_by(origin_id)
187
188 reserved.add(to_be_reserved)
189 config.write(rrfile_path, reserved)
190
191 self.remember_to_free(to_be_reserved)
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200192 return ReservedResources(self, origin, to_be_reserved, modifiers)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200193
194 def free(self, origin, to_be_freed):
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200195 log.ctx(origin)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200196 with self.state_dir.lock(origin.origin_id()):
197 rrfile_path = self.state_dir.mk_parentdir(RESERVED_RESOURCES_FILE)
198 reserved = Resources(config.read(rrfile_path, if_missing_return={}))
199 reserved.drop(to_be_freed)
200 config.write(rrfile_path, reserved)
201 self.forget_freed(to_be_freed)
202
203 def register_exit_handler(self):
204 if self._registered_exit_handler:
205 return
206 atexit.register(self.clean_up_registered_resources)
207 self._registered_exit_handler = True
208
209 def unregister_exit_handler(self):
210 if not self._registered_exit_handler:
211 return
212 atexit.unregister(self.clean_up_registered_resources)
213 self._registered_exit_handler = False
214
215 def clean_up_registered_resources(self):
216 if not self._remember_to_free:
217 return
218 self.free(log.Origin('atexit.clean_up_registered_resources()'),
219 self._remember_to_free)
220
221 def remember_to_free(self, to_be_reserved):
222 self.register_exit_handler()
223 if not self._remember_to_free:
224 self._remember_to_free = Resources()
225 self._remember_to_free.add(to_be_reserved)
226
227 def forget_freed(self, freed):
228 if freed is self._remember_to_free:
229 self._remember_to_free.clear()
230 else:
231 self._remember_to_free.drop(freed)
232 if not self._remember_to_free:
233 self.unregister_exit_handler()
234
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100235 def next_persistent_value(self, token, first_val, validate_func, inc_func, origin):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200236 origin_id = origin.origin_id()
237
238 with self.state_dir.lock(origin_id):
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100239 token_path = self.state_dir.child('last_used_%s.state' % token)
240 log.ctx(token_path)
241 last_value = first_val
242 if os.path.exists(token_path):
243 if not os.path.isfile(token_path):
244 raise RuntimeError('path should be a file but is not: %r' % token_path)
245 with open(token_path, 'r') as f:
246 last_value = f.read().strip()
247 validate_func(last_value)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200248
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100249 next_value = inc_func(last_value)
250 with open(token_path, 'w') as f:
251 f.write(next_value)
252 return next_value
253
254 def next_msisdn(self, origin):
255 return self.next_persistent_value('msisdn', '1000', schema.msisdn, util.msisdn_inc, origin)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200256
Pau Espin Pedrol5e0c2512017-11-06 18:40:23 +0100257 def next_lac(self, origin):
Pau Espin Pedrolf7f06362017-11-28 12:56:35 +0100258 # LAC=0 has special meaning (MS detached), avoid it
259 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 +0200260
Pau Espin Pedrol8a3a7b52017-11-28 15:50:02 +0100261 def next_rac(self, origin):
262 return self.next_persistent_value('rac', '1', schema.uint8, lambda x: str((int(x)+1) % pow(2,8) or 1), origin)
263
Pau Espin Pedrol4ccce7c2017-11-07 11:13:20 +0100264 def next_cellid(self, origin):
265 return self.next_persistent_value('cellid', '1', schema.uint16, lambda x: str((int(x)+1) % pow(2,16)), origin)
266
Pau Espin Pedrol8a3a7b52017-11-28 15:50:02 +0100267 def next_bvci(self, origin):
268 # BVCI=0 and =1 are reserved, avoid them.
269 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)
270
Pau Espin Pedrol4676cbd2017-09-14 17:35:03 +0200271class NoResourceExn(log.Error):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200272 pass
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200273
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200274class Resources(dict):
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200275
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200276 def __init__(self, all_resources={}, do_copy=True):
277 if do_copy:
278 all_resources = copy.deepcopy(all_resources)
279 self.update(all_resources)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200280
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200281 def drop(self, reserved, fail_if_not_found=True):
282 # protect from modifying reserved because we're the same object
283 if reserved is self:
284 raise RuntimeError('Refusing to drop a list of resources from itself.'
285 ' This is probably a bug where a list of Resources()'
286 ' should have been copied but is passed as-is.'
287 ' use Resources.clear() instead.')
288
289 for key, reserved_list in reserved.items():
290 my_list = self.get(key) or []
291
292 if my_list is reserved_list:
293 self.pop(key)
294 continue
295
296 for reserved_item in reserved_list:
297 found = False
298 reserved_hash = reserved_item.get(HASH_KEY)
299 if not reserved_hash:
300 raise RuntimeError('Resources.drop() only works with hashed items')
301
302 for i in range(len(my_list)):
303 my_item = my_list[i]
304 my_hash = my_item.get(HASH_KEY)
305 if not my_hash:
306 raise RuntimeError('Resources.drop() only works with hashed items')
307 if my_hash == reserved_hash:
308 found = True
309 my_list.pop(i)
310 break
311
312 if fail_if_not_found and not found:
313 raise RuntimeError('Asked to drop resource from a pool, but the'
314 ' resource was not found: %s = %r' % (key, reserved_item))
315
316 if not my_list:
317 self.pop(key)
318 return self
319
320 def without(self, reserved):
321 return Resources(self).drop(reserved)
322
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200323 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 +0200324 '''
325 Pass a dict of resource requirements, e.g.:
326 want = {
327 'bts': [ {'type': 'osmo-bts-sysmo',}, {} ],
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200328 'modem': [ {}, {}, {} ]
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200329 }
330 This function tries to find a combination from the available resources that
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200331 matches these requirements. The return value is a dict (wrapped in a Resources class)
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200332 that contains the matching resources in the order of 'want' dict: in above
333 example, the returned dict would have a 'bts' list with the first item being
334 a sysmoBTS, the second item being any other available BTS.
335
336 If skip_if_marked is passed, any resource that contains this key is skipped.
337 E.g. if a BTS has the USED_KEY set like
338 reserved_resources = { 'bts' : {..., '_used': True} }
339 then this may be skipped by passing skip_if_marked='_used'
340 (or rather skip_if_marked=USED_KEY).
341
342 If do_copy is True, the returned dict is a deep copy and does not share
343 lists with any other Resources dict.
344
345 If raise_if_missing is False, this will return an empty item for any
346 resource that had no match, instead of immediately raising an exception.
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200347
348 This function expects input dictionaries whose contents have already
349 been replicated based on its the 'times' attributes. See
350 config.replicate_times() for more details.
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200351 '''
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200352 matches = {}
Neels Hofmeyr17c139e2017-04-12 02:42:02 +0200353 for key, want_list in sorted(want.items()): # sorted for deterministic test results
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200354 # here we have a resource of a given type, e.g. 'bts', with a list
355 # containing as many BTSes as the caller wants to reserve/use. Each
356 # list item contains specifics for the particular BTS.
Neels Hofmeyr2a1a1fa2017-05-29 01:36:21 +0200357 my_list = self.get(key, [])
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200358
Neels Hofmeyrf9e86932017-06-06 19:47:40 +0200359 if log_label:
360 for_origin.log(log_label, len(want_list), 'x', key, '(candidates: %d)'%len(my_list))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200361
362 # Try to avoid a less constrained item snatching away a resource
363 # from a more detailed constrained requirement.
364
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200365 # first record all matches, so that each requested item has a list
366 # of all available resources that match it. Some resources may
367 # appear for multiple requested items. Store matching indexes.
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200368 all_matches = []
369 for want_item in want_list:
370 item_match_list = []
371 for i in range(len(my_list)):
372 my_item = my_list[i]
373 if skip_if_marked and my_item.get(skip_if_marked):
374 continue
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200375 if item_matches(my_item, want_item):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200376 item_match_list.append(i)
377 if not item_match_list:
Neels Hofmeyr9b907702017-05-06 23:20:33 +0200378 if raise_if_missing:
379 raise NoResourceExn('No matching resource available for %s = %r'
380 % (key, want_item))
381 else:
382 # this one failed... see below
383 all_matches = []
384 break
385
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200386 all_matches.append( item_match_list )
387
388 if not all_matches:
Neels Hofmeyr9b907702017-05-06 23:20:33 +0200389 # ...this one failed. Makes no sense to solve resource
390 # allocations, return an empty list for this key to mark
391 # failure.
392 matches[key] = []
393 continue
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200394
395 # figure out who gets what
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200396 try:
397 solution = solve(all_matches)
398 except NotSolvable:
399 # instead of a cryptic error message, raise an exception that
400 # conveys meaning to the user.
401 raise NoResourceExn('Could not resolve request to reserve resources: '
402 '%d x %s with requirements: %r' % (len(want_list), key, want_list))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200403 picked = [ my_list[i] for i in solution if i is not None ]
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200404 for_origin.dbg('Picked', config.tostr(picked))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200405 matches[key] = picked
406
407 return Resources(matches, do_copy=do_copy)
408
409 def set_hashes(self):
410 for key, item_list in self.items():
411 for item in item_list:
412 item[HASH_KEY] = util.hash_obj(item, HASH_KEY, RESERVED_KEY, USED_KEY)
413
414 def add(self, more):
415 if more is self:
416 raise RuntimeError('adding a list of resources to itself?')
417 config.add(self, copy.deepcopy(more))
418
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200419 def mark_reserved_by(self, origin_id):
420 for key, item_list in self.items():
421 for item in item_list:
422 item[RESERVED_KEY] = origin_id
423
424
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200425class NotSolvable(Exception):
426 pass
427
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200428def solve(all_matches):
429 '''
430 all_matches shall be a list of index-lists.
431 all_matches[i] is the list of indexes that item i can use.
432 Return a solution so that each i gets a different index.
433 solve([ [0, 1, 2],
434 [0],
435 [0, 2] ]) == [1, 0, 2]
436 '''
437
438 def all_differ(l):
439 return len(set(l)) == len(l)
440
441 def search_in_permutations(fixed=[]):
442 idx = len(fixed)
443 for i in range(len(all_matches[idx])):
444 val = all_matches[idx][i]
445 # don't add a val that's already in the list
446 if val in fixed:
447 continue
448 l = list(fixed)
449 l.append(val)
450 if len(l) == len(all_matches):
451 # found a solution
452 return l
453 # not at the end yet, add next digit
454 r = search_in_permutations(l)
455 if r:
456 # nested search_in_permutations() call found a solution
457 return r
458 # this entire branch yielded no solution
459 return None
460
461 if not all_matches:
462 raise RuntimeError('Cannot solve: no candidates')
463
464 solution = search_in_permutations()
465 if not solution:
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200466 raise NotSolvable('The requested resource requirements are not solvable %r'
467 % all_matches)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200468 return solution
469
470
471def contains_hash(list_of_dicts, a_hash):
472 for d in list_of_dicts:
473 if d.get(HASH_KEY) == a_hash:
474 return True
475 return False
476
477def item_matches(item, wanted_item, ignore_keys=None):
478 if is_dict(wanted_item):
479 # match up two dicts
480 if not isinstance(item, dict):
481 return False
482 for key, wanted_val in wanted_item.items():
483 if ignore_keys and key in ignore_keys:
484 continue
485 if not item_matches(item.get(key), wanted_val, ignore_keys=ignore_keys):
486 return False
487 return True
488
489 if is_list(wanted_item):
Pau Espin Pedrol4e36f7c2017-08-28 13:29:28 +0200490 if not is_list(item):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200491 return False
Pau Espin Pedrol58475512017-09-14 15:33:15 +0200492 # Validate that all elements in both lists are of the same type:
493 t = util.list_validate_same_elem_type(wanted_item + item)
494 if t is None:
495 return True # both lists are empty, return
496 # For lists of complex objects, we expect them to be sorted lists:
497 if t in (dict, list, tuple):
498 for i in range(max(len(wanted_item), len(item))):
499 log.ctx(idx=i)
500 subitem = item[i] if i < len(item) else util.empty_instance_type(t)
501 wanted_subitem = wanted_item[i] if i < len(wanted_item) else util.empty_instance_type(t)
502 if not item_matches(subitem, wanted_subitem, ignore_keys=ignore_keys):
503 return False
504 else: # for lists of basic elements, we handle them as unsorted sets:
505 for val in wanted_item:
506 if val not in item:
507 return False
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200508 return True
509
510 return item == wanted_item
511
512
513class ReservedResources(log.Origin):
514 '''
515 After all resources have been figured out, this is the API that a test case
516 gets to interact with resources. From those resources that have been
517 reserved for it, it can pick some to mark them as currently in use.
518 Functions like nitb() provide a resource by automatically picking its
519 dependencies from so far unused (but reserved) resource.
520 '''
521
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200522 def __init__(self, resources_pool, origin, reserved, modifiers):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200523 self.resources_pool = resources_pool
524 self.origin = origin
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200525 self.reserved_original = reserved
526 self.reserved = copy.deepcopy(self.reserved_original)
527 config.overlay(self.reserved, modifiers)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200528
529 def __repr__(self):
530 return 'resources(%s)=%s' % (self.origin.name(), pprint.pformat(self.reserved))
531
532 def get(self, kind, specifics=None):
533 if specifics is None:
534 specifics = {}
535 self.dbg('requesting use of', kind, specifics=specifics)
536 want = { kind: [specifics] }
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200537 available_dict = self.reserved.find(self.origin, want, skip_if_marked=USED_KEY,
538 do_copy=False, raise_if_missing=False,
539 log_label='Using')
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200540 available = available_dict.get(kind)
541 self.dbg(available=len(available))
542 if not available:
Neels Hofmeyrf9e86932017-06-06 19:47:40 +0200543 # cook up a detailed error message for the current situation
544 kind_reserved = self.reserved.get(kind, [])
545 used_count = len([r for r in kind_reserved if USED_KEY in r])
546 matching = self.reserved.find(self.origin, want, raise_if_missing=False, log_label=None).get(kind, [])
547 if not matching:
548 msg = 'none of the reserved resources matches requirements %r' % specifics
549 elif not (used_count < len(kind_reserved)):
550 msg = 'suite.conf reserved only %d x %r.' % (len(kind_reserved), kind)
551 else:
552 msg = ('No unused resource left that matches the requirements;'
553 ' Of reserved %d x %r, %d match the requirements, but all are already in use;'
554 ' Requirements: %r'
555 % (len(kind_reserved), kind, len(matching), specifics))
556 raise NoResourceExn('When trying to use instance nr %d of %r: %s' % (used_count + 1, kind, msg))
557
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200558 pick = available[0]
559 self.dbg(using=pick)
560 assert not pick.get(USED_KEY)
561 pick[USED_KEY] = True
562 return copy.deepcopy(pick)
563
564 def put(self, item):
565 if not item.get(USED_KEY):
566 raise RuntimeError('Can only put() a resource that is used: %r' % item)
567 hash_to_put = item.get(HASH_KEY)
568 if not hash_to_put:
569 raise RuntimeError('Can only put() a resource that has a hash marker: %r' % item)
570 for key, item_list in self.reserved.items():
571 my_list = self.get(key)
572 for my_item in my_list:
573 if hash_to_put == my_item.get(HASH_KEY):
574 my_item.pop(USED_KEY)
575
576 def put_all(self):
Pau Espin Pedrol1dd29552017-06-13 18:07:57 +0200577 if not self.reserved:
578 return
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200579 for key, item_list in self.reserved.items():
Pau Espin Pedrol1dd29552017-06-13 18:07:57 +0200580 for item in item_list:
581 item.pop(USED_KEY, None)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200582
583 def free(self):
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200584 if self.reserved_original:
585 self.resources_pool.free(self.origin, self.reserved_original)
586 self.reserved_original = None
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200587
Neels Hofmeyr2d1d5612017-05-22 20:02:41 +0200588 def counts(self):
589 counts = {}
590 for key in self.reserved.keys():
591 counts[key] = self.count(key)
592 return counts
593
594 def count(self, key):
595 return len(self.reserved.get(key) or [])
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200596
597# vim: expandtab tabstop=4 shiftwidth=4