blob: 07d32e267225c5047034f188b936439ff8751022 [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
Pau Espin Pedrolc8b0f932020-02-11 17:45:26 +010032from . import srs_ue, srs_enb
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +020033
Neels Hofmeyr3531a192017-03-28 14:30:28 +020034from .util import is_dict, is_list
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +020035
Neels Hofmeyr3531a192017-03-28 14:30:28 +020036HASH_KEY = '_hash'
37RESERVED_KEY = '_reserved_by'
38USED_KEY = '_used'
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +020039
Neels Hofmeyr3531a192017-03-28 14:30:28 +020040RESOURCES_CONF = 'resources.conf'
Neels Hofmeyr3531a192017-03-28 14:30:28 +020041RESERVED_RESOURCES_FILE = 'reserved_resources.state'
42
Neels Hofmeyr76d81032017-05-18 18:35:32 +020043R_IP_ADDRESS = 'ip_address'
Pau Espin Pedrol116a2c42020-02-11 17:41:13 +010044R_RUN_NODE = 'run_node'
Neels Hofmeyr3531a192017-03-28 14:30:28 +020045R_BTS = 'bts'
46R_ARFCN = 'arfcn'
47R_MODEM = 'modem'
Pau Espin Pedrolbc1ed882018-05-17 16:59:58 +020048R_OSMOCON = 'osmocon_phone'
Pau Espin Pedrolc8b0f932020-02-11 17:45:26 +010049R_ENB = 'enb'
50R_ALL = (R_IP_ADDRESS, R_RUN_NODE, R_BTS, R_ARFCN, R_MODEM, R_OSMOCON, R_ENB)
Neels Hofmeyr3531a192017-03-28 14:30:28 +020051
52RESOURCES_SCHEMA = {
Neels Hofmeyr76d81032017-05-18 18:35:32 +020053 'ip_address[].addr': schema.IPV4,
Pau Espin Pedrol116a2c42020-02-11 17:41:13 +010054 'run_node[].run_type': schema.STR,
55 'run_node[].run_addr': schema.IPV4,
56 'run_node[].ssh_user': schema.STR,
57 'run_node[].ssh_addr': schema.IPV4,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020058 'bts[].label': schema.STR,
59 'bts[].type': schema.STR,
Pau Espin Pedrolfa9a6d32017-09-12 15:13:21 +020060 'bts[].ipa_unit_id': schema.UINT,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020061 'bts[].addr': schema.IPV4,
62 'bts[].band': schema.BAND,
Pau Espin Pedrolce35d912017-11-23 11:01:24 +010063 'bts[].direct_pcu': schema.BOOL_STR,
Pau Espin Pedrol57497a62017-08-28 14:21:15 +020064 'bts[].ciphers[]': schema.CIPHER,
Pau Espin Pedrol722e94e2018-08-22 11:01:32 +020065 'bts[].channel_allocator': schema.CHAN_ALLOCATOR,
Pau Espin Pedrol4f23ab52018-10-29 11:30:00 +010066 'bts[].gprs_mode': schema.GPRS_MODE,
Pau Espin Pedrol39df7f42018-05-07 13:49:33 +020067 'bts[].num_trx': schema.UINT,
68 'bts[].max_trx': schema.UINT,
Pau Espin Pedrolf6a07122018-07-27 16:24:29 +020069 'bts[].trx_list[].addr': schema.IPV4,
Your Name44af3412017-04-13 03:11:59 +020070 'bts[].trx_list[].hw_addr': schema.HWADDR,
71 'bts[].trx_list[].net_device': schema.STR,
Pau Espin Pedrolb26f32a2017-09-14 13:52:28 +020072 'bts[].trx_list[].nominal_power': schema.UINT,
73 'bts[].trx_list[].max_power_red': schema.UINT,
Pau Espin Pedrolc9b63762018-05-07 01:57:01 +020074 'bts[].trx_list[].timeslot_list[].phys_chan_config': schema.PHY_CHAN,
Pau Espin Pedrolf6a07122018-07-27 16:24:29 +020075 'bts[].trx_list[].power_supply.type': schema.STR,
76 'bts[].trx_list[].power_supply.device': schema.STR,
77 'bts[].trx_list[].power_supply.port': schema.STR,
Pau Espin Pedrol0d455042018-08-27 17:07:41 +020078 'bts[].osmo_trx.launch_trx': schema.BOOL_STR,
79 'bts[].osmo_trx.type': schema.STR,
80 'bts[].osmo_trx.clock_reference': schema.OSMO_TRX_CLOCK_REF,
81 'bts[].osmo_trx.trx_ip': schema.IPV4,
Pau Espin Pedrola9006df2018-10-01 12:26:39 +020082 'bts[].osmo_trx.remote_user': schema.STR,
Pau Espin Pedrol8cfa10f2018-11-06 12:05:19 +010083 'bts[].osmo_trx.dev_args': schema.STR,
Pau Espin Pedrol94eab262018-09-17 20:25:55 +020084 'bts[].osmo_trx.multi_arfcn': schema.BOOL_STR,
Pau Espin Pedrol0cde25f2019-07-24 19:55:08 +020085 'bts[].osmo_trx.max_trxd_version': schema.UINT,
Pau Espin Pedrolc18c5b82019-11-26 14:24:24 +010086 'bts[].osmo_trx.channels[].rx_path': schema.STR,
87 'bts[].osmo_trx.channels[].tx_path': schema.STR,
Pau Espin Pedrolc8b0f932020-02-11 17:45:26 +010088 'enb[].label': schema.STR,
89 'enb[].type': schema.STR,
90 'enb[].remote_user': schema.STR,
91 'enb[].addr': schema.IPV4,
Pau Espin Pedrol1deb1ae2020-02-27 15:16:09 +010092 'enb[].num_prb': schema.UINT,
Pau Espin Pedrolb6937712020-02-27 18:02:20 +010093 'enb[].transmission_mode': schema.LTE_TRANSMISSION_MODE,
Pau Espin Pedrola9a2fe22020-02-13 19:29:55 +010094 'enb[].rf_dev_type': schema.STR,
95 'enb[].rf_dev_args': schema.STR,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020096 'arfcn[].arfcn': schema.INT,
97 'arfcn[].band': schema.BAND,
Holger Hans Peter Freyther93a89422019-02-27 06:36:36 +000098 'modem[].type': schema.STR,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020099 'modem[].label': schema.STR,
100 'modem[].path': schema.STR,
101 'modem[].imsi': schema.IMSI,
102 'modem[].ki': schema.KI,
Pau Espin Pedrol713ce2c2017-08-24 16:57:17 +0200103 'modem[].auth_algo': schema.AUTH_ALGO,
Pau Espin Pedrolc8b0f932020-02-11 17:45:26 +0100104 'modem[].remote_user': schema.STR,
105 'modem[].addr': schema.IPV4,
Pau Espin Pedrol57497a62017-08-28 14:21:15 +0200106 'modem[].ciphers[]': schema.CIPHER,
Pau Espin Pedrolac18fd32017-08-31 18:49:47 +0200107 'modem[].features[]': schema.MODEM_FEATURE,
Pau Espin Pedrola9a2fe22020-02-13 19:29:55 +0100108 'modem[].rf_dev_type': schema.STR,
109 'modem[].rf_dev_args': schema.STR,
Pau Espin Pedrolbc1ed882018-05-17 16:59:58 +0200110 'osmocon_phone[].serial_device': schema.STR,
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200111 }
112
113WANT_SCHEMA = util.dict_add(
Pau Espin Pedrolead79ac2017-09-12 15:19:18 +0200114 dict([('%s[].times' % r, schema.TIMES) for r in R_ALL]),
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200115 RESOURCES_SCHEMA)
116
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200117CONF_SCHEMA = util.dict_add(
Pau Espin Pedrol5dc24592018-08-27 12:53:41 +0200118 { 'defaults.timeout': schema.STR,
Pau Espin Pedrolb6937712020-02-27 18:02:20 +0100119 'config.bsc.net.codec_list[]': schema.CODEC,
120 'config.epc.rlc_drb_mode': schema.LTE_RLC_DRB_MODE, },
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200121 dict([('resources.%s' % key, val) for key, val in WANT_SCHEMA.items()]),
122 dict([('modifiers.%s' % key, val) for key, val in WANT_SCHEMA.items()]))
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200123
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200124KNOWN_BTS_TYPES = {
Your Name44af3412017-04-13 03:11:59 +0200125 'osmo-bts-sysmo': bts_sysmo.SysmoBts,
126 'osmo-bts-trx': bts_osmotrx.OsmoBtsTrx,
Pau Espin Pedrolfeb66e72019-03-04 18:37:04 +0100127 'osmo-bts-oc2g': bts_oc2g.OsmoBtsOC2G,
Pau Espin Pedroldaed4472017-09-15 14:11:35 +0200128 'osmo-bts-octphy': bts_octphy.OsmoBtsOctphy,
Holger Hans Peter Freytherd2db10d2018-12-06 18:34:53 +0000129 'osmo-bts-virtual': bts_osmovirtual.OsmoBtsVirtual,
Pau Espin Pedrol1b28a582018-03-08 21:01:26 +0100130 'nanobts': bts_nanobts.NanoBts,
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200131 }
132
Pau Espin Pedrolc8b0f932020-02-11 17:45:26 +0100133KNOWN_ENB_TYPES = {
134 'srsenb': srs_enb.srsENB,
135}
Holger Hans Peter Freyther74243012019-02-27 08:18:38 +0000136
137KNOWN_MS_TYPES = {
138 # Map None to ofono for forward compability
139 None: modem.Modem,
140 'ofono': modem.Modem,
Holger Hans Peter Freyther10501cc2019-02-25 03:20:16 +0000141 'osmo-mobile': ms_osmo_mobile.MSOsmoMobile,
Pau Espin Pedrolc8b0f932020-02-11 17:45:26 +0100142 'srsue': srs_ue.srsUE,
Holger Hans Peter Freyther74243012019-02-27 08:18:38 +0000143}
144
145
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200146def register_bts_type(name, clazz):
147 KNOWN_BTS_TYPES[name] = clazz
148
149class ResourcesPool(log.Origin):
150 _remember_to_free = None
151 _registered_exit_handler = False
152
153 def __init__(self):
154 self.config_path = config.get_config_file(RESOURCES_CONF)
155 self.state_dir = config.get_state_dir()
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200156 super().__init__(log.C_CNF, conf=self.config_path, state=self.state_dir.path)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200157 self.read_conf()
158
159 def read_conf(self):
160 self.all_resources = Resources(config.read(self.config_path, RESOURCES_SCHEMA))
161 self.all_resources.set_hashes()
162
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200163 def reserve(self, origin, want, modifiers):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200164 '''
165 attempt to reserve the resources specified in the dict 'want' for
166 'origin'. Obtain a lock on the resources lock dir, verify that all
167 wanted resources are available, and if yes mark them as reserved.
168
169 On success, return a reservation object which can be used to release
170 the reservation. The reservation will be freed automatically on program
171 exit, if not yet done manually.
172
173 'origin' should be an Origin() instance.
174
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200175 'want' is a dict matching RESOURCES_SCHEMA, used to specify what to
176 reserve.
177
178 'modifiers' is a dict matching RESOURCES_SCHEMA, it is overlaid on top
179 of 'want'.
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200180
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200181 If an entry has no attribute set, any of the resources may be
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200182 reserved without further limitations.
183
184 ResourcesPool may also be selected with narrowed down constraints.
Neels Hofmeyr76d81032017-05-18 18:35:32 +0200185 This would reserve one IP address, two modems, one BTS of type
Neels Hofmeyr391afe32017-05-18 19:22:12 +0200186 sysmo and one of type trx, plus 2 ARFCNs in the 1800 band:
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200187
188 {
Neels Hofmeyr76d81032017-05-18 18:35:32 +0200189 'ip_address': [ {} ],
Neels Hofmeyr391afe32017-05-18 19:22:12 +0200190 'bts': [ { 'type': 'sysmo' }, { 'type': 'trx' } ],
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200191 'arfcn': [ { 'band': 'GSM-1800' }, { 'band': 'GSM-1800' } ],
192 'modem': [ {}, {} ],
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200193 }
194 '''
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200195 schema.validate(want, RESOURCES_SCHEMA)
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200196 schema.validate(modifiers, RESOURCES_SCHEMA)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200197
198 origin_id = origin.origin_id()
199
200 with self.state_dir.lock(origin_id):
201 rrfile_path = self.state_dir.mk_parentdir(RESERVED_RESOURCES_FILE)
202 reserved = Resources(config.read(rrfile_path, if_missing_return={}))
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200203 to_be_reserved = self.all_resources.without(reserved).find(origin, want)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200204
205 to_be_reserved.mark_reserved_by(origin_id)
206
207 reserved.add(to_be_reserved)
208 config.write(rrfile_path, reserved)
209
210 self.remember_to_free(to_be_reserved)
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200211 return ReservedResources(self, origin, to_be_reserved, modifiers)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200212
213 def free(self, origin, to_be_freed):
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200214 log.ctx(origin)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200215 with self.state_dir.lock(origin.origin_id()):
216 rrfile_path = self.state_dir.mk_parentdir(RESERVED_RESOURCES_FILE)
217 reserved = Resources(config.read(rrfile_path, if_missing_return={}))
218 reserved.drop(to_be_freed)
219 config.write(rrfile_path, reserved)
220 self.forget_freed(to_be_freed)
221
222 def register_exit_handler(self):
223 if self._registered_exit_handler:
224 return
225 atexit.register(self.clean_up_registered_resources)
226 self._registered_exit_handler = True
227
228 def unregister_exit_handler(self):
229 if not self._registered_exit_handler:
230 return
231 atexit.unregister(self.clean_up_registered_resources)
232 self._registered_exit_handler = False
233
234 def clean_up_registered_resources(self):
235 if not self._remember_to_free:
236 return
237 self.free(log.Origin('atexit.clean_up_registered_resources()'),
238 self._remember_to_free)
239
240 def remember_to_free(self, to_be_reserved):
241 self.register_exit_handler()
242 if not self._remember_to_free:
243 self._remember_to_free = Resources()
244 self._remember_to_free.add(to_be_reserved)
245
246 def forget_freed(self, freed):
247 if freed is self._remember_to_free:
248 self._remember_to_free.clear()
249 else:
250 self._remember_to_free.drop(freed)
251 if not self._remember_to_free:
252 self.unregister_exit_handler()
253
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100254 def next_persistent_value(self, token, first_val, validate_func, inc_func, origin):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200255 origin_id = origin.origin_id()
256
257 with self.state_dir.lock(origin_id):
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100258 token_path = self.state_dir.child('last_used_%s.state' % token)
259 log.ctx(token_path)
260 last_value = first_val
261 if os.path.exists(token_path):
262 if not os.path.isfile(token_path):
263 raise RuntimeError('path should be a file but is not: %r' % token_path)
264 with open(token_path, 'r') as f:
265 last_value = f.read().strip()
266 validate_func(last_value)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200267
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100268 next_value = inc_func(last_value)
269 with open(token_path, 'w') as f:
270 f.write(next_value)
271 return next_value
272
273 def next_msisdn(self, origin):
274 return self.next_persistent_value('msisdn', '1000', schema.msisdn, util.msisdn_inc, origin)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200275
Pau Espin Pedrol5e0c2512017-11-06 18:40:23 +0100276 def next_lac(self, origin):
Pau Espin Pedrolf7f06362017-11-28 12:56:35 +0100277 # LAC=0 has special meaning (MS detached), avoid it
278 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 +0200279
Pau Espin Pedrol8a3a7b52017-11-28 15:50:02 +0100280 def next_rac(self, origin):
281 return self.next_persistent_value('rac', '1', schema.uint8, lambda x: str((int(x)+1) % pow(2,8) or 1), origin)
282
Pau Espin Pedrol4ccce7c2017-11-07 11:13:20 +0100283 def next_cellid(self, origin):
284 return self.next_persistent_value('cellid', '1', schema.uint16, lambda x: str((int(x)+1) % pow(2,16)), origin)
285
Pau Espin Pedrol8a3a7b52017-11-28 15:50:02 +0100286 def next_bvci(self, origin):
287 # BVCI=0 and =1 are reserved, avoid them.
288 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)
289
Pau Espin Pedrol4676cbd2017-09-14 17:35:03 +0200290class NoResourceExn(log.Error):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200291 pass
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200292
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200293class Resources(dict):
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200294
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200295 def __init__(self, all_resources={}, do_copy=True):
296 if do_copy:
297 all_resources = copy.deepcopy(all_resources)
298 self.update(all_resources)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200299
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200300 def drop(self, reserved, fail_if_not_found=True):
301 # protect from modifying reserved because we're the same object
302 if reserved is self:
303 raise RuntimeError('Refusing to drop a list of resources from itself.'
304 ' This is probably a bug where a list of Resources()'
305 ' should have been copied but is passed as-is.'
306 ' use Resources.clear() instead.')
307
308 for key, reserved_list in reserved.items():
309 my_list = self.get(key) or []
310
311 if my_list is reserved_list:
312 self.pop(key)
313 continue
314
315 for reserved_item in reserved_list:
316 found = False
317 reserved_hash = reserved_item.get(HASH_KEY)
318 if not reserved_hash:
319 raise RuntimeError('Resources.drop() only works with hashed items')
320
321 for i in range(len(my_list)):
322 my_item = my_list[i]
323 my_hash = my_item.get(HASH_KEY)
324 if not my_hash:
325 raise RuntimeError('Resources.drop() only works with hashed items')
326 if my_hash == reserved_hash:
327 found = True
328 my_list.pop(i)
329 break
330
331 if fail_if_not_found and not found:
332 raise RuntimeError('Asked to drop resource from a pool, but the'
333 ' resource was not found: %s = %r' % (key, reserved_item))
334
335 if not my_list:
336 self.pop(key)
337 return self
338
339 def without(self, reserved):
340 return Resources(self).drop(reserved)
341
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200342 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 +0200343 '''
344 Pass a dict of resource requirements, e.g.:
345 want = {
346 'bts': [ {'type': 'osmo-bts-sysmo',}, {} ],
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200347 'modem': [ {}, {}, {} ]
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200348 }
349 This function tries to find a combination from the available resources that
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200350 matches these requirements. The return value is a dict (wrapped in a Resources class)
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200351 that contains the matching resources in the order of 'want' dict: in above
352 example, the returned dict would have a 'bts' list with the first item being
353 a sysmoBTS, the second item being any other available BTS.
354
355 If skip_if_marked is passed, any resource that contains this key is skipped.
356 E.g. if a BTS has the USED_KEY set like
357 reserved_resources = { 'bts' : {..., '_used': True} }
358 then this may be skipped by passing skip_if_marked='_used'
359 (or rather skip_if_marked=USED_KEY).
360
361 If do_copy is True, the returned dict is a deep copy and does not share
362 lists with any other Resources dict.
363
364 If raise_if_missing is False, this will return an empty item for any
365 resource that had no match, instead of immediately raising an exception.
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200366
367 This function expects input dictionaries whose contents have already
368 been replicated based on its the 'times' attributes. See
369 config.replicate_times() for more details.
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200370 '''
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200371 matches = {}
Neels Hofmeyr17c139e2017-04-12 02:42:02 +0200372 for key, want_list in sorted(want.items()): # sorted for deterministic test results
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200373 # here we have a resource of a given type, e.g. 'bts', with a list
374 # containing as many BTSes as the caller wants to reserve/use. Each
375 # list item contains specifics for the particular BTS.
Neels Hofmeyr2a1a1fa2017-05-29 01:36:21 +0200376 my_list = self.get(key, [])
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200377
Neels Hofmeyrf9e86932017-06-06 19:47:40 +0200378 if log_label:
379 for_origin.log(log_label, len(want_list), 'x', key, '(candidates: %d)'%len(my_list))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200380
381 # Try to avoid a less constrained item snatching away a resource
382 # from a more detailed constrained requirement.
383
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200384 # first record all matches, so that each requested item has a list
385 # of all available resources that match it. Some resources may
386 # appear for multiple requested items. Store matching indexes.
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200387 all_matches = []
388 for want_item in want_list:
389 item_match_list = []
390 for i in range(len(my_list)):
391 my_item = my_list[i]
392 if skip_if_marked and my_item.get(skip_if_marked):
393 continue
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200394 if item_matches(my_item, want_item):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200395 item_match_list.append(i)
396 if not item_match_list:
Neels Hofmeyr9b907702017-05-06 23:20:33 +0200397 if raise_if_missing:
398 raise NoResourceExn('No matching resource available for %s = %r'
399 % (key, want_item))
400 else:
401 # this one failed... see below
402 all_matches = []
403 break
404
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200405 all_matches.append( item_match_list )
406
407 if not all_matches:
Neels Hofmeyr9b907702017-05-06 23:20:33 +0200408 # ...this one failed. Makes no sense to solve resource
409 # allocations, return an empty list for this key to mark
410 # failure.
411 matches[key] = []
412 continue
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200413
414 # figure out who gets what
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200415 try:
416 solution = solve(all_matches)
417 except NotSolvable:
418 # instead of a cryptic error message, raise an exception that
419 # conveys meaning to the user.
420 raise NoResourceExn('Could not resolve request to reserve resources: '
421 '%d x %s with requirements: %r' % (len(want_list), key, want_list))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200422 picked = [ my_list[i] for i in solution if i is not None ]
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200423 for_origin.dbg('Picked', config.tostr(picked))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200424 matches[key] = picked
425
426 return Resources(matches, do_copy=do_copy)
427
428 def set_hashes(self):
429 for key, item_list in self.items():
430 for item in item_list:
431 item[HASH_KEY] = util.hash_obj(item, HASH_KEY, RESERVED_KEY, USED_KEY)
432
433 def add(self, more):
434 if more is self:
435 raise RuntimeError('adding a list of resources to itself?')
436 config.add(self, copy.deepcopy(more))
437
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200438 def mark_reserved_by(self, origin_id):
439 for key, item_list in self.items():
440 for item in item_list:
441 item[RESERVED_KEY] = origin_id
442
443
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200444class NotSolvable(Exception):
445 pass
446
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200447def solve(all_matches):
448 '''
449 all_matches shall be a list of index-lists.
450 all_matches[i] is the list of indexes that item i can use.
451 Return a solution so that each i gets a different index.
452 solve([ [0, 1, 2],
453 [0],
454 [0, 2] ]) == [1, 0, 2]
455 '''
456
457 def all_differ(l):
458 return len(set(l)) == len(l)
459
460 def search_in_permutations(fixed=[]):
461 idx = len(fixed)
462 for i in range(len(all_matches[idx])):
463 val = all_matches[idx][i]
464 # don't add a val that's already in the list
465 if val in fixed:
466 continue
467 l = list(fixed)
468 l.append(val)
469 if len(l) == len(all_matches):
470 # found a solution
471 return l
472 # not at the end yet, add next digit
473 r = search_in_permutations(l)
474 if r:
475 # nested search_in_permutations() call found a solution
476 return r
477 # this entire branch yielded no solution
478 return None
479
480 if not all_matches:
481 raise RuntimeError('Cannot solve: no candidates')
482
483 solution = search_in_permutations()
484 if not solution:
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200485 raise NotSolvable('The requested resource requirements are not solvable %r'
486 % all_matches)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200487 return solution
488
489
490def contains_hash(list_of_dicts, a_hash):
491 for d in list_of_dicts:
492 if d.get(HASH_KEY) == a_hash:
493 return True
494 return False
495
496def item_matches(item, wanted_item, ignore_keys=None):
497 if is_dict(wanted_item):
498 # match up two dicts
499 if not isinstance(item, dict):
500 return False
501 for key, wanted_val in wanted_item.items():
502 if ignore_keys and key in ignore_keys:
503 continue
504 if not item_matches(item.get(key), wanted_val, ignore_keys=ignore_keys):
505 return False
506 return True
507
508 if is_list(wanted_item):
Pau Espin Pedrol4e36f7c2017-08-28 13:29:28 +0200509 if not is_list(item):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200510 return False
Pau Espin Pedrol58475512017-09-14 15:33:15 +0200511 # Validate that all elements in both lists are of the same type:
512 t = util.list_validate_same_elem_type(wanted_item + item)
513 if t is None:
514 return True # both lists are empty, return
515 # For lists of complex objects, we expect them to be sorted lists:
516 if t in (dict, list, tuple):
517 for i in range(max(len(wanted_item), len(item))):
518 log.ctx(idx=i)
519 subitem = item[i] if i < len(item) else util.empty_instance_type(t)
520 wanted_subitem = wanted_item[i] if i < len(wanted_item) else util.empty_instance_type(t)
521 if not item_matches(subitem, wanted_subitem, ignore_keys=ignore_keys):
522 return False
523 else: # for lists of basic elements, we handle them as unsorted sets:
524 for val in wanted_item:
525 if val not in item:
526 return False
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200527 return True
528
529 return item == wanted_item
530
531
532class ReservedResources(log.Origin):
533 '''
534 After all resources have been figured out, this is the API that a test case
535 gets to interact with resources. From those resources that have been
536 reserved for it, it can pick some to mark them as currently in use.
537 Functions like nitb() provide a resource by automatically picking its
538 dependencies from so far unused (but reserved) resource.
539 '''
540
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200541 def __init__(self, resources_pool, origin, reserved, modifiers):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200542 self.resources_pool = resources_pool
543 self.origin = origin
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200544 self.reserved_original = reserved
545 self.reserved = copy.deepcopy(self.reserved_original)
546 config.overlay(self.reserved, modifiers)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200547
548 def __repr__(self):
549 return 'resources(%s)=%s' % (self.origin.name(), pprint.pformat(self.reserved))
550
551 def get(self, kind, specifics=None):
552 if specifics is None:
553 specifics = {}
554 self.dbg('requesting use of', kind, specifics=specifics)
555 want = { kind: [specifics] }
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200556 available_dict = self.reserved.find(self.origin, want, skip_if_marked=USED_KEY,
557 do_copy=False, raise_if_missing=False,
558 log_label='Using')
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200559 available = available_dict.get(kind)
560 self.dbg(available=len(available))
561 if not available:
Neels Hofmeyrf9e86932017-06-06 19:47:40 +0200562 # cook up a detailed error message for the current situation
563 kind_reserved = self.reserved.get(kind, [])
564 used_count = len([r for r in kind_reserved if USED_KEY in r])
565 matching = self.reserved.find(self.origin, want, raise_if_missing=False, log_label=None).get(kind, [])
566 if not matching:
567 msg = 'none of the reserved resources matches requirements %r' % specifics
568 elif not (used_count < len(kind_reserved)):
569 msg = 'suite.conf reserved only %d x %r.' % (len(kind_reserved), kind)
570 else:
571 msg = ('No unused resource left that matches the requirements;'
572 ' Of reserved %d x %r, %d match the requirements, but all are already in use;'
573 ' Requirements: %r'
574 % (len(kind_reserved), kind, len(matching), specifics))
575 raise NoResourceExn('When trying to use instance nr %d of %r: %s' % (used_count + 1, kind, msg))
576
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200577 pick = available[0]
578 self.dbg(using=pick)
579 assert not pick.get(USED_KEY)
580 pick[USED_KEY] = True
581 return copy.deepcopy(pick)
582
583 def put(self, item):
584 if not item.get(USED_KEY):
585 raise RuntimeError('Can only put() a resource that is used: %r' % item)
586 hash_to_put = item.get(HASH_KEY)
587 if not hash_to_put:
588 raise RuntimeError('Can only put() a resource that has a hash marker: %r' % item)
589 for key, item_list in self.reserved.items():
590 my_list = self.get(key)
591 for my_item in my_list:
592 if hash_to_put == my_item.get(HASH_KEY):
593 my_item.pop(USED_KEY)
594
595 def put_all(self):
Pau Espin Pedrol1dd29552017-06-13 18:07:57 +0200596 if not self.reserved:
597 return
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200598 for key, item_list in self.reserved.items():
Pau Espin Pedrol1dd29552017-06-13 18:07:57 +0200599 for item in item_list:
600 item.pop(USED_KEY, None)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200601
602 def free(self):
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200603 if self.reserved_original:
604 self.resources_pool.free(self.origin, self.reserved_original)
605 self.reserved_original = None
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200606
Neels Hofmeyr2d1d5612017-05-22 20:02:41 +0200607 def counts(self):
608 counts = {}
609 for key in self.reserved.keys():
610 counts[key] = self.count(key)
611 return counts
612
613 def count(self, key):
614 return len(self.reserved.get(key) or [])
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200615
616# vim: expandtab tabstop=4 shiftwidth=4