blob: ded8568642935636df2fa75ad93ea6aca1a9eb21 [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 Pedrol491f77c2020-04-20 14:20:43 +020092 'enb[].id': schema.UINT,
Pau Espin Pedrol1deb1ae2020-02-27 15:16:09 +010093 'enb[].num_prb': schema.UINT,
Pau Espin Pedrolb6937712020-02-27 18:02:20 +010094 'enb[].transmission_mode': schema.LTE_TRANSMISSION_MODE,
Pau Espin Pedrola6d63042020-04-20 15:14:51 +020095 'enb[].tx_gain': schema.UINT,
96 'enb[].rx_gain': schema.UINT,
Pau Espin Pedrola9a2fe22020-02-13 19:29:55 +010097 'enb[].rf_dev_type': schema.STR,
98 'enb[].rf_dev_args': schema.STR,
Pau Espin Pedrol76b2c2a2020-04-01 19:51:08 +020099 'enb[].additional_args': schema.STR,
Andre Puschmanna7f19832020-04-07 14:38:27 +0200100 'enb[].enable_measurements': schema.BOOL_STR,
101 'enb[].a1_report_type': schema.STR,
102 'enb[].a1_report_value': schema.INT,
103 'enb[].a1_hysteresis': schema.INT,
104 'enb[].a1_time_to_trigger': schema.INT,
105 'enb[].a2_report_type': schema.STR,
106 'enb[].a2_report_value': schema.INT,
107 'enb[].a2_hysteresis': schema.INT,
108 'enb[].a2_time_to_trigger': schema.INT,
109 'enb[].a3_report_type': schema.STR,
110 'enb[].a3_report_value': schema.INT,
111 'enb[].a3_hysteresis': schema.INT,
112 'enb[].a3_time_to_trigger': schema.INT,
Pau Espin Pedrolf46ae222020-04-17 16:23:54 +0200113 'enb[].num_cells': schema.UINT,
114 'enb[].cell_list[].cell_id': schema.UINT,
115 'enb[].cell_list[].scell_list[]': schema.UINT,
116 'enb[].cell_list[].dl_earfcn': schema.UINT,
Pau Espin Pedrold4404d52020-04-20 13:29:31 +0200117 'enb[].cell_list[].dl_rfemu.type': schema.STR,
118 'enb[].cell_list[].dl_rfemu.addr': schema.IPV4,
119 'enb[].cell_list[].dl_rfemu.ports[]': schema.UINT,
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200120 'arfcn[].arfcn': schema.INT,
121 'arfcn[].band': schema.BAND,
Holger Hans Peter Freyther93a89422019-02-27 06:36:36 +0000122 'modem[].type': schema.STR,
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200123 'modem[].label': schema.STR,
124 'modem[].path': schema.STR,
125 'modem[].imsi': schema.IMSI,
126 'modem[].ki': schema.KI,
Pau Espin Pedrol713ce2c2017-08-24 16:57:17 +0200127 'modem[].auth_algo': schema.AUTH_ALGO,
Andre Puschmann22ec00a2020-03-24 09:58:06 +0100128 'modem[].apn_ipaddr': schema.IPV4,
Pau Espin Pedrolc8b0f932020-02-11 17:45:26 +0100129 'modem[].remote_user': schema.STR,
130 'modem[].addr': schema.IPV4,
Pau Espin Pedrol57497a62017-08-28 14:21:15 +0200131 'modem[].ciphers[]': schema.CIPHER,
Pau Espin Pedrolac18fd32017-08-31 18:49:47 +0200132 'modem[].features[]': schema.MODEM_FEATURE,
Pau Espin Pedrola9a2fe22020-02-13 19:29:55 +0100133 'modem[].rf_dev_type': schema.STR,
134 'modem[].rf_dev_args': schema.STR,
Andre Puschmann65e769f2020-04-06 14:53:13 +0200135 'modem[].num_carriers': schema.UINT,
Pau Espin Pedrol76b2c2a2020-04-01 19:51:08 +0200136 'modem[].additional_args': schema.STR,
Andre Puschmann35234f22020-03-23 18:52:41 +0100137 'modem[].airplane_t_on_ms': schema.INT,
138 'modem[].airplane_t_off_ms': schema.INT,
Pau Espin Pedrola6d63042020-04-20 15:14:51 +0200139 'modem[].tx_gain': schema.UINT,
140 'modem[].rx_gain': schema.UINT,
Pau Espin Pedrolbc1ed882018-05-17 16:59:58 +0200141 'osmocon_phone[].serial_device': schema.STR,
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200142 }
143
144WANT_SCHEMA = util.dict_add(
Pau Espin Pedrolead79ac2017-09-12 15:19:18 +0200145 dict([('%s[].times' % r, schema.TIMES) for r in R_ALL]),
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200146 RESOURCES_SCHEMA)
147
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200148CONF_SCHEMA = util.dict_add(
Pau Espin Pedrol5dc24592018-08-27 12:53:41 +0200149 { 'defaults.timeout': schema.STR,
Pau Espin Pedrolb6937712020-02-27 18:02:20 +0100150 'config.bsc.net.codec_list[]': schema.CODEC,
Pau Espin Pedrol1e81b5a2020-03-16 12:42:17 +0100151 'config.enb.enable_pcap': schema.BOOL_STR,
Pau Espin Pedrolda2e31f2020-03-31 13:45:01 +0200152 'config.epc.type': schema.STR,
Pau Espin Pedrol04ad3b52020-04-06 12:25:22 +0200153 'config.epc.qci': schema.UINT,
Pau Espin Pedrol1e81b5a2020-03-16 12:42:17 +0100154 'config.epc.enable_pcap': schema.BOOL_STR,
155 'config.modem.enable_pcap': schema.BOOL_STR,
Pau Espin Pedrolc04528c2020-04-01 13:55:51 +0200156 'config.amarisoft.license_server_addr': schema.IPV4,
Andre Puschmann2dcc4312020-03-28 15:34:00 +0100157 'config.iperf3cli.time': schema.DURATION,
Pau Espin Pedrol1e81b5a2020-03-16 12:42:17 +0100158 },
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200159 dict([('resources.%s' % key, val) for key, val in WANT_SCHEMA.items()]),
160 dict([('modifiers.%s' % key, val) for key, val in WANT_SCHEMA.items()]))
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200161
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200162KNOWN_BTS_TYPES = {
Your Name44af3412017-04-13 03:11:59 +0200163 'osmo-bts-sysmo': bts_sysmo.SysmoBts,
164 'osmo-bts-trx': bts_osmotrx.OsmoBtsTrx,
Pau Espin Pedrolfeb66e72019-03-04 18:37:04 +0100165 'osmo-bts-oc2g': bts_oc2g.OsmoBtsOC2G,
Pau Espin Pedroldaed4472017-09-15 14:11:35 +0200166 'osmo-bts-octphy': bts_octphy.OsmoBtsOctphy,
Holger Hans Peter Freytherd2db10d2018-12-06 18:34:53 +0000167 'osmo-bts-virtual': bts_osmovirtual.OsmoBtsVirtual,
Pau Espin Pedrol1b28a582018-03-08 21:01:26 +0100168 'nanobts': bts_nanobts.NanoBts,
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200169 }
170
Pau Espin Pedrolc8b0f932020-02-11 17:45:26 +0100171KNOWN_ENB_TYPES = {
Pau Espin Pedrol9b486ee2020-04-10 19:41:06 +0200172 'srsenb': enb_srs.srsENB,
173 'amarisoftenb': enb_amarisoft.AmarisoftENB,
Pau Espin Pedrolc8b0f932020-02-11 17:45:26 +0100174}
Holger Hans Peter Freyther74243012019-02-27 08:18:38 +0000175
Pau Espin Pedrolda2e31f2020-03-31 13:45:01 +0200176KNOWN_EPC_TYPES = {
Pau Espin Pedrol9b486ee2020-04-10 19:41:06 +0200177 'srsepc': epc_srs.srsEPC,
178 'amarisoftepc': epc_amarisoft.AmarisoftEPC,
Pau Espin Pedrolda2e31f2020-03-31 13:45:01 +0200179}
180
Holger Hans Peter Freyther74243012019-02-27 08:18:38 +0000181KNOWN_MS_TYPES = {
182 # Map None to ofono for forward compability
Pau Espin Pedrol0dbd6942020-04-10 20:57:36 +0200183 None: ms_ofono.Modem,
184 'ofono': ms_ofono.Modem,
Holger Hans Peter Freyther10501cc2019-02-25 03:20:16 +0000185 'osmo-mobile': ms_osmo_mobile.MSOsmoMobile,
Pau Espin Pedrol9b486ee2020-04-10 19:41:06 +0200186 'srsue': ms_srs.srsUE,
187 'amarisoftue': ms_amarisoft.AmarisoftUE,
Holger Hans Peter Freyther74243012019-02-27 08:18:38 +0000188}
189
190
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200191def register_bts_type(name, clazz):
192 KNOWN_BTS_TYPES[name] = clazz
193
194class ResourcesPool(log.Origin):
195 _remember_to_free = None
196 _registered_exit_handler = False
197
198 def __init__(self):
Pau Espin Pedrol66a38912020-03-11 20:11:08 +0100199 self.config_path = config.get_config_file(config.RESOURCES_CONF)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200200 self.state_dir = config.get_state_dir()
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200201 super().__init__(log.C_CNF, conf=self.config_path, state=self.state_dir.path)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200202 self.read_conf()
203
204 def read_conf(self):
205 self.all_resources = Resources(config.read(self.config_path, RESOURCES_SCHEMA))
206 self.all_resources.set_hashes()
207
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200208 def reserve(self, origin, want, modifiers):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200209 '''
210 attempt to reserve the resources specified in the dict 'want' for
211 'origin'. Obtain a lock on the resources lock dir, verify that all
212 wanted resources are available, and if yes mark them as reserved.
213
214 On success, return a reservation object which can be used to release
215 the reservation. The reservation will be freed automatically on program
216 exit, if not yet done manually.
217
218 'origin' should be an Origin() instance.
219
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200220 'want' is a dict matching RESOURCES_SCHEMA, used to specify what to
221 reserve.
222
223 'modifiers' is a dict matching RESOURCES_SCHEMA, it is overlaid on top
224 of 'want'.
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200225
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200226 If an entry has no attribute set, any of the resources may be
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200227 reserved without further limitations.
228
229 ResourcesPool may also be selected with narrowed down constraints.
Neels Hofmeyr76d81032017-05-18 18:35:32 +0200230 This would reserve one IP address, two modems, one BTS of type
Neels Hofmeyr391afe32017-05-18 19:22:12 +0200231 sysmo and one of type trx, plus 2 ARFCNs in the 1800 band:
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200232
233 {
Neels Hofmeyr76d81032017-05-18 18:35:32 +0200234 'ip_address': [ {} ],
Neels Hofmeyr391afe32017-05-18 19:22:12 +0200235 'bts': [ { 'type': 'sysmo' }, { 'type': 'trx' } ],
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200236 'arfcn': [ { 'band': 'GSM-1800' }, { 'band': 'GSM-1800' } ],
237 'modem': [ {}, {} ],
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200238 }
239 '''
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200240 schema.validate(want, RESOURCES_SCHEMA)
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200241 schema.validate(modifiers, RESOURCES_SCHEMA)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200242
243 origin_id = origin.origin_id()
244
245 with self.state_dir.lock(origin_id):
246 rrfile_path = self.state_dir.mk_parentdir(RESERVED_RESOURCES_FILE)
247 reserved = Resources(config.read(rrfile_path, if_missing_return={}))
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200248 to_be_reserved = self.all_resources.without(reserved).find(origin, want)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200249
250 to_be_reserved.mark_reserved_by(origin_id)
251
252 reserved.add(to_be_reserved)
253 config.write(rrfile_path, reserved)
254
255 self.remember_to_free(to_be_reserved)
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200256 return ReservedResources(self, origin, to_be_reserved, modifiers)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200257
258 def free(self, origin, to_be_freed):
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200259 log.ctx(origin)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200260 with self.state_dir.lock(origin.origin_id()):
261 rrfile_path = self.state_dir.mk_parentdir(RESERVED_RESOURCES_FILE)
262 reserved = Resources(config.read(rrfile_path, if_missing_return={}))
263 reserved.drop(to_be_freed)
264 config.write(rrfile_path, reserved)
265 self.forget_freed(to_be_freed)
266
267 def register_exit_handler(self):
268 if self._registered_exit_handler:
269 return
270 atexit.register(self.clean_up_registered_resources)
271 self._registered_exit_handler = True
272
273 def unregister_exit_handler(self):
274 if not self._registered_exit_handler:
275 return
276 atexit.unregister(self.clean_up_registered_resources)
277 self._registered_exit_handler = False
278
279 def clean_up_registered_resources(self):
280 if not self._remember_to_free:
281 return
282 self.free(log.Origin('atexit.clean_up_registered_resources()'),
283 self._remember_to_free)
284
285 def remember_to_free(self, to_be_reserved):
286 self.register_exit_handler()
287 if not self._remember_to_free:
288 self._remember_to_free = Resources()
289 self._remember_to_free.add(to_be_reserved)
290
291 def forget_freed(self, freed):
292 if freed is self._remember_to_free:
293 self._remember_to_free.clear()
294 else:
295 self._remember_to_free.drop(freed)
296 if not self._remember_to_free:
297 self.unregister_exit_handler()
298
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100299 def next_persistent_value(self, token, first_val, validate_func, inc_func, origin):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200300 origin_id = origin.origin_id()
301
302 with self.state_dir.lock(origin_id):
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100303 token_path = self.state_dir.child('last_used_%s.state' % token)
304 log.ctx(token_path)
305 last_value = first_val
306 if os.path.exists(token_path):
307 if not os.path.isfile(token_path):
308 raise RuntimeError('path should be a file but is not: %r' % token_path)
309 with open(token_path, 'r') as f:
310 last_value = f.read().strip()
311 validate_func(last_value)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200312
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100313 next_value = inc_func(last_value)
314 with open(token_path, 'w') as f:
315 f.write(next_value)
316 return next_value
317
318 def next_msisdn(self, origin):
319 return self.next_persistent_value('msisdn', '1000', schema.msisdn, util.msisdn_inc, origin)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200320
Pau Espin Pedrol5e0c2512017-11-06 18:40:23 +0100321 def next_lac(self, origin):
Pau Espin Pedrolf7f06362017-11-28 12:56:35 +0100322 # LAC=0 has special meaning (MS detached), avoid it
323 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 +0200324
Pau Espin Pedrol8a3a7b52017-11-28 15:50:02 +0100325 def next_rac(self, origin):
326 return self.next_persistent_value('rac', '1', schema.uint8, lambda x: str((int(x)+1) % pow(2,8) or 1), origin)
327
Pau Espin Pedrol4ccce7c2017-11-07 11:13:20 +0100328 def next_cellid(self, origin):
329 return self.next_persistent_value('cellid', '1', schema.uint16, lambda x: str((int(x)+1) % pow(2,16)), origin)
330
Pau Espin Pedrol8a3a7b52017-11-28 15:50:02 +0100331 def next_bvci(self, origin):
332 # BVCI=0 and =1 are reserved, avoid them.
333 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)
334
Pau Espin Pedrol4676cbd2017-09-14 17:35:03 +0200335class NoResourceExn(log.Error):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200336 pass
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200337
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200338class Resources(dict):
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200339
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200340 def __init__(self, all_resources={}, do_copy=True):
341 if do_copy:
342 all_resources = copy.deepcopy(all_resources)
343 self.update(all_resources)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200344
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200345 def drop(self, reserved, fail_if_not_found=True):
346 # protect from modifying reserved because we're the same object
347 if reserved is self:
348 raise RuntimeError('Refusing to drop a list of resources from itself.'
349 ' This is probably a bug where a list of Resources()'
350 ' should have been copied but is passed as-is.'
351 ' use Resources.clear() instead.')
352
353 for key, reserved_list in reserved.items():
354 my_list = self.get(key) or []
355
356 if my_list is reserved_list:
357 self.pop(key)
358 continue
359
360 for reserved_item in reserved_list:
361 found = False
362 reserved_hash = reserved_item.get(HASH_KEY)
363 if not reserved_hash:
364 raise RuntimeError('Resources.drop() only works with hashed items')
365
366 for i in range(len(my_list)):
367 my_item = my_list[i]
368 my_hash = my_item.get(HASH_KEY)
369 if not my_hash:
370 raise RuntimeError('Resources.drop() only works with hashed items')
371 if my_hash == reserved_hash:
372 found = True
373 my_list.pop(i)
374 break
375
376 if fail_if_not_found and not found:
377 raise RuntimeError('Asked to drop resource from a pool, but the'
378 ' resource was not found: %s = %r' % (key, reserved_item))
379
380 if not my_list:
381 self.pop(key)
382 return self
383
384 def without(self, reserved):
385 return Resources(self).drop(reserved)
386
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200387 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 +0200388 '''
389 Pass a dict of resource requirements, e.g.:
390 want = {
391 'bts': [ {'type': 'osmo-bts-sysmo',}, {} ],
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200392 'modem': [ {}, {}, {} ]
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200393 }
394 This function tries to find a combination from the available resources that
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200395 matches these requirements. The return value is a dict (wrapped in a Resources class)
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200396 that contains the matching resources in the order of 'want' dict: in above
397 example, the returned dict would have a 'bts' list with the first item being
398 a sysmoBTS, the second item being any other available BTS.
399
400 If skip_if_marked is passed, any resource that contains this key is skipped.
401 E.g. if a BTS has the USED_KEY set like
402 reserved_resources = { 'bts' : {..., '_used': True} }
403 then this may be skipped by passing skip_if_marked='_used'
404 (or rather skip_if_marked=USED_KEY).
405
406 If do_copy is True, the returned dict is a deep copy and does not share
407 lists with any other Resources dict.
408
409 If raise_if_missing is False, this will return an empty item for any
410 resource that had no match, instead of immediately raising an exception.
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200411
412 This function expects input dictionaries whose contents have already
413 been replicated based on its the 'times' attributes. See
414 config.replicate_times() for more details.
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200415 '''
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200416 matches = {}
Neels Hofmeyr17c139e2017-04-12 02:42:02 +0200417 for key, want_list in sorted(want.items()): # sorted for deterministic test results
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200418 # here we have a resource of a given type, e.g. 'bts', with a list
419 # containing as many BTSes as the caller wants to reserve/use. Each
420 # list item contains specifics for the particular BTS.
Neels Hofmeyr2a1a1fa2017-05-29 01:36:21 +0200421 my_list = self.get(key, [])
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200422
Neels Hofmeyrf9e86932017-06-06 19:47:40 +0200423 if log_label:
424 for_origin.log(log_label, len(want_list), 'x', key, '(candidates: %d)'%len(my_list))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200425
426 # Try to avoid a less constrained item snatching away a resource
427 # from a more detailed constrained requirement.
428
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200429 # first record all matches, so that each requested item has a list
430 # of all available resources that match it. Some resources may
431 # appear for multiple requested items. Store matching indexes.
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200432 all_matches = []
433 for want_item in want_list:
434 item_match_list = []
435 for i in range(len(my_list)):
436 my_item = my_list[i]
437 if skip_if_marked and my_item.get(skip_if_marked):
438 continue
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200439 if item_matches(my_item, want_item):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200440 item_match_list.append(i)
441 if not item_match_list:
Neels Hofmeyr9b907702017-05-06 23:20:33 +0200442 if raise_if_missing:
443 raise NoResourceExn('No matching resource available for %s = %r'
444 % (key, want_item))
445 else:
446 # this one failed... see below
447 all_matches = []
448 break
449
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200450 all_matches.append( item_match_list )
451
452 if not all_matches:
Neels Hofmeyr9b907702017-05-06 23:20:33 +0200453 # ...this one failed. Makes no sense to solve resource
454 # allocations, return an empty list for this key to mark
455 # failure.
456 matches[key] = []
457 continue
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200458
459 # figure out who gets what
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200460 try:
461 solution = solve(all_matches)
462 except NotSolvable:
463 # instead of a cryptic error message, raise an exception that
464 # conveys meaning to the user.
465 raise NoResourceExn('Could not resolve request to reserve resources: '
466 '%d x %s with requirements: %r' % (len(want_list), key, want_list))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200467 picked = [ my_list[i] for i in solution if i is not None ]
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200468 for_origin.dbg('Picked', config.tostr(picked))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200469 matches[key] = picked
470
471 return Resources(matches, do_copy=do_copy)
472
473 def set_hashes(self):
474 for key, item_list in self.items():
475 for item in item_list:
476 item[HASH_KEY] = util.hash_obj(item, HASH_KEY, RESERVED_KEY, USED_KEY)
477
478 def add(self, more):
479 if more is self:
480 raise RuntimeError('adding a list of resources to itself?')
481 config.add(self, copy.deepcopy(more))
482
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200483 def mark_reserved_by(self, origin_id):
484 for key, item_list in self.items():
485 for item in item_list:
486 item[RESERVED_KEY] = origin_id
487
488
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200489class NotSolvable(Exception):
490 pass
491
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200492def solve(all_matches):
493 '''
494 all_matches shall be a list of index-lists.
495 all_matches[i] is the list of indexes that item i can use.
496 Return a solution so that each i gets a different index.
497 solve([ [0, 1, 2],
498 [0],
499 [0, 2] ]) == [1, 0, 2]
500 '''
501
502 def all_differ(l):
503 return len(set(l)) == len(l)
504
505 def search_in_permutations(fixed=[]):
506 idx = len(fixed)
507 for i in range(len(all_matches[idx])):
508 val = all_matches[idx][i]
509 # don't add a val that's already in the list
510 if val in fixed:
511 continue
512 l = list(fixed)
513 l.append(val)
514 if len(l) == len(all_matches):
515 # found a solution
516 return l
517 # not at the end yet, add next digit
518 r = search_in_permutations(l)
519 if r:
520 # nested search_in_permutations() call found a solution
521 return r
522 # this entire branch yielded no solution
523 return None
524
525 if not all_matches:
526 raise RuntimeError('Cannot solve: no candidates')
527
528 solution = search_in_permutations()
529 if not solution:
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200530 raise NotSolvable('The requested resource requirements are not solvable %r'
531 % all_matches)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200532 return solution
533
534
535def contains_hash(list_of_dicts, a_hash):
536 for d in list_of_dicts:
537 if d.get(HASH_KEY) == a_hash:
538 return True
539 return False
540
541def item_matches(item, wanted_item, ignore_keys=None):
542 if is_dict(wanted_item):
543 # match up two dicts
544 if not isinstance(item, dict):
545 return False
546 for key, wanted_val in wanted_item.items():
547 if ignore_keys and key in ignore_keys:
548 continue
549 if not item_matches(item.get(key), wanted_val, ignore_keys=ignore_keys):
550 return False
551 return True
552
553 if is_list(wanted_item):
Pau Espin Pedrol4e36f7c2017-08-28 13:29:28 +0200554 if not is_list(item):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200555 return False
Pau Espin Pedrol58475512017-09-14 15:33:15 +0200556 # Validate that all elements in both lists are of the same type:
557 t = util.list_validate_same_elem_type(wanted_item + item)
558 if t is None:
559 return True # both lists are empty, return
560 # For lists of complex objects, we expect them to be sorted lists:
561 if t in (dict, list, tuple):
562 for i in range(max(len(wanted_item), len(item))):
563 log.ctx(idx=i)
564 subitem = item[i] if i < len(item) else util.empty_instance_type(t)
565 wanted_subitem = wanted_item[i] if i < len(wanted_item) else util.empty_instance_type(t)
566 if not item_matches(subitem, wanted_subitem, ignore_keys=ignore_keys):
567 return False
568 else: # for lists of basic elements, we handle them as unsorted sets:
569 for val in wanted_item:
570 if val not in item:
571 return False
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200572 return True
573
574 return item == wanted_item
575
576
577class ReservedResources(log.Origin):
578 '''
579 After all resources have been figured out, this is the API that a test case
580 gets to interact with resources. From those resources that have been
581 reserved for it, it can pick some to mark them as currently in use.
582 Functions like nitb() provide a resource by automatically picking its
583 dependencies from so far unused (but reserved) resource.
584 '''
585
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200586 def __init__(self, resources_pool, origin, reserved, modifiers):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200587 self.resources_pool = resources_pool
588 self.origin = origin
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200589 self.reserved_original = reserved
590 self.reserved = copy.deepcopy(self.reserved_original)
591 config.overlay(self.reserved, modifiers)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200592
593 def __repr__(self):
594 return 'resources(%s)=%s' % (self.origin.name(), pprint.pformat(self.reserved))
595
596 def get(self, kind, specifics=None):
597 if specifics is None:
598 specifics = {}
599 self.dbg('requesting use of', kind, specifics=specifics)
600 want = { kind: [specifics] }
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200601 available_dict = self.reserved.find(self.origin, want, skip_if_marked=USED_KEY,
602 do_copy=False, raise_if_missing=False,
603 log_label='Using')
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200604 available = available_dict.get(kind)
605 self.dbg(available=len(available))
606 if not available:
Neels Hofmeyrf9e86932017-06-06 19:47:40 +0200607 # cook up a detailed error message for the current situation
608 kind_reserved = self.reserved.get(kind, [])
609 used_count = len([r for r in kind_reserved if USED_KEY in r])
610 matching = self.reserved.find(self.origin, want, raise_if_missing=False, log_label=None).get(kind, [])
611 if not matching:
612 msg = 'none of the reserved resources matches requirements %r' % specifics
613 elif not (used_count < len(kind_reserved)):
614 msg = 'suite.conf reserved only %d x %r.' % (len(kind_reserved), kind)
615 else:
616 msg = ('No unused resource left that matches the requirements;'
617 ' Of reserved %d x %r, %d match the requirements, but all are already in use;'
618 ' Requirements: %r'
619 % (len(kind_reserved), kind, len(matching), specifics))
620 raise NoResourceExn('When trying to use instance nr %d of %r: %s' % (used_count + 1, kind, msg))
621
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200622 pick = available[0]
623 self.dbg(using=pick)
624 assert not pick.get(USED_KEY)
625 pick[USED_KEY] = True
626 return copy.deepcopy(pick)
627
628 def put(self, item):
629 if not item.get(USED_KEY):
630 raise RuntimeError('Can only put() a resource that is used: %r' % item)
631 hash_to_put = item.get(HASH_KEY)
632 if not hash_to_put:
633 raise RuntimeError('Can only put() a resource that has a hash marker: %r' % item)
634 for key, item_list in self.reserved.items():
635 my_list = self.get(key)
636 for my_item in my_list:
637 if hash_to_put == my_item.get(HASH_KEY):
638 my_item.pop(USED_KEY)
639
640 def put_all(self):
Pau Espin Pedrol1dd29552017-06-13 18:07:57 +0200641 if not self.reserved:
642 return
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200643 for key, item_list in self.reserved.items():
Pau Espin Pedrol1dd29552017-06-13 18:07:57 +0200644 for item in item_list:
645 item.pop(USED_KEY, None)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200646
647 def free(self):
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200648 if self.reserved_original:
649 self.resources_pool.free(self.origin, self.reserved_original)
650 self.reserved_original = None
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200651
Neels Hofmeyr2d1d5612017-05-22 20:02:41 +0200652 def counts(self):
653 counts = {}
654 for key in self.reserved.keys():
655 counts[key] = self.count(key)
656 return counts
657
658 def count(self, key):
659 return len(self.reserved.get(key) or [])
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200660
661# vim: expandtab tabstop=4 shiftwidth=4