blob: efb7d5a1b581481b240b1fb385262adef39fc61d [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,
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,
Andre Puschmanna7f19832020-04-07 14:38:27 +020097 'enb[].enable_measurements': schema.BOOL_STR,
98 'enb[].a1_report_type': schema.STR,
99 'enb[].a1_report_value': schema.INT,
100 'enb[].a1_hysteresis': schema.INT,
101 'enb[].a1_time_to_trigger': schema.INT,
102 'enb[].a2_report_type': schema.STR,
103 'enb[].a2_report_value': schema.INT,
104 'enb[].a2_hysteresis': schema.INT,
105 'enb[].a2_time_to_trigger': schema.INT,
106 'enb[].a3_report_type': schema.STR,
107 'enb[].a3_report_value': schema.INT,
108 'enb[].a3_hysteresis': schema.INT,
109 'enb[].a3_time_to_trigger': schema.INT,
Pau Espin Pedrolf46ae222020-04-17 16:23:54 +0200110 'enb[].num_cells': schema.UINT,
111 'enb[].cell_list[].cell_id': schema.UINT,
112 'enb[].cell_list[].scell_list[]': schema.UINT,
113 'enb[].cell_list[].dl_earfcn': schema.UINT,
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200114 'arfcn[].arfcn': schema.INT,
115 'arfcn[].band': schema.BAND,
Holger Hans Peter Freyther93a89422019-02-27 06:36:36 +0000116 'modem[].type': schema.STR,
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200117 'modem[].label': schema.STR,
118 'modem[].path': schema.STR,
119 'modem[].imsi': schema.IMSI,
120 'modem[].ki': schema.KI,
Pau Espin Pedrol713ce2c2017-08-24 16:57:17 +0200121 'modem[].auth_algo': schema.AUTH_ALGO,
Andre Puschmann22ec00a2020-03-24 09:58:06 +0100122 'modem[].apn_ipaddr': schema.IPV4,
Pau Espin Pedrolc8b0f932020-02-11 17:45:26 +0100123 'modem[].remote_user': schema.STR,
124 'modem[].addr': schema.IPV4,
Pau Espin Pedrol57497a62017-08-28 14:21:15 +0200125 'modem[].ciphers[]': schema.CIPHER,
Pau Espin Pedrolac18fd32017-08-31 18:49:47 +0200126 'modem[].features[]': schema.MODEM_FEATURE,
Pau Espin Pedrola9a2fe22020-02-13 19:29:55 +0100127 'modem[].rf_dev_type': schema.STR,
128 'modem[].rf_dev_args': schema.STR,
Andre Puschmann65e769f2020-04-06 14:53:13 +0200129 'modem[].num_carriers': schema.UINT,
Pau Espin Pedrol76b2c2a2020-04-01 19:51:08 +0200130 'modem[].additional_args': schema.STR,
Andre Puschmann35234f22020-03-23 18:52:41 +0100131 'modem[].airplane_t_on_ms': schema.INT,
132 'modem[].airplane_t_off_ms': schema.INT,
Pau Espin Pedrolbc1ed882018-05-17 16:59:58 +0200133 'osmocon_phone[].serial_device': schema.STR,
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200134 }
135
136WANT_SCHEMA = util.dict_add(
Pau Espin Pedrolead79ac2017-09-12 15:19:18 +0200137 dict([('%s[].times' % r, schema.TIMES) for r in R_ALL]),
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200138 RESOURCES_SCHEMA)
139
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200140CONF_SCHEMA = util.dict_add(
Pau Espin Pedrol5dc24592018-08-27 12:53:41 +0200141 { 'defaults.timeout': schema.STR,
Pau Espin Pedrolb6937712020-02-27 18:02:20 +0100142 'config.bsc.net.codec_list[]': schema.CODEC,
Pau Espin Pedrol1e81b5a2020-03-16 12:42:17 +0100143 'config.enb.enable_pcap': schema.BOOL_STR,
Pau Espin Pedrolda2e31f2020-03-31 13:45:01 +0200144 'config.epc.type': schema.STR,
Pau Espin Pedrol04ad3b52020-04-06 12:25:22 +0200145 'config.epc.qci': schema.UINT,
Pau Espin Pedrol1e81b5a2020-03-16 12:42:17 +0100146 'config.epc.enable_pcap': schema.BOOL_STR,
147 'config.modem.enable_pcap': schema.BOOL_STR,
Pau Espin Pedrolc04528c2020-04-01 13:55:51 +0200148 'config.amarisoft.license_server_addr': schema.IPV4,
Andre Puschmann2dcc4312020-03-28 15:34:00 +0100149 'config.iperf3cli.time': schema.DURATION,
Pau Espin Pedrol1e81b5a2020-03-16 12:42:17 +0100150 },
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200151 dict([('resources.%s' % key, val) for key, val in WANT_SCHEMA.items()]),
152 dict([('modifiers.%s' % key, val) for key, val in WANT_SCHEMA.items()]))
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200153
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200154KNOWN_BTS_TYPES = {
Your Name44af3412017-04-13 03:11:59 +0200155 'osmo-bts-sysmo': bts_sysmo.SysmoBts,
156 'osmo-bts-trx': bts_osmotrx.OsmoBtsTrx,
Pau Espin Pedrolfeb66e72019-03-04 18:37:04 +0100157 'osmo-bts-oc2g': bts_oc2g.OsmoBtsOC2G,
Pau Espin Pedroldaed4472017-09-15 14:11:35 +0200158 'osmo-bts-octphy': bts_octphy.OsmoBtsOctphy,
Holger Hans Peter Freytherd2db10d2018-12-06 18:34:53 +0000159 'osmo-bts-virtual': bts_osmovirtual.OsmoBtsVirtual,
Pau Espin Pedrol1b28a582018-03-08 21:01:26 +0100160 'nanobts': bts_nanobts.NanoBts,
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200161 }
162
Pau Espin Pedrolc8b0f932020-02-11 17:45:26 +0100163KNOWN_ENB_TYPES = {
Pau Espin Pedrol9b486ee2020-04-10 19:41:06 +0200164 'srsenb': enb_srs.srsENB,
165 'amarisoftenb': enb_amarisoft.AmarisoftENB,
Pau Espin Pedrolc8b0f932020-02-11 17:45:26 +0100166}
Holger Hans Peter Freyther74243012019-02-27 08:18:38 +0000167
Pau Espin Pedrolda2e31f2020-03-31 13:45:01 +0200168KNOWN_EPC_TYPES = {
Pau Espin Pedrol9b486ee2020-04-10 19:41:06 +0200169 'srsepc': epc_srs.srsEPC,
170 'amarisoftepc': epc_amarisoft.AmarisoftEPC,
Pau Espin Pedrolda2e31f2020-03-31 13:45:01 +0200171}
172
Holger Hans Peter Freyther74243012019-02-27 08:18:38 +0000173KNOWN_MS_TYPES = {
174 # Map None to ofono for forward compability
Pau Espin Pedrol0dbd6942020-04-10 20:57:36 +0200175 None: ms_ofono.Modem,
176 'ofono': ms_ofono.Modem,
Holger Hans Peter Freyther10501cc2019-02-25 03:20:16 +0000177 'osmo-mobile': ms_osmo_mobile.MSOsmoMobile,
Pau Espin Pedrol9b486ee2020-04-10 19:41:06 +0200178 'srsue': ms_srs.srsUE,
179 'amarisoftue': ms_amarisoft.AmarisoftUE,
Holger Hans Peter Freyther74243012019-02-27 08:18:38 +0000180}
181
182
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200183def register_bts_type(name, clazz):
184 KNOWN_BTS_TYPES[name] = clazz
185
186class ResourcesPool(log.Origin):
187 _remember_to_free = None
188 _registered_exit_handler = False
189
190 def __init__(self):
Pau Espin Pedrol66a38912020-03-11 20:11:08 +0100191 self.config_path = config.get_config_file(config.RESOURCES_CONF)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200192 self.state_dir = config.get_state_dir()
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200193 super().__init__(log.C_CNF, conf=self.config_path, state=self.state_dir.path)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200194 self.read_conf()
195
196 def read_conf(self):
197 self.all_resources = Resources(config.read(self.config_path, RESOURCES_SCHEMA))
198 self.all_resources.set_hashes()
199
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200200 def reserve(self, origin, want, modifiers):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200201 '''
202 attempt to reserve the resources specified in the dict 'want' for
203 'origin'. Obtain a lock on the resources lock dir, verify that all
204 wanted resources are available, and if yes mark them as reserved.
205
206 On success, return a reservation object which can be used to release
207 the reservation. The reservation will be freed automatically on program
208 exit, if not yet done manually.
209
210 'origin' should be an Origin() instance.
211
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200212 'want' is a dict matching RESOURCES_SCHEMA, used to specify what to
213 reserve.
214
215 'modifiers' is a dict matching RESOURCES_SCHEMA, it is overlaid on top
216 of 'want'.
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200217
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200218 If an entry has no attribute set, any of the resources may be
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200219 reserved without further limitations.
220
221 ResourcesPool may also be selected with narrowed down constraints.
Neels Hofmeyr76d81032017-05-18 18:35:32 +0200222 This would reserve one IP address, two modems, one BTS of type
Neels Hofmeyr391afe32017-05-18 19:22:12 +0200223 sysmo and one of type trx, plus 2 ARFCNs in the 1800 band:
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200224
225 {
Neels Hofmeyr76d81032017-05-18 18:35:32 +0200226 'ip_address': [ {} ],
Neels Hofmeyr391afe32017-05-18 19:22:12 +0200227 'bts': [ { 'type': 'sysmo' }, { 'type': 'trx' } ],
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200228 'arfcn': [ { 'band': 'GSM-1800' }, { 'band': 'GSM-1800' } ],
229 'modem': [ {}, {} ],
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200230 }
231 '''
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200232 schema.validate(want, RESOURCES_SCHEMA)
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200233 schema.validate(modifiers, RESOURCES_SCHEMA)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200234
235 origin_id = origin.origin_id()
236
237 with self.state_dir.lock(origin_id):
238 rrfile_path = self.state_dir.mk_parentdir(RESERVED_RESOURCES_FILE)
239 reserved = Resources(config.read(rrfile_path, if_missing_return={}))
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200240 to_be_reserved = self.all_resources.without(reserved).find(origin, want)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200241
242 to_be_reserved.mark_reserved_by(origin_id)
243
244 reserved.add(to_be_reserved)
245 config.write(rrfile_path, reserved)
246
247 self.remember_to_free(to_be_reserved)
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200248 return ReservedResources(self, origin, to_be_reserved, modifiers)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200249
250 def free(self, origin, to_be_freed):
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200251 log.ctx(origin)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200252 with self.state_dir.lock(origin.origin_id()):
253 rrfile_path = self.state_dir.mk_parentdir(RESERVED_RESOURCES_FILE)
254 reserved = Resources(config.read(rrfile_path, if_missing_return={}))
255 reserved.drop(to_be_freed)
256 config.write(rrfile_path, reserved)
257 self.forget_freed(to_be_freed)
258
259 def register_exit_handler(self):
260 if self._registered_exit_handler:
261 return
262 atexit.register(self.clean_up_registered_resources)
263 self._registered_exit_handler = True
264
265 def unregister_exit_handler(self):
266 if not self._registered_exit_handler:
267 return
268 atexit.unregister(self.clean_up_registered_resources)
269 self._registered_exit_handler = False
270
271 def clean_up_registered_resources(self):
272 if not self._remember_to_free:
273 return
274 self.free(log.Origin('atexit.clean_up_registered_resources()'),
275 self._remember_to_free)
276
277 def remember_to_free(self, to_be_reserved):
278 self.register_exit_handler()
279 if not self._remember_to_free:
280 self._remember_to_free = Resources()
281 self._remember_to_free.add(to_be_reserved)
282
283 def forget_freed(self, freed):
284 if freed is self._remember_to_free:
285 self._remember_to_free.clear()
286 else:
287 self._remember_to_free.drop(freed)
288 if not self._remember_to_free:
289 self.unregister_exit_handler()
290
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100291 def next_persistent_value(self, token, first_val, validate_func, inc_func, origin):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200292 origin_id = origin.origin_id()
293
294 with self.state_dir.lock(origin_id):
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100295 token_path = self.state_dir.child('last_used_%s.state' % token)
296 log.ctx(token_path)
297 last_value = first_val
298 if os.path.exists(token_path):
299 if not os.path.isfile(token_path):
300 raise RuntimeError('path should be a file but is not: %r' % token_path)
301 with open(token_path, 'r') as f:
302 last_value = f.read().strip()
303 validate_func(last_value)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200304
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100305 next_value = inc_func(last_value)
306 with open(token_path, 'w') as f:
307 f.write(next_value)
308 return next_value
309
310 def next_msisdn(self, origin):
311 return self.next_persistent_value('msisdn', '1000', schema.msisdn, util.msisdn_inc, origin)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200312
Pau Espin Pedrol5e0c2512017-11-06 18:40:23 +0100313 def next_lac(self, origin):
Pau Espin Pedrolf7f06362017-11-28 12:56:35 +0100314 # LAC=0 has special meaning (MS detached), avoid it
315 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 +0200316
Pau Espin Pedrol8a3a7b52017-11-28 15:50:02 +0100317 def next_rac(self, origin):
318 return self.next_persistent_value('rac', '1', schema.uint8, lambda x: str((int(x)+1) % pow(2,8) or 1), origin)
319
Pau Espin Pedrol4ccce7c2017-11-07 11:13:20 +0100320 def next_cellid(self, origin):
321 return self.next_persistent_value('cellid', '1', schema.uint16, lambda x: str((int(x)+1) % pow(2,16)), origin)
322
Pau Espin Pedrol8a3a7b52017-11-28 15:50:02 +0100323 def next_bvci(self, origin):
324 # BVCI=0 and =1 are reserved, avoid them.
325 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)
326
Pau Espin Pedrol4676cbd2017-09-14 17:35:03 +0200327class NoResourceExn(log.Error):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200328 pass
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200329
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200330class Resources(dict):
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200331
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200332 def __init__(self, all_resources={}, do_copy=True):
333 if do_copy:
334 all_resources = copy.deepcopy(all_resources)
335 self.update(all_resources)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200336
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200337 def drop(self, reserved, fail_if_not_found=True):
338 # protect from modifying reserved because we're the same object
339 if reserved is self:
340 raise RuntimeError('Refusing to drop a list of resources from itself.'
341 ' This is probably a bug where a list of Resources()'
342 ' should have been copied but is passed as-is.'
343 ' use Resources.clear() instead.')
344
345 for key, reserved_list in reserved.items():
346 my_list = self.get(key) or []
347
348 if my_list is reserved_list:
349 self.pop(key)
350 continue
351
352 for reserved_item in reserved_list:
353 found = False
354 reserved_hash = reserved_item.get(HASH_KEY)
355 if not reserved_hash:
356 raise RuntimeError('Resources.drop() only works with hashed items')
357
358 for i in range(len(my_list)):
359 my_item = my_list[i]
360 my_hash = my_item.get(HASH_KEY)
361 if not my_hash:
362 raise RuntimeError('Resources.drop() only works with hashed items')
363 if my_hash == reserved_hash:
364 found = True
365 my_list.pop(i)
366 break
367
368 if fail_if_not_found and not found:
369 raise RuntimeError('Asked to drop resource from a pool, but the'
370 ' resource was not found: %s = %r' % (key, reserved_item))
371
372 if not my_list:
373 self.pop(key)
374 return self
375
376 def without(self, reserved):
377 return Resources(self).drop(reserved)
378
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200379 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 +0200380 '''
381 Pass a dict of resource requirements, e.g.:
382 want = {
383 'bts': [ {'type': 'osmo-bts-sysmo',}, {} ],
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200384 'modem': [ {}, {}, {} ]
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200385 }
386 This function tries to find a combination from the available resources that
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200387 matches these requirements. The return value is a dict (wrapped in a Resources class)
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200388 that contains the matching resources in the order of 'want' dict: in above
389 example, the returned dict would have a 'bts' list with the first item being
390 a sysmoBTS, the second item being any other available BTS.
391
392 If skip_if_marked is passed, any resource that contains this key is skipped.
393 E.g. if a BTS has the USED_KEY set like
394 reserved_resources = { 'bts' : {..., '_used': True} }
395 then this may be skipped by passing skip_if_marked='_used'
396 (or rather skip_if_marked=USED_KEY).
397
398 If do_copy is True, the returned dict is a deep copy and does not share
399 lists with any other Resources dict.
400
401 If raise_if_missing is False, this will return an empty item for any
402 resource that had no match, instead of immediately raising an exception.
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200403
404 This function expects input dictionaries whose contents have already
405 been replicated based on its the 'times' attributes. See
406 config.replicate_times() for more details.
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200407 '''
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200408 matches = {}
Neels Hofmeyr17c139e2017-04-12 02:42:02 +0200409 for key, want_list in sorted(want.items()): # sorted for deterministic test results
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200410 # here we have a resource of a given type, e.g. 'bts', with a list
411 # containing as many BTSes as the caller wants to reserve/use. Each
412 # list item contains specifics for the particular BTS.
Neels Hofmeyr2a1a1fa2017-05-29 01:36:21 +0200413 my_list = self.get(key, [])
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200414
Neels Hofmeyrf9e86932017-06-06 19:47:40 +0200415 if log_label:
416 for_origin.log(log_label, len(want_list), 'x', key, '(candidates: %d)'%len(my_list))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200417
418 # Try to avoid a less constrained item snatching away a resource
419 # from a more detailed constrained requirement.
420
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200421 # first record all matches, so that each requested item has a list
422 # of all available resources that match it. Some resources may
423 # appear for multiple requested items. Store matching indexes.
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200424 all_matches = []
425 for want_item in want_list:
426 item_match_list = []
427 for i in range(len(my_list)):
428 my_item = my_list[i]
429 if skip_if_marked and my_item.get(skip_if_marked):
430 continue
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200431 if item_matches(my_item, want_item):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200432 item_match_list.append(i)
433 if not item_match_list:
Neels Hofmeyr9b907702017-05-06 23:20:33 +0200434 if raise_if_missing:
435 raise NoResourceExn('No matching resource available for %s = %r'
436 % (key, want_item))
437 else:
438 # this one failed... see below
439 all_matches = []
440 break
441
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200442 all_matches.append( item_match_list )
443
444 if not all_matches:
Neels Hofmeyr9b907702017-05-06 23:20:33 +0200445 # ...this one failed. Makes no sense to solve resource
446 # allocations, return an empty list for this key to mark
447 # failure.
448 matches[key] = []
449 continue
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200450
451 # figure out who gets what
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200452 try:
453 solution = solve(all_matches)
454 except NotSolvable:
455 # instead of a cryptic error message, raise an exception that
456 # conveys meaning to the user.
457 raise NoResourceExn('Could not resolve request to reserve resources: '
458 '%d x %s with requirements: %r' % (len(want_list), key, want_list))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200459 picked = [ my_list[i] for i in solution if i is not None ]
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200460 for_origin.dbg('Picked', config.tostr(picked))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200461 matches[key] = picked
462
463 return Resources(matches, do_copy=do_copy)
464
465 def set_hashes(self):
466 for key, item_list in self.items():
467 for item in item_list:
468 item[HASH_KEY] = util.hash_obj(item, HASH_KEY, RESERVED_KEY, USED_KEY)
469
470 def add(self, more):
471 if more is self:
472 raise RuntimeError('adding a list of resources to itself?')
473 config.add(self, copy.deepcopy(more))
474
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200475 def mark_reserved_by(self, origin_id):
476 for key, item_list in self.items():
477 for item in item_list:
478 item[RESERVED_KEY] = origin_id
479
480
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200481class NotSolvable(Exception):
482 pass
483
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200484def solve(all_matches):
485 '''
486 all_matches shall be a list of index-lists.
487 all_matches[i] is the list of indexes that item i can use.
488 Return a solution so that each i gets a different index.
489 solve([ [0, 1, 2],
490 [0],
491 [0, 2] ]) == [1, 0, 2]
492 '''
493
494 def all_differ(l):
495 return len(set(l)) == len(l)
496
497 def search_in_permutations(fixed=[]):
498 idx = len(fixed)
499 for i in range(len(all_matches[idx])):
500 val = all_matches[idx][i]
501 # don't add a val that's already in the list
502 if val in fixed:
503 continue
504 l = list(fixed)
505 l.append(val)
506 if len(l) == len(all_matches):
507 # found a solution
508 return l
509 # not at the end yet, add next digit
510 r = search_in_permutations(l)
511 if r:
512 # nested search_in_permutations() call found a solution
513 return r
514 # this entire branch yielded no solution
515 return None
516
517 if not all_matches:
518 raise RuntimeError('Cannot solve: no candidates')
519
520 solution = search_in_permutations()
521 if not solution:
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200522 raise NotSolvable('The requested resource requirements are not solvable %r'
523 % all_matches)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200524 return solution
525
526
527def contains_hash(list_of_dicts, a_hash):
528 for d in list_of_dicts:
529 if d.get(HASH_KEY) == a_hash:
530 return True
531 return False
532
533def item_matches(item, wanted_item, ignore_keys=None):
534 if is_dict(wanted_item):
535 # match up two dicts
536 if not isinstance(item, dict):
537 return False
538 for key, wanted_val in wanted_item.items():
539 if ignore_keys and key in ignore_keys:
540 continue
541 if not item_matches(item.get(key), wanted_val, ignore_keys=ignore_keys):
542 return False
543 return True
544
545 if is_list(wanted_item):
Pau Espin Pedrol4e36f7c2017-08-28 13:29:28 +0200546 if not is_list(item):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200547 return False
Pau Espin Pedrol58475512017-09-14 15:33:15 +0200548 # Validate that all elements in both lists are of the same type:
549 t = util.list_validate_same_elem_type(wanted_item + item)
550 if t is None:
551 return True # both lists are empty, return
552 # For lists of complex objects, we expect them to be sorted lists:
553 if t in (dict, list, tuple):
554 for i in range(max(len(wanted_item), len(item))):
555 log.ctx(idx=i)
556 subitem = item[i] if i < len(item) else util.empty_instance_type(t)
557 wanted_subitem = wanted_item[i] if i < len(wanted_item) else util.empty_instance_type(t)
558 if not item_matches(subitem, wanted_subitem, ignore_keys=ignore_keys):
559 return False
560 else: # for lists of basic elements, we handle them as unsorted sets:
561 for val in wanted_item:
562 if val not in item:
563 return False
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200564 return True
565
566 return item == wanted_item
567
568
569class ReservedResources(log.Origin):
570 '''
571 After all resources have been figured out, this is the API that a test case
572 gets to interact with resources. From those resources that have been
573 reserved for it, it can pick some to mark them as currently in use.
574 Functions like nitb() provide a resource by automatically picking its
575 dependencies from so far unused (but reserved) resource.
576 '''
577
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200578 def __init__(self, resources_pool, origin, reserved, modifiers):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200579 self.resources_pool = resources_pool
580 self.origin = origin
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200581 self.reserved_original = reserved
582 self.reserved = copy.deepcopy(self.reserved_original)
583 config.overlay(self.reserved, modifiers)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200584
585 def __repr__(self):
586 return 'resources(%s)=%s' % (self.origin.name(), pprint.pformat(self.reserved))
587
588 def get(self, kind, specifics=None):
589 if specifics is None:
590 specifics = {}
591 self.dbg('requesting use of', kind, specifics=specifics)
592 want = { kind: [specifics] }
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200593 available_dict = self.reserved.find(self.origin, want, skip_if_marked=USED_KEY,
594 do_copy=False, raise_if_missing=False,
595 log_label='Using')
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200596 available = available_dict.get(kind)
597 self.dbg(available=len(available))
598 if not available:
Neels Hofmeyrf9e86932017-06-06 19:47:40 +0200599 # cook up a detailed error message for the current situation
600 kind_reserved = self.reserved.get(kind, [])
601 used_count = len([r for r in kind_reserved if USED_KEY in r])
602 matching = self.reserved.find(self.origin, want, raise_if_missing=False, log_label=None).get(kind, [])
603 if not matching:
604 msg = 'none of the reserved resources matches requirements %r' % specifics
605 elif not (used_count < len(kind_reserved)):
606 msg = 'suite.conf reserved only %d x %r.' % (len(kind_reserved), kind)
607 else:
608 msg = ('No unused resource left that matches the requirements;'
609 ' Of reserved %d x %r, %d match the requirements, but all are already in use;'
610 ' Requirements: %r'
611 % (len(kind_reserved), kind, len(matching), specifics))
612 raise NoResourceExn('When trying to use instance nr %d of %r: %s' % (used_count + 1, kind, msg))
613
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200614 pick = available[0]
615 self.dbg(using=pick)
616 assert not pick.get(USED_KEY)
617 pick[USED_KEY] = True
618 return copy.deepcopy(pick)
619
620 def put(self, item):
621 if not item.get(USED_KEY):
622 raise RuntimeError('Can only put() a resource that is used: %r' % item)
623 hash_to_put = item.get(HASH_KEY)
624 if not hash_to_put:
625 raise RuntimeError('Can only put() a resource that has a hash marker: %r' % item)
626 for key, item_list in self.reserved.items():
627 my_list = self.get(key)
628 for my_item in my_list:
629 if hash_to_put == my_item.get(HASH_KEY):
630 my_item.pop(USED_KEY)
631
632 def put_all(self):
Pau Espin Pedrol1dd29552017-06-13 18:07:57 +0200633 if not self.reserved:
634 return
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200635 for key, item_list in self.reserved.items():
Pau Espin Pedrol1dd29552017-06-13 18:07:57 +0200636 for item in item_list:
637 item.pop(USED_KEY, None)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200638
639 def free(self):
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200640 if self.reserved_original:
641 self.resources_pool.free(self.origin, self.reserved_original)
642 self.reserved_original = None
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200643
Neels Hofmeyr2d1d5612017-05-22 20:02:41 +0200644 def counts(self):
645 counts = {}
646 for key in self.reserved.keys():
647 counts[key] = self.count(key)
648 return counts
649
650 def count(self, key):
651 return len(self.reserved.get(key) or [])
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200652
653# vim: expandtab tabstop=4 shiftwidth=4