blob: c69617cd955fb97c1d5909ded8f0fdc6e96913db [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 Pedrolda2e31f2020-03-31 13:45:01 +020032from . import srs_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 Puschmannd61613a2020-03-24 12:05:05 +0100112 'modem[].num_carriers': schema.STR,
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 Pedrol1e81b5a2020-03-16 12:42:17 +0100128 'config.epc.rlc_drb_mode': schema.LTE_RLC_DRB_MODE,
129 '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,
Holger Hans Peter Freyther74243012019-02-27 08:18:38 +0000162}
163
164
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200165def register_bts_type(name, clazz):
166 KNOWN_BTS_TYPES[name] = clazz
167
168class ResourcesPool(log.Origin):
169 _remember_to_free = None
170 _registered_exit_handler = False
171
172 def __init__(self):
Pau Espin Pedrol66a38912020-03-11 20:11:08 +0100173 self.config_path = config.get_config_file(config.RESOURCES_CONF)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200174 self.state_dir = config.get_state_dir()
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200175 super().__init__(log.C_CNF, conf=self.config_path, state=self.state_dir.path)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200176 self.read_conf()
177
178 def read_conf(self):
179 self.all_resources = Resources(config.read(self.config_path, RESOURCES_SCHEMA))
180 self.all_resources.set_hashes()
181
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200182 def reserve(self, origin, want, modifiers):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200183 '''
184 attempt to reserve the resources specified in the dict 'want' for
185 'origin'. Obtain a lock on the resources lock dir, verify that all
186 wanted resources are available, and if yes mark them as reserved.
187
188 On success, return a reservation object which can be used to release
189 the reservation. The reservation will be freed automatically on program
190 exit, if not yet done manually.
191
192 'origin' should be an Origin() instance.
193
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200194 'want' is a dict matching RESOURCES_SCHEMA, used to specify what to
195 reserve.
196
197 'modifiers' is a dict matching RESOURCES_SCHEMA, it is overlaid on top
198 of 'want'.
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200199
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200200 If an entry has no attribute set, any of the resources may be
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200201 reserved without further limitations.
202
203 ResourcesPool may also be selected with narrowed down constraints.
Neels Hofmeyr76d81032017-05-18 18:35:32 +0200204 This would reserve one IP address, two modems, one BTS of type
Neels Hofmeyr391afe32017-05-18 19:22:12 +0200205 sysmo and one of type trx, plus 2 ARFCNs in the 1800 band:
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200206
207 {
Neels Hofmeyr76d81032017-05-18 18:35:32 +0200208 'ip_address': [ {} ],
Neels Hofmeyr391afe32017-05-18 19:22:12 +0200209 'bts': [ { 'type': 'sysmo' }, { 'type': 'trx' } ],
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200210 'arfcn': [ { 'band': 'GSM-1800' }, { 'band': 'GSM-1800' } ],
211 'modem': [ {}, {} ],
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200212 }
213 '''
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200214 schema.validate(want, RESOURCES_SCHEMA)
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200215 schema.validate(modifiers, RESOURCES_SCHEMA)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200216
217 origin_id = origin.origin_id()
218
219 with self.state_dir.lock(origin_id):
220 rrfile_path = self.state_dir.mk_parentdir(RESERVED_RESOURCES_FILE)
221 reserved = Resources(config.read(rrfile_path, if_missing_return={}))
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200222 to_be_reserved = self.all_resources.without(reserved).find(origin, want)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200223
224 to_be_reserved.mark_reserved_by(origin_id)
225
226 reserved.add(to_be_reserved)
227 config.write(rrfile_path, reserved)
228
229 self.remember_to_free(to_be_reserved)
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200230 return ReservedResources(self, origin, to_be_reserved, modifiers)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200231
232 def free(self, origin, to_be_freed):
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200233 log.ctx(origin)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200234 with self.state_dir.lock(origin.origin_id()):
235 rrfile_path = self.state_dir.mk_parentdir(RESERVED_RESOURCES_FILE)
236 reserved = Resources(config.read(rrfile_path, if_missing_return={}))
237 reserved.drop(to_be_freed)
238 config.write(rrfile_path, reserved)
239 self.forget_freed(to_be_freed)
240
241 def register_exit_handler(self):
242 if self._registered_exit_handler:
243 return
244 atexit.register(self.clean_up_registered_resources)
245 self._registered_exit_handler = True
246
247 def unregister_exit_handler(self):
248 if not self._registered_exit_handler:
249 return
250 atexit.unregister(self.clean_up_registered_resources)
251 self._registered_exit_handler = False
252
253 def clean_up_registered_resources(self):
254 if not self._remember_to_free:
255 return
256 self.free(log.Origin('atexit.clean_up_registered_resources()'),
257 self._remember_to_free)
258
259 def remember_to_free(self, to_be_reserved):
260 self.register_exit_handler()
261 if not self._remember_to_free:
262 self._remember_to_free = Resources()
263 self._remember_to_free.add(to_be_reserved)
264
265 def forget_freed(self, freed):
266 if freed is self._remember_to_free:
267 self._remember_to_free.clear()
268 else:
269 self._remember_to_free.drop(freed)
270 if not self._remember_to_free:
271 self.unregister_exit_handler()
272
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100273 def next_persistent_value(self, token, first_val, validate_func, inc_func, origin):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200274 origin_id = origin.origin_id()
275
276 with self.state_dir.lock(origin_id):
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100277 token_path = self.state_dir.child('last_used_%s.state' % token)
278 log.ctx(token_path)
279 last_value = first_val
280 if os.path.exists(token_path):
281 if not os.path.isfile(token_path):
282 raise RuntimeError('path should be a file but is not: %r' % token_path)
283 with open(token_path, 'r') as f:
284 last_value = f.read().strip()
285 validate_func(last_value)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200286
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100287 next_value = inc_func(last_value)
288 with open(token_path, 'w') as f:
289 f.write(next_value)
290 return next_value
291
292 def next_msisdn(self, origin):
293 return self.next_persistent_value('msisdn', '1000', schema.msisdn, util.msisdn_inc, origin)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200294
Pau Espin Pedrol5e0c2512017-11-06 18:40:23 +0100295 def next_lac(self, origin):
Pau Espin Pedrolf7f06362017-11-28 12:56:35 +0100296 # LAC=0 has special meaning (MS detached), avoid it
297 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 +0200298
Pau Espin Pedrol8a3a7b52017-11-28 15:50:02 +0100299 def next_rac(self, origin):
300 return self.next_persistent_value('rac', '1', schema.uint8, lambda x: str((int(x)+1) % pow(2,8) or 1), origin)
301
Pau Espin Pedrol4ccce7c2017-11-07 11:13:20 +0100302 def next_cellid(self, origin):
303 return self.next_persistent_value('cellid', '1', schema.uint16, lambda x: str((int(x)+1) % pow(2,16)), origin)
304
Pau Espin Pedrol8a3a7b52017-11-28 15:50:02 +0100305 def next_bvci(self, origin):
306 # BVCI=0 and =1 are reserved, avoid them.
307 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)
308
Pau Espin Pedrol4676cbd2017-09-14 17:35:03 +0200309class NoResourceExn(log.Error):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200310 pass
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200311
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200312class Resources(dict):
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200313
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200314 def __init__(self, all_resources={}, do_copy=True):
315 if do_copy:
316 all_resources = copy.deepcopy(all_resources)
317 self.update(all_resources)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200318
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200319 def drop(self, reserved, fail_if_not_found=True):
320 # protect from modifying reserved because we're the same object
321 if reserved is self:
322 raise RuntimeError('Refusing to drop a list of resources from itself.'
323 ' This is probably a bug where a list of Resources()'
324 ' should have been copied but is passed as-is.'
325 ' use Resources.clear() instead.')
326
327 for key, reserved_list in reserved.items():
328 my_list = self.get(key) or []
329
330 if my_list is reserved_list:
331 self.pop(key)
332 continue
333
334 for reserved_item in reserved_list:
335 found = False
336 reserved_hash = reserved_item.get(HASH_KEY)
337 if not reserved_hash:
338 raise RuntimeError('Resources.drop() only works with hashed items')
339
340 for i in range(len(my_list)):
341 my_item = my_list[i]
342 my_hash = my_item.get(HASH_KEY)
343 if not my_hash:
344 raise RuntimeError('Resources.drop() only works with hashed items')
345 if my_hash == reserved_hash:
346 found = True
347 my_list.pop(i)
348 break
349
350 if fail_if_not_found and not found:
351 raise RuntimeError('Asked to drop resource from a pool, but the'
352 ' resource was not found: %s = %r' % (key, reserved_item))
353
354 if not my_list:
355 self.pop(key)
356 return self
357
358 def without(self, reserved):
359 return Resources(self).drop(reserved)
360
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200361 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 +0200362 '''
363 Pass a dict of resource requirements, e.g.:
364 want = {
365 'bts': [ {'type': 'osmo-bts-sysmo',}, {} ],
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200366 'modem': [ {}, {}, {} ]
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200367 }
368 This function tries to find a combination from the available resources that
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200369 matches these requirements. The return value is a dict (wrapped in a Resources class)
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200370 that contains the matching resources in the order of 'want' dict: in above
371 example, the returned dict would have a 'bts' list with the first item being
372 a sysmoBTS, the second item being any other available BTS.
373
374 If skip_if_marked is passed, any resource that contains this key is skipped.
375 E.g. if a BTS has the USED_KEY set like
376 reserved_resources = { 'bts' : {..., '_used': True} }
377 then this may be skipped by passing skip_if_marked='_used'
378 (or rather skip_if_marked=USED_KEY).
379
380 If do_copy is True, the returned dict is a deep copy and does not share
381 lists with any other Resources dict.
382
383 If raise_if_missing is False, this will return an empty item for any
384 resource that had no match, instead of immediately raising an exception.
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200385
386 This function expects input dictionaries whose contents have already
387 been replicated based on its the 'times' attributes. See
388 config.replicate_times() for more details.
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200389 '''
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200390 matches = {}
Neels Hofmeyr17c139e2017-04-12 02:42:02 +0200391 for key, want_list in sorted(want.items()): # sorted for deterministic test results
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200392 # here we have a resource of a given type, e.g. 'bts', with a list
393 # containing as many BTSes as the caller wants to reserve/use. Each
394 # list item contains specifics for the particular BTS.
Neels Hofmeyr2a1a1fa2017-05-29 01:36:21 +0200395 my_list = self.get(key, [])
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200396
Neels Hofmeyrf9e86932017-06-06 19:47:40 +0200397 if log_label:
398 for_origin.log(log_label, len(want_list), 'x', key, '(candidates: %d)'%len(my_list))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200399
400 # Try to avoid a less constrained item snatching away a resource
401 # from a more detailed constrained requirement.
402
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200403 # first record all matches, so that each requested item has a list
404 # of all available resources that match it. Some resources may
405 # appear for multiple requested items. Store matching indexes.
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200406 all_matches = []
407 for want_item in want_list:
408 item_match_list = []
409 for i in range(len(my_list)):
410 my_item = my_list[i]
411 if skip_if_marked and my_item.get(skip_if_marked):
412 continue
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200413 if item_matches(my_item, want_item):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200414 item_match_list.append(i)
415 if not item_match_list:
Neels Hofmeyr9b907702017-05-06 23:20:33 +0200416 if raise_if_missing:
417 raise NoResourceExn('No matching resource available for %s = %r'
418 % (key, want_item))
419 else:
420 # this one failed... see below
421 all_matches = []
422 break
423
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200424 all_matches.append( item_match_list )
425
426 if not all_matches:
Neels Hofmeyr9b907702017-05-06 23:20:33 +0200427 # ...this one failed. Makes no sense to solve resource
428 # allocations, return an empty list for this key to mark
429 # failure.
430 matches[key] = []
431 continue
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200432
433 # figure out who gets what
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200434 try:
435 solution = solve(all_matches)
436 except NotSolvable:
437 # instead of a cryptic error message, raise an exception that
438 # conveys meaning to the user.
439 raise NoResourceExn('Could not resolve request to reserve resources: '
440 '%d x %s with requirements: %r' % (len(want_list), key, want_list))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200441 picked = [ my_list[i] for i in solution if i is not None ]
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200442 for_origin.dbg('Picked', config.tostr(picked))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200443 matches[key] = picked
444
445 return Resources(matches, do_copy=do_copy)
446
447 def set_hashes(self):
448 for key, item_list in self.items():
449 for item in item_list:
450 item[HASH_KEY] = util.hash_obj(item, HASH_KEY, RESERVED_KEY, USED_KEY)
451
452 def add(self, more):
453 if more is self:
454 raise RuntimeError('adding a list of resources to itself?')
455 config.add(self, copy.deepcopy(more))
456
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200457 def mark_reserved_by(self, origin_id):
458 for key, item_list in self.items():
459 for item in item_list:
460 item[RESERVED_KEY] = origin_id
461
462
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200463class NotSolvable(Exception):
464 pass
465
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200466def solve(all_matches):
467 '''
468 all_matches shall be a list of index-lists.
469 all_matches[i] is the list of indexes that item i can use.
470 Return a solution so that each i gets a different index.
471 solve([ [0, 1, 2],
472 [0],
473 [0, 2] ]) == [1, 0, 2]
474 '''
475
476 def all_differ(l):
477 return len(set(l)) == len(l)
478
479 def search_in_permutations(fixed=[]):
480 idx = len(fixed)
481 for i in range(len(all_matches[idx])):
482 val = all_matches[idx][i]
483 # don't add a val that's already in the list
484 if val in fixed:
485 continue
486 l = list(fixed)
487 l.append(val)
488 if len(l) == len(all_matches):
489 # found a solution
490 return l
491 # not at the end yet, add next digit
492 r = search_in_permutations(l)
493 if r:
494 # nested search_in_permutations() call found a solution
495 return r
496 # this entire branch yielded no solution
497 return None
498
499 if not all_matches:
500 raise RuntimeError('Cannot solve: no candidates')
501
502 solution = search_in_permutations()
503 if not solution:
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200504 raise NotSolvable('The requested resource requirements are not solvable %r'
505 % all_matches)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200506 return solution
507
508
509def contains_hash(list_of_dicts, a_hash):
510 for d in list_of_dicts:
511 if d.get(HASH_KEY) == a_hash:
512 return True
513 return False
514
515def item_matches(item, wanted_item, ignore_keys=None):
516 if is_dict(wanted_item):
517 # match up two dicts
518 if not isinstance(item, dict):
519 return False
520 for key, wanted_val in wanted_item.items():
521 if ignore_keys and key in ignore_keys:
522 continue
523 if not item_matches(item.get(key), wanted_val, ignore_keys=ignore_keys):
524 return False
525 return True
526
527 if is_list(wanted_item):
Pau Espin Pedrol4e36f7c2017-08-28 13:29:28 +0200528 if not is_list(item):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200529 return False
Pau Espin Pedrol58475512017-09-14 15:33:15 +0200530 # Validate that all elements in both lists are of the same type:
531 t = util.list_validate_same_elem_type(wanted_item + item)
532 if t is None:
533 return True # both lists are empty, return
534 # For lists of complex objects, we expect them to be sorted lists:
535 if t in (dict, list, tuple):
536 for i in range(max(len(wanted_item), len(item))):
537 log.ctx(idx=i)
538 subitem = item[i] if i < len(item) else util.empty_instance_type(t)
539 wanted_subitem = wanted_item[i] if i < len(wanted_item) else util.empty_instance_type(t)
540 if not item_matches(subitem, wanted_subitem, ignore_keys=ignore_keys):
541 return False
542 else: # for lists of basic elements, we handle them as unsorted sets:
543 for val in wanted_item:
544 if val not in item:
545 return False
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200546 return True
547
548 return item == wanted_item
549
550
551class ReservedResources(log.Origin):
552 '''
553 After all resources have been figured out, this is the API that a test case
554 gets to interact with resources. From those resources that have been
555 reserved for it, it can pick some to mark them as currently in use.
556 Functions like nitb() provide a resource by automatically picking its
557 dependencies from so far unused (but reserved) resource.
558 '''
559
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200560 def __init__(self, resources_pool, origin, reserved, modifiers):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200561 self.resources_pool = resources_pool
562 self.origin = origin
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200563 self.reserved_original = reserved
564 self.reserved = copy.deepcopy(self.reserved_original)
565 config.overlay(self.reserved, modifiers)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200566
567 def __repr__(self):
568 return 'resources(%s)=%s' % (self.origin.name(), pprint.pformat(self.reserved))
569
570 def get(self, kind, specifics=None):
571 if specifics is None:
572 specifics = {}
573 self.dbg('requesting use of', kind, specifics=specifics)
574 want = { kind: [specifics] }
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200575 available_dict = self.reserved.find(self.origin, want, skip_if_marked=USED_KEY,
576 do_copy=False, raise_if_missing=False,
577 log_label='Using')
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200578 available = available_dict.get(kind)
579 self.dbg(available=len(available))
580 if not available:
Neels Hofmeyrf9e86932017-06-06 19:47:40 +0200581 # cook up a detailed error message for the current situation
582 kind_reserved = self.reserved.get(kind, [])
583 used_count = len([r for r in kind_reserved if USED_KEY in r])
584 matching = self.reserved.find(self.origin, want, raise_if_missing=False, log_label=None).get(kind, [])
585 if not matching:
586 msg = 'none of the reserved resources matches requirements %r' % specifics
587 elif not (used_count < len(kind_reserved)):
588 msg = 'suite.conf reserved only %d x %r.' % (len(kind_reserved), kind)
589 else:
590 msg = ('No unused resource left that matches the requirements;'
591 ' Of reserved %d x %r, %d match the requirements, but all are already in use;'
592 ' Requirements: %r'
593 % (len(kind_reserved), kind, len(matching), specifics))
594 raise NoResourceExn('When trying to use instance nr %d of %r: %s' % (used_count + 1, kind, msg))
595
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200596 pick = available[0]
597 self.dbg(using=pick)
598 assert not pick.get(USED_KEY)
599 pick[USED_KEY] = True
600 return copy.deepcopy(pick)
601
602 def put(self, item):
603 if not item.get(USED_KEY):
604 raise RuntimeError('Can only put() a resource that is used: %r' % item)
605 hash_to_put = item.get(HASH_KEY)
606 if not hash_to_put:
607 raise RuntimeError('Can only put() a resource that has a hash marker: %r' % item)
608 for key, item_list in self.reserved.items():
609 my_list = self.get(key)
610 for my_item in my_list:
611 if hash_to_put == my_item.get(HASH_KEY):
612 my_item.pop(USED_KEY)
613
614 def put_all(self):
Pau Espin Pedrol1dd29552017-06-13 18:07:57 +0200615 if not self.reserved:
616 return
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200617 for key, item_list in self.reserved.items():
Pau Espin Pedrol1dd29552017-06-13 18:07:57 +0200618 for item in item_list:
619 item.pop(USED_KEY, None)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200620
621 def free(self):
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200622 if self.reserved_original:
623 self.resources_pool.free(self.origin, self.reserved_original)
624 self.reserved_original = None
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200625
Neels Hofmeyr2d1d5612017-05-22 20:02:41 +0200626 def counts(self):
627 counts = {}
628 for key in self.reserved.keys():
629 counts[key] = self.count(key)
630 return counts
631
632 def count(self, key):
633 return len(self.reserved.get(key) or [])
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200634
635# vim: expandtab tabstop=4 shiftwidth=4