blob: bcdbb9466f2e0f56dd334c4e4e38ba5bace8cd41 [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
Pau Espin Pedrole8bbcbf2020-04-10 19:51:31 +020025from .core import log
26from .core import config
27from .core import util
28from .core import schema
Pau Espin Pedrole1a58bd2020-04-10 20:46:07 +020029from .obj import bts_sysmo, bts_osmotrx, bts_osmovirtual, bts_octphy, bts_nanobts, bts_oc2g
Pau Espin Pedrol0dbd6942020-04-10 20:57:36 +020030from .obj import ms_ofono
Pau Espin Pedrole1a58bd2020-04-10 20:46:07 +020031from .obj import ms_osmo_mobile
32from .obj import ms_srs, ms_amarisoft, enb_srs, enb_amarisoft, epc_srs, epc_amarisoft
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +020033
Pau Espin Pedrole8bbcbf2020-04-10 19:51:31 +020034from .core.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,
Andre Puschmann4b5a09a2020-04-14 22:24:00 +020091 'enb[].gtp_bind_addr': schema.IPV4,
Pau Espin Pedrol1deb1ae2020-02-27 15:16:09 +010092 'enb[].num_prb': schema.UINT,
Pau Espin Pedrolb6937712020-02-27 18:02:20 +010093 'enb[].transmission_mode': schema.LTE_TRANSMISSION_MODE,
Andre Puschmann82b88902020-03-24 10:04:48 +010094 'enb[].num_cells': schema.UINT,
Pau Espin Pedrola9a2fe22020-02-13 19:29:55 +010095 'enb[].rf_dev_type': schema.STR,
96 'enb[].rf_dev_args': schema.STR,
Pau Espin Pedrol76b2c2a2020-04-01 19:51:08 +020097 'enb[].additional_args': schema.STR,
Andre Puschmanna7f19832020-04-07 14:38:27 +020098 'enb[].enable_measurements': schema.BOOL_STR,
99 'enb[].a1_report_type': schema.STR,
100 'enb[].a1_report_value': schema.INT,
101 'enb[].a1_hysteresis': schema.INT,
102 'enb[].a1_time_to_trigger': schema.INT,
103 'enb[].a2_report_type': schema.STR,
104 'enb[].a2_report_value': schema.INT,
105 'enb[].a2_hysteresis': schema.INT,
106 'enb[].a2_time_to_trigger': schema.INT,
107 'enb[].a3_report_type': schema.STR,
108 'enb[].a3_report_value': schema.INT,
109 'enb[].a3_hysteresis': schema.INT,
110 'enb[].a3_time_to_trigger': schema.INT,
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200111 'arfcn[].arfcn': schema.INT,
112 'arfcn[].band': schema.BAND,
Holger Hans Peter Freyther93a89422019-02-27 06:36:36 +0000113 'modem[].type': schema.STR,
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200114 'modem[].label': schema.STR,
115 'modem[].path': schema.STR,
116 'modem[].imsi': schema.IMSI,
117 'modem[].ki': schema.KI,
Pau Espin Pedrol713ce2c2017-08-24 16:57:17 +0200118 'modem[].auth_algo': schema.AUTH_ALGO,
Andre Puschmann22ec00a2020-03-24 09:58:06 +0100119 'modem[].apn_ipaddr': schema.IPV4,
Pau Espin Pedrolc8b0f932020-02-11 17:45:26 +0100120 'modem[].remote_user': schema.STR,
121 'modem[].addr': schema.IPV4,
Pau Espin Pedrol57497a62017-08-28 14:21:15 +0200122 'modem[].ciphers[]': schema.CIPHER,
Pau Espin Pedrolac18fd32017-08-31 18:49:47 +0200123 'modem[].features[]': schema.MODEM_FEATURE,
Pau Espin Pedrola9a2fe22020-02-13 19:29:55 +0100124 'modem[].rf_dev_type': schema.STR,
125 'modem[].rf_dev_args': schema.STR,
Andre Puschmann65e769f2020-04-06 14:53:13 +0200126 'modem[].num_carriers': schema.UINT,
Pau Espin Pedrol76b2c2a2020-04-01 19:51:08 +0200127 'modem[].additional_args': schema.STR,
Andre Puschmann35234f22020-03-23 18:52:41 +0100128 'modem[].airplane_t_on_ms': schema.INT,
129 'modem[].airplane_t_off_ms': schema.INT,
Pau Espin Pedrolbc1ed882018-05-17 16:59:58 +0200130 'osmocon_phone[].serial_device': schema.STR,
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200131 }
132
133WANT_SCHEMA = util.dict_add(
Pau Espin Pedrolead79ac2017-09-12 15:19:18 +0200134 dict([('%s[].times' % r, schema.TIMES) for r in R_ALL]),
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200135 RESOURCES_SCHEMA)
136
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200137CONF_SCHEMA = util.dict_add(
Pau Espin Pedrol5dc24592018-08-27 12:53:41 +0200138 { 'defaults.timeout': schema.STR,
Pau Espin Pedrolb6937712020-02-27 18:02:20 +0100139 'config.bsc.net.codec_list[]': schema.CODEC,
Pau Espin Pedrol1e81b5a2020-03-16 12:42:17 +0100140 'config.enb.enable_pcap': schema.BOOL_STR,
Pau Espin Pedrolda2e31f2020-03-31 13:45:01 +0200141 'config.epc.type': schema.STR,
Pau Espin Pedrol04ad3b52020-04-06 12:25:22 +0200142 'config.epc.qci': schema.UINT,
Pau Espin Pedrol1e81b5a2020-03-16 12:42:17 +0100143 'config.epc.enable_pcap': schema.BOOL_STR,
144 'config.modem.enable_pcap': schema.BOOL_STR,
Pau Espin Pedrolc04528c2020-04-01 13:55:51 +0200145 'config.amarisoft.license_server_addr': schema.IPV4,
Andre Puschmann2dcc4312020-03-28 15:34:00 +0100146 'config.iperf3cli.time': schema.DURATION,
Pau Espin Pedrol1e81b5a2020-03-16 12:42:17 +0100147 },
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200148 dict([('resources.%s' % key, val) for key, val in WANT_SCHEMA.items()]),
149 dict([('modifiers.%s' % key, val) for key, val in WANT_SCHEMA.items()]))
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200150
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200151KNOWN_BTS_TYPES = {
Your Name44af3412017-04-13 03:11:59 +0200152 'osmo-bts-sysmo': bts_sysmo.SysmoBts,
153 'osmo-bts-trx': bts_osmotrx.OsmoBtsTrx,
Pau Espin Pedrolfeb66e72019-03-04 18:37:04 +0100154 'osmo-bts-oc2g': bts_oc2g.OsmoBtsOC2G,
Pau Espin Pedroldaed4472017-09-15 14:11:35 +0200155 'osmo-bts-octphy': bts_octphy.OsmoBtsOctphy,
Holger Hans Peter Freytherd2db10d2018-12-06 18:34:53 +0000156 'osmo-bts-virtual': bts_osmovirtual.OsmoBtsVirtual,
Pau Espin Pedrol1b28a582018-03-08 21:01:26 +0100157 'nanobts': bts_nanobts.NanoBts,
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200158 }
159
Pau Espin Pedrolc8b0f932020-02-11 17:45:26 +0100160KNOWN_ENB_TYPES = {
Pau Espin Pedrol9b486ee2020-04-10 19:41:06 +0200161 'srsenb': enb_srs.srsENB,
162 'amarisoftenb': enb_amarisoft.AmarisoftENB,
Pau Espin Pedrolc8b0f932020-02-11 17:45:26 +0100163}
Holger Hans Peter Freyther74243012019-02-27 08:18:38 +0000164
Pau Espin Pedrolda2e31f2020-03-31 13:45:01 +0200165KNOWN_EPC_TYPES = {
Pau Espin Pedrol9b486ee2020-04-10 19:41:06 +0200166 'srsepc': epc_srs.srsEPC,
167 'amarisoftepc': epc_amarisoft.AmarisoftEPC,
Pau Espin Pedrolda2e31f2020-03-31 13:45:01 +0200168}
169
Holger Hans Peter Freyther74243012019-02-27 08:18:38 +0000170KNOWN_MS_TYPES = {
171 # Map None to ofono for forward compability
Pau Espin Pedrol0dbd6942020-04-10 20:57:36 +0200172 None: ms_ofono.Modem,
173 'ofono': ms_ofono.Modem,
Holger Hans Peter Freyther10501cc2019-02-25 03:20:16 +0000174 'osmo-mobile': ms_osmo_mobile.MSOsmoMobile,
Pau Espin Pedrol9b486ee2020-04-10 19:41:06 +0200175 'srsue': ms_srs.srsUE,
176 'amarisoftue': ms_amarisoft.AmarisoftUE,
Holger Hans Peter Freyther74243012019-02-27 08:18:38 +0000177}
178
179
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200180def register_bts_type(name, clazz):
181 KNOWN_BTS_TYPES[name] = clazz
182
183class ResourcesPool(log.Origin):
184 _remember_to_free = None
185 _registered_exit_handler = False
186
187 def __init__(self):
Pau Espin Pedrol66a38912020-03-11 20:11:08 +0100188 self.config_path = config.get_config_file(config.RESOURCES_CONF)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200189 self.state_dir = config.get_state_dir()
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200190 super().__init__(log.C_CNF, conf=self.config_path, state=self.state_dir.path)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200191 self.read_conf()
192
193 def read_conf(self):
194 self.all_resources = Resources(config.read(self.config_path, RESOURCES_SCHEMA))
195 self.all_resources.set_hashes()
196
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200197 def reserve(self, origin, want, modifiers):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200198 '''
199 attempt to reserve the resources specified in the dict 'want' for
200 'origin'. Obtain a lock on the resources lock dir, verify that all
201 wanted resources are available, and if yes mark them as reserved.
202
203 On success, return a reservation object which can be used to release
204 the reservation. The reservation will be freed automatically on program
205 exit, if not yet done manually.
206
207 'origin' should be an Origin() instance.
208
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200209 'want' is a dict matching RESOURCES_SCHEMA, used to specify what to
210 reserve.
211
212 'modifiers' is a dict matching RESOURCES_SCHEMA, it is overlaid on top
213 of 'want'.
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200214
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200215 If an entry has no attribute set, any of the resources may be
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200216 reserved without further limitations.
217
218 ResourcesPool may also be selected with narrowed down constraints.
Neels Hofmeyr76d81032017-05-18 18:35:32 +0200219 This would reserve one IP address, two modems, one BTS of type
Neels Hofmeyr391afe32017-05-18 19:22:12 +0200220 sysmo and one of type trx, plus 2 ARFCNs in the 1800 band:
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200221
222 {
Neels Hofmeyr76d81032017-05-18 18:35:32 +0200223 'ip_address': [ {} ],
Neels Hofmeyr391afe32017-05-18 19:22:12 +0200224 'bts': [ { 'type': 'sysmo' }, { 'type': 'trx' } ],
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200225 'arfcn': [ { 'band': 'GSM-1800' }, { 'band': 'GSM-1800' } ],
226 'modem': [ {}, {} ],
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200227 }
228 '''
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200229 schema.validate(want, RESOURCES_SCHEMA)
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200230 schema.validate(modifiers, RESOURCES_SCHEMA)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200231
232 origin_id = origin.origin_id()
233
234 with self.state_dir.lock(origin_id):
235 rrfile_path = self.state_dir.mk_parentdir(RESERVED_RESOURCES_FILE)
236 reserved = Resources(config.read(rrfile_path, if_missing_return={}))
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200237 to_be_reserved = self.all_resources.without(reserved).find(origin, want)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200238
239 to_be_reserved.mark_reserved_by(origin_id)
240
241 reserved.add(to_be_reserved)
242 config.write(rrfile_path, reserved)
243
244 self.remember_to_free(to_be_reserved)
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200245 return ReservedResources(self, origin, to_be_reserved, modifiers)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200246
247 def free(self, origin, to_be_freed):
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200248 log.ctx(origin)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200249 with self.state_dir.lock(origin.origin_id()):
250 rrfile_path = self.state_dir.mk_parentdir(RESERVED_RESOURCES_FILE)
251 reserved = Resources(config.read(rrfile_path, if_missing_return={}))
252 reserved.drop(to_be_freed)
253 config.write(rrfile_path, reserved)
254 self.forget_freed(to_be_freed)
255
256 def register_exit_handler(self):
257 if self._registered_exit_handler:
258 return
259 atexit.register(self.clean_up_registered_resources)
260 self._registered_exit_handler = True
261
262 def unregister_exit_handler(self):
263 if not self._registered_exit_handler:
264 return
265 atexit.unregister(self.clean_up_registered_resources)
266 self._registered_exit_handler = False
267
268 def clean_up_registered_resources(self):
269 if not self._remember_to_free:
270 return
271 self.free(log.Origin('atexit.clean_up_registered_resources()'),
272 self._remember_to_free)
273
274 def remember_to_free(self, to_be_reserved):
275 self.register_exit_handler()
276 if not self._remember_to_free:
277 self._remember_to_free = Resources()
278 self._remember_to_free.add(to_be_reserved)
279
280 def forget_freed(self, freed):
281 if freed is self._remember_to_free:
282 self._remember_to_free.clear()
283 else:
284 self._remember_to_free.drop(freed)
285 if not self._remember_to_free:
286 self.unregister_exit_handler()
287
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100288 def next_persistent_value(self, token, first_val, validate_func, inc_func, origin):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200289 origin_id = origin.origin_id()
290
291 with self.state_dir.lock(origin_id):
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100292 token_path = self.state_dir.child('last_used_%s.state' % token)
293 log.ctx(token_path)
294 last_value = first_val
295 if os.path.exists(token_path):
296 if not os.path.isfile(token_path):
297 raise RuntimeError('path should be a file but is not: %r' % token_path)
298 with open(token_path, 'r') as f:
299 last_value = f.read().strip()
300 validate_func(last_value)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200301
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100302 next_value = inc_func(last_value)
303 with open(token_path, 'w') as f:
304 f.write(next_value)
305 return next_value
306
307 def next_msisdn(self, origin):
308 return self.next_persistent_value('msisdn', '1000', schema.msisdn, util.msisdn_inc, origin)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200309
Pau Espin Pedrol5e0c2512017-11-06 18:40:23 +0100310 def next_lac(self, origin):
Pau Espin Pedrolf7f06362017-11-28 12:56:35 +0100311 # LAC=0 has special meaning (MS detached), avoid it
312 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 +0200313
Pau Espin Pedrol8a3a7b52017-11-28 15:50:02 +0100314 def next_rac(self, origin):
315 return self.next_persistent_value('rac', '1', schema.uint8, lambda x: str((int(x)+1) % pow(2,8) or 1), origin)
316
Pau Espin Pedrol4ccce7c2017-11-07 11:13:20 +0100317 def next_cellid(self, origin):
318 return self.next_persistent_value('cellid', '1', schema.uint16, lambda x: str((int(x)+1) % pow(2,16)), origin)
319
Pau Espin Pedrol8a3a7b52017-11-28 15:50:02 +0100320 def next_bvci(self, origin):
321 # BVCI=0 and =1 are reserved, avoid them.
322 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)
323
Pau Espin Pedrol4676cbd2017-09-14 17:35:03 +0200324class NoResourceExn(log.Error):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200325 pass
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200326
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200327class Resources(dict):
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200328
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200329 def __init__(self, all_resources={}, do_copy=True):
330 if do_copy:
331 all_resources = copy.deepcopy(all_resources)
332 self.update(all_resources)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200333
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200334 def drop(self, reserved, fail_if_not_found=True):
335 # protect from modifying reserved because we're the same object
336 if reserved is self:
337 raise RuntimeError('Refusing to drop a list of resources from itself.'
338 ' This is probably a bug where a list of Resources()'
339 ' should have been copied but is passed as-is.'
340 ' use Resources.clear() instead.')
341
342 for key, reserved_list in reserved.items():
343 my_list = self.get(key) or []
344
345 if my_list is reserved_list:
346 self.pop(key)
347 continue
348
349 for reserved_item in reserved_list:
350 found = False
351 reserved_hash = reserved_item.get(HASH_KEY)
352 if not reserved_hash:
353 raise RuntimeError('Resources.drop() only works with hashed items')
354
355 for i in range(len(my_list)):
356 my_item = my_list[i]
357 my_hash = my_item.get(HASH_KEY)
358 if not my_hash:
359 raise RuntimeError('Resources.drop() only works with hashed items')
360 if my_hash == reserved_hash:
361 found = True
362 my_list.pop(i)
363 break
364
365 if fail_if_not_found and not found:
366 raise RuntimeError('Asked to drop resource from a pool, but the'
367 ' resource was not found: %s = %r' % (key, reserved_item))
368
369 if not my_list:
370 self.pop(key)
371 return self
372
373 def without(self, reserved):
374 return Resources(self).drop(reserved)
375
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200376 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 +0200377 '''
378 Pass a dict of resource requirements, e.g.:
379 want = {
380 'bts': [ {'type': 'osmo-bts-sysmo',}, {} ],
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200381 'modem': [ {}, {}, {} ]
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200382 }
383 This function tries to find a combination from the available resources that
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200384 matches these requirements. The return value is a dict (wrapped in a Resources class)
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200385 that contains the matching resources in the order of 'want' dict: in above
386 example, the returned dict would have a 'bts' list with the first item being
387 a sysmoBTS, the second item being any other available BTS.
388
389 If skip_if_marked is passed, any resource that contains this key is skipped.
390 E.g. if a BTS has the USED_KEY set like
391 reserved_resources = { 'bts' : {..., '_used': True} }
392 then this may be skipped by passing skip_if_marked='_used'
393 (or rather skip_if_marked=USED_KEY).
394
395 If do_copy is True, the returned dict is a deep copy and does not share
396 lists with any other Resources dict.
397
398 If raise_if_missing is False, this will return an empty item for any
399 resource that had no match, instead of immediately raising an exception.
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200400
401 This function expects input dictionaries whose contents have already
402 been replicated based on its the 'times' attributes. See
403 config.replicate_times() for more details.
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200404 '''
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200405 matches = {}
Neels Hofmeyr17c139e2017-04-12 02:42:02 +0200406 for key, want_list in sorted(want.items()): # sorted for deterministic test results
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200407 # here we have a resource of a given type, e.g. 'bts', with a list
408 # containing as many BTSes as the caller wants to reserve/use. Each
409 # list item contains specifics for the particular BTS.
Neels Hofmeyr2a1a1fa2017-05-29 01:36:21 +0200410 my_list = self.get(key, [])
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200411
Neels Hofmeyrf9e86932017-06-06 19:47:40 +0200412 if log_label:
413 for_origin.log(log_label, len(want_list), 'x', key, '(candidates: %d)'%len(my_list))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200414
415 # Try to avoid a less constrained item snatching away a resource
416 # from a more detailed constrained requirement.
417
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200418 # first record all matches, so that each requested item has a list
419 # of all available resources that match it. Some resources may
420 # appear for multiple requested items. Store matching indexes.
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200421 all_matches = []
422 for want_item in want_list:
423 item_match_list = []
424 for i in range(len(my_list)):
425 my_item = my_list[i]
426 if skip_if_marked and my_item.get(skip_if_marked):
427 continue
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200428 if item_matches(my_item, want_item):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200429 item_match_list.append(i)
430 if not item_match_list:
Neels Hofmeyr9b907702017-05-06 23:20:33 +0200431 if raise_if_missing:
432 raise NoResourceExn('No matching resource available for %s = %r'
433 % (key, want_item))
434 else:
435 # this one failed... see below
436 all_matches = []
437 break
438
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200439 all_matches.append( item_match_list )
440
441 if not all_matches:
Neels Hofmeyr9b907702017-05-06 23:20:33 +0200442 # ...this one failed. Makes no sense to solve resource
443 # allocations, return an empty list for this key to mark
444 # failure.
445 matches[key] = []
446 continue
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200447
448 # figure out who gets what
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200449 try:
450 solution = solve(all_matches)
451 except NotSolvable:
452 # instead of a cryptic error message, raise an exception that
453 # conveys meaning to the user.
454 raise NoResourceExn('Could not resolve request to reserve resources: '
455 '%d x %s with requirements: %r' % (len(want_list), key, want_list))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200456 picked = [ my_list[i] for i in solution if i is not None ]
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200457 for_origin.dbg('Picked', config.tostr(picked))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200458 matches[key] = picked
459
460 return Resources(matches, do_copy=do_copy)
461
462 def set_hashes(self):
463 for key, item_list in self.items():
464 for item in item_list:
465 item[HASH_KEY] = util.hash_obj(item, HASH_KEY, RESERVED_KEY, USED_KEY)
466
467 def add(self, more):
468 if more is self:
469 raise RuntimeError('adding a list of resources to itself?')
470 config.add(self, copy.deepcopy(more))
471
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200472 def mark_reserved_by(self, origin_id):
473 for key, item_list in self.items():
474 for item in item_list:
475 item[RESERVED_KEY] = origin_id
476
477
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200478class NotSolvable(Exception):
479 pass
480
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200481def solve(all_matches):
482 '''
483 all_matches shall be a list of index-lists.
484 all_matches[i] is the list of indexes that item i can use.
485 Return a solution so that each i gets a different index.
486 solve([ [0, 1, 2],
487 [0],
488 [0, 2] ]) == [1, 0, 2]
489 '''
490
491 def all_differ(l):
492 return len(set(l)) == len(l)
493
494 def search_in_permutations(fixed=[]):
495 idx = len(fixed)
496 for i in range(len(all_matches[idx])):
497 val = all_matches[idx][i]
498 # don't add a val that's already in the list
499 if val in fixed:
500 continue
501 l = list(fixed)
502 l.append(val)
503 if len(l) == len(all_matches):
504 # found a solution
505 return l
506 # not at the end yet, add next digit
507 r = search_in_permutations(l)
508 if r:
509 # nested search_in_permutations() call found a solution
510 return r
511 # this entire branch yielded no solution
512 return None
513
514 if not all_matches:
515 raise RuntimeError('Cannot solve: no candidates')
516
517 solution = search_in_permutations()
518 if not solution:
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200519 raise NotSolvable('The requested resource requirements are not solvable %r'
520 % all_matches)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200521 return solution
522
523
524def contains_hash(list_of_dicts, a_hash):
525 for d in list_of_dicts:
526 if d.get(HASH_KEY) == a_hash:
527 return True
528 return False
529
530def item_matches(item, wanted_item, ignore_keys=None):
531 if is_dict(wanted_item):
532 # match up two dicts
533 if not isinstance(item, dict):
534 return False
535 for key, wanted_val in wanted_item.items():
536 if ignore_keys and key in ignore_keys:
537 continue
538 if not item_matches(item.get(key), wanted_val, ignore_keys=ignore_keys):
539 return False
540 return True
541
542 if is_list(wanted_item):
Pau Espin Pedrol4e36f7c2017-08-28 13:29:28 +0200543 if not is_list(item):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200544 return False
Pau Espin Pedrol58475512017-09-14 15:33:15 +0200545 # Validate that all elements in both lists are of the same type:
546 t = util.list_validate_same_elem_type(wanted_item + item)
547 if t is None:
548 return True # both lists are empty, return
549 # For lists of complex objects, we expect them to be sorted lists:
550 if t in (dict, list, tuple):
551 for i in range(max(len(wanted_item), len(item))):
552 log.ctx(idx=i)
553 subitem = item[i] if i < len(item) else util.empty_instance_type(t)
554 wanted_subitem = wanted_item[i] if i < len(wanted_item) else util.empty_instance_type(t)
555 if not item_matches(subitem, wanted_subitem, ignore_keys=ignore_keys):
556 return False
557 else: # for lists of basic elements, we handle them as unsorted sets:
558 for val in wanted_item:
559 if val not in item:
560 return False
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200561 return True
562
563 return item == wanted_item
564
565
566class ReservedResources(log.Origin):
567 '''
568 After all resources have been figured out, this is the API that a test case
569 gets to interact with resources. From those resources that have been
570 reserved for it, it can pick some to mark them as currently in use.
571 Functions like nitb() provide a resource by automatically picking its
572 dependencies from so far unused (but reserved) resource.
573 '''
574
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200575 def __init__(self, resources_pool, origin, reserved, modifiers):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200576 self.resources_pool = resources_pool
577 self.origin = origin
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200578 self.reserved_original = reserved
579 self.reserved = copy.deepcopy(self.reserved_original)
580 config.overlay(self.reserved, modifiers)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200581
582 def __repr__(self):
583 return 'resources(%s)=%s' % (self.origin.name(), pprint.pformat(self.reserved))
584
585 def get(self, kind, specifics=None):
586 if specifics is None:
587 specifics = {}
588 self.dbg('requesting use of', kind, specifics=specifics)
589 want = { kind: [specifics] }
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200590 available_dict = self.reserved.find(self.origin, want, skip_if_marked=USED_KEY,
591 do_copy=False, raise_if_missing=False,
592 log_label='Using')
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200593 available = available_dict.get(kind)
594 self.dbg(available=len(available))
595 if not available:
Neels Hofmeyrf9e86932017-06-06 19:47:40 +0200596 # cook up a detailed error message for the current situation
597 kind_reserved = self.reserved.get(kind, [])
598 used_count = len([r for r in kind_reserved if USED_KEY in r])
599 matching = self.reserved.find(self.origin, want, raise_if_missing=False, log_label=None).get(kind, [])
600 if not matching:
601 msg = 'none of the reserved resources matches requirements %r' % specifics
602 elif not (used_count < len(kind_reserved)):
603 msg = 'suite.conf reserved only %d x %r.' % (len(kind_reserved), kind)
604 else:
605 msg = ('No unused resource left that matches the requirements;'
606 ' Of reserved %d x %r, %d match the requirements, but all are already in use;'
607 ' Requirements: %r'
608 % (len(kind_reserved), kind, len(matching), specifics))
609 raise NoResourceExn('When trying to use instance nr %d of %r: %s' % (used_count + 1, kind, msg))
610
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200611 pick = available[0]
612 self.dbg(using=pick)
613 assert not pick.get(USED_KEY)
614 pick[USED_KEY] = True
615 return copy.deepcopy(pick)
616
617 def put(self, item):
618 if not item.get(USED_KEY):
619 raise RuntimeError('Can only put() a resource that is used: %r' % item)
620 hash_to_put = item.get(HASH_KEY)
621 if not hash_to_put:
622 raise RuntimeError('Can only put() a resource that has a hash marker: %r' % item)
623 for key, item_list in self.reserved.items():
624 my_list = self.get(key)
625 for my_item in my_list:
626 if hash_to_put == my_item.get(HASH_KEY):
627 my_item.pop(USED_KEY)
628
629 def put_all(self):
Pau Espin Pedrol1dd29552017-06-13 18:07:57 +0200630 if not self.reserved:
631 return
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200632 for key, item_list in self.reserved.items():
Pau Espin Pedrol1dd29552017-06-13 18:07:57 +0200633 for item in item_list:
634 item.pop(USED_KEY, None)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200635
636 def free(self):
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200637 if self.reserved_original:
638 self.resources_pool.free(self.origin, self.reserved_original)
639 self.reserved_original = None
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200640
Neels Hofmeyr2d1d5612017-05-22 20:02:41 +0200641 def counts(self):
642 counts = {}
643 for key in self.reserved.keys():
644 counts[key] = self.count(key)
645 return counts
646
647 def count(self, key):
648 return len(self.reserved.get(key) or [])
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200649
650# vim: expandtab tabstop=4 shiftwidth=4