blob: 3b6e341bcd05abd37983a9c3d041b737fbf26512 [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,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020096 'arfcn[].arfcn': schema.INT,
97 'arfcn[].band': schema.BAND,
Holger Hans Peter Freyther93a89422019-02-27 06:36:36 +000098 'modem[].type': schema.STR,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020099 'modem[].label': schema.STR,
100 'modem[].path': schema.STR,
101 'modem[].imsi': schema.IMSI,
102 'modem[].ki': schema.KI,
Pau Espin Pedrol713ce2c2017-08-24 16:57:17 +0200103 'modem[].auth_algo': schema.AUTH_ALGO,
Andre Puschmann22ec00a2020-03-24 09:58:06 +0100104 'modem[].apn_ipaddr': schema.IPV4,
Pau Espin Pedrolc8b0f932020-02-11 17:45:26 +0100105 'modem[].remote_user': schema.STR,
106 'modem[].addr': schema.IPV4,
Pau Espin Pedrol57497a62017-08-28 14:21:15 +0200107 'modem[].ciphers[]': schema.CIPHER,
Pau Espin Pedrolac18fd32017-08-31 18:49:47 +0200108 'modem[].features[]': schema.MODEM_FEATURE,
Pau Espin Pedrola9a2fe22020-02-13 19:29:55 +0100109 'modem[].rf_dev_type': schema.STR,
110 'modem[].rf_dev_args': schema.STR,
Andre Puschmannd61613a2020-03-24 12:05:05 +0100111 'modem[].num_carriers': schema.STR,
Andre Puschmann35234f22020-03-23 18:52:41 +0100112 'modem[].airplane_t_on_ms': schema.INT,
113 'modem[].airplane_t_off_ms': schema.INT,
Pau Espin Pedrolbc1ed882018-05-17 16:59:58 +0200114 'osmocon_phone[].serial_device': schema.STR,
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200115 }
116
117WANT_SCHEMA = util.dict_add(
Pau Espin Pedrolead79ac2017-09-12 15:19:18 +0200118 dict([('%s[].times' % r, schema.TIMES) for r in R_ALL]),
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200119 RESOURCES_SCHEMA)
120
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200121CONF_SCHEMA = util.dict_add(
Pau Espin Pedrol5dc24592018-08-27 12:53:41 +0200122 { 'defaults.timeout': schema.STR,
Pau Espin Pedrolb6937712020-02-27 18:02:20 +0100123 'config.bsc.net.codec_list[]': schema.CODEC,
Pau Espin Pedrol1e81b5a2020-03-16 12:42:17 +0100124 'config.enb.enable_pcap': schema.BOOL_STR,
Pau Espin Pedrolda2e31f2020-03-31 13:45:01 +0200125 'config.epc.type': schema.STR,
Pau Espin Pedrol1e81b5a2020-03-16 12:42:17 +0100126 'config.epc.rlc_drb_mode': schema.LTE_RLC_DRB_MODE,
127 'config.epc.enable_pcap': schema.BOOL_STR,
128 'config.modem.enable_pcap': schema.BOOL_STR,
Pau Espin Pedrolc04528c2020-04-01 13:55:51 +0200129 'config.amarisoft.license_server_addr': schema.IPV4,
Andre Puschmann2dcc4312020-03-28 15:34:00 +0100130 'config.iperf3cli.time': schema.DURATION,
Pau Espin Pedrol1e81b5a2020-03-16 12:42:17 +0100131 },
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200132 dict([('resources.%s' % key, val) for key, val in WANT_SCHEMA.items()]),
133 dict([('modifiers.%s' % key, val) for key, val in WANT_SCHEMA.items()]))
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200134
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200135KNOWN_BTS_TYPES = {
Your Name44af3412017-04-13 03:11:59 +0200136 'osmo-bts-sysmo': bts_sysmo.SysmoBts,
137 'osmo-bts-trx': bts_osmotrx.OsmoBtsTrx,
Pau Espin Pedrolfeb66e72019-03-04 18:37:04 +0100138 'osmo-bts-oc2g': bts_oc2g.OsmoBtsOC2G,
Pau Espin Pedroldaed4472017-09-15 14:11:35 +0200139 'osmo-bts-octphy': bts_octphy.OsmoBtsOctphy,
Holger Hans Peter Freytherd2db10d2018-12-06 18:34:53 +0000140 'osmo-bts-virtual': bts_osmovirtual.OsmoBtsVirtual,
Pau Espin Pedrol1b28a582018-03-08 21:01:26 +0100141 'nanobts': bts_nanobts.NanoBts,
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200142 }
143
Pau Espin Pedrolc8b0f932020-02-11 17:45:26 +0100144KNOWN_ENB_TYPES = {
145 'srsenb': srs_enb.srsENB,
Pau Espin Pedrol786a6bc2020-03-30 13:51:21 +0200146 'amarisoftenb': amarisoft_enb.AmarisoftENB,
Pau Espin Pedrolc8b0f932020-02-11 17:45:26 +0100147}
Holger Hans Peter Freyther74243012019-02-27 08:18:38 +0000148
Pau Espin Pedrolda2e31f2020-03-31 13:45:01 +0200149KNOWN_EPC_TYPES = {
150 'srsepc': srs_epc.srsEPC,
151 'amarisoftepc': amarisoft_epc.AmarisoftEPC,
152}
153
Holger Hans Peter Freyther74243012019-02-27 08:18:38 +0000154KNOWN_MS_TYPES = {
155 # Map None to ofono for forward compability
156 None: modem.Modem,
157 'ofono': modem.Modem,
Holger Hans Peter Freyther10501cc2019-02-25 03:20:16 +0000158 'osmo-mobile': ms_osmo_mobile.MSOsmoMobile,
Pau Espin Pedrolc8b0f932020-02-11 17:45:26 +0100159 'srsue': srs_ue.srsUE,
Holger Hans Peter Freyther74243012019-02-27 08:18:38 +0000160}
161
162
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200163def register_bts_type(name, clazz):
164 KNOWN_BTS_TYPES[name] = clazz
165
166class ResourcesPool(log.Origin):
167 _remember_to_free = None
168 _registered_exit_handler = False
169
170 def __init__(self):
Pau Espin Pedrol66a38912020-03-11 20:11:08 +0100171 self.config_path = config.get_config_file(config.RESOURCES_CONF)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200172 self.state_dir = config.get_state_dir()
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200173 super().__init__(log.C_CNF, conf=self.config_path, state=self.state_dir.path)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200174 self.read_conf()
175
176 def read_conf(self):
177 self.all_resources = Resources(config.read(self.config_path, RESOURCES_SCHEMA))
178 self.all_resources.set_hashes()
179
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200180 def reserve(self, origin, want, modifiers):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200181 '''
182 attempt to reserve the resources specified in the dict 'want' for
183 'origin'. Obtain a lock on the resources lock dir, verify that all
184 wanted resources are available, and if yes mark them as reserved.
185
186 On success, return a reservation object which can be used to release
187 the reservation. The reservation will be freed automatically on program
188 exit, if not yet done manually.
189
190 'origin' should be an Origin() instance.
191
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200192 'want' is a dict matching RESOURCES_SCHEMA, used to specify what to
193 reserve.
194
195 'modifiers' is a dict matching RESOURCES_SCHEMA, it is overlaid on top
196 of 'want'.
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200197
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200198 If an entry has no attribute set, any of the resources may be
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200199 reserved without further limitations.
200
201 ResourcesPool may also be selected with narrowed down constraints.
Neels Hofmeyr76d81032017-05-18 18:35:32 +0200202 This would reserve one IP address, two modems, one BTS of type
Neels Hofmeyr391afe32017-05-18 19:22:12 +0200203 sysmo and one of type trx, plus 2 ARFCNs in the 1800 band:
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200204
205 {
Neels Hofmeyr76d81032017-05-18 18:35:32 +0200206 'ip_address': [ {} ],
Neels Hofmeyr391afe32017-05-18 19:22:12 +0200207 'bts': [ { 'type': 'sysmo' }, { 'type': 'trx' } ],
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200208 'arfcn': [ { 'band': 'GSM-1800' }, { 'band': 'GSM-1800' } ],
209 'modem': [ {}, {} ],
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200210 }
211 '''
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200212 schema.validate(want, RESOURCES_SCHEMA)
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200213 schema.validate(modifiers, RESOURCES_SCHEMA)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200214
215 origin_id = origin.origin_id()
216
217 with self.state_dir.lock(origin_id):
218 rrfile_path = self.state_dir.mk_parentdir(RESERVED_RESOURCES_FILE)
219 reserved = Resources(config.read(rrfile_path, if_missing_return={}))
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200220 to_be_reserved = self.all_resources.without(reserved).find(origin, want)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200221
222 to_be_reserved.mark_reserved_by(origin_id)
223
224 reserved.add(to_be_reserved)
225 config.write(rrfile_path, reserved)
226
227 self.remember_to_free(to_be_reserved)
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200228 return ReservedResources(self, origin, to_be_reserved, modifiers)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200229
230 def free(self, origin, to_be_freed):
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200231 log.ctx(origin)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200232 with self.state_dir.lock(origin.origin_id()):
233 rrfile_path = self.state_dir.mk_parentdir(RESERVED_RESOURCES_FILE)
234 reserved = Resources(config.read(rrfile_path, if_missing_return={}))
235 reserved.drop(to_be_freed)
236 config.write(rrfile_path, reserved)
237 self.forget_freed(to_be_freed)
238
239 def register_exit_handler(self):
240 if self._registered_exit_handler:
241 return
242 atexit.register(self.clean_up_registered_resources)
243 self._registered_exit_handler = True
244
245 def unregister_exit_handler(self):
246 if not self._registered_exit_handler:
247 return
248 atexit.unregister(self.clean_up_registered_resources)
249 self._registered_exit_handler = False
250
251 def clean_up_registered_resources(self):
252 if not self._remember_to_free:
253 return
254 self.free(log.Origin('atexit.clean_up_registered_resources()'),
255 self._remember_to_free)
256
257 def remember_to_free(self, to_be_reserved):
258 self.register_exit_handler()
259 if not self._remember_to_free:
260 self._remember_to_free = Resources()
261 self._remember_to_free.add(to_be_reserved)
262
263 def forget_freed(self, freed):
264 if freed is self._remember_to_free:
265 self._remember_to_free.clear()
266 else:
267 self._remember_to_free.drop(freed)
268 if not self._remember_to_free:
269 self.unregister_exit_handler()
270
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100271 def next_persistent_value(self, token, first_val, validate_func, inc_func, origin):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200272 origin_id = origin.origin_id()
273
274 with self.state_dir.lock(origin_id):
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100275 token_path = self.state_dir.child('last_used_%s.state' % token)
276 log.ctx(token_path)
277 last_value = first_val
278 if os.path.exists(token_path):
279 if not os.path.isfile(token_path):
280 raise RuntimeError('path should be a file but is not: %r' % token_path)
281 with open(token_path, 'r') as f:
282 last_value = f.read().strip()
283 validate_func(last_value)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200284
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100285 next_value = inc_func(last_value)
286 with open(token_path, 'w') as f:
287 f.write(next_value)
288 return next_value
289
290 def next_msisdn(self, origin):
291 return self.next_persistent_value('msisdn', '1000', schema.msisdn, util.msisdn_inc, origin)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200292
Pau Espin Pedrol5e0c2512017-11-06 18:40:23 +0100293 def next_lac(self, origin):
Pau Espin Pedrolf7f06362017-11-28 12:56:35 +0100294 # LAC=0 has special meaning (MS detached), avoid it
295 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 +0200296
Pau Espin Pedrol8a3a7b52017-11-28 15:50:02 +0100297 def next_rac(self, origin):
298 return self.next_persistent_value('rac', '1', schema.uint8, lambda x: str((int(x)+1) % pow(2,8) or 1), origin)
299
Pau Espin Pedrol4ccce7c2017-11-07 11:13:20 +0100300 def next_cellid(self, origin):
301 return self.next_persistent_value('cellid', '1', schema.uint16, lambda x: str((int(x)+1) % pow(2,16)), origin)
302
Pau Espin Pedrol8a3a7b52017-11-28 15:50:02 +0100303 def next_bvci(self, origin):
304 # BVCI=0 and =1 are reserved, avoid them.
305 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)
306
Pau Espin Pedrol4676cbd2017-09-14 17:35:03 +0200307class NoResourceExn(log.Error):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200308 pass
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200309
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200310class Resources(dict):
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200311
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200312 def __init__(self, all_resources={}, do_copy=True):
313 if do_copy:
314 all_resources = copy.deepcopy(all_resources)
315 self.update(all_resources)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200316
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200317 def drop(self, reserved, fail_if_not_found=True):
318 # protect from modifying reserved because we're the same object
319 if reserved is self:
320 raise RuntimeError('Refusing to drop a list of resources from itself.'
321 ' This is probably a bug where a list of Resources()'
322 ' should have been copied but is passed as-is.'
323 ' use Resources.clear() instead.')
324
325 for key, reserved_list in reserved.items():
326 my_list = self.get(key) or []
327
328 if my_list is reserved_list:
329 self.pop(key)
330 continue
331
332 for reserved_item in reserved_list:
333 found = False
334 reserved_hash = reserved_item.get(HASH_KEY)
335 if not reserved_hash:
336 raise RuntimeError('Resources.drop() only works with hashed items')
337
338 for i in range(len(my_list)):
339 my_item = my_list[i]
340 my_hash = my_item.get(HASH_KEY)
341 if not my_hash:
342 raise RuntimeError('Resources.drop() only works with hashed items')
343 if my_hash == reserved_hash:
344 found = True
345 my_list.pop(i)
346 break
347
348 if fail_if_not_found and not found:
349 raise RuntimeError('Asked to drop resource from a pool, but the'
350 ' resource was not found: %s = %r' % (key, reserved_item))
351
352 if not my_list:
353 self.pop(key)
354 return self
355
356 def without(self, reserved):
357 return Resources(self).drop(reserved)
358
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200359 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 +0200360 '''
361 Pass a dict of resource requirements, e.g.:
362 want = {
363 'bts': [ {'type': 'osmo-bts-sysmo',}, {} ],
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200364 'modem': [ {}, {}, {} ]
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200365 }
366 This function tries to find a combination from the available resources that
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200367 matches these requirements. The return value is a dict (wrapped in a Resources class)
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200368 that contains the matching resources in the order of 'want' dict: in above
369 example, the returned dict would have a 'bts' list with the first item being
370 a sysmoBTS, the second item being any other available BTS.
371
372 If skip_if_marked is passed, any resource that contains this key is skipped.
373 E.g. if a BTS has the USED_KEY set like
374 reserved_resources = { 'bts' : {..., '_used': True} }
375 then this may be skipped by passing skip_if_marked='_used'
376 (or rather skip_if_marked=USED_KEY).
377
378 If do_copy is True, the returned dict is a deep copy and does not share
379 lists with any other Resources dict.
380
381 If raise_if_missing is False, this will return an empty item for any
382 resource that had no match, instead of immediately raising an exception.
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200383
384 This function expects input dictionaries whose contents have already
385 been replicated based on its the 'times' attributes. See
386 config.replicate_times() for more details.
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200387 '''
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200388 matches = {}
Neels Hofmeyr17c139e2017-04-12 02:42:02 +0200389 for key, want_list in sorted(want.items()): # sorted for deterministic test results
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200390 # here we have a resource of a given type, e.g. 'bts', with a list
391 # containing as many BTSes as the caller wants to reserve/use. Each
392 # list item contains specifics for the particular BTS.
Neels Hofmeyr2a1a1fa2017-05-29 01:36:21 +0200393 my_list = self.get(key, [])
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200394
Neels Hofmeyrf9e86932017-06-06 19:47:40 +0200395 if log_label:
396 for_origin.log(log_label, len(want_list), 'x', key, '(candidates: %d)'%len(my_list))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200397
398 # Try to avoid a less constrained item snatching away a resource
399 # from a more detailed constrained requirement.
400
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200401 # first record all matches, so that each requested item has a list
402 # of all available resources that match it. Some resources may
403 # appear for multiple requested items. Store matching indexes.
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200404 all_matches = []
405 for want_item in want_list:
406 item_match_list = []
407 for i in range(len(my_list)):
408 my_item = my_list[i]
409 if skip_if_marked and my_item.get(skip_if_marked):
410 continue
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200411 if item_matches(my_item, want_item):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200412 item_match_list.append(i)
413 if not item_match_list:
Neels Hofmeyr9b907702017-05-06 23:20:33 +0200414 if raise_if_missing:
415 raise NoResourceExn('No matching resource available for %s = %r'
416 % (key, want_item))
417 else:
418 # this one failed... see below
419 all_matches = []
420 break
421
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200422 all_matches.append( item_match_list )
423
424 if not all_matches:
Neels Hofmeyr9b907702017-05-06 23:20:33 +0200425 # ...this one failed. Makes no sense to solve resource
426 # allocations, return an empty list for this key to mark
427 # failure.
428 matches[key] = []
429 continue
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200430
431 # figure out who gets what
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200432 try:
433 solution = solve(all_matches)
434 except NotSolvable:
435 # instead of a cryptic error message, raise an exception that
436 # conveys meaning to the user.
437 raise NoResourceExn('Could not resolve request to reserve resources: '
438 '%d x %s with requirements: %r' % (len(want_list), key, want_list))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200439 picked = [ my_list[i] for i in solution if i is not None ]
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200440 for_origin.dbg('Picked', config.tostr(picked))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200441 matches[key] = picked
442
443 return Resources(matches, do_copy=do_copy)
444
445 def set_hashes(self):
446 for key, item_list in self.items():
447 for item in item_list:
448 item[HASH_KEY] = util.hash_obj(item, HASH_KEY, RESERVED_KEY, USED_KEY)
449
450 def add(self, more):
451 if more is self:
452 raise RuntimeError('adding a list of resources to itself?')
453 config.add(self, copy.deepcopy(more))
454
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200455 def mark_reserved_by(self, origin_id):
456 for key, item_list in self.items():
457 for item in item_list:
458 item[RESERVED_KEY] = origin_id
459
460
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200461class NotSolvable(Exception):
462 pass
463
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200464def solve(all_matches):
465 '''
466 all_matches shall be a list of index-lists.
467 all_matches[i] is the list of indexes that item i can use.
468 Return a solution so that each i gets a different index.
469 solve([ [0, 1, 2],
470 [0],
471 [0, 2] ]) == [1, 0, 2]
472 '''
473
474 def all_differ(l):
475 return len(set(l)) == len(l)
476
477 def search_in_permutations(fixed=[]):
478 idx = len(fixed)
479 for i in range(len(all_matches[idx])):
480 val = all_matches[idx][i]
481 # don't add a val that's already in the list
482 if val in fixed:
483 continue
484 l = list(fixed)
485 l.append(val)
486 if len(l) == len(all_matches):
487 # found a solution
488 return l
489 # not at the end yet, add next digit
490 r = search_in_permutations(l)
491 if r:
492 # nested search_in_permutations() call found a solution
493 return r
494 # this entire branch yielded no solution
495 return None
496
497 if not all_matches:
498 raise RuntimeError('Cannot solve: no candidates')
499
500 solution = search_in_permutations()
501 if not solution:
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200502 raise NotSolvable('The requested resource requirements are not solvable %r'
503 % all_matches)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200504 return solution
505
506
507def contains_hash(list_of_dicts, a_hash):
508 for d in list_of_dicts:
509 if d.get(HASH_KEY) == a_hash:
510 return True
511 return False
512
513def item_matches(item, wanted_item, ignore_keys=None):
514 if is_dict(wanted_item):
515 # match up two dicts
516 if not isinstance(item, dict):
517 return False
518 for key, wanted_val in wanted_item.items():
519 if ignore_keys and key in ignore_keys:
520 continue
521 if not item_matches(item.get(key), wanted_val, ignore_keys=ignore_keys):
522 return False
523 return True
524
525 if is_list(wanted_item):
Pau Espin Pedrol4e36f7c2017-08-28 13:29:28 +0200526 if not is_list(item):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200527 return False
Pau Espin Pedrol58475512017-09-14 15:33:15 +0200528 # Validate that all elements in both lists are of the same type:
529 t = util.list_validate_same_elem_type(wanted_item + item)
530 if t is None:
531 return True # both lists are empty, return
532 # For lists of complex objects, we expect them to be sorted lists:
533 if t in (dict, list, tuple):
534 for i in range(max(len(wanted_item), len(item))):
535 log.ctx(idx=i)
536 subitem = item[i] if i < len(item) else util.empty_instance_type(t)
537 wanted_subitem = wanted_item[i] if i < len(wanted_item) else util.empty_instance_type(t)
538 if not item_matches(subitem, wanted_subitem, ignore_keys=ignore_keys):
539 return False
540 else: # for lists of basic elements, we handle them as unsorted sets:
541 for val in wanted_item:
542 if val not in item:
543 return False
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200544 return True
545
546 return item == wanted_item
547
548
549class ReservedResources(log.Origin):
550 '''
551 After all resources have been figured out, this is the API that a test case
552 gets to interact with resources. From those resources that have been
553 reserved for it, it can pick some to mark them as currently in use.
554 Functions like nitb() provide a resource by automatically picking its
555 dependencies from so far unused (but reserved) resource.
556 '''
557
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200558 def __init__(self, resources_pool, origin, reserved, modifiers):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200559 self.resources_pool = resources_pool
560 self.origin = origin
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200561 self.reserved_original = reserved
562 self.reserved = copy.deepcopy(self.reserved_original)
563 config.overlay(self.reserved, modifiers)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200564
565 def __repr__(self):
566 return 'resources(%s)=%s' % (self.origin.name(), pprint.pformat(self.reserved))
567
568 def get(self, kind, specifics=None):
569 if specifics is None:
570 specifics = {}
571 self.dbg('requesting use of', kind, specifics=specifics)
572 want = { kind: [specifics] }
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200573 available_dict = self.reserved.find(self.origin, want, skip_if_marked=USED_KEY,
574 do_copy=False, raise_if_missing=False,
575 log_label='Using')
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200576 available = available_dict.get(kind)
577 self.dbg(available=len(available))
578 if not available:
Neels Hofmeyrf9e86932017-06-06 19:47:40 +0200579 # cook up a detailed error message for the current situation
580 kind_reserved = self.reserved.get(kind, [])
581 used_count = len([r for r in kind_reserved if USED_KEY in r])
582 matching = self.reserved.find(self.origin, want, raise_if_missing=False, log_label=None).get(kind, [])
583 if not matching:
584 msg = 'none of the reserved resources matches requirements %r' % specifics
585 elif not (used_count < len(kind_reserved)):
586 msg = 'suite.conf reserved only %d x %r.' % (len(kind_reserved), kind)
587 else:
588 msg = ('No unused resource left that matches the requirements;'
589 ' Of reserved %d x %r, %d match the requirements, but all are already in use;'
590 ' Requirements: %r'
591 % (len(kind_reserved), kind, len(matching), specifics))
592 raise NoResourceExn('When trying to use instance nr %d of %r: %s' % (used_count + 1, kind, msg))
593
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200594 pick = available[0]
595 self.dbg(using=pick)
596 assert not pick.get(USED_KEY)
597 pick[USED_KEY] = True
598 return copy.deepcopy(pick)
599
600 def put(self, item):
601 if not item.get(USED_KEY):
602 raise RuntimeError('Can only put() a resource that is used: %r' % item)
603 hash_to_put = item.get(HASH_KEY)
604 if not hash_to_put:
605 raise RuntimeError('Can only put() a resource that has a hash marker: %r' % item)
606 for key, item_list in self.reserved.items():
607 my_list = self.get(key)
608 for my_item in my_list:
609 if hash_to_put == my_item.get(HASH_KEY):
610 my_item.pop(USED_KEY)
611
612 def put_all(self):
Pau Espin Pedrol1dd29552017-06-13 18:07:57 +0200613 if not self.reserved:
614 return
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200615 for key, item_list in self.reserved.items():
Pau Espin Pedrol1dd29552017-06-13 18:07:57 +0200616 for item in item_list:
617 item.pop(USED_KEY, None)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200618
619 def free(self):
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200620 if self.reserved_original:
621 self.resources_pool.free(self.origin, self.reserved_original)
622 self.reserved_original = None
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200623
Neels Hofmeyr2d1d5612017-05-22 20:02:41 +0200624 def counts(self):
625 counts = {}
626 for key in self.reserved.keys():
627 counts[key] = self.count(key)
628 return counts
629
630 def count(self, key):
631 return len(self.reserved.get(key) or [])
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200632
633# vim: expandtab tabstop=4 shiftwidth=4