blob: 20302d31497f7c8fede2ba9ae74f39940af506fe [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,
Pau Espin Pedrol154dc932020-04-20 17:02:08 +0200115 'enb[].cell_list[].pci': schema.UINT,
Pau Espin Pedrol6c778742020-04-20 12:15:06 +0200116 'enb[].cell_list[].ncell_list[]': schema.UINT,
Pau Espin Pedrolf46ae222020-04-17 16:23:54 +0200117 'enb[].cell_list[].scell_list[]': schema.UINT,
118 'enb[].cell_list[].dl_earfcn': schema.UINT,
Pau Espin Pedrold4404d52020-04-20 13:29:31 +0200119 'enb[].cell_list[].dl_rfemu.type': schema.STR,
120 'enb[].cell_list[].dl_rfemu.addr': schema.IPV4,
121 'enb[].cell_list[].dl_rfemu.ports[]': schema.UINT,
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200122 'arfcn[].arfcn': schema.INT,
123 'arfcn[].band': schema.BAND,
Holger Hans Peter Freyther93a89422019-02-27 06:36:36 +0000124 'modem[].type': schema.STR,
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200125 'modem[].label': schema.STR,
126 'modem[].path': schema.STR,
127 'modem[].imsi': schema.IMSI,
128 'modem[].ki': schema.KI,
Pau Espin Pedrol713ce2c2017-08-24 16:57:17 +0200129 'modem[].auth_algo': schema.AUTH_ALGO,
Andre Puschmann22ec00a2020-03-24 09:58:06 +0100130 'modem[].apn_ipaddr': schema.IPV4,
Pau Espin Pedrolc8b0f932020-02-11 17:45:26 +0100131 'modem[].remote_user': schema.STR,
132 'modem[].addr': schema.IPV4,
Pau Espin Pedrol57497a62017-08-28 14:21:15 +0200133 'modem[].ciphers[]': schema.CIPHER,
Pau Espin Pedrolac18fd32017-08-31 18:49:47 +0200134 'modem[].features[]': schema.MODEM_FEATURE,
Pau Espin Pedrola9a2fe22020-02-13 19:29:55 +0100135 'modem[].rf_dev_type': schema.STR,
136 'modem[].rf_dev_args': schema.STR,
Andre Puschmann65e769f2020-04-06 14:53:13 +0200137 'modem[].num_carriers': schema.UINT,
Pau Espin Pedrol76b2c2a2020-04-01 19:51:08 +0200138 'modem[].additional_args': schema.STR,
Andre Puschmann35234f22020-03-23 18:52:41 +0100139 'modem[].airplane_t_on_ms': schema.INT,
140 'modem[].airplane_t_off_ms': schema.INT,
Pau Espin Pedrola6d63042020-04-20 15:14:51 +0200141 'modem[].tx_gain': schema.UINT,
142 'modem[].rx_gain': schema.UINT,
Pau Espin Pedrolbc1ed882018-05-17 16:59:58 +0200143 'osmocon_phone[].serial_device': schema.STR,
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200144 }
145
146WANT_SCHEMA = util.dict_add(
Pau Espin Pedrolead79ac2017-09-12 15:19:18 +0200147 dict([('%s[].times' % r, schema.TIMES) for r in R_ALL]),
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200148 RESOURCES_SCHEMA)
149
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200150CONF_SCHEMA = util.dict_add(
Pau Espin Pedrol5dc24592018-08-27 12:53:41 +0200151 { 'defaults.timeout': schema.STR,
Pau Espin Pedrolb6937712020-02-27 18:02:20 +0100152 'config.bsc.net.codec_list[]': schema.CODEC,
Pau Espin Pedrol1e81b5a2020-03-16 12:42:17 +0100153 'config.enb.enable_pcap': schema.BOOL_STR,
Pau Espin Pedrolda2e31f2020-03-31 13:45:01 +0200154 'config.epc.type': schema.STR,
Pau Espin Pedrol04ad3b52020-04-06 12:25:22 +0200155 'config.epc.qci': schema.UINT,
Pau Espin Pedrol1e81b5a2020-03-16 12:42:17 +0100156 'config.epc.enable_pcap': schema.BOOL_STR,
157 'config.modem.enable_pcap': schema.BOOL_STR,
Pau Espin Pedrolc04528c2020-04-01 13:55:51 +0200158 'config.amarisoft.license_server_addr': schema.IPV4,
Andre Puschmann2dcc4312020-03-28 15:34:00 +0100159 'config.iperf3cli.time': schema.DURATION,
Pau Espin Pedrol1e81b5a2020-03-16 12:42:17 +0100160 },
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200161 dict([('resources.%s' % key, val) for key, val in WANT_SCHEMA.items()]),
162 dict([('modifiers.%s' % key, val) for key, val in WANT_SCHEMA.items()]))
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200163
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200164KNOWN_BTS_TYPES = {
Your Name44af3412017-04-13 03:11:59 +0200165 'osmo-bts-sysmo': bts_sysmo.SysmoBts,
166 'osmo-bts-trx': bts_osmotrx.OsmoBtsTrx,
Pau Espin Pedrolfeb66e72019-03-04 18:37:04 +0100167 'osmo-bts-oc2g': bts_oc2g.OsmoBtsOC2G,
Pau Espin Pedroldaed4472017-09-15 14:11:35 +0200168 'osmo-bts-octphy': bts_octphy.OsmoBtsOctphy,
Holger Hans Peter Freytherd2db10d2018-12-06 18:34:53 +0000169 'osmo-bts-virtual': bts_osmovirtual.OsmoBtsVirtual,
Pau Espin Pedrol1b28a582018-03-08 21:01:26 +0100170 'nanobts': bts_nanobts.NanoBts,
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200171 }
172
Pau Espin Pedrolc8b0f932020-02-11 17:45:26 +0100173KNOWN_ENB_TYPES = {
Pau Espin Pedrol9b486ee2020-04-10 19:41:06 +0200174 'srsenb': enb_srs.srsENB,
175 'amarisoftenb': enb_amarisoft.AmarisoftENB,
Pau Espin Pedrolc8b0f932020-02-11 17:45:26 +0100176}
Holger Hans Peter Freyther74243012019-02-27 08:18:38 +0000177
Pau Espin Pedrolda2e31f2020-03-31 13:45:01 +0200178KNOWN_EPC_TYPES = {
Pau Espin Pedrol9b486ee2020-04-10 19:41:06 +0200179 'srsepc': epc_srs.srsEPC,
180 'amarisoftepc': epc_amarisoft.AmarisoftEPC,
Pau Espin Pedrolda2e31f2020-03-31 13:45:01 +0200181}
182
Holger Hans Peter Freyther74243012019-02-27 08:18:38 +0000183KNOWN_MS_TYPES = {
184 # Map None to ofono for forward compability
Pau Espin Pedrol0dbd6942020-04-10 20:57:36 +0200185 None: ms_ofono.Modem,
186 'ofono': ms_ofono.Modem,
Holger Hans Peter Freyther10501cc2019-02-25 03:20:16 +0000187 'osmo-mobile': ms_osmo_mobile.MSOsmoMobile,
Pau Espin Pedrol9b486ee2020-04-10 19:41:06 +0200188 'srsue': ms_srs.srsUE,
189 'amarisoftue': ms_amarisoft.AmarisoftUE,
Holger Hans Peter Freyther74243012019-02-27 08:18:38 +0000190}
191
192
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200193def register_bts_type(name, clazz):
194 KNOWN_BTS_TYPES[name] = clazz
195
196class ResourcesPool(log.Origin):
197 _remember_to_free = None
198 _registered_exit_handler = False
199
200 def __init__(self):
Pau Espin Pedrol66a38912020-03-11 20:11:08 +0100201 self.config_path = config.get_config_file(config.RESOURCES_CONF)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200202 self.state_dir = config.get_state_dir()
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200203 super().__init__(log.C_CNF, conf=self.config_path, state=self.state_dir.path)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200204 self.read_conf()
205
206 def read_conf(self):
207 self.all_resources = Resources(config.read(self.config_path, RESOURCES_SCHEMA))
208 self.all_resources.set_hashes()
209
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200210 def reserve(self, origin, want, modifiers):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200211 '''
212 attempt to reserve the resources specified in the dict 'want' for
213 'origin'. Obtain a lock on the resources lock dir, verify that all
214 wanted resources are available, and if yes mark them as reserved.
215
216 On success, return a reservation object which can be used to release
217 the reservation. The reservation will be freed automatically on program
218 exit, if not yet done manually.
219
220 'origin' should be an Origin() instance.
221
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200222 'want' is a dict matching RESOURCES_SCHEMA, used to specify what to
223 reserve.
224
225 'modifiers' is a dict matching RESOURCES_SCHEMA, it is overlaid on top
226 of 'want'.
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200227
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200228 If an entry has no attribute set, any of the resources may be
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200229 reserved without further limitations.
230
231 ResourcesPool may also be selected with narrowed down constraints.
Neels Hofmeyr76d81032017-05-18 18:35:32 +0200232 This would reserve one IP address, two modems, one BTS of type
Neels Hofmeyr391afe32017-05-18 19:22:12 +0200233 sysmo and one of type trx, plus 2 ARFCNs in the 1800 band:
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200234
235 {
Neels Hofmeyr76d81032017-05-18 18:35:32 +0200236 'ip_address': [ {} ],
Neels Hofmeyr391afe32017-05-18 19:22:12 +0200237 'bts': [ { 'type': 'sysmo' }, { 'type': 'trx' } ],
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200238 'arfcn': [ { 'band': 'GSM-1800' }, { 'band': 'GSM-1800' } ],
239 'modem': [ {}, {} ],
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200240 }
241 '''
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200242 schema.validate(want, RESOURCES_SCHEMA)
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200243 schema.validate(modifiers, RESOURCES_SCHEMA)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200244
245 origin_id = origin.origin_id()
246
247 with self.state_dir.lock(origin_id):
248 rrfile_path = self.state_dir.mk_parentdir(RESERVED_RESOURCES_FILE)
249 reserved = Resources(config.read(rrfile_path, if_missing_return={}))
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200250 to_be_reserved = self.all_resources.without(reserved).find(origin, want)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200251
252 to_be_reserved.mark_reserved_by(origin_id)
253
254 reserved.add(to_be_reserved)
255 config.write(rrfile_path, reserved)
256
257 self.remember_to_free(to_be_reserved)
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200258 return ReservedResources(self, origin, to_be_reserved, modifiers)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200259
260 def free(self, origin, to_be_freed):
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200261 log.ctx(origin)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200262 with self.state_dir.lock(origin.origin_id()):
263 rrfile_path = self.state_dir.mk_parentdir(RESERVED_RESOURCES_FILE)
264 reserved = Resources(config.read(rrfile_path, if_missing_return={}))
265 reserved.drop(to_be_freed)
266 config.write(rrfile_path, reserved)
267 self.forget_freed(to_be_freed)
268
269 def register_exit_handler(self):
270 if self._registered_exit_handler:
271 return
272 atexit.register(self.clean_up_registered_resources)
273 self._registered_exit_handler = True
274
275 def unregister_exit_handler(self):
276 if not self._registered_exit_handler:
277 return
278 atexit.unregister(self.clean_up_registered_resources)
279 self._registered_exit_handler = False
280
281 def clean_up_registered_resources(self):
282 if not self._remember_to_free:
283 return
284 self.free(log.Origin('atexit.clean_up_registered_resources()'),
285 self._remember_to_free)
286
287 def remember_to_free(self, to_be_reserved):
288 self.register_exit_handler()
289 if not self._remember_to_free:
290 self._remember_to_free = Resources()
291 self._remember_to_free.add(to_be_reserved)
292
293 def forget_freed(self, freed):
294 if freed is self._remember_to_free:
295 self._remember_to_free.clear()
296 else:
297 self._remember_to_free.drop(freed)
298 if not self._remember_to_free:
299 self.unregister_exit_handler()
300
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100301 def next_persistent_value(self, token, first_val, validate_func, inc_func, origin):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200302 origin_id = origin.origin_id()
303
304 with self.state_dir.lock(origin_id):
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100305 token_path = self.state_dir.child('last_used_%s.state' % token)
306 log.ctx(token_path)
307 last_value = first_val
308 if os.path.exists(token_path):
309 if not os.path.isfile(token_path):
310 raise RuntimeError('path should be a file but is not: %r' % token_path)
311 with open(token_path, 'r') as f:
312 last_value = f.read().strip()
313 validate_func(last_value)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200314
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100315 next_value = inc_func(last_value)
316 with open(token_path, 'w') as f:
317 f.write(next_value)
318 return next_value
319
320 def next_msisdn(self, origin):
321 return self.next_persistent_value('msisdn', '1000', schema.msisdn, util.msisdn_inc, origin)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200322
Pau Espin Pedrol5e0c2512017-11-06 18:40:23 +0100323 def next_lac(self, origin):
Pau Espin Pedrolf7f06362017-11-28 12:56:35 +0100324 # LAC=0 has special meaning (MS detached), avoid it
325 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 +0200326
Pau Espin Pedrol8a3a7b52017-11-28 15:50:02 +0100327 def next_rac(self, origin):
328 return self.next_persistent_value('rac', '1', schema.uint8, lambda x: str((int(x)+1) % pow(2,8) or 1), origin)
329
Pau Espin Pedrol4ccce7c2017-11-07 11:13:20 +0100330 def next_cellid(self, origin):
331 return self.next_persistent_value('cellid', '1', schema.uint16, lambda x: str((int(x)+1) % pow(2,16)), origin)
332
Pau Espin Pedrol8a3a7b52017-11-28 15:50:02 +0100333 def next_bvci(self, origin):
334 # BVCI=0 and =1 are reserved, avoid them.
335 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)
336
Pau Espin Pedrol4676cbd2017-09-14 17:35:03 +0200337class NoResourceExn(log.Error):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200338 pass
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200339
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200340class Resources(dict):
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200341
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200342 def __init__(self, all_resources={}, do_copy=True):
343 if do_copy:
344 all_resources = copy.deepcopy(all_resources)
345 self.update(all_resources)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200346
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200347 def drop(self, reserved, fail_if_not_found=True):
348 # protect from modifying reserved because we're the same object
349 if reserved is self:
350 raise RuntimeError('Refusing to drop a list of resources from itself.'
351 ' This is probably a bug where a list of Resources()'
352 ' should have been copied but is passed as-is.'
353 ' use Resources.clear() instead.')
354
355 for key, reserved_list in reserved.items():
356 my_list = self.get(key) or []
357
358 if my_list is reserved_list:
359 self.pop(key)
360 continue
361
362 for reserved_item in reserved_list:
363 found = False
364 reserved_hash = reserved_item.get(HASH_KEY)
365 if not reserved_hash:
366 raise RuntimeError('Resources.drop() only works with hashed items')
367
368 for i in range(len(my_list)):
369 my_item = my_list[i]
370 my_hash = my_item.get(HASH_KEY)
371 if not my_hash:
372 raise RuntimeError('Resources.drop() only works with hashed items')
373 if my_hash == reserved_hash:
374 found = True
375 my_list.pop(i)
376 break
377
378 if fail_if_not_found and not found:
379 raise RuntimeError('Asked to drop resource from a pool, but the'
380 ' resource was not found: %s = %r' % (key, reserved_item))
381
382 if not my_list:
383 self.pop(key)
384 return self
385
386 def without(self, reserved):
387 return Resources(self).drop(reserved)
388
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200389 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 +0200390 '''
391 Pass a dict of resource requirements, e.g.:
392 want = {
393 'bts': [ {'type': 'osmo-bts-sysmo',}, {} ],
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200394 'modem': [ {}, {}, {} ]
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200395 }
396 This function tries to find a combination from the available resources that
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200397 matches these requirements. The return value is a dict (wrapped in a Resources class)
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200398 that contains the matching resources in the order of 'want' dict: in above
399 example, the returned dict would have a 'bts' list with the first item being
400 a sysmoBTS, the second item being any other available BTS.
401
402 If skip_if_marked is passed, any resource that contains this key is skipped.
403 E.g. if a BTS has the USED_KEY set like
404 reserved_resources = { 'bts' : {..., '_used': True} }
405 then this may be skipped by passing skip_if_marked='_used'
406 (or rather skip_if_marked=USED_KEY).
407
408 If do_copy is True, the returned dict is a deep copy and does not share
409 lists with any other Resources dict.
410
411 If raise_if_missing is False, this will return an empty item for any
412 resource that had no match, instead of immediately raising an exception.
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200413
414 This function expects input dictionaries whose contents have already
415 been replicated based on its the 'times' attributes. See
416 config.replicate_times() for more details.
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200417 '''
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200418 matches = {}
Neels Hofmeyr17c139e2017-04-12 02:42:02 +0200419 for key, want_list in sorted(want.items()): # sorted for deterministic test results
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200420 # here we have a resource of a given type, e.g. 'bts', with a list
421 # containing as many BTSes as the caller wants to reserve/use. Each
422 # list item contains specifics for the particular BTS.
Neels Hofmeyr2a1a1fa2017-05-29 01:36:21 +0200423 my_list = self.get(key, [])
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200424
Neels Hofmeyrf9e86932017-06-06 19:47:40 +0200425 if log_label:
426 for_origin.log(log_label, len(want_list), 'x', key, '(candidates: %d)'%len(my_list))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200427
428 # Try to avoid a less constrained item snatching away a resource
429 # from a more detailed constrained requirement.
430
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200431 # first record all matches, so that each requested item has a list
432 # of all available resources that match it. Some resources may
433 # appear for multiple requested items. Store matching indexes.
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200434 all_matches = []
435 for want_item in want_list:
436 item_match_list = []
437 for i in range(len(my_list)):
438 my_item = my_list[i]
439 if skip_if_marked and my_item.get(skip_if_marked):
440 continue
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200441 if item_matches(my_item, want_item):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200442 item_match_list.append(i)
443 if not item_match_list:
Neels Hofmeyr9b907702017-05-06 23:20:33 +0200444 if raise_if_missing:
445 raise NoResourceExn('No matching resource available for %s = %r'
446 % (key, want_item))
447 else:
448 # this one failed... see below
449 all_matches = []
450 break
451
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200452 all_matches.append( item_match_list )
453
454 if not all_matches:
Neels Hofmeyr9b907702017-05-06 23:20:33 +0200455 # ...this one failed. Makes no sense to solve resource
456 # allocations, return an empty list for this key to mark
457 # failure.
458 matches[key] = []
459 continue
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200460
461 # figure out who gets what
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200462 try:
463 solution = solve(all_matches)
464 except NotSolvable:
465 # instead of a cryptic error message, raise an exception that
466 # conveys meaning to the user.
467 raise NoResourceExn('Could not resolve request to reserve resources: '
468 '%d x %s with requirements: %r' % (len(want_list), key, want_list))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200469 picked = [ my_list[i] for i in solution if i is not None ]
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200470 for_origin.dbg('Picked', config.tostr(picked))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200471 matches[key] = picked
472
473 return Resources(matches, do_copy=do_copy)
474
475 def set_hashes(self):
476 for key, item_list in self.items():
477 for item in item_list:
478 item[HASH_KEY] = util.hash_obj(item, HASH_KEY, RESERVED_KEY, USED_KEY)
479
480 def add(self, more):
481 if more is self:
482 raise RuntimeError('adding a list of resources to itself?')
483 config.add(self, copy.deepcopy(more))
484
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200485 def mark_reserved_by(self, origin_id):
486 for key, item_list in self.items():
487 for item in item_list:
488 item[RESERVED_KEY] = origin_id
489
490
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200491class NotSolvable(Exception):
492 pass
493
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200494def solve(all_matches):
495 '''
496 all_matches shall be a list of index-lists.
497 all_matches[i] is the list of indexes that item i can use.
498 Return a solution so that each i gets a different index.
499 solve([ [0, 1, 2],
500 [0],
501 [0, 2] ]) == [1, 0, 2]
502 '''
503
504 def all_differ(l):
505 return len(set(l)) == len(l)
506
507 def search_in_permutations(fixed=[]):
508 idx = len(fixed)
509 for i in range(len(all_matches[idx])):
510 val = all_matches[idx][i]
511 # don't add a val that's already in the list
512 if val in fixed:
513 continue
514 l = list(fixed)
515 l.append(val)
516 if len(l) == len(all_matches):
517 # found a solution
518 return l
519 # not at the end yet, add next digit
520 r = search_in_permutations(l)
521 if r:
522 # nested search_in_permutations() call found a solution
523 return r
524 # this entire branch yielded no solution
525 return None
526
527 if not all_matches:
528 raise RuntimeError('Cannot solve: no candidates')
529
530 solution = search_in_permutations()
531 if not solution:
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200532 raise NotSolvable('The requested resource requirements are not solvable %r'
533 % all_matches)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200534 return solution
535
536
537def contains_hash(list_of_dicts, a_hash):
538 for d in list_of_dicts:
539 if d.get(HASH_KEY) == a_hash:
540 return True
541 return False
542
543def item_matches(item, wanted_item, ignore_keys=None):
544 if is_dict(wanted_item):
545 # match up two dicts
546 if not isinstance(item, dict):
547 return False
548 for key, wanted_val in wanted_item.items():
549 if ignore_keys and key in ignore_keys:
550 continue
551 if not item_matches(item.get(key), wanted_val, ignore_keys=ignore_keys):
552 return False
553 return True
554
555 if is_list(wanted_item):
Pau Espin Pedrol4e36f7c2017-08-28 13:29:28 +0200556 if not is_list(item):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200557 return False
Pau Espin Pedrol58475512017-09-14 15:33:15 +0200558 # Validate that all elements in both lists are of the same type:
559 t = util.list_validate_same_elem_type(wanted_item + item)
560 if t is None:
561 return True # both lists are empty, return
562 # For lists of complex objects, we expect them to be sorted lists:
563 if t in (dict, list, tuple):
564 for i in range(max(len(wanted_item), len(item))):
565 log.ctx(idx=i)
566 subitem = item[i] if i < len(item) else util.empty_instance_type(t)
567 wanted_subitem = wanted_item[i] if i < len(wanted_item) else util.empty_instance_type(t)
568 if not item_matches(subitem, wanted_subitem, ignore_keys=ignore_keys):
569 return False
570 else: # for lists of basic elements, we handle them as unsorted sets:
571 for val in wanted_item:
572 if val not in item:
573 return False
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200574 return True
575
576 return item == wanted_item
577
578
579class ReservedResources(log.Origin):
580 '''
581 After all resources have been figured out, this is the API that a test case
582 gets to interact with resources. From those resources that have been
583 reserved for it, it can pick some to mark them as currently in use.
584 Functions like nitb() provide a resource by automatically picking its
585 dependencies from so far unused (but reserved) resource.
586 '''
587
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200588 def __init__(self, resources_pool, origin, reserved, modifiers):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200589 self.resources_pool = resources_pool
590 self.origin = origin
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200591 self.reserved_original = reserved
592 self.reserved = copy.deepcopy(self.reserved_original)
593 config.overlay(self.reserved, modifiers)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200594
595 def __repr__(self):
596 return 'resources(%s)=%s' % (self.origin.name(), pprint.pformat(self.reserved))
597
598 def get(self, kind, specifics=None):
599 if specifics is None:
600 specifics = {}
601 self.dbg('requesting use of', kind, specifics=specifics)
602 want = { kind: [specifics] }
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200603 available_dict = self.reserved.find(self.origin, want, skip_if_marked=USED_KEY,
604 do_copy=False, raise_if_missing=False,
605 log_label='Using')
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200606 available = available_dict.get(kind)
607 self.dbg(available=len(available))
608 if not available:
Neels Hofmeyrf9e86932017-06-06 19:47:40 +0200609 # cook up a detailed error message for the current situation
610 kind_reserved = self.reserved.get(kind, [])
611 used_count = len([r for r in kind_reserved if USED_KEY in r])
612 matching = self.reserved.find(self.origin, want, raise_if_missing=False, log_label=None).get(kind, [])
613 if not matching:
614 msg = 'none of the reserved resources matches requirements %r' % specifics
615 elif not (used_count < len(kind_reserved)):
616 msg = 'suite.conf reserved only %d x %r.' % (len(kind_reserved), kind)
617 else:
618 msg = ('No unused resource left that matches the requirements;'
619 ' Of reserved %d x %r, %d match the requirements, but all are already in use;'
620 ' Requirements: %r'
621 % (len(kind_reserved), kind, len(matching), specifics))
622 raise NoResourceExn('When trying to use instance nr %d of %r: %s' % (used_count + 1, kind, msg))
623
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200624 pick = available[0]
625 self.dbg(using=pick)
626 assert not pick.get(USED_KEY)
627 pick[USED_KEY] = True
628 return copy.deepcopy(pick)
629
630 def put(self, item):
631 if not item.get(USED_KEY):
632 raise RuntimeError('Can only put() a resource that is used: %r' % item)
633 hash_to_put = item.get(HASH_KEY)
634 if not hash_to_put:
635 raise RuntimeError('Can only put() a resource that has a hash marker: %r' % item)
636 for key, item_list in self.reserved.items():
637 my_list = self.get(key)
638 for my_item in my_list:
639 if hash_to_put == my_item.get(HASH_KEY):
640 my_item.pop(USED_KEY)
641
642 def put_all(self):
Pau Espin Pedrol1dd29552017-06-13 18:07:57 +0200643 if not self.reserved:
644 return
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200645 for key, item_list in self.reserved.items():
Pau Espin Pedrol1dd29552017-06-13 18:07:57 +0200646 for item in item_list:
647 item.pop(USED_KEY, None)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200648
649 def free(self):
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200650 if self.reserved_original:
651 self.resources_pool.free(self.origin, self.reserved_original)
652 self.reserved_original = None
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200653
Neels Hofmeyr2d1d5612017-05-22 20:02:41 +0200654 def counts(self):
655 counts = {}
656 for key in self.reserved.keys():
657 counts[key] = self.count(key)
658 return counts
659
660 def count(self, key):
661 return len(self.reserved.get(key) or [])
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200662
663# vim: expandtab tabstop=4 shiftwidth=4