blob: 2e7f5007f40c53885c1563177ce4d44e8e426a62 [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
Holger Hans Peter Freytherd2db10d2018-12-06 18:34:53 +000029from . import bts_sysmo, bts_osmotrx, bts_osmovirtual, bts_octphy, bts_nanobts
Holger Hans Peter Freyther74243012019-02-27 08:18:38 +000030from . import modem
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +020031
Neels Hofmeyr3531a192017-03-28 14:30:28 +020032from .util import is_dict, is_list
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +020033
Neels Hofmeyr3531a192017-03-28 14:30:28 +020034HASH_KEY = '_hash'
35RESERVED_KEY = '_reserved_by'
36USED_KEY = '_used'
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +020037
Neels Hofmeyr3531a192017-03-28 14:30:28 +020038RESOURCES_CONF = 'resources.conf'
Neels Hofmeyr3531a192017-03-28 14:30:28 +020039RESERVED_RESOURCES_FILE = 'reserved_resources.state'
40
Neels Hofmeyr76d81032017-05-18 18:35:32 +020041R_IP_ADDRESS = 'ip_address'
Neels Hofmeyr3531a192017-03-28 14:30:28 +020042R_BTS = 'bts'
43R_ARFCN = 'arfcn'
44R_MODEM = 'modem'
Pau Espin Pedrolbc1ed882018-05-17 16:59:58 +020045R_OSMOCON = 'osmocon_phone'
46R_ALL = (R_IP_ADDRESS, R_BTS, R_ARFCN, R_MODEM, R_OSMOCON)
Neels Hofmeyr3531a192017-03-28 14:30:28 +020047
48RESOURCES_SCHEMA = {
Neels Hofmeyr76d81032017-05-18 18:35:32 +020049 'ip_address[].addr': schema.IPV4,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020050 'bts[].label': schema.STR,
51 'bts[].type': schema.STR,
Pau Espin Pedrolfa9a6d32017-09-12 15:13:21 +020052 'bts[].ipa_unit_id': schema.UINT,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020053 'bts[].addr': schema.IPV4,
54 'bts[].band': schema.BAND,
Pau Espin Pedrolce35d912017-11-23 11:01:24 +010055 'bts[].direct_pcu': schema.BOOL_STR,
Pau Espin Pedrol57497a62017-08-28 14:21:15 +020056 'bts[].ciphers[]': schema.CIPHER,
Pau Espin Pedrol722e94e2018-08-22 11:01:32 +020057 'bts[].channel_allocator': schema.CHAN_ALLOCATOR,
Pau Espin Pedrol4f23ab52018-10-29 11:30:00 +010058 'bts[].gprs_mode': schema.GPRS_MODE,
Pau Espin Pedrol39df7f42018-05-07 13:49:33 +020059 'bts[].num_trx': schema.UINT,
60 'bts[].max_trx': schema.UINT,
Pau Espin Pedrolf6a07122018-07-27 16:24:29 +020061 'bts[].trx_list[].addr': schema.IPV4,
Your Name44af3412017-04-13 03:11:59 +020062 'bts[].trx_list[].hw_addr': schema.HWADDR,
63 'bts[].trx_list[].net_device': schema.STR,
Pau Espin Pedrolb26f32a2017-09-14 13:52:28 +020064 'bts[].trx_list[].nominal_power': schema.UINT,
65 'bts[].trx_list[].max_power_red': schema.UINT,
Pau Espin Pedrolc9b63762018-05-07 01:57:01 +020066 'bts[].trx_list[].timeslot_list[].phys_chan_config': schema.PHY_CHAN,
Pau Espin Pedrolf6a07122018-07-27 16:24:29 +020067 'bts[].trx_list[].power_supply.type': schema.STR,
68 'bts[].trx_list[].power_supply.device': schema.STR,
69 'bts[].trx_list[].power_supply.port': schema.STR,
Pau Espin Pedrol0d455042018-08-27 17:07:41 +020070 'bts[].osmo_trx.launch_trx': schema.BOOL_STR,
71 'bts[].osmo_trx.type': schema.STR,
72 'bts[].osmo_trx.clock_reference': schema.OSMO_TRX_CLOCK_REF,
73 'bts[].osmo_trx.trx_ip': schema.IPV4,
Pau Espin Pedrola9006df2018-10-01 12:26:39 +020074 'bts[].osmo_trx.remote_user': schema.STR,
Pau Espin Pedrol8cfa10f2018-11-06 12:05:19 +010075 'bts[].osmo_trx.dev_args': schema.STR,
Pau Espin Pedrol94eab262018-09-17 20:25:55 +020076 'bts[].osmo_trx.multi_arfcn': schema.BOOL_STR,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020077 'arfcn[].arfcn': schema.INT,
78 'arfcn[].band': schema.BAND,
Holger Hans Peter Freyther93a89422019-02-27 06:36:36 +000079 'modem[].type': schema.STR,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020080 'modem[].label': schema.STR,
81 'modem[].path': schema.STR,
82 'modem[].imsi': schema.IMSI,
83 'modem[].ki': schema.KI,
Pau Espin Pedrol713ce2c2017-08-24 16:57:17 +020084 'modem[].auth_algo': schema.AUTH_ALGO,
Pau Espin Pedrol57497a62017-08-28 14:21:15 +020085 'modem[].ciphers[]': schema.CIPHER,
Pau Espin Pedrolac18fd32017-08-31 18:49:47 +020086 'modem[].features[]': schema.MODEM_FEATURE,
Pau Espin Pedrolbc1ed882018-05-17 16:59:58 +020087 'osmocon_phone[].serial_device': schema.STR,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020088 }
89
90WANT_SCHEMA = util.dict_add(
Pau Espin Pedrolead79ac2017-09-12 15:19:18 +020091 dict([('%s[].times' % r, schema.TIMES) for r in R_ALL]),
Neels Hofmeyr3531a192017-03-28 14:30:28 +020092 RESOURCES_SCHEMA)
93
Pau Espin Pedrol0b302792017-09-10 16:33:10 +020094CONF_SCHEMA = util.dict_add(
Pau Espin Pedrol5dc24592018-08-27 12:53:41 +020095 { 'defaults.timeout': schema.STR,
96 'config.bsc.net.codec_list[]': schema.CODEC },
Pau Espin Pedrolaab56922018-08-21 14:58:29 +020097 dict([('resources.%s' % key, val) for key, val in WANT_SCHEMA.items()]),
98 dict([('modifiers.%s' % key, val) for key, val in WANT_SCHEMA.items()]))
Pau Espin Pedrol0b302792017-09-10 16:33:10 +020099
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200100KNOWN_BTS_TYPES = {
Your Name44af3412017-04-13 03:11:59 +0200101 'osmo-bts-sysmo': bts_sysmo.SysmoBts,
102 'osmo-bts-trx': bts_osmotrx.OsmoBtsTrx,
Pau Espin Pedroldaed4472017-09-15 14:11:35 +0200103 'osmo-bts-octphy': bts_octphy.OsmoBtsOctphy,
Holger Hans Peter Freytherd2db10d2018-12-06 18:34:53 +0000104 'osmo-bts-virtual': bts_osmovirtual.OsmoBtsVirtual,
Pau Espin Pedrol1b28a582018-03-08 21:01:26 +0100105 'nanobts': bts_nanobts.NanoBts,
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200106 }
107
Holger Hans Peter Freyther74243012019-02-27 08:18:38 +0000108
109KNOWN_MS_TYPES = {
110 # Map None to ofono for forward compability
111 None: modem.Modem,
112 'ofono': modem.Modem,
113}
114
115
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200116def register_bts_type(name, clazz):
117 KNOWN_BTS_TYPES[name] = clazz
118
119class ResourcesPool(log.Origin):
120 _remember_to_free = None
121 _registered_exit_handler = False
122
123 def __init__(self):
124 self.config_path = config.get_config_file(RESOURCES_CONF)
125 self.state_dir = config.get_state_dir()
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200126 super().__init__(log.C_CNF, conf=self.config_path, state=self.state_dir.path)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200127 self.read_conf()
128
129 def read_conf(self):
130 self.all_resources = Resources(config.read(self.config_path, RESOURCES_SCHEMA))
131 self.all_resources.set_hashes()
132
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200133 def reserve(self, origin, want, modifiers):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200134 '''
135 attempt to reserve the resources specified in the dict 'want' for
136 'origin'. Obtain a lock on the resources lock dir, verify that all
137 wanted resources are available, and if yes mark them as reserved.
138
139 On success, return a reservation object which can be used to release
140 the reservation. The reservation will be freed automatically on program
141 exit, if not yet done manually.
142
143 'origin' should be an Origin() instance.
144
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200145 'want' is a dict matching RESOURCES_SCHEMA, used to specify what to
146 reserve.
147
148 'modifiers' is a dict matching RESOURCES_SCHEMA, it is overlaid on top
149 of 'want'.
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200150
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200151 If an entry has no attribute set, any of the resources may be
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200152 reserved without further limitations.
153
154 ResourcesPool may also be selected with narrowed down constraints.
Neels Hofmeyr76d81032017-05-18 18:35:32 +0200155 This would reserve one IP address, two modems, one BTS of type
Neels Hofmeyr391afe32017-05-18 19:22:12 +0200156 sysmo and one of type trx, plus 2 ARFCNs in the 1800 band:
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200157
158 {
Neels Hofmeyr76d81032017-05-18 18:35:32 +0200159 'ip_address': [ {} ],
Neels Hofmeyr391afe32017-05-18 19:22:12 +0200160 'bts': [ { 'type': 'sysmo' }, { 'type': 'trx' } ],
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200161 'arfcn': [ { 'band': 'GSM-1800' }, { 'band': 'GSM-1800' } ],
162 'modem': [ {}, {} ],
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200163 }
164 '''
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200165 schema.validate(want, RESOURCES_SCHEMA)
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200166 schema.validate(modifiers, RESOURCES_SCHEMA)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200167
168 origin_id = origin.origin_id()
169
170 with self.state_dir.lock(origin_id):
171 rrfile_path = self.state_dir.mk_parentdir(RESERVED_RESOURCES_FILE)
172 reserved = Resources(config.read(rrfile_path, if_missing_return={}))
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200173 to_be_reserved = self.all_resources.without(reserved).find(origin, want)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200174
175 to_be_reserved.mark_reserved_by(origin_id)
176
177 reserved.add(to_be_reserved)
178 config.write(rrfile_path, reserved)
179
180 self.remember_to_free(to_be_reserved)
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200181 return ReservedResources(self, origin, to_be_reserved, modifiers)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200182
183 def free(self, origin, to_be_freed):
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200184 log.ctx(origin)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200185 with self.state_dir.lock(origin.origin_id()):
186 rrfile_path = self.state_dir.mk_parentdir(RESERVED_RESOURCES_FILE)
187 reserved = Resources(config.read(rrfile_path, if_missing_return={}))
188 reserved.drop(to_be_freed)
189 config.write(rrfile_path, reserved)
190 self.forget_freed(to_be_freed)
191
192 def register_exit_handler(self):
193 if self._registered_exit_handler:
194 return
195 atexit.register(self.clean_up_registered_resources)
196 self._registered_exit_handler = True
197
198 def unregister_exit_handler(self):
199 if not self._registered_exit_handler:
200 return
201 atexit.unregister(self.clean_up_registered_resources)
202 self._registered_exit_handler = False
203
204 def clean_up_registered_resources(self):
205 if not self._remember_to_free:
206 return
207 self.free(log.Origin('atexit.clean_up_registered_resources()'),
208 self._remember_to_free)
209
210 def remember_to_free(self, to_be_reserved):
211 self.register_exit_handler()
212 if not self._remember_to_free:
213 self._remember_to_free = Resources()
214 self._remember_to_free.add(to_be_reserved)
215
216 def forget_freed(self, freed):
217 if freed is self._remember_to_free:
218 self._remember_to_free.clear()
219 else:
220 self._remember_to_free.drop(freed)
221 if not self._remember_to_free:
222 self.unregister_exit_handler()
223
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100224 def next_persistent_value(self, token, first_val, validate_func, inc_func, origin):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200225 origin_id = origin.origin_id()
226
227 with self.state_dir.lock(origin_id):
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100228 token_path = self.state_dir.child('last_used_%s.state' % token)
229 log.ctx(token_path)
230 last_value = first_val
231 if os.path.exists(token_path):
232 if not os.path.isfile(token_path):
233 raise RuntimeError('path should be a file but is not: %r' % token_path)
234 with open(token_path, 'r') as f:
235 last_value = f.read().strip()
236 validate_func(last_value)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200237
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100238 next_value = inc_func(last_value)
239 with open(token_path, 'w') as f:
240 f.write(next_value)
241 return next_value
242
243 def next_msisdn(self, origin):
244 return self.next_persistent_value('msisdn', '1000', schema.msisdn, util.msisdn_inc, origin)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200245
Pau Espin Pedrol5e0c2512017-11-06 18:40:23 +0100246 def next_lac(self, origin):
Pau Espin Pedrolf7f06362017-11-28 12:56:35 +0100247 # LAC=0 has special meaning (MS detached), avoid it
248 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 +0200249
Pau Espin Pedrol8a3a7b52017-11-28 15:50:02 +0100250 def next_rac(self, origin):
251 return self.next_persistent_value('rac', '1', schema.uint8, lambda x: str((int(x)+1) % pow(2,8) or 1), origin)
252
Pau Espin Pedrol4ccce7c2017-11-07 11:13:20 +0100253 def next_cellid(self, origin):
254 return self.next_persistent_value('cellid', '1', schema.uint16, lambda x: str((int(x)+1) % pow(2,16)), origin)
255
Pau Espin Pedrol8a3a7b52017-11-28 15:50:02 +0100256 def next_bvci(self, origin):
257 # BVCI=0 and =1 are reserved, avoid them.
258 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)
259
Pau Espin Pedrol4676cbd2017-09-14 17:35:03 +0200260class NoResourceExn(log.Error):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200261 pass
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200262
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200263class Resources(dict):
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200264
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200265 def __init__(self, all_resources={}, do_copy=True):
266 if do_copy:
267 all_resources = copy.deepcopy(all_resources)
268 self.update(all_resources)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200269
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200270 def drop(self, reserved, fail_if_not_found=True):
271 # protect from modifying reserved because we're the same object
272 if reserved is self:
273 raise RuntimeError('Refusing to drop a list of resources from itself.'
274 ' This is probably a bug where a list of Resources()'
275 ' should have been copied but is passed as-is.'
276 ' use Resources.clear() instead.')
277
278 for key, reserved_list in reserved.items():
279 my_list = self.get(key) or []
280
281 if my_list is reserved_list:
282 self.pop(key)
283 continue
284
285 for reserved_item in reserved_list:
286 found = False
287 reserved_hash = reserved_item.get(HASH_KEY)
288 if not reserved_hash:
289 raise RuntimeError('Resources.drop() only works with hashed items')
290
291 for i in range(len(my_list)):
292 my_item = my_list[i]
293 my_hash = my_item.get(HASH_KEY)
294 if not my_hash:
295 raise RuntimeError('Resources.drop() only works with hashed items')
296 if my_hash == reserved_hash:
297 found = True
298 my_list.pop(i)
299 break
300
301 if fail_if_not_found and not found:
302 raise RuntimeError('Asked to drop resource from a pool, but the'
303 ' resource was not found: %s = %r' % (key, reserved_item))
304
305 if not my_list:
306 self.pop(key)
307 return self
308
309 def without(self, reserved):
310 return Resources(self).drop(reserved)
311
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200312 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 +0200313 '''
314 Pass a dict of resource requirements, e.g.:
315 want = {
316 'bts': [ {'type': 'osmo-bts-sysmo',}, {} ],
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200317 'modem': [ {}, {}, {} ]
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200318 }
319 This function tries to find a combination from the available resources that
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200320 matches these requirements. The return value is a dict (wrapped in a Resources class)
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200321 that contains the matching resources in the order of 'want' dict: in above
322 example, the returned dict would have a 'bts' list with the first item being
323 a sysmoBTS, the second item being any other available BTS.
324
325 If skip_if_marked is passed, any resource that contains this key is skipped.
326 E.g. if a BTS has the USED_KEY set like
327 reserved_resources = { 'bts' : {..., '_used': True} }
328 then this may be skipped by passing skip_if_marked='_used'
329 (or rather skip_if_marked=USED_KEY).
330
331 If do_copy is True, the returned dict is a deep copy and does not share
332 lists with any other Resources dict.
333
334 If raise_if_missing is False, this will return an empty item for any
335 resource that had no match, instead of immediately raising an exception.
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200336
337 This function expects input dictionaries whose contents have already
338 been replicated based on its the 'times' attributes. See
339 config.replicate_times() for more details.
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200340 '''
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200341 matches = {}
Neels Hofmeyr17c139e2017-04-12 02:42:02 +0200342 for key, want_list in sorted(want.items()): # sorted for deterministic test results
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200343 # here we have a resource of a given type, e.g. 'bts', with a list
344 # containing as many BTSes as the caller wants to reserve/use. Each
345 # list item contains specifics for the particular BTS.
Neels Hofmeyr2a1a1fa2017-05-29 01:36:21 +0200346 my_list = self.get(key, [])
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200347
Neels Hofmeyrf9e86932017-06-06 19:47:40 +0200348 if log_label:
349 for_origin.log(log_label, len(want_list), 'x', key, '(candidates: %d)'%len(my_list))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200350
351 # Try to avoid a less constrained item snatching away a resource
352 # from a more detailed constrained requirement.
353
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200354 # first record all matches, so that each requested item has a list
355 # of all available resources that match it. Some resources may
356 # appear for multiple requested items. Store matching indexes.
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200357 all_matches = []
358 for want_item in want_list:
359 item_match_list = []
360 for i in range(len(my_list)):
361 my_item = my_list[i]
362 if skip_if_marked and my_item.get(skip_if_marked):
363 continue
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200364 if item_matches(my_item, want_item):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200365 item_match_list.append(i)
366 if not item_match_list:
Neels Hofmeyr9b907702017-05-06 23:20:33 +0200367 if raise_if_missing:
368 raise NoResourceExn('No matching resource available for %s = %r'
369 % (key, want_item))
370 else:
371 # this one failed... see below
372 all_matches = []
373 break
374
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200375 all_matches.append( item_match_list )
376
377 if not all_matches:
Neels Hofmeyr9b907702017-05-06 23:20:33 +0200378 # ...this one failed. Makes no sense to solve resource
379 # allocations, return an empty list for this key to mark
380 # failure.
381 matches[key] = []
382 continue
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200383
384 # figure out who gets what
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200385 try:
386 solution = solve(all_matches)
387 except NotSolvable:
388 # instead of a cryptic error message, raise an exception that
389 # conveys meaning to the user.
390 raise NoResourceExn('Could not resolve request to reserve resources: '
391 '%d x %s with requirements: %r' % (len(want_list), key, want_list))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200392 picked = [ my_list[i] for i in solution if i is not None ]
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200393 for_origin.dbg('Picked', config.tostr(picked))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200394 matches[key] = picked
395
396 return Resources(matches, do_copy=do_copy)
397
398 def set_hashes(self):
399 for key, item_list in self.items():
400 for item in item_list:
401 item[HASH_KEY] = util.hash_obj(item, HASH_KEY, RESERVED_KEY, USED_KEY)
402
403 def add(self, more):
404 if more is self:
405 raise RuntimeError('adding a list of resources to itself?')
406 config.add(self, copy.deepcopy(more))
407
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200408 def mark_reserved_by(self, origin_id):
409 for key, item_list in self.items():
410 for item in item_list:
411 item[RESERVED_KEY] = origin_id
412
413
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200414class NotSolvable(Exception):
415 pass
416
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200417def solve(all_matches):
418 '''
419 all_matches shall be a list of index-lists.
420 all_matches[i] is the list of indexes that item i can use.
421 Return a solution so that each i gets a different index.
422 solve([ [0, 1, 2],
423 [0],
424 [0, 2] ]) == [1, 0, 2]
425 '''
426
427 def all_differ(l):
428 return len(set(l)) == len(l)
429
430 def search_in_permutations(fixed=[]):
431 idx = len(fixed)
432 for i in range(len(all_matches[idx])):
433 val = all_matches[idx][i]
434 # don't add a val that's already in the list
435 if val in fixed:
436 continue
437 l = list(fixed)
438 l.append(val)
439 if len(l) == len(all_matches):
440 # found a solution
441 return l
442 # not at the end yet, add next digit
443 r = search_in_permutations(l)
444 if r:
445 # nested search_in_permutations() call found a solution
446 return r
447 # this entire branch yielded no solution
448 return None
449
450 if not all_matches:
451 raise RuntimeError('Cannot solve: no candidates')
452
453 solution = search_in_permutations()
454 if not solution:
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200455 raise NotSolvable('The requested resource requirements are not solvable %r'
456 % all_matches)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200457 return solution
458
459
460def contains_hash(list_of_dicts, a_hash):
461 for d in list_of_dicts:
462 if d.get(HASH_KEY) == a_hash:
463 return True
464 return False
465
466def item_matches(item, wanted_item, ignore_keys=None):
467 if is_dict(wanted_item):
468 # match up two dicts
469 if not isinstance(item, dict):
470 return False
471 for key, wanted_val in wanted_item.items():
472 if ignore_keys and key in ignore_keys:
473 continue
474 if not item_matches(item.get(key), wanted_val, ignore_keys=ignore_keys):
475 return False
476 return True
477
478 if is_list(wanted_item):
Pau Espin Pedrol4e36f7c2017-08-28 13:29:28 +0200479 if not is_list(item):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200480 return False
Pau Espin Pedrol58475512017-09-14 15:33:15 +0200481 # Validate that all elements in both lists are of the same type:
482 t = util.list_validate_same_elem_type(wanted_item + item)
483 if t is None:
484 return True # both lists are empty, return
485 # For lists of complex objects, we expect them to be sorted lists:
486 if t in (dict, list, tuple):
487 for i in range(max(len(wanted_item), len(item))):
488 log.ctx(idx=i)
489 subitem = item[i] if i < len(item) else util.empty_instance_type(t)
490 wanted_subitem = wanted_item[i] if i < len(wanted_item) else util.empty_instance_type(t)
491 if not item_matches(subitem, wanted_subitem, ignore_keys=ignore_keys):
492 return False
493 else: # for lists of basic elements, we handle them as unsorted sets:
494 for val in wanted_item:
495 if val not in item:
496 return False
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200497 return True
498
499 return item == wanted_item
500
501
502class ReservedResources(log.Origin):
503 '''
504 After all resources have been figured out, this is the API that a test case
505 gets to interact with resources. From those resources that have been
506 reserved for it, it can pick some to mark them as currently in use.
507 Functions like nitb() provide a resource by automatically picking its
508 dependencies from so far unused (but reserved) resource.
509 '''
510
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200511 def __init__(self, resources_pool, origin, reserved, modifiers):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200512 self.resources_pool = resources_pool
513 self.origin = origin
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200514 self.reserved_original = reserved
515 self.reserved = copy.deepcopy(self.reserved_original)
516 config.overlay(self.reserved, modifiers)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200517
518 def __repr__(self):
519 return 'resources(%s)=%s' % (self.origin.name(), pprint.pformat(self.reserved))
520
521 def get(self, kind, specifics=None):
522 if specifics is None:
523 specifics = {}
524 self.dbg('requesting use of', kind, specifics=specifics)
525 want = { kind: [specifics] }
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200526 available_dict = self.reserved.find(self.origin, want, skip_if_marked=USED_KEY,
527 do_copy=False, raise_if_missing=False,
528 log_label='Using')
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200529 available = available_dict.get(kind)
530 self.dbg(available=len(available))
531 if not available:
Neels Hofmeyrf9e86932017-06-06 19:47:40 +0200532 # cook up a detailed error message for the current situation
533 kind_reserved = self.reserved.get(kind, [])
534 used_count = len([r for r in kind_reserved if USED_KEY in r])
535 matching = self.reserved.find(self.origin, want, raise_if_missing=False, log_label=None).get(kind, [])
536 if not matching:
537 msg = 'none of the reserved resources matches requirements %r' % specifics
538 elif not (used_count < len(kind_reserved)):
539 msg = 'suite.conf reserved only %d x %r.' % (len(kind_reserved), kind)
540 else:
541 msg = ('No unused resource left that matches the requirements;'
542 ' Of reserved %d x %r, %d match the requirements, but all are already in use;'
543 ' Requirements: %r'
544 % (len(kind_reserved), kind, len(matching), specifics))
545 raise NoResourceExn('When trying to use instance nr %d of %r: %s' % (used_count + 1, kind, msg))
546
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200547 pick = available[0]
548 self.dbg(using=pick)
549 assert not pick.get(USED_KEY)
550 pick[USED_KEY] = True
551 return copy.deepcopy(pick)
552
553 def put(self, item):
554 if not item.get(USED_KEY):
555 raise RuntimeError('Can only put() a resource that is used: %r' % item)
556 hash_to_put = item.get(HASH_KEY)
557 if not hash_to_put:
558 raise RuntimeError('Can only put() a resource that has a hash marker: %r' % item)
559 for key, item_list in self.reserved.items():
560 my_list = self.get(key)
561 for my_item in my_list:
562 if hash_to_put == my_item.get(HASH_KEY):
563 my_item.pop(USED_KEY)
564
565 def put_all(self):
Pau Espin Pedrol1dd29552017-06-13 18:07:57 +0200566 if not self.reserved:
567 return
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200568 for key, item_list in self.reserved.items():
Pau Espin Pedrol1dd29552017-06-13 18:07:57 +0200569 for item in item_list:
570 item.pop(USED_KEY, None)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200571
572 def free(self):
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200573 if self.reserved_original:
574 self.resources_pool.free(self.origin, self.reserved_original)
575 self.reserved_original = None
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200576
Neels Hofmeyr2d1d5612017-05-22 20:02:41 +0200577 def counts(self):
578 counts = {}
579 for key in self.reserved.keys():
580 counts[key] = self.count(key)
581 return counts
582
583 def count(self, key):
584 return len(self.reserved.get(key) or [])
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200585
586# vim: expandtab tabstop=4 shiftwidth=4