blob: 1b180764b2401612794fbaa130cc868eb8a295d5 [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,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020079 'arfcn[].arfcn': schema.INT,
80 'arfcn[].band': schema.BAND,
Holger Hans Peter Freyther93a89422019-02-27 06:36:36 +000081 'modem[].type': schema.STR,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020082 'modem[].label': schema.STR,
83 'modem[].path': schema.STR,
84 'modem[].imsi': schema.IMSI,
85 'modem[].ki': schema.KI,
Pau Espin Pedrol713ce2c2017-08-24 16:57:17 +020086 'modem[].auth_algo': schema.AUTH_ALGO,
Pau Espin Pedrol57497a62017-08-28 14:21:15 +020087 'modem[].ciphers[]': schema.CIPHER,
Pau Espin Pedrolac18fd32017-08-31 18:49:47 +020088 'modem[].features[]': schema.MODEM_FEATURE,
Pau Espin Pedrolbc1ed882018-05-17 16:59:58 +020089 'osmocon_phone[].serial_device': schema.STR,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020090 }
91
92WANT_SCHEMA = util.dict_add(
Pau Espin Pedrolead79ac2017-09-12 15:19:18 +020093 dict([('%s[].times' % r, schema.TIMES) for r in R_ALL]),
Neels Hofmeyr3531a192017-03-28 14:30:28 +020094 RESOURCES_SCHEMA)
95
Pau Espin Pedrol0b302792017-09-10 16:33:10 +020096CONF_SCHEMA = util.dict_add(
Pau Espin Pedrol5dc24592018-08-27 12:53:41 +020097 { 'defaults.timeout': schema.STR,
98 'config.bsc.net.codec_list[]': schema.CODEC },
Pau Espin Pedrolaab56922018-08-21 14:58:29 +020099 dict([('resources.%s' % key, val) for key, val in WANT_SCHEMA.items()]),
100 dict([('modifiers.%s' % key, val) for key, val in WANT_SCHEMA.items()]))
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200101
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200102KNOWN_BTS_TYPES = {
Your Name44af3412017-04-13 03:11:59 +0200103 'osmo-bts-sysmo': bts_sysmo.SysmoBts,
104 'osmo-bts-trx': bts_osmotrx.OsmoBtsTrx,
Pau Espin Pedrolfeb66e72019-03-04 18:37:04 +0100105 'osmo-bts-oc2g': bts_oc2g.OsmoBtsOC2G,
Pau Espin Pedroldaed4472017-09-15 14:11:35 +0200106 'osmo-bts-octphy': bts_octphy.OsmoBtsOctphy,
Holger Hans Peter Freytherd2db10d2018-12-06 18:34:53 +0000107 'osmo-bts-virtual': bts_osmovirtual.OsmoBtsVirtual,
Pau Espin Pedrol1b28a582018-03-08 21:01:26 +0100108 'nanobts': bts_nanobts.NanoBts,
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200109 }
110
Holger Hans Peter Freyther74243012019-02-27 08:18:38 +0000111
112KNOWN_MS_TYPES = {
113 # Map None to ofono for forward compability
114 None: modem.Modem,
115 'ofono': modem.Modem,
Holger Hans Peter Freyther10501cc2019-02-25 03:20:16 +0000116 'osmo-mobile': ms_osmo_mobile.MSOsmoMobile,
Holger Hans Peter Freyther74243012019-02-27 08:18:38 +0000117}
118
119
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200120def register_bts_type(name, clazz):
121 KNOWN_BTS_TYPES[name] = clazz
122
123class ResourcesPool(log.Origin):
124 _remember_to_free = None
125 _registered_exit_handler = False
126
127 def __init__(self):
128 self.config_path = config.get_config_file(RESOURCES_CONF)
129 self.state_dir = config.get_state_dir()
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200130 super().__init__(log.C_CNF, conf=self.config_path, state=self.state_dir.path)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200131 self.read_conf()
132
133 def read_conf(self):
134 self.all_resources = Resources(config.read(self.config_path, RESOURCES_SCHEMA))
135 self.all_resources.set_hashes()
136
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200137 def reserve(self, origin, want, modifiers):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200138 '''
139 attempt to reserve the resources specified in the dict 'want' for
140 'origin'. Obtain a lock on the resources lock dir, verify that all
141 wanted resources are available, and if yes mark them as reserved.
142
143 On success, return a reservation object which can be used to release
144 the reservation. The reservation will be freed automatically on program
145 exit, if not yet done manually.
146
147 'origin' should be an Origin() instance.
148
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200149 'want' is a dict matching RESOURCES_SCHEMA, used to specify what to
150 reserve.
151
152 'modifiers' is a dict matching RESOURCES_SCHEMA, it is overlaid on top
153 of 'want'.
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200154
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200155 If an entry has no attribute set, any of the resources may be
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200156 reserved without further limitations.
157
158 ResourcesPool may also be selected with narrowed down constraints.
Neels Hofmeyr76d81032017-05-18 18:35:32 +0200159 This would reserve one IP address, two modems, one BTS of type
Neels Hofmeyr391afe32017-05-18 19:22:12 +0200160 sysmo and one of type trx, plus 2 ARFCNs in the 1800 band:
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200161
162 {
Neels Hofmeyr76d81032017-05-18 18:35:32 +0200163 'ip_address': [ {} ],
Neels Hofmeyr391afe32017-05-18 19:22:12 +0200164 'bts': [ { 'type': 'sysmo' }, { 'type': 'trx' } ],
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200165 'arfcn': [ { 'band': 'GSM-1800' }, { 'band': 'GSM-1800' } ],
166 'modem': [ {}, {} ],
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200167 }
168 '''
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200169 schema.validate(want, RESOURCES_SCHEMA)
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200170 schema.validate(modifiers, RESOURCES_SCHEMA)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200171
172 origin_id = origin.origin_id()
173
174 with self.state_dir.lock(origin_id):
175 rrfile_path = self.state_dir.mk_parentdir(RESERVED_RESOURCES_FILE)
176 reserved = Resources(config.read(rrfile_path, if_missing_return={}))
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200177 to_be_reserved = self.all_resources.without(reserved).find(origin, want)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200178
179 to_be_reserved.mark_reserved_by(origin_id)
180
181 reserved.add(to_be_reserved)
182 config.write(rrfile_path, reserved)
183
184 self.remember_to_free(to_be_reserved)
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200185 return ReservedResources(self, origin, to_be_reserved, modifiers)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200186
187 def free(self, origin, to_be_freed):
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200188 log.ctx(origin)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200189 with self.state_dir.lock(origin.origin_id()):
190 rrfile_path = self.state_dir.mk_parentdir(RESERVED_RESOURCES_FILE)
191 reserved = Resources(config.read(rrfile_path, if_missing_return={}))
192 reserved.drop(to_be_freed)
193 config.write(rrfile_path, reserved)
194 self.forget_freed(to_be_freed)
195
196 def register_exit_handler(self):
197 if self._registered_exit_handler:
198 return
199 atexit.register(self.clean_up_registered_resources)
200 self._registered_exit_handler = True
201
202 def unregister_exit_handler(self):
203 if not self._registered_exit_handler:
204 return
205 atexit.unregister(self.clean_up_registered_resources)
206 self._registered_exit_handler = False
207
208 def clean_up_registered_resources(self):
209 if not self._remember_to_free:
210 return
211 self.free(log.Origin('atexit.clean_up_registered_resources()'),
212 self._remember_to_free)
213
214 def remember_to_free(self, to_be_reserved):
215 self.register_exit_handler()
216 if not self._remember_to_free:
217 self._remember_to_free = Resources()
218 self._remember_to_free.add(to_be_reserved)
219
220 def forget_freed(self, freed):
221 if freed is self._remember_to_free:
222 self._remember_to_free.clear()
223 else:
224 self._remember_to_free.drop(freed)
225 if not self._remember_to_free:
226 self.unregister_exit_handler()
227
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100228 def next_persistent_value(self, token, first_val, validate_func, inc_func, origin):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200229 origin_id = origin.origin_id()
230
231 with self.state_dir.lock(origin_id):
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100232 token_path = self.state_dir.child('last_used_%s.state' % token)
233 log.ctx(token_path)
234 last_value = first_val
235 if os.path.exists(token_path):
236 if not os.path.isfile(token_path):
237 raise RuntimeError('path should be a file but is not: %r' % token_path)
238 with open(token_path, 'r') as f:
239 last_value = f.read().strip()
240 validate_func(last_value)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200241
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100242 next_value = inc_func(last_value)
243 with open(token_path, 'w') as f:
244 f.write(next_value)
245 return next_value
246
247 def next_msisdn(self, origin):
248 return self.next_persistent_value('msisdn', '1000', schema.msisdn, util.msisdn_inc, origin)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200249
Pau Espin Pedrol5e0c2512017-11-06 18:40:23 +0100250 def next_lac(self, origin):
Pau Espin Pedrolf7f06362017-11-28 12:56:35 +0100251 # LAC=0 has special meaning (MS detached), avoid it
252 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 +0200253
Pau Espin Pedrol8a3a7b52017-11-28 15:50:02 +0100254 def next_rac(self, origin):
255 return self.next_persistent_value('rac', '1', schema.uint8, lambda x: str((int(x)+1) % pow(2,8) or 1), origin)
256
Pau Espin Pedrol4ccce7c2017-11-07 11:13:20 +0100257 def next_cellid(self, origin):
258 return self.next_persistent_value('cellid', '1', schema.uint16, lambda x: str((int(x)+1) % pow(2,16)), origin)
259
Pau Espin Pedrol8a3a7b52017-11-28 15:50:02 +0100260 def next_bvci(self, origin):
261 # BVCI=0 and =1 are reserved, avoid them.
262 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)
263
Pau Espin Pedrol4676cbd2017-09-14 17:35:03 +0200264class NoResourceExn(log.Error):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200265 pass
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200266
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200267class Resources(dict):
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200268
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200269 def __init__(self, all_resources={}, do_copy=True):
270 if do_copy:
271 all_resources = copy.deepcopy(all_resources)
272 self.update(all_resources)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200273
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200274 def drop(self, reserved, fail_if_not_found=True):
275 # protect from modifying reserved because we're the same object
276 if reserved is self:
277 raise RuntimeError('Refusing to drop a list of resources from itself.'
278 ' This is probably a bug where a list of Resources()'
279 ' should have been copied but is passed as-is.'
280 ' use Resources.clear() instead.')
281
282 for key, reserved_list in reserved.items():
283 my_list = self.get(key) or []
284
285 if my_list is reserved_list:
286 self.pop(key)
287 continue
288
289 for reserved_item in reserved_list:
290 found = False
291 reserved_hash = reserved_item.get(HASH_KEY)
292 if not reserved_hash:
293 raise RuntimeError('Resources.drop() only works with hashed items')
294
295 for i in range(len(my_list)):
296 my_item = my_list[i]
297 my_hash = my_item.get(HASH_KEY)
298 if not my_hash:
299 raise RuntimeError('Resources.drop() only works with hashed items')
300 if my_hash == reserved_hash:
301 found = True
302 my_list.pop(i)
303 break
304
305 if fail_if_not_found and not found:
306 raise RuntimeError('Asked to drop resource from a pool, but the'
307 ' resource was not found: %s = %r' % (key, reserved_item))
308
309 if not my_list:
310 self.pop(key)
311 return self
312
313 def without(self, reserved):
314 return Resources(self).drop(reserved)
315
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200316 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 +0200317 '''
318 Pass a dict of resource requirements, e.g.:
319 want = {
320 'bts': [ {'type': 'osmo-bts-sysmo',}, {} ],
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200321 'modem': [ {}, {}, {} ]
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200322 }
323 This function tries to find a combination from the available resources that
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200324 matches these requirements. The return value is a dict (wrapped in a Resources class)
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200325 that contains the matching resources in the order of 'want' dict: in above
326 example, the returned dict would have a 'bts' list with the first item being
327 a sysmoBTS, the second item being any other available BTS.
328
329 If skip_if_marked is passed, any resource that contains this key is skipped.
330 E.g. if a BTS has the USED_KEY set like
331 reserved_resources = { 'bts' : {..., '_used': True} }
332 then this may be skipped by passing skip_if_marked='_used'
333 (or rather skip_if_marked=USED_KEY).
334
335 If do_copy is True, the returned dict is a deep copy and does not share
336 lists with any other Resources dict.
337
338 If raise_if_missing is False, this will return an empty item for any
339 resource that had no match, instead of immediately raising an exception.
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200340
341 This function expects input dictionaries whose contents have already
342 been replicated based on its the 'times' attributes. See
343 config.replicate_times() for more details.
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200344 '''
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200345 matches = {}
Neels Hofmeyr17c139e2017-04-12 02:42:02 +0200346 for key, want_list in sorted(want.items()): # sorted for deterministic test results
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200347 # here we have a resource of a given type, e.g. 'bts', with a list
348 # containing as many BTSes as the caller wants to reserve/use. Each
349 # list item contains specifics for the particular BTS.
Neels Hofmeyr2a1a1fa2017-05-29 01:36:21 +0200350 my_list = self.get(key, [])
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200351
Neels Hofmeyrf9e86932017-06-06 19:47:40 +0200352 if log_label:
353 for_origin.log(log_label, len(want_list), 'x', key, '(candidates: %d)'%len(my_list))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200354
355 # Try to avoid a less constrained item snatching away a resource
356 # from a more detailed constrained requirement.
357
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200358 # first record all matches, so that each requested item has a list
359 # of all available resources that match it. Some resources may
360 # appear for multiple requested items. Store matching indexes.
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200361 all_matches = []
362 for want_item in want_list:
363 item_match_list = []
364 for i in range(len(my_list)):
365 my_item = my_list[i]
366 if skip_if_marked and my_item.get(skip_if_marked):
367 continue
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200368 if item_matches(my_item, want_item):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200369 item_match_list.append(i)
370 if not item_match_list:
Neels Hofmeyr9b907702017-05-06 23:20:33 +0200371 if raise_if_missing:
372 raise NoResourceExn('No matching resource available for %s = %r'
373 % (key, want_item))
374 else:
375 # this one failed... see below
376 all_matches = []
377 break
378
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200379 all_matches.append( item_match_list )
380
381 if not all_matches:
Neels Hofmeyr9b907702017-05-06 23:20:33 +0200382 # ...this one failed. Makes no sense to solve resource
383 # allocations, return an empty list for this key to mark
384 # failure.
385 matches[key] = []
386 continue
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200387
388 # figure out who gets what
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200389 try:
390 solution = solve(all_matches)
391 except NotSolvable:
392 # instead of a cryptic error message, raise an exception that
393 # conveys meaning to the user.
394 raise NoResourceExn('Could not resolve request to reserve resources: '
395 '%d x %s with requirements: %r' % (len(want_list), key, want_list))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200396 picked = [ my_list[i] for i in solution if i is not None ]
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200397 for_origin.dbg('Picked', config.tostr(picked))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200398 matches[key] = picked
399
400 return Resources(matches, do_copy=do_copy)
401
402 def set_hashes(self):
403 for key, item_list in self.items():
404 for item in item_list:
405 item[HASH_KEY] = util.hash_obj(item, HASH_KEY, RESERVED_KEY, USED_KEY)
406
407 def add(self, more):
408 if more is self:
409 raise RuntimeError('adding a list of resources to itself?')
410 config.add(self, copy.deepcopy(more))
411
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200412 def mark_reserved_by(self, origin_id):
413 for key, item_list in self.items():
414 for item in item_list:
415 item[RESERVED_KEY] = origin_id
416
417
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200418class NotSolvable(Exception):
419 pass
420
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200421def solve(all_matches):
422 '''
423 all_matches shall be a list of index-lists.
424 all_matches[i] is the list of indexes that item i can use.
425 Return a solution so that each i gets a different index.
426 solve([ [0, 1, 2],
427 [0],
428 [0, 2] ]) == [1, 0, 2]
429 '''
430
431 def all_differ(l):
432 return len(set(l)) == len(l)
433
434 def search_in_permutations(fixed=[]):
435 idx = len(fixed)
436 for i in range(len(all_matches[idx])):
437 val = all_matches[idx][i]
438 # don't add a val that's already in the list
439 if val in fixed:
440 continue
441 l = list(fixed)
442 l.append(val)
443 if len(l) == len(all_matches):
444 # found a solution
445 return l
446 # not at the end yet, add next digit
447 r = search_in_permutations(l)
448 if r:
449 # nested search_in_permutations() call found a solution
450 return r
451 # this entire branch yielded no solution
452 return None
453
454 if not all_matches:
455 raise RuntimeError('Cannot solve: no candidates')
456
457 solution = search_in_permutations()
458 if not solution:
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200459 raise NotSolvable('The requested resource requirements are not solvable %r'
460 % all_matches)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200461 return solution
462
463
464def contains_hash(list_of_dicts, a_hash):
465 for d in list_of_dicts:
466 if d.get(HASH_KEY) == a_hash:
467 return True
468 return False
469
470def item_matches(item, wanted_item, ignore_keys=None):
471 if is_dict(wanted_item):
472 # match up two dicts
473 if not isinstance(item, dict):
474 return False
475 for key, wanted_val in wanted_item.items():
476 if ignore_keys and key in ignore_keys:
477 continue
478 if not item_matches(item.get(key), wanted_val, ignore_keys=ignore_keys):
479 return False
480 return True
481
482 if is_list(wanted_item):
Pau Espin Pedrol4e36f7c2017-08-28 13:29:28 +0200483 if not is_list(item):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200484 return False
Pau Espin Pedrol58475512017-09-14 15:33:15 +0200485 # Validate that all elements in both lists are of the same type:
486 t = util.list_validate_same_elem_type(wanted_item + item)
487 if t is None:
488 return True # both lists are empty, return
489 # For lists of complex objects, we expect them to be sorted lists:
490 if t in (dict, list, tuple):
491 for i in range(max(len(wanted_item), len(item))):
492 log.ctx(idx=i)
493 subitem = item[i] if i < len(item) else util.empty_instance_type(t)
494 wanted_subitem = wanted_item[i] if i < len(wanted_item) else util.empty_instance_type(t)
495 if not item_matches(subitem, wanted_subitem, ignore_keys=ignore_keys):
496 return False
497 else: # for lists of basic elements, we handle them as unsorted sets:
498 for val in wanted_item:
499 if val not in item:
500 return False
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200501 return True
502
503 return item == wanted_item
504
505
506class ReservedResources(log.Origin):
507 '''
508 After all resources have been figured out, this is the API that a test case
509 gets to interact with resources. From those resources that have been
510 reserved for it, it can pick some to mark them as currently in use.
511 Functions like nitb() provide a resource by automatically picking its
512 dependencies from so far unused (but reserved) resource.
513 '''
514
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200515 def __init__(self, resources_pool, origin, reserved, modifiers):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200516 self.resources_pool = resources_pool
517 self.origin = origin
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200518 self.reserved_original = reserved
519 self.reserved = copy.deepcopy(self.reserved_original)
520 config.overlay(self.reserved, modifiers)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200521
522 def __repr__(self):
523 return 'resources(%s)=%s' % (self.origin.name(), pprint.pformat(self.reserved))
524
525 def get(self, kind, specifics=None):
526 if specifics is None:
527 specifics = {}
528 self.dbg('requesting use of', kind, specifics=specifics)
529 want = { kind: [specifics] }
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200530 available_dict = self.reserved.find(self.origin, want, skip_if_marked=USED_KEY,
531 do_copy=False, raise_if_missing=False,
532 log_label='Using')
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200533 available = available_dict.get(kind)
534 self.dbg(available=len(available))
535 if not available:
Neels Hofmeyrf9e86932017-06-06 19:47:40 +0200536 # cook up a detailed error message for the current situation
537 kind_reserved = self.reserved.get(kind, [])
538 used_count = len([r for r in kind_reserved if USED_KEY in r])
539 matching = self.reserved.find(self.origin, want, raise_if_missing=False, log_label=None).get(kind, [])
540 if not matching:
541 msg = 'none of the reserved resources matches requirements %r' % specifics
542 elif not (used_count < len(kind_reserved)):
543 msg = 'suite.conf reserved only %d x %r.' % (len(kind_reserved), kind)
544 else:
545 msg = ('No unused resource left that matches the requirements;'
546 ' Of reserved %d x %r, %d match the requirements, but all are already in use;'
547 ' Requirements: %r'
548 % (len(kind_reserved), kind, len(matching), specifics))
549 raise NoResourceExn('When trying to use instance nr %d of %r: %s' % (used_count + 1, kind, msg))
550
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200551 pick = available[0]
552 self.dbg(using=pick)
553 assert not pick.get(USED_KEY)
554 pick[USED_KEY] = True
555 return copy.deepcopy(pick)
556
557 def put(self, item):
558 if not item.get(USED_KEY):
559 raise RuntimeError('Can only put() a resource that is used: %r' % item)
560 hash_to_put = item.get(HASH_KEY)
561 if not hash_to_put:
562 raise RuntimeError('Can only put() a resource that has a hash marker: %r' % item)
563 for key, item_list in self.reserved.items():
564 my_list = self.get(key)
565 for my_item in my_list:
566 if hash_to_put == my_item.get(HASH_KEY):
567 my_item.pop(USED_KEY)
568
569 def put_all(self):
Pau Espin Pedrol1dd29552017-06-13 18:07:57 +0200570 if not self.reserved:
571 return
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200572 for key, item_list in self.reserved.items():
Pau Espin Pedrol1dd29552017-06-13 18:07:57 +0200573 for item in item_list:
574 item.pop(USED_KEY, None)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200575
576 def free(self):
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200577 if self.reserved_original:
578 self.resources_pool.free(self.origin, self.reserved_original)
579 self.reserved_original = None
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200580
Neels Hofmeyr2d1d5612017-05-22 20:02:41 +0200581 def counts(self):
582 counts = {}
583 for key in self.reserved.keys():
584 counts[key] = self.count(key)
585 return counts
586
587 def count(self, key):
588 return len(self.reserved.get(key) or [])
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200589
590# vim: expandtab tabstop=4 shiftwidth=4