blob: d14ee969da826299ce55f860fa7761fa189a43d3 [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 Pedrol786a6bc2020-03-30 13:51:21 +020032from . import srs_ue, srs_enb, amarisoft_enb
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,
125 'config.epc.rlc_drb_mode': schema.LTE_RLC_DRB_MODE,
126 'config.epc.enable_pcap': schema.BOOL_STR,
127 'config.modem.enable_pcap': schema.BOOL_STR,
128 },
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200129 dict([('resources.%s' % key, val) for key, val in WANT_SCHEMA.items()]),
130 dict([('modifiers.%s' % key, val) for key, val in WANT_SCHEMA.items()]))
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200131
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200132KNOWN_BTS_TYPES = {
Your Name44af3412017-04-13 03:11:59 +0200133 'osmo-bts-sysmo': bts_sysmo.SysmoBts,
134 'osmo-bts-trx': bts_osmotrx.OsmoBtsTrx,
Pau Espin Pedrolfeb66e72019-03-04 18:37:04 +0100135 'osmo-bts-oc2g': bts_oc2g.OsmoBtsOC2G,
Pau Espin Pedroldaed4472017-09-15 14:11:35 +0200136 'osmo-bts-octphy': bts_octphy.OsmoBtsOctphy,
Holger Hans Peter Freytherd2db10d2018-12-06 18:34:53 +0000137 'osmo-bts-virtual': bts_osmovirtual.OsmoBtsVirtual,
Pau Espin Pedrol1b28a582018-03-08 21:01:26 +0100138 'nanobts': bts_nanobts.NanoBts,
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200139 }
140
Pau Espin Pedrolc8b0f932020-02-11 17:45:26 +0100141KNOWN_ENB_TYPES = {
142 'srsenb': srs_enb.srsENB,
Pau Espin Pedrol786a6bc2020-03-30 13:51:21 +0200143 'amarisoftenb': amarisoft_enb.AmarisoftENB,
Pau Espin Pedrolc8b0f932020-02-11 17:45:26 +0100144}
Holger Hans Peter Freyther74243012019-02-27 08:18:38 +0000145
146KNOWN_MS_TYPES = {
147 # Map None to ofono for forward compability
148 None: modem.Modem,
149 'ofono': modem.Modem,
Holger Hans Peter Freyther10501cc2019-02-25 03:20:16 +0000150 'osmo-mobile': ms_osmo_mobile.MSOsmoMobile,
Pau Espin Pedrolc8b0f932020-02-11 17:45:26 +0100151 'srsue': srs_ue.srsUE,
Holger Hans Peter Freyther74243012019-02-27 08:18:38 +0000152}
153
154
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200155def register_bts_type(name, clazz):
156 KNOWN_BTS_TYPES[name] = clazz
157
158class ResourcesPool(log.Origin):
159 _remember_to_free = None
160 _registered_exit_handler = False
161
162 def __init__(self):
Pau Espin Pedrol66a38912020-03-11 20:11:08 +0100163 self.config_path = config.get_config_file(config.RESOURCES_CONF)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200164 self.state_dir = config.get_state_dir()
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200165 super().__init__(log.C_CNF, conf=self.config_path, state=self.state_dir.path)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200166 self.read_conf()
167
168 def read_conf(self):
169 self.all_resources = Resources(config.read(self.config_path, RESOURCES_SCHEMA))
170 self.all_resources.set_hashes()
171
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200172 def reserve(self, origin, want, modifiers):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200173 '''
174 attempt to reserve the resources specified in the dict 'want' for
175 'origin'. Obtain a lock on the resources lock dir, verify that all
176 wanted resources are available, and if yes mark them as reserved.
177
178 On success, return a reservation object which can be used to release
179 the reservation. The reservation will be freed automatically on program
180 exit, if not yet done manually.
181
182 'origin' should be an Origin() instance.
183
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200184 'want' is a dict matching RESOURCES_SCHEMA, used to specify what to
185 reserve.
186
187 'modifiers' is a dict matching RESOURCES_SCHEMA, it is overlaid on top
188 of 'want'.
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200189
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200190 If an entry has no attribute set, any of the resources may be
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200191 reserved without further limitations.
192
193 ResourcesPool may also be selected with narrowed down constraints.
Neels Hofmeyr76d81032017-05-18 18:35:32 +0200194 This would reserve one IP address, two modems, one BTS of type
Neels Hofmeyr391afe32017-05-18 19:22:12 +0200195 sysmo and one of type trx, plus 2 ARFCNs in the 1800 band:
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200196
197 {
Neels Hofmeyr76d81032017-05-18 18:35:32 +0200198 'ip_address': [ {} ],
Neels Hofmeyr391afe32017-05-18 19:22:12 +0200199 'bts': [ { 'type': 'sysmo' }, { 'type': 'trx' } ],
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200200 'arfcn': [ { 'band': 'GSM-1800' }, { 'band': 'GSM-1800' } ],
201 'modem': [ {}, {} ],
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200202 }
203 '''
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200204 schema.validate(want, RESOURCES_SCHEMA)
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200205 schema.validate(modifiers, RESOURCES_SCHEMA)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200206
207 origin_id = origin.origin_id()
208
209 with self.state_dir.lock(origin_id):
210 rrfile_path = self.state_dir.mk_parentdir(RESERVED_RESOURCES_FILE)
211 reserved = Resources(config.read(rrfile_path, if_missing_return={}))
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200212 to_be_reserved = self.all_resources.without(reserved).find(origin, want)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200213
214 to_be_reserved.mark_reserved_by(origin_id)
215
216 reserved.add(to_be_reserved)
217 config.write(rrfile_path, reserved)
218
219 self.remember_to_free(to_be_reserved)
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200220 return ReservedResources(self, origin, to_be_reserved, modifiers)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200221
222 def free(self, origin, to_be_freed):
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200223 log.ctx(origin)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200224 with self.state_dir.lock(origin.origin_id()):
225 rrfile_path = self.state_dir.mk_parentdir(RESERVED_RESOURCES_FILE)
226 reserved = Resources(config.read(rrfile_path, if_missing_return={}))
227 reserved.drop(to_be_freed)
228 config.write(rrfile_path, reserved)
229 self.forget_freed(to_be_freed)
230
231 def register_exit_handler(self):
232 if self._registered_exit_handler:
233 return
234 atexit.register(self.clean_up_registered_resources)
235 self._registered_exit_handler = True
236
237 def unregister_exit_handler(self):
238 if not self._registered_exit_handler:
239 return
240 atexit.unregister(self.clean_up_registered_resources)
241 self._registered_exit_handler = False
242
243 def clean_up_registered_resources(self):
244 if not self._remember_to_free:
245 return
246 self.free(log.Origin('atexit.clean_up_registered_resources()'),
247 self._remember_to_free)
248
249 def remember_to_free(self, to_be_reserved):
250 self.register_exit_handler()
251 if not self._remember_to_free:
252 self._remember_to_free = Resources()
253 self._remember_to_free.add(to_be_reserved)
254
255 def forget_freed(self, freed):
256 if freed is self._remember_to_free:
257 self._remember_to_free.clear()
258 else:
259 self._remember_to_free.drop(freed)
260 if not self._remember_to_free:
261 self.unregister_exit_handler()
262
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100263 def next_persistent_value(self, token, first_val, validate_func, inc_func, origin):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200264 origin_id = origin.origin_id()
265
266 with self.state_dir.lock(origin_id):
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100267 token_path = self.state_dir.child('last_used_%s.state' % token)
268 log.ctx(token_path)
269 last_value = first_val
270 if os.path.exists(token_path):
271 if not os.path.isfile(token_path):
272 raise RuntimeError('path should be a file but is not: %r' % token_path)
273 with open(token_path, 'r') as f:
274 last_value = f.read().strip()
275 validate_func(last_value)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200276
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100277 next_value = inc_func(last_value)
278 with open(token_path, 'w') as f:
279 f.write(next_value)
280 return next_value
281
282 def next_msisdn(self, origin):
283 return self.next_persistent_value('msisdn', '1000', schema.msisdn, util.msisdn_inc, origin)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200284
Pau Espin Pedrol5e0c2512017-11-06 18:40:23 +0100285 def next_lac(self, origin):
Pau Espin Pedrolf7f06362017-11-28 12:56:35 +0100286 # LAC=0 has special meaning (MS detached), avoid it
287 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 +0200288
Pau Espin Pedrol8a3a7b52017-11-28 15:50:02 +0100289 def next_rac(self, origin):
290 return self.next_persistent_value('rac', '1', schema.uint8, lambda x: str((int(x)+1) % pow(2,8) or 1), origin)
291
Pau Espin Pedrol4ccce7c2017-11-07 11:13:20 +0100292 def next_cellid(self, origin):
293 return self.next_persistent_value('cellid', '1', schema.uint16, lambda x: str((int(x)+1) % pow(2,16)), origin)
294
Pau Espin Pedrol8a3a7b52017-11-28 15:50:02 +0100295 def next_bvci(self, origin):
296 # BVCI=0 and =1 are reserved, avoid them.
297 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)
298
Pau Espin Pedrol4676cbd2017-09-14 17:35:03 +0200299class NoResourceExn(log.Error):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200300 pass
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200301
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200302class Resources(dict):
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200303
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200304 def __init__(self, all_resources={}, do_copy=True):
305 if do_copy:
306 all_resources = copy.deepcopy(all_resources)
307 self.update(all_resources)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200308
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200309 def drop(self, reserved, fail_if_not_found=True):
310 # protect from modifying reserved because we're the same object
311 if reserved is self:
312 raise RuntimeError('Refusing to drop a list of resources from itself.'
313 ' This is probably a bug where a list of Resources()'
314 ' should have been copied but is passed as-is.'
315 ' use Resources.clear() instead.')
316
317 for key, reserved_list in reserved.items():
318 my_list = self.get(key) or []
319
320 if my_list is reserved_list:
321 self.pop(key)
322 continue
323
324 for reserved_item in reserved_list:
325 found = False
326 reserved_hash = reserved_item.get(HASH_KEY)
327 if not reserved_hash:
328 raise RuntimeError('Resources.drop() only works with hashed items')
329
330 for i in range(len(my_list)):
331 my_item = my_list[i]
332 my_hash = my_item.get(HASH_KEY)
333 if not my_hash:
334 raise RuntimeError('Resources.drop() only works with hashed items')
335 if my_hash == reserved_hash:
336 found = True
337 my_list.pop(i)
338 break
339
340 if fail_if_not_found and not found:
341 raise RuntimeError('Asked to drop resource from a pool, but the'
342 ' resource was not found: %s = %r' % (key, reserved_item))
343
344 if not my_list:
345 self.pop(key)
346 return self
347
348 def without(self, reserved):
349 return Resources(self).drop(reserved)
350
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200351 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 +0200352 '''
353 Pass a dict of resource requirements, e.g.:
354 want = {
355 'bts': [ {'type': 'osmo-bts-sysmo',}, {} ],
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200356 'modem': [ {}, {}, {} ]
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200357 }
358 This function tries to find a combination from the available resources that
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200359 matches these requirements. The return value is a dict (wrapped in a Resources class)
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200360 that contains the matching resources in the order of 'want' dict: in above
361 example, the returned dict would have a 'bts' list with the first item being
362 a sysmoBTS, the second item being any other available BTS.
363
364 If skip_if_marked is passed, any resource that contains this key is skipped.
365 E.g. if a BTS has the USED_KEY set like
366 reserved_resources = { 'bts' : {..., '_used': True} }
367 then this may be skipped by passing skip_if_marked='_used'
368 (or rather skip_if_marked=USED_KEY).
369
370 If do_copy is True, the returned dict is a deep copy and does not share
371 lists with any other Resources dict.
372
373 If raise_if_missing is False, this will return an empty item for any
374 resource that had no match, instead of immediately raising an exception.
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200375
376 This function expects input dictionaries whose contents have already
377 been replicated based on its the 'times' attributes. See
378 config.replicate_times() for more details.
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200379 '''
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200380 matches = {}
Neels Hofmeyr17c139e2017-04-12 02:42:02 +0200381 for key, want_list in sorted(want.items()): # sorted for deterministic test results
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200382 # here we have a resource of a given type, e.g. 'bts', with a list
383 # containing as many BTSes as the caller wants to reserve/use. Each
384 # list item contains specifics for the particular BTS.
Neels Hofmeyr2a1a1fa2017-05-29 01:36:21 +0200385 my_list = self.get(key, [])
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200386
Neels Hofmeyrf9e86932017-06-06 19:47:40 +0200387 if log_label:
388 for_origin.log(log_label, len(want_list), 'x', key, '(candidates: %d)'%len(my_list))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200389
390 # Try to avoid a less constrained item snatching away a resource
391 # from a more detailed constrained requirement.
392
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200393 # first record all matches, so that each requested item has a list
394 # of all available resources that match it. Some resources may
395 # appear for multiple requested items. Store matching indexes.
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200396 all_matches = []
397 for want_item in want_list:
398 item_match_list = []
399 for i in range(len(my_list)):
400 my_item = my_list[i]
401 if skip_if_marked and my_item.get(skip_if_marked):
402 continue
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200403 if item_matches(my_item, want_item):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200404 item_match_list.append(i)
405 if not item_match_list:
Neels Hofmeyr9b907702017-05-06 23:20:33 +0200406 if raise_if_missing:
407 raise NoResourceExn('No matching resource available for %s = %r'
408 % (key, want_item))
409 else:
410 # this one failed... see below
411 all_matches = []
412 break
413
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200414 all_matches.append( item_match_list )
415
416 if not all_matches:
Neels Hofmeyr9b907702017-05-06 23:20:33 +0200417 # ...this one failed. Makes no sense to solve resource
418 # allocations, return an empty list for this key to mark
419 # failure.
420 matches[key] = []
421 continue
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200422
423 # figure out who gets what
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200424 try:
425 solution = solve(all_matches)
426 except NotSolvable:
427 # instead of a cryptic error message, raise an exception that
428 # conveys meaning to the user.
429 raise NoResourceExn('Could not resolve request to reserve resources: '
430 '%d x %s with requirements: %r' % (len(want_list), key, want_list))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200431 picked = [ my_list[i] for i in solution if i is not None ]
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200432 for_origin.dbg('Picked', config.tostr(picked))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200433 matches[key] = picked
434
435 return Resources(matches, do_copy=do_copy)
436
437 def set_hashes(self):
438 for key, item_list in self.items():
439 for item in item_list:
440 item[HASH_KEY] = util.hash_obj(item, HASH_KEY, RESERVED_KEY, USED_KEY)
441
442 def add(self, more):
443 if more is self:
444 raise RuntimeError('adding a list of resources to itself?')
445 config.add(self, copy.deepcopy(more))
446
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200447 def mark_reserved_by(self, origin_id):
448 for key, item_list in self.items():
449 for item in item_list:
450 item[RESERVED_KEY] = origin_id
451
452
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200453class NotSolvable(Exception):
454 pass
455
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200456def solve(all_matches):
457 '''
458 all_matches shall be a list of index-lists.
459 all_matches[i] is the list of indexes that item i can use.
460 Return a solution so that each i gets a different index.
461 solve([ [0, 1, 2],
462 [0],
463 [0, 2] ]) == [1, 0, 2]
464 '''
465
466 def all_differ(l):
467 return len(set(l)) == len(l)
468
469 def search_in_permutations(fixed=[]):
470 idx = len(fixed)
471 for i in range(len(all_matches[idx])):
472 val = all_matches[idx][i]
473 # don't add a val that's already in the list
474 if val in fixed:
475 continue
476 l = list(fixed)
477 l.append(val)
478 if len(l) == len(all_matches):
479 # found a solution
480 return l
481 # not at the end yet, add next digit
482 r = search_in_permutations(l)
483 if r:
484 # nested search_in_permutations() call found a solution
485 return r
486 # this entire branch yielded no solution
487 return None
488
489 if not all_matches:
490 raise RuntimeError('Cannot solve: no candidates')
491
492 solution = search_in_permutations()
493 if not solution:
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200494 raise NotSolvable('The requested resource requirements are not solvable %r'
495 % all_matches)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200496 return solution
497
498
499def contains_hash(list_of_dicts, a_hash):
500 for d in list_of_dicts:
501 if d.get(HASH_KEY) == a_hash:
502 return True
503 return False
504
505def item_matches(item, wanted_item, ignore_keys=None):
506 if is_dict(wanted_item):
507 # match up two dicts
508 if not isinstance(item, dict):
509 return False
510 for key, wanted_val in wanted_item.items():
511 if ignore_keys and key in ignore_keys:
512 continue
513 if not item_matches(item.get(key), wanted_val, ignore_keys=ignore_keys):
514 return False
515 return True
516
517 if is_list(wanted_item):
Pau Espin Pedrol4e36f7c2017-08-28 13:29:28 +0200518 if not is_list(item):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200519 return False
Pau Espin Pedrol58475512017-09-14 15:33:15 +0200520 # Validate that all elements in both lists are of the same type:
521 t = util.list_validate_same_elem_type(wanted_item + item)
522 if t is None:
523 return True # both lists are empty, return
524 # For lists of complex objects, we expect them to be sorted lists:
525 if t in (dict, list, tuple):
526 for i in range(max(len(wanted_item), len(item))):
527 log.ctx(idx=i)
528 subitem = item[i] if i < len(item) else util.empty_instance_type(t)
529 wanted_subitem = wanted_item[i] if i < len(wanted_item) else util.empty_instance_type(t)
530 if not item_matches(subitem, wanted_subitem, ignore_keys=ignore_keys):
531 return False
532 else: # for lists of basic elements, we handle them as unsorted sets:
533 for val in wanted_item:
534 if val not in item:
535 return False
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200536 return True
537
538 return item == wanted_item
539
540
541class ReservedResources(log.Origin):
542 '''
543 After all resources have been figured out, this is the API that a test case
544 gets to interact with resources. From those resources that have been
545 reserved for it, it can pick some to mark them as currently in use.
546 Functions like nitb() provide a resource by automatically picking its
547 dependencies from so far unused (but reserved) resource.
548 '''
549
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200550 def __init__(self, resources_pool, origin, reserved, modifiers):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200551 self.resources_pool = resources_pool
552 self.origin = origin
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200553 self.reserved_original = reserved
554 self.reserved = copy.deepcopy(self.reserved_original)
555 config.overlay(self.reserved, modifiers)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200556
557 def __repr__(self):
558 return 'resources(%s)=%s' % (self.origin.name(), pprint.pformat(self.reserved))
559
560 def get(self, kind, specifics=None):
561 if specifics is None:
562 specifics = {}
563 self.dbg('requesting use of', kind, specifics=specifics)
564 want = { kind: [specifics] }
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200565 available_dict = self.reserved.find(self.origin, want, skip_if_marked=USED_KEY,
566 do_copy=False, raise_if_missing=False,
567 log_label='Using')
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200568 available = available_dict.get(kind)
569 self.dbg(available=len(available))
570 if not available:
Neels Hofmeyrf9e86932017-06-06 19:47:40 +0200571 # cook up a detailed error message for the current situation
572 kind_reserved = self.reserved.get(kind, [])
573 used_count = len([r for r in kind_reserved if USED_KEY in r])
574 matching = self.reserved.find(self.origin, want, raise_if_missing=False, log_label=None).get(kind, [])
575 if not matching:
576 msg = 'none of the reserved resources matches requirements %r' % specifics
577 elif not (used_count < len(kind_reserved)):
578 msg = 'suite.conf reserved only %d x %r.' % (len(kind_reserved), kind)
579 else:
580 msg = ('No unused resource left that matches the requirements;'
581 ' Of reserved %d x %r, %d match the requirements, but all are already in use;'
582 ' Requirements: %r'
583 % (len(kind_reserved), kind, len(matching), specifics))
584 raise NoResourceExn('When trying to use instance nr %d of %r: %s' % (used_count + 1, kind, msg))
585
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200586 pick = available[0]
587 self.dbg(using=pick)
588 assert not pick.get(USED_KEY)
589 pick[USED_KEY] = True
590 return copy.deepcopy(pick)
591
592 def put(self, item):
593 if not item.get(USED_KEY):
594 raise RuntimeError('Can only put() a resource that is used: %r' % item)
595 hash_to_put = item.get(HASH_KEY)
596 if not hash_to_put:
597 raise RuntimeError('Can only put() a resource that has a hash marker: %r' % item)
598 for key, item_list in self.reserved.items():
599 my_list = self.get(key)
600 for my_item in my_list:
601 if hash_to_put == my_item.get(HASH_KEY):
602 my_item.pop(USED_KEY)
603
604 def put_all(self):
Pau Espin Pedrol1dd29552017-06-13 18:07:57 +0200605 if not self.reserved:
606 return
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200607 for key, item_list in self.reserved.items():
Pau Espin Pedrol1dd29552017-06-13 18:07:57 +0200608 for item in item_list:
609 item.pop(USED_KEY, None)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200610
611 def free(self):
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200612 if self.reserved_original:
613 self.resources_pool.free(self.origin, self.reserved_original)
614 self.reserved_original = None
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200615
Neels Hofmeyr2d1d5612017-05-22 20:02:41 +0200616 def counts(self):
617 counts = {}
618 for key in self.reserved.keys():
619 counts[key] = self.count(key)
620 return counts
621
622 def count(self, key):
623 return len(self.reserved.get(key) or [])
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200624
625# vim: expandtab tabstop=4 shiftwidth=4