blob: ea1b543ad838f476f79c3c7c921203b3ad1cd048 [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 Pedrol65beb8f2020-03-31 12:03:19 +020032from . import srs_ue, amarisoft_ue, srs_enb, amarisoft_enb, srs_epc, amarisoft_epc
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 +020040RESERVED_RESOURCES_FILE = 'reserved_resources.state'
41
Neels Hofmeyr76d81032017-05-18 18:35:32 +020042R_IP_ADDRESS = 'ip_address'
Pau Espin Pedrol116a2c42020-02-11 17:41:13 +010043R_RUN_NODE = 'run_node'
Neels Hofmeyr3531a192017-03-28 14:30:28 +020044R_BTS = 'bts'
45R_ARFCN = 'arfcn'
46R_MODEM = 'modem'
Pau Espin Pedrolbc1ed882018-05-17 16:59:58 +020047R_OSMOCON = 'osmocon_phone'
Pau Espin Pedrolc8b0f932020-02-11 17:45:26 +010048R_ENB = 'enb'
49R_ALL = (R_IP_ADDRESS, R_RUN_NODE, R_BTS, R_ARFCN, R_MODEM, R_OSMOCON, R_ENB)
Neels Hofmeyr3531a192017-03-28 14:30:28 +020050
51RESOURCES_SCHEMA = {
Neels Hofmeyr76d81032017-05-18 18:35:32 +020052 'ip_address[].addr': schema.IPV4,
Pau Espin Pedrol116a2c42020-02-11 17:41:13 +010053 'run_node[].run_type': schema.STR,
54 'run_node[].run_addr': schema.IPV4,
55 'run_node[].ssh_user': schema.STR,
56 'run_node[].ssh_addr': schema.IPV4,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020057 'bts[].label': schema.STR,
58 'bts[].type': schema.STR,
Pau Espin Pedrolfa9a6d32017-09-12 15:13:21 +020059 'bts[].ipa_unit_id': schema.UINT,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020060 'bts[].addr': schema.IPV4,
61 'bts[].band': schema.BAND,
Pau Espin Pedrolce35d912017-11-23 11:01:24 +010062 'bts[].direct_pcu': schema.BOOL_STR,
Pau Espin Pedrol57497a62017-08-28 14:21:15 +020063 'bts[].ciphers[]': schema.CIPHER,
Pau Espin Pedrol722e94e2018-08-22 11:01:32 +020064 'bts[].channel_allocator': schema.CHAN_ALLOCATOR,
Pau Espin Pedrol4f23ab52018-10-29 11:30:00 +010065 'bts[].gprs_mode': schema.GPRS_MODE,
Pau Espin Pedrol39df7f42018-05-07 13:49:33 +020066 'bts[].num_trx': schema.UINT,
67 'bts[].max_trx': schema.UINT,
Pau Espin Pedrolf6a07122018-07-27 16:24:29 +020068 'bts[].trx_list[].addr': schema.IPV4,
Your Name44af3412017-04-13 03:11:59 +020069 'bts[].trx_list[].hw_addr': schema.HWADDR,
70 'bts[].trx_list[].net_device': schema.STR,
Pau Espin Pedrolb26f32a2017-09-14 13:52:28 +020071 'bts[].trx_list[].nominal_power': schema.UINT,
72 'bts[].trx_list[].max_power_red': schema.UINT,
Pau Espin Pedrolc9b63762018-05-07 01:57:01 +020073 'bts[].trx_list[].timeslot_list[].phys_chan_config': schema.PHY_CHAN,
Pau Espin Pedrolf6a07122018-07-27 16:24:29 +020074 'bts[].trx_list[].power_supply.type': schema.STR,
75 'bts[].trx_list[].power_supply.device': schema.STR,
76 'bts[].trx_list[].power_supply.port': schema.STR,
Pau Espin Pedrol0d455042018-08-27 17:07:41 +020077 'bts[].osmo_trx.launch_trx': schema.BOOL_STR,
78 'bts[].osmo_trx.type': schema.STR,
79 'bts[].osmo_trx.clock_reference': schema.OSMO_TRX_CLOCK_REF,
80 'bts[].osmo_trx.trx_ip': schema.IPV4,
Pau Espin Pedrola9006df2018-10-01 12:26:39 +020081 'bts[].osmo_trx.remote_user': schema.STR,
Pau Espin Pedrol8cfa10f2018-11-06 12:05:19 +010082 'bts[].osmo_trx.dev_args': schema.STR,
Pau Espin Pedrol94eab262018-09-17 20:25:55 +020083 'bts[].osmo_trx.multi_arfcn': schema.BOOL_STR,
Pau Espin Pedrol0cde25f2019-07-24 19:55:08 +020084 'bts[].osmo_trx.max_trxd_version': schema.UINT,
Pau Espin Pedrolc18c5b82019-11-26 14:24:24 +010085 'bts[].osmo_trx.channels[].rx_path': schema.STR,
86 'bts[].osmo_trx.channels[].tx_path': schema.STR,
Pau Espin Pedrolc8b0f932020-02-11 17:45:26 +010087 'enb[].label': schema.STR,
88 'enb[].type': schema.STR,
89 'enb[].remote_user': schema.STR,
90 'enb[].addr': schema.IPV4,
Pau Espin Pedrol1deb1ae2020-02-27 15:16:09 +010091 'enb[].num_prb': schema.UINT,
Pau Espin Pedrolb6937712020-02-27 18:02:20 +010092 'enb[].transmission_mode': schema.LTE_TRANSMISSION_MODE,
Andre Puschmann82b88902020-03-24 10:04:48 +010093 'enb[].num_cells': schema.UINT,
Pau Espin Pedrola9a2fe22020-02-13 19:29:55 +010094 'enb[].rf_dev_type': schema.STR,
95 'enb[].rf_dev_args': schema.STR,
Pau Espin Pedrol76b2c2a2020-04-01 19:51:08 +020096 'enb[].additional_args': schema.STR,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020097 'arfcn[].arfcn': schema.INT,
98 'arfcn[].band': schema.BAND,
Holger Hans Peter Freyther93a89422019-02-27 06:36:36 +000099 'modem[].type': schema.STR,
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200100 'modem[].label': schema.STR,
101 'modem[].path': schema.STR,
102 'modem[].imsi': schema.IMSI,
103 'modem[].ki': schema.KI,
Pau Espin Pedrol713ce2c2017-08-24 16:57:17 +0200104 'modem[].auth_algo': schema.AUTH_ALGO,
Andre Puschmann22ec00a2020-03-24 09:58:06 +0100105 'modem[].apn_ipaddr': schema.IPV4,
Pau Espin Pedrolc8b0f932020-02-11 17:45:26 +0100106 'modem[].remote_user': schema.STR,
107 'modem[].addr': schema.IPV4,
Pau Espin Pedrol57497a62017-08-28 14:21:15 +0200108 'modem[].ciphers[]': schema.CIPHER,
Pau Espin Pedrolac18fd32017-08-31 18:49:47 +0200109 'modem[].features[]': schema.MODEM_FEATURE,
Pau Espin Pedrola9a2fe22020-02-13 19:29:55 +0100110 'modem[].rf_dev_type': schema.STR,
111 'modem[].rf_dev_args': schema.STR,
Andre Puschmann65e769f2020-04-06 14:53:13 +0200112 'modem[].num_carriers': schema.UINT,
Pau Espin Pedrol76b2c2a2020-04-01 19:51:08 +0200113 'modem[].additional_args': schema.STR,
Andre Puschmann35234f22020-03-23 18:52:41 +0100114 'modem[].airplane_t_on_ms': schema.INT,
115 'modem[].airplane_t_off_ms': schema.INT,
Pau Espin Pedrolbc1ed882018-05-17 16:59:58 +0200116 'osmocon_phone[].serial_device': schema.STR,
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200117 }
118
119WANT_SCHEMA = util.dict_add(
Pau Espin Pedrolead79ac2017-09-12 15:19:18 +0200120 dict([('%s[].times' % r, schema.TIMES) for r in R_ALL]),
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200121 RESOURCES_SCHEMA)
122
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200123CONF_SCHEMA = util.dict_add(
Pau Espin Pedrol5dc24592018-08-27 12:53:41 +0200124 { 'defaults.timeout': schema.STR,
Pau Espin Pedrolb6937712020-02-27 18:02:20 +0100125 'config.bsc.net.codec_list[]': schema.CODEC,
Pau Espin Pedrol1e81b5a2020-03-16 12:42:17 +0100126 'config.enb.enable_pcap': schema.BOOL_STR,
Pau Espin Pedrolda2e31f2020-03-31 13:45:01 +0200127 'config.epc.type': schema.STR,
Pau Espin Pedrol04ad3b52020-04-06 12:25:22 +0200128 'config.epc.qci': schema.UINT,
Pau Espin Pedrol1e81b5a2020-03-16 12:42:17 +0100129 'config.epc.enable_pcap': schema.BOOL_STR,
130 'config.modem.enable_pcap': schema.BOOL_STR,
Pau Espin Pedrolc04528c2020-04-01 13:55:51 +0200131 'config.amarisoft.license_server_addr': schema.IPV4,
Andre Puschmann2dcc4312020-03-28 15:34:00 +0100132 'config.iperf3cli.time': schema.DURATION,
Pau Espin Pedrol1e81b5a2020-03-16 12:42:17 +0100133 },
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200134 dict([('resources.%s' % key, val) for key, val in WANT_SCHEMA.items()]),
135 dict([('modifiers.%s' % key, val) for key, val in WANT_SCHEMA.items()]))
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200136
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200137KNOWN_BTS_TYPES = {
Your Name44af3412017-04-13 03:11:59 +0200138 'osmo-bts-sysmo': bts_sysmo.SysmoBts,
139 'osmo-bts-trx': bts_osmotrx.OsmoBtsTrx,
Pau Espin Pedrolfeb66e72019-03-04 18:37:04 +0100140 'osmo-bts-oc2g': bts_oc2g.OsmoBtsOC2G,
Pau Espin Pedroldaed4472017-09-15 14:11:35 +0200141 'osmo-bts-octphy': bts_octphy.OsmoBtsOctphy,
Holger Hans Peter Freytherd2db10d2018-12-06 18:34:53 +0000142 'osmo-bts-virtual': bts_osmovirtual.OsmoBtsVirtual,
Pau Espin Pedrol1b28a582018-03-08 21:01:26 +0100143 'nanobts': bts_nanobts.NanoBts,
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200144 }
145
Pau Espin Pedrolc8b0f932020-02-11 17:45:26 +0100146KNOWN_ENB_TYPES = {
147 'srsenb': srs_enb.srsENB,
Pau Espin Pedrol786a6bc2020-03-30 13:51:21 +0200148 'amarisoftenb': amarisoft_enb.AmarisoftENB,
Pau Espin Pedrolc8b0f932020-02-11 17:45:26 +0100149}
Holger Hans Peter Freyther74243012019-02-27 08:18:38 +0000150
Pau Espin Pedrolda2e31f2020-03-31 13:45:01 +0200151KNOWN_EPC_TYPES = {
152 'srsepc': srs_epc.srsEPC,
153 'amarisoftepc': amarisoft_epc.AmarisoftEPC,
154}
155
Holger Hans Peter Freyther74243012019-02-27 08:18:38 +0000156KNOWN_MS_TYPES = {
157 # Map None to ofono for forward compability
158 None: modem.Modem,
159 'ofono': modem.Modem,
Holger Hans Peter Freyther10501cc2019-02-25 03:20:16 +0000160 'osmo-mobile': ms_osmo_mobile.MSOsmoMobile,
Pau Espin Pedrolc8b0f932020-02-11 17:45:26 +0100161 'srsue': srs_ue.srsUE,
Pau Espin Pedrol65beb8f2020-03-31 12:03:19 +0200162 'amarisoftue': amarisoft_ue.AmarisoftUE,
Holger Hans Peter Freyther74243012019-02-27 08:18:38 +0000163}
164
165
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200166def register_bts_type(name, clazz):
167 KNOWN_BTS_TYPES[name] = clazz
168
169class ResourcesPool(log.Origin):
170 _remember_to_free = None
171 _registered_exit_handler = False
172
173 def __init__(self):
Pau Espin Pedrol66a38912020-03-11 20:11:08 +0100174 self.config_path = config.get_config_file(config.RESOURCES_CONF)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200175 self.state_dir = config.get_state_dir()
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200176 super().__init__(log.C_CNF, conf=self.config_path, state=self.state_dir.path)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200177 self.read_conf()
178
179 def read_conf(self):
180 self.all_resources = Resources(config.read(self.config_path, RESOURCES_SCHEMA))
181 self.all_resources.set_hashes()
182
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200183 def reserve(self, origin, want, modifiers):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200184 '''
185 attempt to reserve the resources specified in the dict 'want' for
186 'origin'. Obtain a lock on the resources lock dir, verify that all
187 wanted resources are available, and if yes mark them as reserved.
188
189 On success, return a reservation object which can be used to release
190 the reservation. The reservation will be freed automatically on program
191 exit, if not yet done manually.
192
193 'origin' should be an Origin() instance.
194
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200195 'want' is a dict matching RESOURCES_SCHEMA, used to specify what to
196 reserve.
197
198 'modifiers' is a dict matching RESOURCES_SCHEMA, it is overlaid on top
199 of 'want'.
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200200
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200201 If an entry has no attribute set, any of the resources may be
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200202 reserved without further limitations.
203
204 ResourcesPool may also be selected with narrowed down constraints.
Neels Hofmeyr76d81032017-05-18 18:35:32 +0200205 This would reserve one IP address, two modems, one BTS of type
Neels Hofmeyr391afe32017-05-18 19:22:12 +0200206 sysmo and one of type trx, plus 2 ARFCNs in the 1800 band:
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200207
208 {
Neels Hofmeyr76d81032017-05-18 18:35:32 +0200209 'ip_address': [ {} ],
Neels Hofmeyr391afe32017-05-18 19:22:12 +0200210 'bts': [ { 'type': 'sysmo' }, { 'type': 'trx' } ],
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200211 'arfcn': [ { 'band': 'GSM-1800' }, { 'band': 'GSM-1800' } ],
212 'modem': [ {}, {} ],
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200213 }
214 '''
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200215 schema.validate(want, RESOURCES_SCHEMA)
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200216 schema.validate(modifiers, RESOURCES_SCHEMA)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200217
218 origin_id = origin.origin_id()
219
220 with self.state_dir.lock(origin_id):
221 rrfile_path = self.state_dir.mk_parentdir(RESERVED_RESOURCES_FILE)
222 reserved = Resources(config.read(rrfile_path, if_missing_return={}))
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200223 to_be_reserved = self.all_resources.without(reserved).find(origin, want)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200224
225 to_be_reserved.mark_reserved_by(origin_id)
226
227 reserved.add(to_be_reserved)
228 config.write(rrfile_path, reserved)
229
230 self.remember_to_free(to_be_reserved)
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200231 return ReservedResources(self, origin, to_be_reserved, modifiers)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200232
233 def free(self, origin, to_be_freed):
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200234 log.ctx(origin)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200235 with self.state_dir.lock(origin.origin_id()):
236 rrfile_path = self.state_dir.mk_parentdir(RESERVED_RESOURCES_FILE)
237 reserved = Resources(config.read(rrfile_path, if_missing_return={}))
238 reserved.drop(to_be_freed)
239 config.write(rrfile_path, reserved)
240 self.forget_freed(to_be_freed)
241
242 def register_exit_handler(self):
243 if self._registered_exit_handler:
244 return
245 atexit.register(self.clean_up_registered_resources)
246 self._registered_exit_handler = True
247
248 def unregister_exit_handler(self):
249 if not self._registered_exit_handler:
250 return
251 atexit.unregister(self.clean_up_registered_resources)
252 self._registered_exit_handler = False
253
254 def clean_up_registered_resources(self):
255 if not self._remember_to_free:
256 return
257 self.free(log.Origin('atexit.clean_up_registered_resources()'),
258 self._remember_to_free)
259
260 def remember_to_free(self, to_be_reserved):
261 self.register_exit_handler()
262 if not self._remember_to_free:
263 self._remember_to_free = Resources()
264 self._remember_to_free.add(to_be_reserved)
265
266 def forget_freed(self, freed):
267 if freed is self._remember_to_free:
268 self._remember_to_free.clear()
269 else:
270 self._remember_to_free.drop(freed)
271 if not self._remember_to_free:
272 self.unregister_exit_handler()
273
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100274 def next_persistent_value(self, token, first_val, validate_func, inc_func, origin):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200275 origin_id = origin.origin_id()
276
277 with self.state_dir.lock(origin_id):
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100278 token_path = self.state_dir.child('last_used_%s.state' % token)
279 log.ctx(token_path)
280 last_value = first_val
281 if os.path.exists(token_path):
282 if not os.path.isfile(token_path):
283 raise RuntimeError('path should be a file but is not: %r' % token_path)
284 with open(token_path, 'r') as f:
285 last_value = f.read().strip()
286 validate_func(last_value)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200287
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100288 next_value = inc_func(last_value)
289 with open(token_path, 'w') as f:
290 f.write(next_value)
291 return next_value
292
293 def next_msisdn(self, origin):
294 return self.next_persistent_value('msisdn', '1000', schema.msisdn, util.msisdn_inc, origin)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200295
Pau Espin Pedrol5e0c2512017-11-06 18:40:23 +0100296 def next_lac(self, origin):
Pau Espin Pedrolf7f06362017-11-28 12:56:35 +0100297 # LAC=0 has special meaning (MS detached), avoid it
298 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 +0200299
Pau Espin Pedrol8a3a7b52017-11-28 15:50:02 +0100300 def next_rac(self, origin):
301 return self.next_persistent_value('rac', '1', schema.uint8, lambda x: str((int(x)+1) % pow(2,8) or 1), origin)
302
Pau Espin Pedrol4ccce7c2017-11-07 11:13:20 +0100303 def next_cellid(self, origin):
304 return self.next_persistent_value('cellid', '1', schema.uint16, lambda x: str((int(x)+1) % pow(2,16)), origin)
305
Pau Espin Pedrol8a3a7b52017-11-28 15:50:02 +0100306 def next_bvci(self, origin):
307 # BVCI=0 and =1 are reserved, avoid them.
308 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)
309
Pau Espin Pedrol4676cbd2017-09-14 17:35:03 +0200310class NoResourceExn(log.Error):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200311 pass
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200312
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200313class Resources(dict):
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200314
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200315 def __init__(self, all_resources={}, do_copy=True):
316 if do_copy:
317 all_resources = copy.deepcopy(all_resources)
318 self.update(all_resources)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200319
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200320 def drop(self, reserved, fail_if_not_found=True):
321 # protect from modifying reserved because we're the same object
322 if reserved is self:
323 raise RuntimeError('Refusing to drop a list of resources from itself.'
324 ' This is probably a bug where a list of Resources()'
325 ' should have been copied but is passed as-is.'
326 ' use Resources.clear() instead.')
327
328 for key, reserved_list in reserved.items():
329 my_list = self.get(key) or []
330
331 if my_list is reserved_list:
332 self.pop(key)
333 continue
334
335 for reserved_item in reserved_list:
336 found = False
337 reserved_hash = reserved_item.get(HASH_KEY)
338 if not reserved_hash:
339 raise RuntimeError('Resources.drop() only works with hashed items')
340
341 for i in range(len(my_list)):
342 my_item = my_list[i]
343 my_hash = my_item.get(HASH_KEY)
344 if not my_hash:
345 raise RuntimeError('Resources.drop() only works with hashed items')
346 if my_hash == reserved_hash:
347 found = True
348 my_list.pop(i)
349 break
350
351 if fail_if_not_found and not found:
352 raise RuntimeError('Asked to drop resource from a pool, but the'
353 ' resource was not found: %s = %r' % (key, reserved_item))
354
355 if not my_list:
356 self.pop(key)
357 return self
358
359 def without(self, reserved):
360 return Resources(self).drop(reserved)
361
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200362 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 +0200363 '''
364 Pass a dict of resource requirements, e.g.:
365 want = {
366 'bts': [ {'type': 'osmo-bts-sysmo',}, {} ],
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200367 'modem': [ {}, {}, {} ]
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200368 }
369 This function tries to find a combination from the available resources that
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200370 matches these requirements. The return value is a dict (wrapped in a Resources class)
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200371 that contains the matching resources in the order of 'want' dict: in above
372 example, the returned dict would have a 'bts' list with the first item being
373 a sysmoBTS, the second item being any other available BTS.
374
375 If skip_if_marked is passed, any resource that contains this key is skipped.
376 E.g. if a BTS has the USED_KEY set like
377 reserved_resources = { 'bts' : {..., '_used': True} }
378 then this may be skipped by passing skip_if_marked='_used'
379 (or rather skip_if_marked=USED_KEY).
380
381 If do_copy is True, the returned dict is a deep copy and does not share
382 lists with any other Resources dict.
383
384 If raise_if_missing is False, this will return an empty item for any
385 resource that had no match, instead of immediately raising an exception.
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200386
387 This function expects input dictionaries whose contents have already
388 been replicated based on its the 'times' attributes. See
389 config.replicate_times() for more details.
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200390 '''
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200391 matches = {}
Neels Hofmeyr17c139e2017-04-12 02:42:02 +0200392 for key, want_list in sorted(want.items()): # sorted for deterministic test results
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200393 # here we have a resource of a given type, e.g. 'bts', with a list
394 # containing as many BTSes as the caller wants to reserve/use. Each
395 # list item contains specifics for the particular BTS.
Neels Hofmeyr2a1a1fa2017-05-29 01:36:21 +0200396 my_list = self.get(key, [])
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200397
Neels Hofmeyrf9e86932017-06-06 19:47:40 +0200398 if log_label:
399 for_origin.log(log_label, len(want_list), 'x', key, '(candidates: %d)'%len(my_list))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200400
401 # Try to avoid a less constrained item snatching away a resource
402 # from a more detailed constrained requirement.
403
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200404 # first record all matches, so that each requested item has a list
405 # of all available resources that match it. Some resources may
406 # appear for multiple requested items. Store matching indexes.
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200407 all_matches = []
408 for want_item in want_list:
409 item_match_list = []
410 for i in range(len(my_list)):
411 my_item = my_list[i]
412 if skip_if_marked and my_item.get(skip_if_marked):
413 continue
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200414 if item_matches(my_item, want_item):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200415 item_match_list.append(i)
416 if not item_match_list:
Neels Hofmeyr9b907702017-05-06 23:20:33 +0200417 if raise_if_missing:
418 raise NoResourceExn('No matching resource available for %s = %r'
419 % (key, want_item))
420 else:
421 # this one failed... see below
422 all_matches = []
423 break
424
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200425 all_matches.append( item_match_list )
426
427 if not all_matches:
Neels Hofmeyr9b907702017-05-06 23:20:33 +0200428 # ...this one failed. Makes no sense to solve resource
429 # allocations, return an empty list for this key to mark
430 # failure.
431 matches[key] = []
432 continue
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200433
434 # figure out who gets what
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200435 try:
436 solution = solve(all_matches)
437 except NotSolvable:
438 # instead of a cryptic error message, raise an exception that
439 # conveys meaning to the user.
440 raise NoResourceExn('Could not resolve request to reserve resources: '
441 '%d x %s with requirements: %r' % (len(want_list), key, want_list))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200442 picked = [ my_list[i] for i in solution if i is not None ]
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200443 for_origin.dbg('Picked', config.tostr(picked))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200444 matches[key] = picked
445
446 return Resources(matches, do_copy=do_copy)
447
448 def set_hashes(self):
449 for key, item_list in self.items():
450 for item in item_list:
451 item[HASH_KEY] = util.hash_obj(item, HASH_KEY, RESERVED_KEY, USED_KEY)
452
453 def add(self, more):
454 if more is self:
455 raise RuntimeError('adding a list of resources to itself?')
456 config.add(self, copy.deepcopy(more))
457
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200458 def mark_reserved_by(self, origin_id):
459 for key, item_list in self.items():
460 for item in item_list:
461 item[RESERVED_KEY] = origin_id
462
463
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200464class NotSolvable(Exception):
465 pass
466
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200467def solve(all_matches):
468 '''
469 all_matches shall be a list of index-lists.
470 all_matches[i] is the list of indexes that item i can use.
471 Return a solution so that each i gets a different index.
472 solve([ [0, 1, 2],
473 [0],
474 [0, 2] ]) == [1, 0, 2]
475 '''
476
477 def all_differ(l):
478 return len(set(l)) == len(l)
479
480 def search_in_permutations(fixed=[]):
481 idx = len(fixed)
482 for i in range(len(all_matches[idx])):
483 val = all_matches[idx][i]
484 # don't add a val that's already in the list
485 if val in fixed:
486 continue
487 l = list(fixed)
488 l.append(val)
489 if len(l) == len(all_matches):
490 # found a solution
491 return l
492 # not at the end yet, add next digit
493 r = search_in_permutations(l)
494 if r:
495 # nested search_in_permutations() call found a solution
496 return r
497 # this entire branch yielded no solution
498 return None
499
500 if not all_matches:
501 raise RuntimeError('Cannot solve: no candidates')
502
503 solution = search_in_permutations()
504 if not solution:
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200505 raise NotSolvable('The requested resource requirements are not solvable %r'
506 % all_matches)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200507 return solution
508
509
510def contains_hash(list_of_dicts, a_hash):
511 for d in list_of_dicts:
512 if d.get(HASH_KEY) == a_hash:
513 return True
514 return False
515
516def item_matches(item, wanted_item, ignore_keys=None):
517 if is_dict(wanted_item):
518 # match up two dicts
519 if not isinstance(item, dict):
520 return False
521 for key, wanted_val in wanted_item.items():
522 if ignore_keys and key in ignore_keys:
523 continue
524 if not item_matches(item.get(key), wanted_val, ignore_keys=ignore_keys):
525 return False
526 return True
527
528 if is_list(wanted_item):
Pau Espin Pedrol4e36f7c2017-08-28 13:29:28 +0200529 if not is_list(item):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200530 return False
Pau Espin Pedrol58475512017-09-14 15:33:15 +0200531 # Validate that all elements in both lists are of the same type:
532 t = util.list_validate_same_elem_type(wanted_item + item)
533 if t is None:
534 return True # both lists are empty, return
535 # For lists of complex objects, we expect them to be sorted lists:
536 if t in (dict, list, tuple):
537 for i in range(max(len(wanted_item), len(item))):
538 log.ctx(idx=i)
539 subitem = item[i] if i < len(item) else util.empty_instance_type(t)
540 wanted_subitem = wanted_item[i] if i < len(wanted_item) else util.empty_instance_type(t)
541 if not item_matches(subitem, wanted_subitem, ignore_keys=ignore_keys):
542 return False
543 else: # for lists of basic elements, we handle them as unsorted sets:
544 for val in wanted_item:
545 if val not in item:
546 return False
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200547 return True
548
549 return item == wanted_item
550
551
552class ReservedResources(log.Origin):
553 '''
554 After all resources have been figured out, this is the API that a test case
555 gets to interact with resources. From those resources that have been
556 reserved for it, it can pick some to mark them as currently in use.
557 Functions like nitb() provide a resource by automatically picking its
558 dependencies from so far unused (but reserved) resource.
559 '''
560
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200561 def __init__(self, resources_pool, origin, reserved, modifiers):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200562 self.resources_pool = resources_pool
563 self.origin = origin
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200564 self.reserved_original = reserved
565 self.reserved = copy.deepcopy(self.reserved_original)
566 config.overlay(self.reserved, modifiers)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200567
568 def __repr__(self):
569 return 'resources(%s)=%s' % (self.origin.name(), pprint.pformat(self.reserved))
570
571 def get(self, kind, specifics=None):
572 if specifics is None:
573 specifics = {}
574 self.dbg('requesting use of', kind, specifics=specifics)
575 want = { kind: [specifics] }
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200576 available_dict = self.reserved.find(self.origin, want, skip_if_marked=USED_KEY,
577 do_copy=False, raise_if_missing=False,
578 log_label='Using')
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200579 available = available_dict.get(kind)
580 self.dbg(available=len(available))
581 if not available:
Neels Hofmeyrf9e86932017-06-06 19:47:40 +0200582 # cook up a detailed error message for the current situation
583 kind_reserved = self.reserved.get(kind, [])
584 used_count = len([r for r in kind_reserved if USED_KEY in r])
585 matching = self.reserved.find(self.origin, want, raise_if_missing=False, log_label=None).get(kind, [])
586 if not matching:
587 msg = 'none of the reserved resources matches requirements %r' % specifics
588 elif not (used_count < len(kind_reserved)):
589 msg = 'suite.conf reserved only %d x %r.' % (len(kind_reserved), kind)
590 else:
591 msg = ('No unused resource left that matches the requirements;'
592 ' Of reserved %d x %r, %d match the requirements, but all are already in use;'
593 ' Requirements: %r'
594 % (len(kind_reserved), kind, len(matching), specifics))
595 raise NoResourceExn('When trying to use instance nr %d of %r: %s' % (used_count + 1, kind, msg))
596
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200597 pick = available[0]
598 self.dbg(using=pick)
599 assert not pick.get(USED_KEY)
600 pick[USED_KEY] = True
601 return copy.deepcopy(pick)
602
603 def put(self, item):
604 if not item.get(USED_KEY):
605 raise RuntimeError('Can only put() a resource that is used: %r' % item)
606 hash_to_put = item.get(HASH_KEY)
607 if not hash_to_put:
608 raise RuntimeError('Can only put() a resource that has a hash marker: %r' % item)
609 for key, item_list in self.reserved.items():
610 my_list = self.get(key)
611 for my_item in my_list:
612 if hash_to_put == my_item.get(HASH_KEY):
613 my_item.pop(USED_KEY)
614
615 def put_all(self):
Pau Espin Pedrol1dd29552017-06-13 18:07:57 +0200616 if not self.reserved:
617 return
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200618 for key, item_list in self.reserved.items():
Pau Espin Pedrol1dd29552017-06-13 18:07:57 +0200619 for item in item_list:
620 item.pop(USED_KEY, None)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200621
622 def free(self):
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200623 if self.reserved_original:
624 self.resources_pool.free(self.origin, self.reserved_original)
625 self.reserved_original = None
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200626
Neels Hofmeyr2d1d5612017-05-22 20:02:41 +0200627 def counts(self):
628 counts = {}
629 for key in self.reserved.keys():
630 counts[key] = self.count(key)
631 return counts
632
633 def count(self, key):
634 return len(self.reserved.get(key) or [])
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200635
636# vim: expandtab tabstop=4 shiftwidth=4