blob: a13fd2d266dab95926a4a09614c61cfc31962390 [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,
Pau Espin Pedrolc04528c2020-04-01 13:55:51 +0200129 'config.amarisoft.license_server_addr': schema.IPV4,
Pau Espin Pedrol1e81b5a2020-03-16 12:42:17 +0100130 },
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200131 dict([('resources.%s' % key, val) for key, val in WANT_SCHEMA.items()]),
132 dict([('modifiers.%s' % key, val) for key, val in WANT_SCHEMA.items()]))
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200133
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200134KNOWN_BTS_TYPES = {
Your Name44af3412017-04-13 03:11:59 +0200135 'osmo-bts-sysmo': bts_sysmo.SysmoBts,
136 'osmo-bts-trx': bts_osmotrx.OsmoBtsTrx,
Pau Espin Pedrolfeb66e72019-03-04 18:37:04 +0100137 'osmo-bts-oc2g': bts_oc2g.OsmoBtsOC2G,
Pau Espin Pedroldaed4472017-09-15 14:11:35 +0200138 'osmo-bts-octphy': bts_octphy.OsmoBtsOctphy,
Holger Hans Peter Freytherd2db10d2018-12-06 18:34:53 +0000139 'osmo-bts-virtual': bts_osmovirtual.OsmoBtsVirtual,
Pau Espin Pedrol1b28a582018-03-08 21:01:26 +0100140 'nanobts': bts_nanobts.NanoBts,
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200141 }
142
Pau Espin Pedrolc8b0f932020-02-11 17:45:26 +0100143KNOWN_ENB_TYPES = {
144 'srsenb': srs_enb.srsENB,
Pau Espin Pedrol786a6bc2020-03-30 13:51:21 +0200145 'amarisoftenb': amarisoft_enb.AmarisoftENB,
Pau Espin Pedrolc8b0f932020-02-11 17:45:26 +0100146}
Holger Hans Peter Freyther74243012019-02-27 08:18:38 +0000147
Pau Espin Pedrolda2e31f2020-03-31 13:45:01 +0200148KNOWN_EPC_TYPES = {
149 'srsepc': srs_epc.srsEPC,
150 'amarisoftepc': amarisoft_epc.AmarisoftEPC,
151}
152
Holger Hans Peter Freyther74243012019-02-27 08:18:38 +0000153KNOWN_MS_TYPES = {
154 # Map None to ofono for forward compability
155 None: modem.Modem,
156 'ofono': modem.Modem,
Holger Hans Peter Freyther10501cc2019-02-25 03:20:16 +0000157 'osmo-mobile': ms_osmo_mobile.MSOsmoMobile,
Pau Espin Pedrolc8b0f932020-02-11 17:45:26 +0100158 'srsue': srs_ue.srsUE,
Holger Hans Peter Freyther74243012019-02-27 08:18:38 +0000159}
160
161
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200162def register_bts_type(name, clazz):
163 KNOWN_BTS_TYPES[name] = clazz
164
165class ResourcesPool(log.Origin):
166 _remember_to_free = None
167 _registered_exit_handler = False
168
169 def __init__(self):
Pau Espin Pedrol66a38912020-03-11 20:11:08 +0100170 self.config_path = config.get_config_file(config.RESOURCES_CONF)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200171 self.state_dir = config.get_state_dir()
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200172 super().__init__(log.C_CNF, conf=self.config_path, state=self.state_dir.path)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200173 self.read_conf()
174
175 def read_conf(self):
176 self.all_resources = Resources(config.read(self.config_path, RESOURCES_SCHEMA))
177 self.all_resources.set_hashes()
178
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200179 def reserve(self, origin, want, modifiers):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200180 '''
181 attempt to reserve the resources specified in the dict 'want' for
182 'origin'. Obtain a lock on the resources lock dir, verify that all
183 wanted resources are available, and if yes mark them as reserved.
184
185 On success, return a reservation object which can be used to release
186 the reservation. The reservation will be freed automatically on program
187 exit, if not yet done manually.
188
189 'origin' should be an Origin() instance.
190
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200191 'want' is a dict matching RESOURCES_SCHEMA, used to specify what to
192 reserve.
193
194 'modifiers' is a dict matching RESOURCES_SCHEMA, it is overlaid on top
195 of 'want'.
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200196
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200197 If an entry has no attribute set, any of the resources may be
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200198 reserved without further limitations.
199
200 ResourcesPool may also be selected with narrowed down constraints.
Neels Hofmeyr76d81032017-05-18 18:35:32 +0200201 This would reserve one IP address, two modems, one BTS of type
Neels Hofmeyr391afe32017-05-18 19:22:12 +0200202 sysmo and one of type trx, plus 2 ARFCNs in the 1800 band:
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200203
204 {
Neels Hofmeyr76d81032017-05-18 18:35:32 +0200205 'ip_address': [ {} ],
Neels Hofmeyr391afe32017-05-18 19:22:12 +0200206 'bts': [ { 'type': 'sysmo' }, { 'type': 'trx' } ],
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200207 'arfcn': [ { 'band': 'GSM-1800' }, { 'band': 'GSM-1800' } ],
208 'modem': [ {}, {} ],
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200209 }
210 '''
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200211 schema.validate(want, RESOURCES_SCHEMA)
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200212 schema.validate(modifiers, RESOURCES_SCHEMA)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200213
214 origin_id = origin.origin_id()
215
216 with self.state_dir.lock(origin_id):
217 rrfile_path = self.state_dir.mk_parentdir(RESERVED_RESOURCES_FILE)
218 reserved = Resources(config.read(rrfile_path, if_missing_return={}))
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200219 to_be_reserved = self.all_resources.without(reserved).find(origin, want)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200220
221 to_be_reserved.mark_reserved_by(origin_id)
222
223 reserved.add(to_be_reserved)
224 config.write(rrfile_path, reserved)
225
226 self.remember_to_free(to_be_reserved)
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200227 return ReservedResources(self, origin, to_be_reserved, modifiers)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200228
229 def free(self, origin, to_be_freed):
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200230 log.ctx(origin)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200231 with self.state_dir.lock(origin.origin_id()):
232 rrfile_path = self.state_dir.mk_parentdir(RESERVED_RESOURCES_FILE)
233 reserved = Resources(config.read(rrfile_path, if_missing_return={}))
234 reserved.drop(to_be_freed)
235 config.write(rrfile_path, reserved)
236 self.forget_freed(to_be_freed)
237
238 def register_exit_handler(self):
239 if self._registered_exit_handler:
240 return
241 atexit.register(self.clean_up_registered_resources)
242 self._registered_exit_handler = True
243
244 def unregister_exit_handler(self):
245 if not self._registered_exit_handler:
246 return
247 atexit.unregister(self.clean_up_registered_resources)
248 self._registered_exit_handler = False
249
250 def clean_up_registered_resources(self):
251 if not self._remember_to_free:
252 return
253 self.free(log.Origin('atexit.clean_up_registered_resources()'),
254 self._remember_to_free)
255
256 def remember_to_free(self, to_be_reserved):
257 self.register_exit_handler()
258 if not self._remember_to_free:
259 self._remember_to_free = Resources()
260 self._remember_to_free.add(to_be_reserved)
261
262 def forget_freed(self, freed):
263 if freed is self._remember_to_free:
264 self._remember_to_free.clear()
265 else:
266 self._remember_to_free.drop(freed)
267 if not self._remember_to_free:
268 self.unregister_exit_handler()
269
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100270 def next_persistent_value(self, token, first_val, validate_func, inc_func, origin):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200271 origin_id = origin.origin_id()
272
273 with self.state_dir.lock(origin_id):
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100274 token_path = self.state_dir.child('last_used_%s.state' % token)
275 log.ctx(token_path)
276 last_value = first_val
277 if os.path.exists(token_path):
278 if not os.path.isfile(token_path):
279 raise RuntimeError('path should be a file but is not: %r' % token_path)
280 with open(token_path, 'r') as f:
281 last_value = f.read().strip()
282 validate_func(last_value)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200283
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100284 next_value = inc_func(last_value)
285 with open(token_path, 'w') as f:
286 f.write(next_value)
287 return next_value
288
289 def next_msisdn(self, origin):
290 return self.next_persistent_value('msisdn', '1000', schema.msisdn, util.msisdn_inc, origin)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200291
Pau Espin Pedrol5e0c2512017-11-06 18:40:23 +0100292 def next_lac(self, origin):
Pau Espin Pedrolf7f06362017-11-28 12:56:35 +0100293 # LAC=0 has special meaning (MS detached), avoid it
294 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 +0200295
Pau Espin Pedrol8a3a7b52017-11-28 15:50:02 +0100296 def next_rac(self, origin):
297 return self.next_persistent_value('rac', '1', schema.uint8, lambda x: str((int(x)+1) % pow(2,8) or 1), origin)
298
Pau Espin Pedrol4ccce7c2017-11-07 11:13:20 +0100299 def next_cellid(self, origin):
300 return self.next_persistent_value('cellid', '1', schema.uint16, lambda x: str((int(x)+1) % pow(2,16)), origin)
301
Pau Espin Pedrol8a3a7b52017-11-28 15:50:02 +0100302 def next_bvci(self, origin):
303 # BVCI=0 and =1 are reserved, avoid them.
304 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)
305
Pau Espin Pedrol4676cbd2017-09-14 17:35:03 +0200306class NoResourceExn(log.Error):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200307 pass
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200308
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200309class Resources(dict):
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200310
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200311 def __init__(self, all_resources={}, do_copy=True):
312 if do_copy:
313 all_resources = copy.deepcopy(all_resources)
314 self.update(all_resources)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200315
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200316 def drop(self, reserved, fail_if_not_found=True):
317 # protect from modifying reserved because we're the same object
318 if reserved is self:
319 raise RuntimeError('Refusing to drop a list of resources from itself.'
320 ' This is probably a bug where a list of Resources()'
321 ' should have been copied but is passed as-is.'
322 ' use Resources.clear() instead.')
323
324 for key, reserved_list in reserved.items():
325 my_list = self.get(key) or []
326
327 if my_list is reserved_list:
328 self.pop(key)
329 continue
330
331 for reserved_item in reserved_list:
332 found = False
333 reserved_hash = reserved_item.get(HASH_KEY)
334 if not reserved_hash:
335 raise RuntimeError('Resources.drop() only works with hashed items')
336
337 for i in range(len(my_list)):
338 my_item = my_list[i]
339 my_hash = my_item.get(HASH_KEY)
340 if not my_hash:
341 raise RuntimeError('Resources.drop() only works with hashed items')
342 if my_hash == reserved_hash:
343 found = True
344 my_list.pop(i)
345 break
346
347 if fail_if_not_found and not found:
348 raise RuntimeError('Asked to drop resource from a pool, but the'
349 ' resource was not found: %s = %r' % (key, reserved_item))
350
351 if not my_list:
352 self.pop(key)
353 return self
354
355 def without(self, reserved):
356 return Resources(self).drop(reserved)
357
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200358 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 +0200359 '''
360 Pass a dict of resource requirements, e.g.:
361 want = {
362 'bts': [ {'type': 'osmo-bts-sysmo',}, {} ],
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200363 'modem': [ {}, {}, {} ]
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200364 }
365 This function tries to find a combination from the available resources that
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200366 matches these requirements. The return value is a dict (wrapped in a Resources class)
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200367 that contains the matching resources in the order of 'want' dict: in above
368 example, the returned dict would have a 'bts' list with the first item being
369 a sysmoBTS, the second item being any other available BTS.
370
371 If skip_if_marked is passed, any resource that contains this key is skipped.
372 E.g. if a BTS has the USED_KEY set like
373 reserved_resources = { 'bts' : {..., '_used': True} }
374 then this may be skipped by passing skip_if_marked='_used'
375 (or rather skip_if_marked=USED_KEY).
376
377 If do_copy is True, the returned dict is a deep copy and does not share
378 lists with any other Resources dict.
379
380 If raise_if_missing is False, this will return an empty item for any
381 resource that had no match, instead of immediately raising an exception.
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200382
383 This function expects input dictionaries whose contents have already
384 been replicated based on its the 'times' attributes. See
385 config.replicate_times() for more details.
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200386 '''
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200387 matches = {}
Neels Hofmeyr17c139e2017-04-12 02:42:02 +0200388 for key, want_list in sorted(want.items()): # sorted for deterministic test results
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200389 # here we have a resource of a given type, e.g. 'bts', with a list
390 # containing as many BTSes as the caller wants to reserve/use. Each
391 # list item contains specifics for the particular BTS.
Neels Hofmeyr2a1a1fa2017-05-29 01:36:21 +0200392 my_list = self.get(key, [])
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200393
Neels Hofmeyrf9e86932017-06-06 19:47:40 +0200394 if log_label:
395 for_origin.log(log_label, len(want_list), 'x', key, '(candidates: %d)'%len(my_list))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200396
397 # Try to avoid a less constrained item snatching away a resource
398 # from a more detailed constrained requirement.
399
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200400 # first record all matches, so that each requested item has a list
401 # of all available resources that match it. Some resources may
402 # appear for multiple requested items. Store matching indexes.
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200403 all_matches = []
404 for want_item in want_list:
405 item_match_list = []
406 for i in range(len(my_list)):
407 my_item = my_list[i]
408 if skip_if_marked and my_item.get(skip_if_marked):
409 continue
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200410 if item_matches(my_item, want_item):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200411 item_match_list.append(i)
412 if not item_match_list:
Neels Hofmeyr9b907702017-05-06 23:20:33 +0200413 if raise_if_missing:
414 raise NoResourceExn('No matching resource available for %s = %r'
415 % (key, want_item))
416 else:
417 # this one failed... see below
418 all_matches = []
419 break
420
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200421 all_matches.append( item_match_list )
422
423 if not all_matches:
Neels Hofmeyr9b907702017-05-06 23:20:33 +0200424 # ...this one failed. Makes no sense to solve resource
425 # allocations, return an empty list for this key to mark
426 # failure.
427 matches[key] = []
428 continue
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200429
430 # figure out who gets what
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200431 try:
432 solution = solve(all_matches)
433 except NotSolvable:
434 # instead of a cryptic error message, raise an exception that
435 # conveys meaning to the user.
436 raise NoResourceExn('Could not resolve request to reserve resources: '
437 '%d x %s with requirements: %r' % (len(want_list), key, want_list))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200438 picked = [ my_list[i] for i in solution if i is not None ]
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200439 for_origin.dbg('Picked', config.tostr(picked))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200440 matches[key] = picked
441
442 return Resources(matches, do_copy=do_copy)
443
444 def set_hashes(self):
445 for key, item_list in self.items():
446 for item in item_list:
447 item[HASH_KEY] = util.hash_obj(item, HASH_KEY, RESERVED_KEY, USED_KEY)
448
449 def add(self, more):
450 if more is self:
451 raise RuntimeError('adding a list of resources to itself?')
452 config.add(self, copy.deepcopy(more))
453
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200454 def mark_reserved_by(self, origin_id):
455 for key, item_list in self.items():
456 for item in item_list:
457 item[RESERVED_KEY] = origin_id
458
459
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200460class NotSolvable(Exception):
461 pass
462
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200463def solve(all_matches):
464 '''
465 all_matches shall be a list of index-lists.
466 all_matches[i] is the list of indexes that item i can use.
467 Return a solution so that each i gets a different index.
468 solve([ [0, 1, 2],
469 [0],
470 [0, 2] ]) == [1, 0, 2]
471 '''
472
473 def all_differ(l):
474 return len(set(l)) == len(l)
475
476 def search_in_permutations(fixed=[]):
477 idx = len(fixed)
478 for i in range(len(all_matches[idx])):
479 val = all_matches[idx][i]
480 # don't add a val that's already in the list
481 if val in fixed:
482 continue
483 l = list(fixed)
484 l.append(val)
485 if len(l) == len(all_matches):
486 # found a solution
487 return l
488 # not at the end yet, add next digit
489 r = search_in_permutations(l)
490 if r:
491 # nested search_in_permutations() call found a solution
492 return r
493 # this entire branch yielded no solution
494 return None
495
496 if not all_matches:
497 raise RuntimeError('Cannot solve: no candidates')
498
499 solution = search_in_permutations()
500 if not solution:
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200501 raise NotSolvable('The requested resource requirements are not solvable %r'
502 % all_matches)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200503 return solution
504
505
506def contains_hash(list_of_dicts, a_hash):
507 for d in list_of_dicts:
508 if d.get(HASH_KEY) == a_hash:
509 return True
510 return False
511
512def item_matches(item, wanted_item, ignore_keys=None):
513 if is_dict(wanted_item):
514 # match up two dicts
515 if not isinstance(item, dict):
516 return False
517 for key, wanted_val in wanted_item.items():
518 if ignore_keys and key in ignore_keys:
519 continue
520 if not item_matches(item.get(key), wanted_val, ignore_keys=ignore_keys):
521 return False
522 return True
523
524 if is_list(wanted_item):
Pau Espin Pedrol4e36f7c2017-08-28 13:29:28 +0200525 if not is_list(item):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200526 return False
Pau Espin Pedrol58475512017-09-14 15:33:15 +0200527 # Validate that all elements in both lists are of the same type:
528 t = util.list_validate_same_elem_type(wanted_item + item)
529 if t is None:
530 return True # both lists are empty, return
531 # For lists of complex objects, we expect them to be sorted lists:
532 if t in (dict, list, tuple):
533 for i in range(max(len(wanted_item), len(item))):
534 log.ctx(idx=i)
535 subitem = item[i] if i < len(item) else util.empty_instance_type(t)
536 wanted_subitem = wanted_item[i] if i < len(wanted_item) else util.empty_instance_type(t)
537 if not item_matches(subitem, wanted_subitem, ignore_keys=ignore_keys):
538 return False
539 else: # for lists of basic elements, we handle them as unsorted sets:
540 for val in wanted_item:
541 if val not in item:
542 return False
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200543 return True
544
545 return item == wanted_item
546
547
548class ReservedResources(log.Origin):
549 '''
550 After all resources have been figured out, this is the API that a test case
551 gets to interact with resources. From those resources that have been
552 reserved for it, it can pick some to mark them as currently in use.
553 Functions like nitb() provide a resource by automatically picking its
554 dependencies from so far unused (but reserved) resource.
555 '''
556
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200557 def __init__(self, resources_pool, origin, reserved, modifiers):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200558 self.resources_pool = resources_pool
559 self.origin = origin
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200560 self.reserved_original = reserved
561 self.reserved = copy.deepcopy(self.reserved_original)
562 config.overlay(self.reserved, modifiers)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200563
564 def __repr__(self):
565 return 'resources(%s)=%s' % (self.origin.name(), pprint.pformat(self.reserved))
566
567 def get(self, kind, specifics=None):
568 if specifics is None:
569 specifics = {}
570 self.dbg('requesting use of', kind, specifics=specifics)
571 want = { kind: [specifics] }
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200572 available_dict = self.reserved.find(self.origin, want, skip_if_marked=USED_KEY,
573 do_copy=False, raise_if_missing=False,
574 log_label='Using')
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200575 available = available_dict.get(kind)
576 self.dbg(available=len(available))
577 if not available:
Neels Hofmeyrf9e86932017-06-06 19:47:40 +0200578 # cook up a detailed error message for the current situation
579 kind_reserved = self.reserved.get(kind, [])
580 used_count = len([r for r in kind_reserved if USED_KEY in r])
581 matching = self.reserved.find(self.origin, want, raise_if_missing=False, log_label=None).get(kind, [])
582 if not matching:
583 msg = 'none of the reserved resources matches requirements %r' % specifics
584 elif not (used_count < len(kind_reserved)):
585 msg = 'suite.conf reserved only %d x %r.' % (len(kind_reserved), kind)
586 else:
587 msg = ('No unused resource left that matches the requirements;'
588 ' Of reserved %d x %r, %d match the requirements, but all are already in use;'
589 ' Requirements: %r'
590 % (len(kind_reserved), kind, len(matching), specifics))
591 raise NoResourceExn('When trying to use instance nr %d of %r: %s' % (used_count + 1, kind, msg))
592
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200593 pick = available[0]
594 self.dbg(using=pick)
595 assert not pick.get(USED_KEY)
596 pick[USED_KEY] = True
597 return copy.deepcopy(pick)
598
599 def put(self, item):
600 if not item.get(USED_KEY):
601 raise RuntimeError('Can only put() a resource that is used: %r' % item)
602 hash_to_put = item.get(HASH_KEY)
603 if not hash_to_put:
604 raise RuntimeError('Can only put() a resource that has a hash marker: %r' % item)
605 for key, item_list in self.reserved.items():
606 my_list = self.get(key)
607 for my_item in my_list:
608 if hash_to_put == my_item.get(HASH_KEY):
609 my_item.pop(USED_KEY)
610
611 def put_all(self):
Pau Espin Pedrol1dd29552017-06-13 18:07:57 +0200612 if not self.reserved:
613 return
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200614 for key, item_list in self.reserved.items():
Pau Espin Pedrol1dd29552017-06-13 18:07:57 +0200615 for item in item_list:
616 item.pop(USED_KEY, None)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200617
618 def free(self):
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200619 if self.reserved_original:
620 self.resources_pool.free(self.origin, self.reserved_original)
621 self.reserved_original = None
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200622
Neels Hofmeyr2d1d5612017-05-22 20:02:41 +0200623 def counts(self):
624 counts = {}
625 for key in self.reserved.keys():
626 counts[key] = self.count(key)
627 return counts
628
629 def count(self, key):
630 return len(self.reserved.get(key) or [])
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200631
632# vim: expandtab tabstop=4 shiftwidth=4