blob: 5b86a0874ea7c47c6310845d83185d98a6fcafc3 [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 Pedrolfeb66e72019-03-04 18:37:04 +010029from . import bts_sysmo, bts_osmotrx, bts_osmovirtual, bts_octphy, bts_nanobts, bts_oc2g
Pau Espin Pedrole8bbcbf2020-04-10 19:51:31 +020030from . import modem
31from . import ms_osmo_mobile
32from . 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,
Pau Espin Pedrol1deb1ae2020-02-27 15:16:09 +010091 'enb[].num_prb': schema.UINT,
Pau Espin Pedrolb6937712020-02-27 18:02:20 +010092 'enb[].transmission_mode': schema.LTE_TRANSMISSION_MODE,
Andre Puschmann82b88902020-03-24 10:04:48 +010093 'enb[].num_cells': schema.UINT,
Pau Espin Pedrola9a2fe22020-02-13 19:29:55 +010094 'enb[].rf_dev_type': schema.STR,
95 'enb[].rf_dev_args': schema.STR,
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,
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200110 'arfcn[].arfcn': schema.INT,
111 'arfcn[].band': schema.BAND,
Holger Hans Peter Freyther93a89422019-02-27 06:36:36 +0000112 'modem[].type': schema.STR,
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200113 'modem[].label': schema.STR,
114 'modem[].path': schema.STR,
115 'modem[].imsi': schema.IMSI,
116 'modem[].ki': schema.KI,
Pau Espin Pedrol713ce2c2017-08-24 16:57:17 +0200117 'modem[].auth_algo': schema.AUTH_ALGO,
Andre Puschmann22ec00a2020-03-24 09:58:06 +0100118 'modem[].apn_ipaddr': schema.IPV4,
Pau Espin Pedrolc8b0f932020-02-11 17:45:26 +0100119 'modem[].remote_user': schema.STR,
120 'modem[].addr': schema.IPV4,
Pau Espin Pedrol57497a62017-08-28 14:21:15 +0200121 'modem[].ciphers[]': schema.CIPHER,
Pau Espin Pedrolac18fd32017-08-31 18:49:47 +0200122 'modem[].features[]': schema.MODEM_FEATURE,
Pau Espin Pedrola9a2fe22020-02-13 19:29:55 +0100123 'modem[].rf_dev_type': schema.STR,
124 'modem[].rf_dev_args': schema.STR,
Andre Puschmann65e769f2020-04-06 14:53:13 +0200125 'modem[].num_carriers': schema.UINT,
Pau Espin Pedrol76b2c2a2020-04-01 19:51:08 +0200126 'modem[].additional_args': schema.STR,
Andre Puschmann35234f22020-03-23 18:52:41 +0100127 'modem[].airplane_t_on_ms': schema.INT,
128 'modem[].airplane_t_off_ms': schema.INT,
Pau Espin Pedrolbc1ed882018-05-17 16:59:58 +0200129 'osmocon_phone[].serial_device': schema.STR,
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200130 }
131
132WANT_SCHEMA = util.dict_add(
Pau Espin Pedrolead79ac2017-09-12 15:19:18 +0200133 dict([('%s[].times' % r, schema.TIMES) for r in R_ALL]),
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200134 RESOURCES_SCHEMA)
135
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200136CONF_SCHEMA = util.dict_add(
Pau Espin Pedrol5dc24592018-08-27 12:53:41 +0200137 { 'defaults.timeout': schema.STR,
Pau Espin Pedrolb6937712020-02-27 18:02:20 +0100138 'config.bsc.net.codec_list[]': schema.CODEC,
Pau Espin Pedrol1e81b5a2020-03-16 12:42:17 +0100139 'config.enb.enable_pcap': schema.BOOL_STR,
Pau Espin Pedrolda2e31f2020-03-31 13:45:01 +0200140 'config.epc.type': schema.STR,
Pau Espin Pedrol04ad3b52020-04-06 12:25:22 +0200141 'config.epc.qci': schema.UINT,
Pau Espin Pedrol1e81b5a2020-03-16 12:42:17 +0100142 'config.epc.enable_pcap': schema.BOOL_STR,
143 'config.modem.enable_pcap': schema.BOOL_STR,
Pau Espin Pedrolc04528c2020-04-01 13:55:51 +0200144 'config.amarisoft.license_server_addr': schema.IPV4,
Andre Puschmann2dcc4312020-03-28 15:34:00 +0100145 'config.iperf3cli.time': schema.DURATION,
Pau Espin Pedrol1e81b5a2020-03-16 12:42:17 +0100146 },
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200147 dict([('resources.%s' % key, val) for key, val in WANT_SCHEMA.items()]),
148 dict([('modifiers.%s' % key, val) for key, val in WANT_SCHEMA.items()]))
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200149
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200150KNOWN_BTS_TYPES = {
Your Name44af3412017-04-13 03:11:59 +0200151 'osmo-bts-sysmo': bts_sysmo.SysmoBts,
152 'osmo-bts-trx': bts_osmotrx.OsmoBtsTrx,
Pau Espin Pedrolfeb66e72019-03-04 18:37:04 +0100153 'osmo-bts-oc2g': bts_oc2g.OsmoBtsOC2G,
Pau Espin Pedroldaed4472017-09-15 14:11:35 +0200154 'osmo-bts-octphy': bts_octphy.OsmoBtsOctphy,
Holger Hans Peter Freytherd2db10d2018-12-06 18:34:53 +0000155 'osmo-bts-virtual': bts_osmovirtual.OsmoBtsVirtual,
Pau Espin Pedrol1b28a582018-03-08 21:01:26 +0100156 'nanobts': bts_nanobts.NanoBts,
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200157 }
158
Pau Espin Pedrolc8b0f932020-02-11 17:45:26 +0100159KNOWN_ENB_TYPES = {
Pau Espin Pedrol9b486ee2020-04-10 19:41:06 +0200160 'srsenb': enb_srs.srsENB,
161 'amarisoftenb': enb_amarisoft.AmarisoftENB,
Pau Espin Pedrolc8b0f932020-02-11 17:45:26 +0100162}
Holger Hans Peter Freyther74243012019-02-27 08:18:38 +0000163
Pau Espin Pedrolda2e31f2020-03-31 13:45:01 +0200164KNOWN_EPC_TYPES = {
Pau Espin Pedrol9b486ee2020-04-10 19:41:06 +0200165 'srsepc': epc_srs.srsEPC,
166 'amarisoftepc': epc_amarisoft.AmarisoftEPC,
Pau Espin Pedrolda2e31f2020-03-31 13:45:01 +0200167}
168
Holger Hans Peter Freyther74243012019-02-27 08:18:38 +0000169KNOWN_MS_TYPES = {
170 # Map None to ofono for forward compability
171 None: modem.Modem,
172 'ofono': modem.Modem,
Holger Hans Peter Freyther10501cc2019-02-25 03:20:16 +0000173 'osmo-mobile': ms_osmo_mobile.MSOsmoMobile,
Pau Espin Pedrol9b486ee2020-04-10 19:41:06 +0200174 'srsue': ms_srs.srsUE,
175 'amarisoftue': ms_amarisoft.AmarisoftUE,
Holger Hans Peter Freyther74243012019-02-27 08:18:38 +0000176}
177
178
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200179def register_bts_type(name, clazz):
180 KNOWN_BTS_TYPES[name] = clazz
181
182class ResourcesPool(log.Origin):
183 _remember_to_free = None
184 _registered_exit_handler = False
185
186 def __init__(self):
Pau Espin Pedrol66a38912020-03-11 20:11:08 +0100187 self.config_path = config.get_config_file(config.RESOURCES_CONF)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200188 self.state_dir = config.get_state_dir()
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200189 super().__init__(log.C_CNF, conf=self.config_path, state=self.state_dir.path)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200190 self.read_conf()
191
192 def read_conf(self):
193 self.all_resources = Resources(config.read(self.config_path, RESOURCES_SCHEMA))
194 self.all_resources.set_hashes()
195
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200196 def reserve(self, origin, want, modifiers):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200197 '''
198 attempt to reserve the resources specified in the dict 'want' for
199 'origin'. Obtain a lock on the resources lock dir, verify that all
200 wanted resources are available, and if yes mark them as reserved.
201
202 On success, return a reservation object which can be used to release
203 the reservation. The reservation will be freed automatically on program
204 exit, if not yet done manually.
205
206 'origin' should be an Origin() instance.
207
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200208 'want' is a dict matching RESOURCES_SCHEMA, used to specify what to
209 reserve.
210
211 'modifiers' is a dict matching RESOURCES_SCHEMA, it is overlaid on top
212 of 'want'.
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200213
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200214 If an entry has no attribute set, any of the resources may be
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200215 reserved without further limitations.
216
217 ResourcesPool may also be selected with narrowed down constraints.
Neels Hofmeyr76d81032017-05-18 18:35:32 +0200218 This would reserve one IP address, two modems, one BTS of type
Neels Hofmeyr391afe32017-05-18 19:22:12 +0200219 sysmo and one of type trx, plus 2 ARFCNs in the 1800 band:
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200220
221 {
Neels Hofmeyr76d81032017-05-18 18:35:32 +0200222 'ip_address': [ {} ],
Neels Hofmeyr391afe32017-05-18 19:22:12 +0200223 'bts': [ { 'type': 'sysmo' }, { 'type': 'trx' } ],
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200224 'arfcn': [ { 'band': 'GSM-1800' }, { 'band': 'GSM-1800' } ],
225 'modem': [ {}, {} ],
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200226 }
227 '''
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200228 schema.validate(want, RESOURCES_SCHEMA)
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200229 schema.validate(modifiers, RESOURCES_SCHEMA)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200230
231 origin_id = origin.origin_id()
232
233 with self.state_dir.lock(origin_id):
234 rrfile_path = self.state_dir.mk_parentdir(RESERVED_RESOURCES_FILE)
235 reserved = Resources(config.read(rrfile_path, if_missing_return={}))
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200236 to_be_reserved = self.all_resources.without(reserved).find(origin, want)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200237
238 to_be_reserved.mark_reserved_by(origin_id)
239
240 reserved.add(to_be_reserved)
241 config.write(rrfile_path, reserved)
242
243 self.remember_to_free(to_be_reserved)
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200244 return ReservedResources(self, origin, to_be_reserved, modifiers)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200245
246 def free(self, origin, to_be_freed):
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200247 log.ctx(origin)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200248 with self.state_dir.lock(origin.origin_id()):
249 rrfile_path = self.state_dir.mk_parentdir(RESERVED_RESOURCES_FILE)
250 reserved = Resources(config.read(rrfile_path, if_missing_return={}))
251 reserved.drop(to_be_freed)
252 config.write(rrfile_path, reserved)
253 self.forget_freed(to_be_freed)
254
255 def register_exit_handler(self):
256 if self._registered_exit_handler:
257 return
258 atexit.register(self.clean_up_registered_resources)
259 self._registered_exit_handler = True
260
261 def unregister_exit_handler(self):
262 if not self._registered_exit_handler:
263 return
264 atexit.unregister(self.clean_up_registered_resources)
265 self._registered_exit_handler = False
266
267 def clean_up_registered_resources(self):
268 if not self._remember_to_free:
269 return
270 self.free(log.Origin('atexit.clean_up_registered_resources()'),
271 self._remember_to_free)
272
273 def remember_to_free(self, to_be_reserved):
274 self.register_exit_handler()
275 if not self._remember_to_free:
276 self._remember_to_free = Resources()
277 self._remember_to_free.add(to_be_reserved)
278
279 def forget_freed(self, freed):
280 if freed is self._remember_to_free:
281 self._remember_to_free.clear()
282 else:
283 self._remember_to_free.drop(freed)
284 if not self._remember_to_free:
285 self.unregister_exit_handler()
286
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100287 def next_persistent_value(self, token, first_val, validate_func, inc_func, origin):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200288 origin_id = origin.origin_id()
289
290 with self.state_dir.lock(origin_id):
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100291 token_path = self.state_dir.child('last_used_%s.state' % token)
292 log.ctx(token_path)
293 last_value = first_val
294 if os.path.exists(token_path):
295 if not os.path.isfile(token_path):
296 raise RuntimeError('path should be a file but is not: %r' % token_path)
297 with open(token_path, 'r') as f:
298 last_value = f.read().strip()
299 validate_func(last_value)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200300
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100301 next_value = inc_func(last_value)
302 with open(token_path, 'w') as f:
303 f.write(next_value)
304 return next_value
305
306 def next_msisdn(self, origin):
307 return self.next_persistent_value('msisdn', '1000', schema.msisdn, util.msisdn_inc, origin)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200308
Pau Espin Pedrol5e0c2512017-11-06 18:40:23 +0100309 def next_lac(self, origin):
Pau Espin Pedrolf7f06362017-11-28 12:56:35 +0100310 # LAC=0 has special meaning (MS detached), avoid it
311 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 +0200312
Pau Espin Pedrol8a3a7b52017-11-28 15:50:02 +0100313 def next_rac(self, origin):
314 return self.next_persistent_value('rac', '1', schema.uint8, lambda x: str((int(x)+1) % pow(2,8) or 1), origin)
315
Pau Espin Pedrol4ccce7c2017-11-07 11:13:20 +0100316 def next_cellid(self, origin):
317 return self.next_persistent_value('cellid', '1', schema.uint16, lambda x: str((int(x)+1) % pow(2,16)), origin)
318
Pau Espin Pedrol8a3a7b52017-11-28 15:50:02 +0100319 def next_bvci(self, origin):
320 # BVCI=0 and =1 are reserved, avoid them.
321 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)
322
Pau Espin Pedrol4676cbd2017-09-14 17:35:03 +0200323class NoResourceExn(log.Error):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200324 pass
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200325
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200326class Resources(dict):
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200327
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200328 def __init__(self, all_resources={}, do_copy=True):
329 if do_copy:
330 all_resources = copy.deepcopy(all_resources)
331 self.update(all_resources)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200332
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200333 def drop(self, reserved, fail_if_not_found=True):
334 # protect from modifying reserved because we're the same object
335 if reserved is self:
336 raise RuntimeError('Refusing to drop a list of resources from itself.'
337 ' This is probably a bug where a list of Resources()'
338 ' should have been copied but is passed as-is.'
339 ' use Resources.clear() instead.')
340
341 for key, reserved_list in reserved.items():
342 my_list = self.get(key) or []
343
344 if my_list is reserved_list:
345 self.pop(key)
346 continue
347
348 for reserved_item in reserved_list:
349 found = False
350 reserved_hash = reserved_item.get(HASH_KEY)
351 if not reserved_hash:
352 raise RuntimeError('Resources.drop() only works with hashed items')
353
354 for i in range(len(my_list)):
355 my_item = my_list[i]
356 my_hash = my_item.get(HASH_KEY)
357 if not my_hash:
358 raise RuntimeError('Resources.drop() only works with hashed items')
359 if my_hash == reserved_hash:
360 found = True
361 my_list.pop(i)
362 break
363
364 if fail_if_not_found and not found:
365 raise RuntimeError('Asked to drop resource from a pool, but the'
366 ' resource was not found: %s = %r' % (key, reserved_item))
367
368 if not my_list:
369 self.pop(key)
370 return self
371
372 def without(self, reserved):
373 return Resources(self).drop(reserved)
374
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200375 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 +0200376 '''
377 Pass a dict of resource requirements, e.g.:
378 want = {
379 'bts': [ {'type': 'osmo-bts-sysmo',}, {} ],
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200380 'modem': [ {}, {}, {} ]
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200381 }
382 This function tries to find a combination from the available resources that
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200383 matches these requirements. The return value is a dict (wrapped in a Resources class)
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200384 that contains the matching resources in the order of 'want' dict: in above
385 example, the returned dict would have a 'bts' list with the first item being
386 a sysmoBTS, the second item being any other available BTS.
387
388 If skip_if_marked is passed, any resource that contains this key is skipped.
389 E.g. if a BTS has the USED_KEY set like
390 reserved_resources = { 'bts' : {..., '_used': True} }
391 then this may be skipped by passing skip_if_marked='_used'
392 (or rather skip_if_marked=USED_KEY).
393
394 If do_copy is True, the returned dict is a deep copy and does not share
395 lists with any other Resources dict.
396
397 If raise_if_missing is False, this will return an empty item for any
398 resource that had no match, instead of immediately raising an exception.
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200399
400 This function expects input dictionaries whose contents have already
401 been replicated based on its the 'times' attributes. See
402 config.replicate_times() for more details.
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200403 '''
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200404 matches = {}
Neels Hofmeyr17c139e2017-04-12 02:42:02 +0200405 for key, want_list in sorted(want.items()): # sorted for deterministic test results
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200406 # here we have a resource of a given type, e.g. 'bts', with a list
407 # containing as many BTSes as the caller wants to reserve/use. Each
408 # list item contains specifics for the particular BTS.
Neels Hofmeyr2a1a1fa2017-05-29 01:36:21 +0200409 my_list = self.get(key, [])
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200410
Neels Hofmeyrf9e86932017-06-06 19:47:40 +0200411 if log_label:
412 for_origin.log(log_label, len(want_list), 'x', key, '(candidates: %d)'%len(my_list))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200413
414 # Try to avoid a less constrained item snatching away a resource
415 # from a more detailed constrained requirement.
416
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200417 # first record all matches, so that each requested item has a list
418 # of all available resources that match it. Some resources may
419 # appear for multiple requested items. Store matching indexes.
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200420 all_matches = []
421 for want_item in want_list:
422 item_match_list = []
423 for i in range(len(my_list)):
424 my_item = my_list[i]
425 if skip_if_marked and my_item.get(skip_if_marked):
426 continue
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200427 if item_matches(my_item, want_item):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200428 item_match_list.append(i)
429 if not item_match_list:
Neels Hofmeyr9b907702017-05-06 23:20:33 +0200430 if raise_if_missing:
431 raise NoResourceExn('No matching resource available for %s = %r'
432 % (key, want_item))
433 else:
434 # this one failed... see below
435 all_matches = []
436 break
437
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200438 all_matches.append( item_match_list )
439
440 if not all_matches:
Neels Hofmeyr9b907702017-05-06 23:20:33 +0200441 # ...this one failed. Makes no sense to solve resource
442 # allocations, return an empty list for this key to mark
443 # failure.
444 matches[key] = []
445 continue
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200446
447 # figure out who gets what
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200448 try:
449 solution = solve(all_matches)
450 except NotSolvable:
451 # instead of a cryptic error message, raise an exception that
452 # conveys meaning to the user.
453 raise NoResourceExn('Could not resolve request to reserve resources: '
454 '%d x %s with requirements: %r' % (len(want_list), key, want_list))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200455 picked = [ my_list[i] for i in solution if i is not None ]
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200456 for_origin.dbg('Picked', config.tostr(picked))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200457 matches[key] = picked
458
459 return Resources(matches, do_copy=do_copy)
460
461 def set_hashes(self):
462 for key, item_list in self.items():
463 for item in item_list:
464 item[HASH_KEY] = util.hash_obj(item, HASH_KEY, RESERVED_KEY, USED_KEY)
465
466 def add(self, more):
467 if more is self:
468 raise RuntimeError('adding a list of resources to itself?')
469 config.add(self, copy.deepcopy(more))
470
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200471 def mark_reserved_by(self, origin_id):
472 for key, item_list in self.items():
473 for item in item_list:
474 item[RESERVED_KEY] = origin_id
475
476
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200477class NotSolvable(Exception):
478 pass
479
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200480def solve(all_matches):
481 '''
482 all_matches shall be a list of index-lists.
483 all_matches[i] is the list of indexes that item i can use.
484 Return a solution so that each i gets a different index.
485 solve([ [0, 1, 2],
486 [0],
487 [0, 2] ]) == [1, 0, 2]
488 '''
489
490 def all_differ(l):
491 return len(set(l)) == len(l)
492
493 def search_in_permutations(fixed=[]):
494 idx = len(fixed)
495 for i in range(len(all_matches[idx])):
496 val = all_matches[idx][i]
497 # don't add a val that's already in the list
498 if val in fixed:
499 continue
500 l = list(fixed)
501 l.append(val)
502 if len(l) == len(all_matches):
503 # found a solution
504 return l
505 # not at the end yet, add next digit
506 r = search_in_permutations(l)
507 if r:
508 # nested search_in_permutations() call found a solution
509 return r
510 # this entire branch yielded no solution
511 return None
512
513 if not all_matches:
514 raise RuntimeError('Cannot solve: no candidates')
515
516 solution = search_in_permutations()
517 if not solution:
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200518 raise NotSolvable('The requested resource requirements are not solvable %r'
519 % all_matches)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200520 return solution
521
522
523def contains_hash(list_of_dicts, a_hash):
524 for d in list_of_dicts:
525 if d.get(HASH_KEY) == a_hash:
526 return True
527 return False
528
529def item_matches(item, wanted_item, ignore_keys=None):
530 if is_dict(wanted_item):
531 # match up two dicts
532 if not isinstance(item, dict):
533 return False
534 for key, wanted_val in wanted_item.items():
535 if ignore_keys and key in ignore_keys:
536 continue
537 if not item_matches(item.get(key), wanted_val, ignore_keys=ignore_keys):
538 return False
539 return True
540
541 if is_list(wanted_item):
Pau Espin Pedrol4e36f7c2017-08-28 13:29:28 +0200542 if not is_list(item):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200543 return False
Pau Espin Pedrol58475512017-09-14 15:33:15 +0200544 # Validate that all elements in both lists are of the same type:
545 t = util.list_validate_same_elem_type(wanted_item + item)
546 if t is None:
547 return True # both lists are empty, return
548 # For lists of complex objects, we expect them to be sorted lists:
549 if t in (dict, list, tuple):
550 for i in range(max(len(wanted_item), len(item))):
551 log.ctx(idx=i)
552 subitem = item[i] if i < len(item) else util.empty_instance_type(t)
553 wanted_subitem = wanted_item[i] if i < len(wanted_item) else util.empty_instance_type(t)
554 if not item_matches(subitem, wanted_subitem, ignore_keys=ignore_keys):
555 return False
556 else: # for lists of basic elements, we handle them as unsorted sets:
557 for val in wanted_item:
558 if val not in item:
559 return False
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200560 return True
561
562 return item == wanted_item
563
564
565class ReservedResources(log.Origin):
566 '''
567 After all resources have been figured out, this is the API that a test case
568 gets to interact with resources. From those resources that have been
569 reserved for it, it can pick some to mark them as currently in use.
570 Functions like nitb() provide a resource by automatically picking its
571 dependencies from so far unused (but reserved) resource.
572 '''
573
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200574 def __init__(self, resources_pool, origin, reserved, modifiers):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200575 self.resources_pool = resources_pool
576 self.origin = origin
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200577 self.reserved_original = reserved
578 self.reserved = copy.deepcopy(self.reserved_original)
579 config.overlay(self.reserved, modifiers)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200580
581 def __repr__(self):
582 return 'resources(%s)=%s' % (self.origin.name(), pprint.pformat(self.reserved))
583
584 def get(self, kind, specifics=None):
585 if specifics is None:
586 specifics = {}
587 self.dbg('requesting use of', kind, specifics=specifics)
588 want = { kind: [specifics] }
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200589 available_dict = self.reserved.find(self.origin, want, skip_if_marked=USED_KEY,
590 do_copy=False, raise_if_missing=False,
591 log_label='Using')
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200592 available = available_dict.get(kind)
593 self.dbg(available=len(available))
594 if not available:
Neels Hofmeyrf9e86932017-06-06 19:47:40 +0200595 # cook up a detailed error message for the current situation
596 kind_reserved = self.reserved.get(kind, [])
597 used_count = len([r for r in kind_reserved if USED_KEY in r])
598 matching = self.reserved.find(self.origin, want, raise_if_missing=False, log_label=None).get(kind, [])
599 if not matching:
600 msg = 'none of the reserved resources matches requirements %r' % specifics
601 elif not (used_count < len(kind_reserved)):
602 msg = 'suite.conf reserved only %d x %r.' % (len(kind_reserved), kind)
603 else:
604 msg = ('No unused resource left that matches the requirements;'
605 ' Of reserved %d x %r, %d match the requirements, but all are already in use;'
606 ' Requirements: %r'
607 % (len(kind_reserved), kind, len(matching), specifics))
608 raise NoResourceExn('When trying to use instance nr %d of %r: %s' % (used_count + 1, kind, msg))
609
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200610 pick = available[0]
611 self.dbg(using=pick)
612 assert not pick.get(USED_KEY)
613 pick[USED_KEY] = True
614 return copy.deepcopy(pick)
615
616 def put(self, item):
617 if not item.get(USED_KEY):
618 raise RuntimeError('Can only put() a resource that is used: %r' % item)
619 hash_to_put = item.get(HASH_KEY)
620 if not hash_to_put:
621 raise RuntimeError('Can only put() a resource that has a hash marker: %r' % item)
622 for key, item_list in self.reserved.items():
623 my_list = self.get(key)
624 for my_item in my_list:
625 if hash_to_put == my_item.get(HASH_KEY):
626 my_item.pop(USED_KEY)
627
628 def put_all(self):
Pau Espin Pedrol1dd29552017-06-13 18:07:57 +0200629 if not self.reserved:
630 return
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200631 for key, item_list in self.reserved.items():
Pau Espin Pedrol1dd29552017-06-13 18:07:57 +0200632 for item in item_list:
633 item.pop(USED_KEY, None)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200634
635 def free(self):
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200636 if self.reserved_original:
637 self.resources_pool.free(self.origin, self.reserved_original)
638 self.reserved_original = None
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200639
Neels Hofmeyr2d1d5612017-05-22 20:02:41 +0200640 def counts(self):
641 counts = {}
642 for key in self.reserved.keys():
643 counts[key] = self.count(key)
644 return counts
645
646 def count(self, key):
647 return len(self.reserved.get(key) or [])
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200648
649# vim: expandtab tabstop=4 shiftwidth=4