blob: 383f09e11d79deac19808d1008386e1bfc07bd38 [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
25from . import log
26from . import config
Neels Hofmeyr3531a192017-03-28 14:30:28 +020027from . import util
28from . import schema
Pau Espin Pedrolfeb66e72019-03-04 18:37:04 +010029from . import bts_sysmo, bts_osmotrx, bts_osmovirtual, bts_octphy, bts_nanobts, bts_oc2g
Holger Hans Peter Freyther74243012019-02-27 08:18:38 +000030from . import modem
Holger Hans Peter Freyther10501cc2019-02-25 03:20:16 +000031from . import ms_osmo_mobile
Pau Espin Pedrolda2e31f2020-03-31 13:45:01 +020032from . import srs_ue, srs_enb, amarisoft_enb, srs_epc, amarisoft_epc
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +020033
Neels Hofmeyr3531a192017-03-28 14:30:28 +020034from .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,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020096 'arfcn[].arfcn': schema.INT,
97 'arfcn[].band': schema.BAND,
Holger Hans Peter Freyther93a89422019-02-27 06:36:36 +000098 'modem[].type': schema.STR,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020099 'modem[].label': schema.STR,
100 'modem[].path': schema.STR,
101 'modem[].imsi': schema.IMSI,
102 'modem[].ki': schema.KI,
Pau Espin Pedrol713ce2c2017-08-24 16:57:17 +0200103 'modem[].auth_algo': schema.AUTH_ALGO,
Andre Puschmann22ec00a2020-03-24 09:58:06 +0100104 'modem[].apn_ipaddr': schema.IPV4,
Pau Espin Pedrolc8b0f932020-02-11 17:45:26 +0100105 'modem[].remote_user': schema.STR,
106 'modem[].addr': schema.IPV4,
Pau Espin Pedrol57497a62017-08-28 14:21:15 +0200107 'modem[].ciphers[]': schema.CIPHER,
Pau Espin Pedrolac18fd32017-08-31 18:49:47 +0200108 'modem[].features[]': schema.MODEM_FEATURE,
Pau Espin Pedrola9a2fe22020-02-13 19:29:55 +0100109 'modem[].rf_dev_type': schema.STR,
110 'modem[].rf_dev_args': schema.STR,
Andre Puschmannd61613a2020-03-24 12:05:05 +0100111 'modem[].num_carriers': schema.STR,
Andre Puschmann35234f22020-03-23 18:52:41 +0100112 'modem[].airplane_t_on_ms': schema.INT,
113 'modem[].airplane_t_off_ms': schema.INT,
Pau Espin Pedrolbc1ed882018-05-17 16:59:58 +0200114 'osmocon_phone[].serial_device': schema.STR,
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200115 }
116
117WANT_SCHEMA = util.dict_add(
Pau Espin Pedrolead79ac2017-09-12 15:19:18 +0200118 dict([('%s[].times' % r, schema.TIMES) for r in R_ALL]),
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200119 RESOURCES_SCHEMA)
120
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200121CONF_SCHEMA = util.dict_add(
Pau Espin Pedrol5dc24592018-08-27 12:53:41 +0200122 { 'defaults.timeout': schema.STR,
Pau Espin Pedrolb6937712020-02-27 18:02:20 +0100123 'config.bsc.net.codec_list[]': schema.CODEC,
Pau Espin Pedrol1e81b5a2020-03-16 12:42:17 +0100124 'config.enb.enable_pcap': schema.BOOL_STR,
Pau Espin Pedrolda2e31f2020-03-31 13:45:01 +0200125 'config.epc.type': schema.STR,
Pau Espin Pedrol1e81b5a2020-03-16 12:42:17 +0100126 'config.epc.rlc_drb_mode': schema.LTE_RLC_DRB_MODE,
127 'config.epc.enable_pcap': schema.BOOL_STR,
128 'config.modem.enable_pcap': schema.BOOL_STR,
129 },
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200130 dict([('resources.%s' % key, val) for key, val in WANT_SCHEMA.items()]),
131 dict([('modifiers.%s' % key, val) for key, val in WANT_SCHEMA.items()]))
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200132
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200133KNOWN_BTS_TYPES = {
Your Name44af3412017-04-13 03:11:59 +0200134 'osmo-bts-sysmo': bts_sysmo.SysmoBts,
135 'osmo-bts-trx': bts_osmotrx.OsmoBtsTrx,
Pau Espin Pedrolfeb66e72019-03-04 18:37:04 +0100136 'osmo-bts-oc2g': bts_oc2g.OsmoBtsOC2G,
Pau Espin Pedroldaed4472017-09-15 14:11:35 +0200137 'osmo-bts-octphy': bts_octphy.OsmoBtsOctphy,
Holger Hans Peter Freytherd2db10d2018-12-06 18:34:53 +0000138 'osmo-bts-virtual': bts_osmovirtual.OsmoBtsVirtual,
Pau Espin Pedrol1b28a582018-03-08 21:01:26 +0100139 'nanobts': bts_nanobts.NanoBts,
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200140 }
141
Pau Espin Pedrolc8b0f932020-02-11 17:45:26 +0100142KNOWN_ENB_TYPES = {
143 'srsenb': srs_enb.srsENB,
Pau Espin Pedrol786a6bc2020-03-30 13:51:21 +0200144 'amarisoftenb': amarisoft_enb.AmarisoftENB,
Pau Espin Pedrolc8b0f932020-02-11 17:45:26 +0100145}
Holger Hans Peter Freyther74243012019-02-27 08:18:38 +0000146
Pau Espin Pedrolda2e31f2020-03-31 13:45:01 +0200147KNOWN_EPC_TYPES = {
148 'srsepc': srs_epc.srsEPC,
149 'amarisoftepc': amarisoft_epc.AmarisoftEPC,
150}
151
Holger Hans Peter Freyther74243012019-02-27 08:18:38 +0000152KNOWN_MS_TYPES = {
153 # Map None to ofono for forward compability
154 None: modem.Modem,
155 'ofono': modem.Modem,
Holger Hans Peter Freyther10501cc2019-02-25 03:20:16 +0000156 'osmo-mobile': ms_osmo_mobile.MSOsmoMobile,
Pau Espin Pedrolc8b0f932020-02-11 17:45:26 +0100157 'srsue': srs_ue.srsUE,
Holger Hans Peter Freyther74243012019-02-27 08:18:38 +0000158}
159
160
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200161def register_bts_type(name, clazz):
162 KNOWN_BTS_TYPES[name] = clazz
163
164class ResourcesPool(log.Origin):
165 _remember_to_free = None
166 _registered_exit_handler = False
167
168 def __init__(self):
Pau Espin Pedrol66a38912020-03-11 20:11:08 +0100169 self.config_path = config.get_config_file(config.RESOURCES_CONF)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200170 self.state_dir = config.get_state_dir()
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200171 super().__init__(log.C_CNF, conf=self.config_path, state=self.state_dir.path)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200172 self.read_conf()
173
174 def read_conf(self):
175 self.all_resources = Resources(config.read(self.config_path, RESOURCES_SCHEMA))
176 self.all_resources.set_hashes()
177
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200178 def reserve(self, origin, want, modifiers):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200179 '''
180 attempt to reserve the resources specified in the dict 'want' for
181 'origin'. Obtain a lock on the resources lock dir, verify that all
182 wanted resources are available, and if yes mark them as reserved.
183
184 On success, return a reservation object which can be used to release
185 the reservation. The reservation will be freed automatically on program
186 exit, if not yet done manually.
187
188 'origin' should be an Origin() instance.
189
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200190 'want' is a dict matching RESOURCES_SCHEMA, used to specify what to
191 reserve.
192
193 'modifiers' is a dict matching RESOURCES_SCHEMA, it is overlaid on top
194 of 'want'.
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200195
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200196 If an entry has no attribute set, any of the resources may be
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200197 reserved without further limitations.
198
199 ResourcesPool may also be selected with narrowed down constraints.
Neels Hofmeyr76d81032017-05-18 18:35:32 +0200200 This would reserve one IP address, two modems, one BTS of type
Neels Hofmeyr391afe32017-05-18 19:22:12 +0200201 sysmo and one of type trx, plus 2 ARFCNs in the 1800 band:
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200202
203 {
Neels Hofmeyr76d81032017-05-18 18:35:32 +0200204 'ip_address': [ {} ],
Neels Hofmeyr391afe32017-05-18 19:22:12 +0200205 'bts': [ { 'type': 'sysmo' }, { 'type': 'trx' } ],
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200206 'arfcn': [ { 'band': 'GSM-1800' }, { 'band': 'GSM-1800' } ],
207 'modem': [ {}, {} ],
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200208 }
209 '''
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200210 schema.validate(want, RESOURCES_SCHEMA)
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200211 schema.validate(modifiers, RESOURCES_SCHEMA)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200212
213 origin_id = origin.origin_id()
214
215 with self.state_dir.lock(origin_id):
216 rrfile_path = self.state_dir.mk_parentdir(RESERVED_RESOURCES_FILE)
217 reserved = Resources(config.read(rrfile_path, if_missing_return={}))
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200218 to_be_reserved = self.all_resources.without(reserved).find(origin, want)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200219
220 to_be_reserved.mark_reserved_by(origin_id)
221
222 reserved.add(to_be_reserved)
223 config.write(rrfile_path, reserved)
224
225 self.remember_to_free(to_be_reserved)
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200226 return ReservedResources(self, origin, to_be_reserved, modifiers)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200227
228 def free(self, origin, to_be_freed):
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200229 log.ctx(origin)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200230 with self.state_dir.lock(origin.origin_id()):
231 rrfile_path = self.state_dir.mk_parentdir(RESERVED_RESOURCES_FILE)
232 reserved = Resources(config.read(rrfile_path, if_missing_return={}))
233 reserved.drop(to_be_freed)
234 config.write(rrfile_path, reserved)
235 self.forget_freed(to_be_freed)
236
237 def register_exit_handler(self):
238 if self._registered_exit_handler:
239 return
240 atexit.register(self.clean_up_registered_resources)
241 self._registered_exit_handler = True
242
243 def unregister_exit_handler(self):
244 if not self._registered_exit_handler:
245 return
246 atexit.unregister(self.clean_up_registered_resources)
247 self._registered_exit_handler = False
248
249 def clean_up_registered_resources(self):
250 if not self._remember_to_free:
251 return
252 self.free(log.Origin('atexit.clean_up_registered_resources()'),
253 self._remember_to_free)
254
255 def remember_to_free(self, to_be_reserved):
256 self.register_exit_handler()
257 if not self._remember_to_free:
258 self._remember_to_free = Resources()
259 self._remember_to_free.add(to_be_reserved)
260
261 def forget_freed(self, freed):
262 if freed is self._remember_to_free:
263 self._remember_to_free.clear()
264 else:
265 self._remember_to_free.drop(freed)
266 if not self._remember_to_free:
267 self.unregister_exit_handler()
268
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100269 def next_persistent_value(self, token, first_val, validate_func, inc_func, origin):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200270 origin_id = origin.origin_id()
271
272 with self.state_dir.lock(origin_id):
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100273 token_path = self.state_dir.child('last_used_%s.state' % token)
274 log.ctx(token_path)
275 last_value = first_val
276 if os.path.exists(token_path):
277 if not os.path.isfile(token_path):
278 raise RuntimeError('path should be a file but is not: %r' % token_path)
279 with open(token_path, 'r') as f:
280 last_value = f.read().strip()
281 validate_func(last_value)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200282
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100283 next_value = inc_func(last_value)
284 with open(token_path, 'w') as f:
285 f.write(next_value)
286 return next_value
287
288 def next_msisdn(self, origin):
289 return self.next_persistent_value('msisdn', '1000', schema.msisdn, util.msisdn_inc, origin)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200290
Pau Espin Pedrol5e0c2512017-11-06 18:40:23 +0100291 def next_lac(self, origin):
Pau Espin Pedrolf7f06362017-11-28 12:56:35 +0100292 # LAC=0 has special meaning (MS detached), avoid it
293 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 +0200294
Pau Espin Pedrol8a3a7b52017-11-28 15:50:02 +0100295 def next_rac(self, origin):
296 return self.next_persistent_value('rac', '1', schema.uint8, lambda x: str((int(x)+1) % pow(2,8) or 1), origin)
297
Pau Espin Pedrol4ccce7c2017-11-07 11:13:20 +0100298 def next_cellid(self, origin):
299 return self.next_persistent_value('cellid', '1', schema.uint16, lambda x: str((int(x)+1) % pow(2,16)), origin)
300
Pau Espin Pedrol8a3a7b52017-11-28 15:50:02 +0100301 def next_bvci(self, origin):
302 # BVCI=0 and =1 are reserved, avoid them.
303 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)
304
Pau Espin Pedrol4676cbd2017-09-14 17:35:03 +0200305class NoResourceExn(log.Error):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200306 pass
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200307
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200308class Resources(dict):
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200309
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200310 def __init__(self, all_resources={}, do_copy=True):
311 if do_copy:
312 all_resources = copy.deepcopy(all_resources)
313 self.update(all_resources)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200314
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200315 def drop(self, reserved, fail_if_not_found=True):
316 # protect from modifying reserved because we're the same object
317 if reserved is self:
318 raise RuntimeError('Refusing to drop a list of resources from itself.'
319 ' This is probably a bug where a list of Resources()'
320 ' should have been copied but is passed as-is.'
321 ' use Resources.clear() instead.')
322
323 for key, reserved_list in reserved.items():
324 my_list = self.get(key) or []
325
326 if my_list is reserved_list:
327 self.pop(key)
328 continue
329
330 for reserved_item in reserved_list:
331 found = False
332 reserved_hash = reserved_item.get(HASH_KEY)
333 if not reserved_hash:
334 raise RuntimeError('Resources.drop() only works with hashed items')
335
336 for i in range(len(my_list)):
337 my_item = my_list[i]
338 my_hash = my_item.get(HASH_KEY)
339 if not my_hash:
340 raise RuntimeError('Resources.drop() only works with hashed items')
341 if my_hash == reserved_hash:
342 found = True
343 my_list.pop(i)
344 break
345
346 if fail_if_not_found and not found:
347 raise RuntimeError('Asked to drop resource from a pool, but the'
348 ' resource was not found: %s = %r' % (key, reserved_item))
349
350 if not my_list:
351 self.pop(key)
352 return self
353
354 def without(self, reserved):
355 return Resources(self).drop(reserved)
356
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200357 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 +0200358 '''
359 Pass a dict of resource requirements, e.g.:
360 want = {
361 'bts': [ {'type': 'osmo-bts-sysmo',}, {} ],
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200362 'modem': [ {}, {}, {} ]
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200363 }
364 This function tries to find a combination from the available resources that
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200365 matches these requirements. The return value is a dict (wrapped in a Resources class)
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200366 that contains the matching resources in the order of 'want' dict: in above
367 example, the returned dict would have a 'bts' list with the first item being
368 a sysmoBTS, the second item being any other available BTS.
369
370 If skip_if_marked is passed, any resource that contains this key is skipped.
371 E.g. if a BTS has the USED_KEY set like
372 reserved_resources = { 'bts' : {..., '_used': True} }
373 then this may be skipped by passing skip_if_marked='_used'
374 (or rather skip_if_marked=USED_KEY).
375
376 If do_copy is True, the returned dict is a deep copy and does not share
377 lists with any other Resources dict.
378
379 If raise_if_missing is False, this will return an empty item for any
380 resource that had no match, instead of immediately raising an exception.
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200381
382 This function expects input dictionaries whose contents have already
383 been replicated based on its the 'times' attributes. See
384 config.replicate_times() for more details.
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200385 '''
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200386 matches = {}
Neels Hofmeyr17c139e2017-04-12 02:42:02 +0200387 for key, want_list in sorted(want.items()): # sorted for deterministic test results
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200388 # here we have a resource of a given type, e.g. 'bts', with a list
389 # containing as many BTSes as the caller wants to reserve/use. Each
390 # list item contains specifics for the particular BTS.
Neels Hofmeyr2a1a1fa2017-05-29 01:36:21 +0200391 my_list = self.get(key, [])
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200392
Neels Hofmeyrf9e86932017-06-06 19:47:40 +0200393 if log_label:
394 for_origin.log(log_label, len(want_list), 'x', key, '(candidates: %d)'%len(my_list))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200395
396 # Try to avoid a less constrained item snatching away a resource
397 # from a more detailed constrained requirement.
398
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200399 # first record all matches, so that each requested item has a list
400 # of all available resources that match it. Some resources may
401 # appear for multiple requested items. Store matching indexes.
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200402 all_matches = []
403 for want_item in want_list:
404 item_match_list = []
405 for i in range(len(my_list)):
406 my_item = my_list[i]
407 if skip_if_marked and my_item.get(skip_if_marked):
408 continue
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200409 if item_matches(my_item, want_item):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200410 item_match_list.append(i)
411 if not item_match_list:
Neels Hofmeyr9b907702017-05-06 23:20:33 +0200412 if raise_if_missing:
413 raise NoResourceExn('No matching resource available for %s = %r'
414 % (key, want_item))
415 else:
416 # this one failed... see below
417 all_matches = []
418 break
419
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200420 all_matches.append( item_match_list )
421
422 if not all_matches:
Neels Hofmeyr9b907702017-05-06 23:20:33 +0200423 # ...this one failed. Makes no sense to solve resource
424 # allocations, return an empty list for this key to mark
425 # failure.
426 matches[key] = []
427 continue
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200428
429 # figure out who gets what
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200430 try:
431 solution = solve(all_matches)
432 except NotSolvable:
433 # instead of a cryptic error message, raise an exception that
434 # conveys meaning to the user.
435 raise NoResourceExn('Could not resolve request to reserve resources: '
436 '%d x %s with requirements: %r' % (len(want_list), key, want_list))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200437 picked = [ my_list[i] for i in solution if i is not None ]
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200438 for_origin.dbg('Picked', config.tostr(picked))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200439 matches[key] = picked
440
441 return Resources(matches, do_copy=do_copy)
442
443 def set_hashes(self):
444 for key, item_list in self.items():
445 for item in item_list:
446 item[HASH_KEY] = util.hash_obj(item, HASH_KEY, RESERVED_KEY, USED_KEY)
447
448 def add(self, more):
449 if more is self:
450 raise RuntimeError('adding a list of resources to itself?')
451 config.add(self, copy.deepcopy(more))
452
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200453 def mark_reserved_by(self, origin_id):
454 for key, item_list in self.items():
455 for item in item_list:
456 item[RESERVED_KEY] = origin_id
457
458
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200459class NotSolvable(Exception):
460 pass
461
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200462def solve(all_matches):
463 '''
464 all_matches shall be a list of index-lists.
465 all_matches[i] is the list of indexes that item i can use.
466 Return a solution so that each i gets a different index.
467 solve([ [0, 1, 2],
468 [0],
469 [0, 2] ]) == [1, 0, 2]
470 '''
471
472 def all_differ(l):
473 return len(set(l)) == len(l)
474
475 def search_in_permutations(fixed=[]):
476 idx = len(fixed)
477 for i in range(len(all_matches[idx])):
478 val = all_matches[idx][i]
479 # don't add a val that's already in the list
480 if val in fixed:
481 continue
482 l = list(fixed)
483 l.append(val)
484 if len(l) == len(all_matches):
485 # found a solution
486 return l
487 # not at the end yet, add next digit
488 r = search_in_permutations(l)
489 if r:
490 # nested search_in_permutations() call found a solution
491 return r
492 # this entire branch yielded no solution
493 return None
494
495 if not all_matches:
496 raise RuntimeError('Cannot solve: no candidates')
497
498 solution = search_in_permutations()
499 if not solution:
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200500 raise NotSolvable('The requested resource requirements are not solvable %r'
501 % all_matches)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200502 return solution
503
504
505def contains_hash(list_of_dicts, a_hash):
506 for d in list_of_dicts:
507 if d.get(HASH_KEY) == a_hash:
508 return True
509 return False
510
511def item_matches(item, wanted_item, ignore_keys=None):
512 if is_dict(wanted_item):
513 # match up two dicts
514 if not isinstance(item, dict):
515 return False
516 for key, wanted_val in wanted_item.items():
517 if ignore_keys and key in ignore_keys:
518 continue
519 if not item_matches(item.get(key), wanted_val, ignore_keys=ignore_keys):
520 return False
521 return True
522
523 if is_list(wanted_item):
Pau Espin Pedrol4e36f7c2017-08-28 13:29:28 +0200524 if not is_list(item):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200525 return False
Pau Espin Pedrol58475512017-09-14 15:33:15 +0200526 # Validate that all elements in both lists are of the same type:
527 t = util.list_validate_same_elem_type(wanted_item + item)
528 if t is None:
529 return True # both lists are empty, return
530 # For lists of complex objects, we expect them to be sorted lists:
531 if t in (dict, list, tuple):
532 for i in range(max(len(wanted_item), len(item))):
533 log.ctx(idx=i)
534 subitem = item[i] if i < len(item) else util.empty_instance_type(t)
535 wanted_subitem = wanted_item[i] if i < len(wanted_item) else util.empty_instance_type(t)
536 if not item_matches(subitem, wanted_subitem, ignore_keys=ignore_keys):
537 return False
538 else: # for lists of basic elements, we handle them as unsorted sets:
539 for val in wanted_item:
540 if val not in item:
541 return False
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200542 return True
543
544 return item == wanted_item
545
546
547class ReservedResources(log.Origin):
548 '''
549 After all resources have been figured out, this is the API that a test case
550 gets to interact with resources. From those resources that have been
551 reserved for it, it can pick some to mark them as currently in use.
552 Functions like nitb() provide a resource by automatically picking its
553 dependencies from so far unused (but reserved) resource.
554 '''
555
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200556 def __init__(self, resources_pool, origin, reserved, modifiers):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200557 self.resources_pool = resources_pool
558 self.origin = origin
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200559 self.reserved_original = reserved
560 self.reserved = copy.deepcopy(self.reserved_original)
561 config.overlay(self.reserved, modifiers)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200562
563 def __repr__(self):
564 return 'resources(%s)=%s' % (self.origin.name(), pprint.pformat(self.reserved))
565
566 def get(self, kind, specifics=None):
567 if specifics is None:
568 specifics = {}
569 self.dbg('requesting use of', kind, specifics=specifics)
570 want = { kind: [specifics] }
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200571 available_dict = self.reserved.find(self.origin, want, skip_if_marked=USED_KEY,
572 do_copy=False, raise_if_missing=False,
573 log_label='Using')
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200574 available = available_dict.get(kind)
575 self.dbg(available=len(available))
576 if not available:
Neels Hofmeyrf9e86932017-06-06 19:47:40 +0200577 # cook up a detailed error message for the current situation
578 kind_reserved = self.reserved.get(kind, [])
579 used_count = len([r for r in kind_reserved if USED_KEY in r])
580 matching = self.reserved.find(self.origin, want, raise_if_missing=False, log_label=None).get(kind, [])
581 if not matching:
582 msg = 'none of the reserved resources matches requirements %r' % specifics
583 elif not (used_count < len(kind_reserved)):
584 msg = 'suite.conf reserved only %d x %r.' % (len(kind_reserved), kind)
585 else:
586 msg = ('No unused resource left that matches the requirements;'
587 ' Of reserved %d x %r, %d match the requirements, but all are already in use;'
588 ' Requirements: %r'
589 % (len(kind_reserved), kind, len(matching), specifics))
590 raise NoResourceExn('When trying to use instance nr %d of %r: %s' % (used_count + 1, kind, msg))
591
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200592 pick = available[0]
593 self.dbg(using=pick)
594 assert not pick.get(USED_KEY)
595 pick[USED_KEY] = True
596 return copy.deepcopy(pick)
597
598 def put(self, item):
599 if not item.get(USED_KEY):
600 raise RuntimeError('Can only put() a resource that is used: %r' % item)
601 hash_to_put = item.get(HASH_KEY)
602 if not hash_to_put:
603 raise RuntimeError('Can only put() a resource that has a hash marker: %r' % item)
604 for key, item_list in self.reserved.items():
605 my_list = self.get(key)
606 for my_item in my_list:
607 if hash_to_put == my_item.get(HASH_KEY):
608 my_item.pop(USED_KEY)
609
610 def put_all(self):
Pau Espin Pedrol1dd29552017-06-13 18:07:57 +0200611 if not self.reserved:
612 return
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200613 for key, item_list in self.reserved.items():
Pau Espin Pedrol1dd29552017-06-13 18:07:57 +0200614 for item in item_list:
615 item.pop(USED_KEY, None)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200616
617 def free(self):
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200618 if self.reserved_original:
619 self.resources_pool.free(self.origin, self.reserved_original)
620 self.reserved_original = None
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200621
Neels Hofmeyr2d1d5612017-05-22 20:02:41 +0200622 def counts(self):
623 counts = {}
624 for key in self.reserved.keys():
625 counts[key] = self.count(key)
626 return counts
627
628 def count(self, key):
629 return len(self.reserved.get(key) or [])
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200630
631# vim: expandtab tabstop=4 shiftwidth=4