blob: 31e846325210cae1a54f7cb3a1a79c9233d74418 [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 Pedrola9a2fe22020-02-13 19:29:55 +010093 'enb[].rf_dev_type': schema.STR,
94 'enb[].rf_dev_args': schema.STR,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020095 'arfcn[].arfcn': schema.INT,
96 'arfcn[].band': schema.BAND,
Holger Hans Peter Freyther93a89422019-02-27 06:36:36 +000097 'modem[].type': schema.STR,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020098 'modem[].label': schema.STR,
99 'modem[].path': schema.STR,
100 'modem[].imsi': schema.IMSI,
101 'modem[].ki': schema.KI,
Pau Espin Pedrol713ce2c2017-08-24 16:57:17 +0200102 'modem[].auth_algo': schema.AUTH_ALGO,
Pau Espin Pedrolc8b0f932020-02-11 17:45:26 +0100103 'modem[].remote_user': schema.STR,
104 'modem[].addr': schema.IPV4,
Pau Espin Pedrol57497a62017-08-28 14:21:15 +0200105 'modem[].ciphers[]': schema.CIPHER,
Pau Espin Pedrolac18fd32017-08-31 18:49:47 +0200106 'modem[].features[]': schema.MODEM_FEATURE,
Pau Espin Pedrola9a2fe22020-02-13 19:29:55 +0100107 'modem[].rf_dev_type': schema.STR,
108 'modem[].rf_dev_args': schema.STR,
Pau Espin Pedrolbc1ed882018-05-17 16:59:58 +0200109 'osmocon_phone[].serial_device': schema.STR,
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200110 }
111
112WANT_SCHEMA = util.dict_add(
Pau Espin Pedrolead79ac2017-09-12 15:19:18 +0200113 dict([('%s[].times' % r, schema.TIMES) for r in R_ALL]),
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200114 RESOURCES_SCHEMA)
115
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200116CONF_SCHEMA = util.dict_add(
Pau Espin Pedrol5dc24592018-08-27 12:53:41 +0200117 { 'defaults.timeout': schema.STR,
118 'config.bsc.net.codec_list[]': schema.CODEC },
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200119 dict([('resources.%s' % key, val) for key, val in WANT_SCHEMA.items()]),
120 dict([('modifiers.%s' % key, val) for key, val in WANT_SCHEMA.items()]))
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200121
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200122KNOWN_BTS_TYPES = {
Your Name44af3412017-04-13 03:11:59 +0200123 'osmo-bts-sysmo': bts_sysmo.SysmoBts,
124 'osmo-bts-trx': bts_osmotrx.OsmoBtsTrx,
Pau Espin Pedrolfeb66e72019-03-04 18:37:04 +0100125 'osmo-bts-oc2g': bts_oc2g.OsmoBtsOC2G,
Pau Espin Pedroldaed4472017-09-15 14:11:35 +0200126 'osmo-bts-octphy': bts_octphy.OsmoBtsOctphy,
Holger Hans Peter Freytherd2db10d2018-12-06 18:34:53 +0000127 'osmo-bts-virtual': bts_osmovirtual.OsmoBtsVirtual,
Pau Espin Pedrol1b28a582018-03-08 21:01:26 +0100128 'nanobts': bts_nanobts.NanoBts,
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200129 }
130
Pau Espin Pedrolc8b0f932020-02-11 17:45:26 +0100131KNOWN_ENB_TYPES = {
132 'srsenb': srs_enb.srsENB,
133}
Holger Hans Peter Freyther74243012019-02-27 08:18:38 +0000134
135KNOWN_MS_TYPES = {
136 # Map None to ofono for forward compability
137 None: modem.Modem,
138 'ofono': modem.Modem,
Holger Hans Peter Freyther10501cc2019-02-25 03:20:16 +0000139 'osmo-mobile': ms_osmo_mobile.MSOsmoMobile,
Pau Espin Pedrolc8b0f932020-02-11 17:45:26 +0100140 'srsue': srs_ue.srsUE,
Holger Hans Peter Freyther74243012019-02-27 08:18:38 +0000141}
142
143
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200144def register_bts_type(name, clazz):
145 KNOWN_BTS_TYPES[name] = clazz
146
147class ResourcesPool(log.Origin):
148 _remember_to_free = None
149 _registered_exit_handler = False
150
151 def __init__(self):
152 self.config_path = config.get_config_file(RESOURCES_CONF)
153 self.state_dir = config.get_state_dir()
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200154 super().__init__(log.C_CNF, conf=self.config_path, state=self.state_dir.path)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200155 self.read_conf()
156
157 def read_conf(self):
158 self.all_resources = Resources(config.read(self.config_path, RESOURCES_SCHEMA))
159 self.all_resources.set_hashes()
160
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200161 def reserve(self, origin, want, modifiers):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200162 '''
163 attempt to reserve the resources specified in the dict 'want' for
164 'origin'. Obtain a lock on the resources lock dir, verify that all
165 wanted resources are available, and if yes mark them as reserved.
166
167 On success, return a reservation object which can be used to release
168 the reservation. The reservation will be freed automatically on program
169 exit, if not yet done manually.
170
171 'origin' should be an Origin() instance.
172
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200173 'want' is a dict matching RESOURCES_SCHEMA, used to specify what to
174 reserve.
175
176 'modifiers' is a dict matching RESOURCES_SCHEMA, it is overlaid on top
177 of 'want'.
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200178
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200179 If an entry has no attribute set, any of the resources may be
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200180 reserved without further limitations.
181
182 ResourcesPool may also be selected with narrowed down constraints.
Neels Hofmeyr76d81032017-05-18 18:35:32 +0200183 This would reserve one IP address, two modems, one BTS of type
Neels Hofmeyr391afe32017-05-18 19:22:12 +0200184 sysmo and one of type trx, plus 2 ARFCNs in the 1800 band:
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200185
186 {
Neels Hofmeyr76d81032017-05-18 18:35:32 +0200187 'ip_address': [ {} ],
Neels Hofmeyr391afe32017-05-18 19:22:12 +0200188 'bts': [ { 'type': 'sysmo' }, { 'type': 'trx' } ],
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200189 'arfcn': [ { 'band': 'GSM-1800' }, { 'band': 'GSM-1800' } ],
190 'modem': [ {}, {} ],
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200191 }
192 '''
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200193 schema.validate(want, RESOURCES_SCHEMA)
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200194 schema.validate(modifiers, RESOURCES_SCHEMA)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200195
196 origin_id = origin.origin_id()
197
198 with self.state_dir.lock(origin_id):
199 rrfile_path = self.state_dir.mk_parentdir(RESERVED_RESOURCES_FILE)
200 reserved = Resources(config.read(rrfile_path, if_missing_return={}))
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200201 to_be_reserved = self.all_resources.without(reserved).find(origin, want)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200202
203 to_be_reserved.mark_reserved_by(origin_id)
204
205 reserved.add(to_be_reserved)
206 config.write(rrfile_path, reserved)
207
208 self.remember_to_free(to_be_reserved)
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200209 return ReservedResources(self, origin, to_be_reserved, modifiers)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200210
211 def free(self, origin, to_be_freed):
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200212 log.ctx(origin)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200213 with self.state_dir.lock(origin.origin_id()):
214 rrfile_path = self.state_dir.mk_parentdir(RESERVED_RESOURCES_FILE)
215 reserved = Resources(config.read(rrfile_path, if_missing_return={}))
216 reserved.drop(to_be_freed)
217 config.write(rrfile_path, reserved)
218 self.forget_freed(to_be_freed)
219
220 def register_exit_handler(self):
221 if self._registered_exit_handler:
222 return
223 atexit.register(self.clean_up_registered_resources)
224 self._registered_exit_handler = True
225
226 def unregister_exit_handler(self):
227 if not self._registered_exit_handler:
228 return
229 atexit.unregister(self.clean_up_registered_resources)
230 self._registered_exit_handler = False
231
232 def clean_up_registered_resources(self):
233 if not self._remember_to_free:
234 return
235 self.free(log.Origin('atexit.clean_up_registered_resources()'),
236 self._remember_to_free)
237
238 def remember_to_free(self, to_be_reserved):
239 self.register_exit_handler()
240 if not self._remember_to_free:
241 self._remember_to_free = Resources()
242 self._remember_to_free.add(to_be_reserved)
243
244 def forget_freed(self, freed):
245 if freed is self._remember_to_free:
246 self._remember_to_free.clear()
247 else:
248 self._remember_to_free.drop(freed)
249 if not self._remember_to_free:
250 self.unregister_exit_handler()
251
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100252 def next_persistent_value(self, token, first_val, validate_func, inc_func, origin):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200253 origin_id = origin.origin_id()
254
255 with self.state_dir.lock(origin_id):
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100256 token_path = self.state_dir.child('last_used_%s.state' % token)
257 log.ctx(token_path)
258 last_value = first_val
259 if os.path.exists(token_path):
260 if not os.path.isfile(token_path):
261 raise RuntimeError('path should be a file but is not: %r' % token_path)
262 with open(token_path, 'r') as f:
263 last_value = f.read().strip()
264 validate_func(last_value)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200265
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100266 next_value = inc_func(last_value)
267 with open(token_path, 'w') as f:
268 f.write(next_value)
269 return next_value
270
271 def next_msisdn(self, origin):
272 return self.next_persistent_value('msisdn', '1000', schema.msisdn, util.msisdn_inc, origin)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200273
Pau Espin Pedrol5e0c2512017-11-06 18:40:23 +0100274 def next_lac(self, origin):
Pau Espin Pedrolf7f06362017-11-28 12:56:35 +0100275 # LAC=0 has special meaning (MS detached), avoid it
276 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 +0200277
Pau Espin Pedrol8a3a7b52017-11-28 15:50:02 +0100278 def next_rac(self, origin):
279 return self.next_persistent_value('rac', '1', schema.uint8, lambda x: str((int(x)+1) % pow(2,8) or 1), origin)
280
Pau Espin Pedrol4ccce7c2017-11-07 11:13:20 +0100281 def next_cellid(self, origin):
282 return self.next_persistent_value('cellid', '1', schema.uint16, lambda x: str((int(x)+1) % pow(2,16)), origin)
283
Pau Espin Pedrol8a3a7b52017-11-28 15:50:02 +0100284 def next_bvci(self, origin):
285 # BVCI=0 and =1 are reserved, avoid them.
286 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)
287
Pau Espin Pedrol4676cbd2017-09-14 17:35:03 +0200288class NoResourceExn(log.Error):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200289 pass
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200290
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200291class Resources(dict):
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200292
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200293 def __init__(self, all_resources={}, do_copy=True):
294 if do_copy:
295 all_resources = copy.deepcopy(all_resources)
296 self.update(all_resources)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200297
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200298 def drop(self, reserved, fail_if_not_found=True):
299 # protect from modifying reserved because we're the same object
300 if reserved is self:
301 raise RuntimeError('Refusing to drop a list of resources from itself.'
302 ' This is probably a bug where a list of Resources()'
303 ' should have been copied but is passed as-is.'
304 ' use Resources.clear() instead.')
305
306 for key, reserved_list in reserved.items():
307 my_list = self.get(key) or []
308
309 if my_list is reserved_list:
310 self.pop(key)
311 continue
312
313 for reserved_item in reserved_list:
314 found = False
315 reserved_hash = reserved_item.get(HASH_KEY)
316 if not reserved_hash:
317 raise RuntimeError('Resources.drop() only works with hashed items')
318
319 for i in range(len(my_list)):
320 my_item = my_list[i]
321 my_hash = my_item.get(HASH_KEY)
322 if not my_hash:
323 raise RuntimeError('Resources.drop() only works with hashed items')
324 if my_hash == reserved_hash:
325 found = True
326 my_list.pop(i)
327 break
328
329 if fail_if_not_found and not found:
330 raise RuntimeError('Asked to drop resource from a pool, but the'
331 ' resource was not found: %s = %r' % (key, reserved_item))
332
333 if not my_list:
334 self.pop(key)
335 return self
336
337 def without(self, reserved):
338 return Resources(self).drop(reserved)
339
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200340 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 +0200341 '''
342 Pass a dict of resource requirements, e.g.:
343 want = {
344 'bts': [ {'type': 'osmo-bts-sysmo',}, {} ],
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200345 'modem': [ {}, {}, {} ]
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200346 }
347 This function tries to find a combination from the available resources that
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200348 matches these requirements. The return value is a dict (wrapped in a Resources class)
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200349 that contains the matching resources in the order of 'want' dict: in above
350 example, the returned dict would have a 'bts' list with the first item being
351 a sysmoBTS, the second item being any other available BTS.
352
353 If skip_if_marked is passed, any resource that contains this key is skipped.
354 E.g. if a BTS has the USED_KEY set like
355 reserved_resources = { 'bts' : {..., '_used': True} }
356 then this may be skipped by passing skip_if_marked='_used'
357 (or rather skip_if_marked=USED_KEY).
358
359 If do_copy is True, the returned dict is a deep copy and does not share
360 lists with any other Resources dict.
361
362 If raise_if_missing is False, this will return an empty item for any
363 resource that had no match, instead of immediately raising an exception.
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200364
365 This function expects input dictionaries whose contents have already
366 been replicated based on its the 'times' attributes. See
367 config.replicate_times() for more details.
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200368 '''
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200369 matches = {}
Neels Hofmeyr17c139e2017-04-12 02:42:02 +0200370 for key, want_list in sorted(want.items()): # sorted for deterministic test results
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200371 # here we have a resource of a given type, e.g. 'bts', with a list
372 # containing as many BTSes as the caller wants to reserve/use. Each
373 # list item contains specifics for the particular BTS.
Neels Hofmeyr2a1a1fa2017-05-29 01:36:21 +0200374 my_list = self.get(key, [])
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200375
Neels Hofmeyrf9e86932017-06-06 19:47:40 +0200376 if log_label:
377 for_origin.log(log_label, len(want_list), 'x', key, '(candidates: %d)'%len(my_list))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200378
379 # Try to avoid a less constrained item snatching away a resource
380 # from a more detailed constrained requirement.
381
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200382 # first record all matches, so that each requested item has a list
383 # of all available resources that match it. Some resources may
384 # appear for multiple requested items. Store matching indexes.
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200385 all_matches = []
386 for want_item in want_list:
387 item_match_list = []
388 for i in range(len(my_list)):
389 my_item = my_list[i]
390 if skip_if_marked and my_item.get(skip_if_marked):
391 continue
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200392 if item_matches(my_item, want_item):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200393 item_match_list.append(i)
394 if not item_match_list:
Neels Hofmeyr9b907702017-05-06 23:20:33 +0200395 if raise_if_missing:
396 raise NoResourceExn('No matching resource available for %s = %r'
397 % (key, want_item))
398 else:
399 # this one failed... see below
400 all_matches = []
401 break
402
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200403 all_matches.append( item_match_list )
404
405 if not all_matches:
Neels Hofmeyr9b907702017-05-06 23:20:33 +0200406 # ...this one failed. Makes no sense to solve resource
407 # allocations, return an empty list for this key to mark
408 # failure.
409 matches[key] = []
410 continue
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200411
412 # figure out who gets what
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200413 try:
414 solution = solve(all_matches)
415 except NotSolvable:
416 # instead of a cryptic error message, raise an exception that
417 # conveys meaning to the user.
418 raise NoResourceExn('Could not resolve request to reserve resources: '
419 '%d x %s with requirements: %r' % (len(want_list), key, want_list))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200420 picked = [ my_list[i] for i in solution if i is not None ]
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200421 for_origin.dbg('Picked', config.tostr(picked))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200422 matches[key] = picked
423
424 return Resources(matches, do_copy=do_copy)
425
426 def set_hashes(self):
427 for key, item_list in self.items():
428 for item in item_list:
429 item[HASH_KEY] = util.hash_obj(item, HASH_KEY, RESERVED_KEY, USED_KEY)
430
431 def add(self, more):
432 if more is self:
433 raise RuntimeError('adding a list of resources to itself?')
434 config.add(self, copy.deepcopy(more))
435
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200436 def mark_reserved_by(self, origin_id):
437 for key, item_list in self.items():
438 for item in item_list:
439 item[RESERVED_KEY] = origin_id
440
441
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200442class NotSolvable(Exception):
443 pass
444
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200445def solve(all_matches):
446 '''
447 all_matches shall be a list of index-lists.
448 all_matches[i] is the list of indexes that item i can use.
449 Return a solution so that each i gets a different index.
450 solve([ [0, 1, 2],
451 [0],
452 [0, 2] ]) == [1, 0, 2]
453 '''
454
455 def all_differ(l):
456 return len(set(l)) == len(l)
457
458 def search_in_permutations(fixed=[]):
459 idx = len(fixed)
460 for i in range(len(all_matches[idx])):
461 val = all_matches[idx][i]
462 # don't add a val that's already in the list
463 if val in fixed:
464 continue
465 l = list(fixed)
466 l.append(val)
467 if len(l) == len(all_matches):
468 # found a solution
469 return l
470 # not at the end yet, add next digit
471 r = search_in_permutations(l)
472 if r:
473 # nested search_in_permutations() call found a solution
474 return r
475 # this entire branch yielded no solution
476 return None
477
478 if not all_matches:
479 raise RuntimeError('Cannot solve: no candidates')
480
481 solution = search_in_permutations()
482 if not solution:
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200483 raise NotSolvable('The requested resource requirements are not solvable %r'
484 % all_matches)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200485 return solution
486
487
488def contains_hash(list_of_dicts, a_hash):
489 for d in list_of_dicts:
490 if d.get(HASH_KEY) == a_hash:
491 return True
492 return False
493
494def item_matches(item, wanted_item, ignore_keys=None):
495 if is_dict(wanted_item):
496 # match up two dicts
497 if not isinstance(item, dict):
498 return False
499 for key, wanted_val in wanted_item.items():
500 if ignore_keys and key in ignore_keys:
501 continue
502 if not item_matches(item.get(key), wanted_val, ignore_keys=ignore_keys):
503 return False
504 return True
505
506 if is_list(wanted_item):
Pau Espin Pedrol4e36f7c2017-08-28 13:29:28 +0200507 if not is_list(item):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200508 return False
Pau Espin Pedrol58475512017-09-14 15:33:15 +0200509 # Validate that all elements in both lists are of the same type:
510 t = util.list_validate_same_elem_type(wanted_item + item)
511 if t is None:
512 return True # both lists are empty, return
513 # For lists of complex objects, we expect them to be sorted lists:
514 if t in (dict, list, tuple):
515 for i in range(max(len(wanted_item), len(item))):
516 log.ctx(idx=i)
517 subitem = item[i] if i < len(item) else util.empty_instance_type(t)
518 wanted_subitem = wanted_item[i] if i < len(wanted_item) else util.empty_instance_type(t)
519 if not item_matches(subitem, wanted_subitem, ignore_keys=ignore_keys):
520 return False
521 else: # for lists of basic elements, we handle them as unsorted sets:
522 for val in wanted_item:
523 if val not in item:
524 return False
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200525 return True
526
527 return item == wanted_item
528
529
530class ReservedResources(log.Origin):
531 '''
532 After all resources have been figured out, this is the API that a test case
533 gets to interact with resources. From those resources that have been
534 reserved for it, it can pick some to mark them as currently in use.
535 Functions like nitb() provide a resource by automatically picking its
536 dependencies from so far unused (but reserved) resource.
537 '''
538
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200539 def __init__(self, resources_pool, origin, reserved, modifiers):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200540 self.resources_pool = resources_pool
541 self.origin = origin
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200542 self.reserved_original = reserved
543 self.reserved = copy.deepcopy(self.reserved_original)
544 config.overlay(self.reserved, modifiers)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200545
546 def __repr__(self):
547 return 'resources(%s)=%s' % (self.origin.name(), pprint.pformat(self.reserved))
548
549 def get(self, kind, specifics=None):
550 if specifics is None:
551 specifics = {}
552 self.dbg('requesting use of', kind, specifics=specifics)
553 want = { kind: [specifics] }
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200554 available_dict = self.reserved.find(self.origin, want, skip_if_marked=USED_KEY,
555 do_copy=False, raise_if_missing=False,
556 log_label='Using')
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200557 available = available_dict.get(kind)
558 self.dbg(available=len(available))
559 if not available:
Neels Hofmeyrf9e86932017-06-06 19:47:40 +0200560 # cook up a detailed error message for the current situation
561 kind_reserved = self.reserved.get(kind, [])
562 used_count = len([r for r in kind_reserved if USED_KEY in r])
563 matching = self.reserved.find(self.origin, want, raise_if_missing=False, log_label=None).get(kind, [])
564 if not matching:
565 msg = 'none of the reserved resources matches requirements %r' % specifics
566 elif not (used_count < len(kind_reserved)):
567 msg = 'suite.conf reserved only %d x %r.' % (len(kind_reserved), kind)
568 else:
569 msg = ('No unused resource left that matches the requirements;'
570 ' Of reserved %d x %r, %d match the requirements, but all are already in use;'
571 ' Requirements: %r'
572 % (len(kind_reserved), kind, len(matching), specifics))
573 raise NoResourceExn('When trying to use instance nr %d of %r: %s' % (used_count + 1, kind, msg))
574
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200575 pick = available[0]
576 self.dbg(using=pick)
577 assert not pick.get(USED_KEY)
578 pick[USED_KEY] = True
579 return copy.deepcopy(pick)
580
581 def put(self, item):
582 if not item.get(USED_KEY):
583 raise RuntimeError('Can only put() a resource that is used: %r' % item)
584 hash_to_put = item.get(HASH_KEY)
585 if not hash_to_put:
586 raise RuntimeError('Can only put() a resource that has a hash marker: %r' % item)
587 for key, item_list in self.reserved.items():
588 my_list = self.get(key)
589 for my_item in my_list:
590 if hash_to_put == my_item.get(HASH_KEY):
591 my_item.pop(USED_KEY)
592
593 def put_all(self):
Pau Espin Pedrol1dd29552017-06-13 18:07:57 +0200594 if not self.reserved:
595 return
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200596 for key, item_list in self.reserved.items():
Pau Espin Pedrol1dd29552017-06-13 18:07:57 +0200597 for item in item_list:
598 item.pop(USED_KEY, None)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200599
600 def free(self):
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200601 if self.reserved_original:
602 self.resources_pool.free(self.origin, self.reserved_original)
603 self.reserved_original = None
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200604
Neels Hofmeyr2d1d5612017-05-22 20:02:41 +0200605 def counts(self):
606 counts = {}
607 for key in self.reserved.keys():
608 counts[key] = self.count(key)
609 return counts
610
611 def count(self, key):
612 return len(self.reserved.get(key) or [])
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200613
614# vim: expandtab tabstop=4 shiftwidth=4