blob: 8a93ea49a7a128926e6a080e5f37fb30db532a84 [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 Pedrolc8b0f932020-02-11 17:45:26 +010032from . import srs_ue, srs_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,
Pau Espin Pedrola9a2fe22020-02-13 19:29:55 +010093 'enb[].rf_dev_type': schema.STR,
94 'enb[].rf_dev_args': schema.STR,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020095 'arfcn[].arfcn': schema.INT,
96 'arfcn[].band': schema.BAND,
Holger Hans Peter Freyther93a89422019-02-27 06:36:36 +000097 'modem[].type': schema.STR,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020098 'modem[].label': schema.STR,
99 'modem[].path': schema.STR,
100 'modem[].imsi': schema.IMSI,
101 'modem[].ki': schema.KI,
Pau Espin Pedrol713ce2c2017-08-24 16:57:17 +0200102 'modem[].auth_algo': schema.AUTH_ALGO,
Pau Espin Pedrolc8b0f932020-02-11 17:45:26 +0100103 'modem[].remote_user': schema.STR,
104 'modem[].addr': schema.IPV4,
Pau Espin Pedrol57497a62017-08-28 14:21:15 +0200105 'modem[].ciphers[]': schema.CIPHER,
Pau Espin Pedrolac18fd32017-08-31 18:49:47 +0200106 'modem[].features[]': schema.MODEM_FEATURE,
Pau Espin Pedrola9a2fe22020-02-13 19:29:55 +0100107 'modem[].rf_dev_type': schema.STR,
108 'modem[].rf_dev_args': schema.STR,
Pau Espin Pedrolbc1ed882018-05-17 16:59:58 +0200109 'osmocon_phone[].serial_device': schema.STR,
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200110 }
111
112WANT_SCHEMA = util.dict_add(
Pau Espin Pedrolead79ac2017-09-12 15:19:18 +0200113 dict([('%s[].times' % r, schema.TIMES) for r in R_ALL]),
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200114 RESOURCES_SCHEMA)
115
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200116CONF_SCHEMA = util.dict_add(
Pau Espin Pedrol5dc24592018-08-27 12:53:41 +0200117 { 'defaults.timeout': schema.STR,
Pau Espin Pedrolb6937712020-02-27 18:02:20 +0100118 'config.bsc.net.codec_list[]': schema.CODEC,
Pau Espin Pedrol1e81b5a2020-03-16 12:42:17 +0100119 'config.enb.enable_pcap': schema.BOOL_STR,
120 'config.epc.rlc_drb_mode': schema.LTE_RLC_DRB_MODE,
121 'config.epc.enable_pcap': schema.BOOL_STR,
122 'config.modem.enable_pcap': schema.BOOL_STR,
123 },
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200124 dict([('resources.%s' % key, val) for key, val in WANT_SCHEMA.items()]),
125 dict([('modifiers.%s' % key, val) for key, val in WANT_SCHEMA.items()]))
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200126
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200127KNOWN_BTS_TYPES = {
Your Name44af3412017-04-13 03:11:59 +0200128 'osmo-bts-sysmo': bts_sysmo.SysmoBts,
129 'osmo-bts-trx': bts_osmotrx.OsmoBtsTrx,
Pau Espin Pedrolfeb66e72019-03-04 18:37:04 +0100130 'osmo-bts-oc2g': bts_oc2g.OsmoBtsOC2G,
Pau Espin Pedroldaed4472017-09-15 14:11:35 +0200131 'osmo-bts-octphy': bts_octphy.OsmoBtsOctphy,
Holger Hans Peter Freytherd2db10d2018-12-06 18:34:53 +0000132 'osmo-bts-virtual': bts_osmovirtual.OsmoBtsVirtual,
Pau Espin Pedrol1b28a582018-03-08 21:01:26 +0100133 'nanobts': bts_nanobts.NanoBts,
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200134 }
135
Pau Espin Pedrolc8b0f932020-02-11 17:45:26 +0100136KNOWN_ENB_TYPES = {
137 'srsenb': srs_enb.srsENB,
138}
Holger Hans Peter Freyther74243012019-02-27 08:18:38 +0000139
140KNOWN_MS_TYPES = {
141 # Map None to ofono for forward compability
142 None: modem.Modem,
143 'ofono': modem.Modem,
Holger Hans Peter Freyther10501cc2019-02-25 03:20:16 +0000144 'osmo-mobile': ms_osmo_mobile.MSOsmoMobile,
Pau Espin Pedrolc8b0f932020-02-11 17:45:26 +0100145 'srsue': srs_ue.srsUE,
Holger Hans Peter Freyther74243012019-02-27 08:18:38 +0000146}
147
148
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200149def register_bts_type(name, clazz):
150 KNOWN_BTS_TYPES[name] = clazz
151
152class ResourcesPool(log.Origin):
153 _remember_to_free = None
154 _registered_exit_handler = False
155
156 def __init__(self):
Pau Espin Pedrol66a38912020-03-11 20:11:08 +0100157 self.config_path = config.get_config_file(config.RESOURCES_CONF)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200158 self.state_dir = config.get_state_dir()
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200159 super().__init__(log.C_CNF, conf=self.config_path, state=self.state_dir.path)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200160 self.read_conf()
161
162 def read_conf(self):
163 self.all_resources = Resources(config.read(self.config_path, RESOURCES_SCHEMA))
164 self.all_resources.set_hashes()
165
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200166 def reserve(self, origin, want, modifiers):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200167 '''
168 attempt to reserve the resources specified in the dict 'want' for
169 'origin'. Obtain a lock on the resources lock dir, verify that all
170 wanted resources are available, and if yes mark them as reserved.
171
172 On success, return a reservation object which can be used to release
173 the reservation. The reservation will be freed automatically on program
174 exit, if not yet done manually.
175
176 'origin' should be an Origin() instance.
177
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200178 'want' is a dict matching RESOURCES_SCHEMA, used to specify what to
179 reserve.
180
181 'modifiers' is a dict matching RESOURCES_SCHEMA, it is overlaid on top
182 of 'want'.
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200183
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200184 If an entry has no attribute set, any of the resources may be
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200185 reserved without further limitations.
186
187 ResourcesPool may also be selected with narrowed down constraints.
Neels Hofmeyr76d81032017-05-18 18:35:32 +0200188 This would reserve one IP address, two modems, one BTS of type
Neels Hofmeyr391afe32017-05-18 19:22:12 +0200189 sysmo and one of type trx, plus 2 ARFCNs in the 1800 band:
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200190
191 {
Neels Hofmeyr76d81032017-05-18 18:35:32 +0200192 'ip_address': [ {} ],
Neels Hofmeyr391afe32017-05-18 19:22:12 +0200193 'bts': [ { 'type': 'sysmo' }, { 'type': 'trx' } ],
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200194 'arfcn': [ { 'band': 'GSM-1800' }, { 'band': 'GSM-1800' } ],
195 'modem': [ {}, {} ],
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200196 }
197 '''
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200198 schema.validate(want, RESOURCES_SCHEMA)
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200199 schema.validate(modifiers, RESOURCES_SCHEMA)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200200
201 origin_id = origin.origin_id()
202
203 with self.state_dir.lock(origin_id):
204 rrfile_path = self.state_dir.mk_parentdir(RESERVED_RESOURCES_FILE)
205 reserved = Resources(config.read(rrfile_path, if_missing_return={}))
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200206 to_be_reserved = self.all_resources.without(reserved).find(origin, want)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200207
208 to_be_reserved.mark_reserved_by(origin_id)
209
210 reserved.add(to_be_reserved)
211 config.write(rrfile_path, reserved)
212
213 self.remember_to_free(to_be_reserved)
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200214 return ReservedResources(self, origin, to_be_reserved, modifiers)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200215
216 def free(self, origin, to_be_freed):
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200217 log.ctx(origin)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200218 with self.state_dir.lock(origin.origin_id()):
219 rrfile_path = self.state_dir.mk_parentdir(RESERVED_RESOURCES_FILE)
220 reserved = Resources(config.read(rrfile_path, if_missing_return={}))
221 reserved.drop(to_be_freed)
222 config.write(rrfile_path, reserved)
223 self.forget_freed(to_be_freed)
224
225 def register_exit_handler(self):
226 if self._registered_exit_handler:
227 return
228 atexit.register(self.clean_up_registered_resources)
229 self._registered_exit_handler = True
230
231 def unregister_exit_handler(self):
232 if not self._registered_exit_handler:
233 return
234 atexit.unregister(self.clean_up_registered_resources)
235 self._registered_exit_handler = False
236
237 def clean_up_registered_resources(self):
238 if not self._remember_to_free:
239 return
240 self.free(log.Origin('atexit.clean_up_registered_resources()'),
241 self._remember_to_free)
242
243 def remember_to_free(self, to_be_reserved):
244 self.register_exit_handler()
245 if not self._remember_to_free:
246 self._remember_to_free = Resources()
247 self._remember_to_free.add(to_be_reserved)
248
249 def forget_freed(self, freed):
250 if freed is self._remember_to_free:
251 self._remember_to_free.clear()
252 else:
253 self._remember_to_free.drop(freed)
254 if not self._remember_to_free:
255 self.unregister_exit_handler()
256
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100257 def next_persistent_value(self, token, first_val, validate_func, inc_func, origin):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200258 origin_id = origin.origin_id()
259
260 with self.state_dir.lock(origin_id):
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100261 token_path = self.state_dir.child('last_used_%s.state' % token)
262 log.ctx(token_path)
263 last_value = first_val
264 if os.path.exists(token_path):
265 if not os.path.isfile(token_path):
266 raise RuntimeError('path should be a file but is not: %r' % token_path)
267 with open(token_path, 'r') as f:
268 last_value = f.read().strip()
269 validate_func(last_value)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200270
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100271 next_value = inc_func(last_value)
272 with open(token_path, 'w') as f:
273 f.write(next_value)
274 return next_value
275
276 def next_msisdn(self, origin):
277 return self.next_persistent_value('msisdn', '1000', schema.msisdn, util.msisdn_inc, origin)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200278
Pau Espin Pedrol5e0c2512017-11-06 18:40:23 +0100279 def next_lac(self, origin):
Pau Espin Pedrolf7f06362017-11-28 12:56:35 +0100280 # LAC=0 has special meaning (MS detached), avoid it
281 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 +0200282
Pau Espin Pedrol8a3a7b52017-11-28 15:50:02 +0100283 def next_rac(self, origin):
284 return self.next_persistent_value('rac', '1', schema.uint8, lambda x: str((int(x)+1) % pow(2,8) or 1), origin)
285
Pau Espin Pedrol4ccce7c2017-11-07 11:13:20 +0100286 def next_cellid(self, origin):
287 return self.next_persistent_value('cellid', '1', schema.uint16, lambda x: str((int(x)+1) % pow(2,16)), origin)
288
Pau Espin Pedrol8a3a7b52017-11-28 15:50:02 +0100289 def next_bvci(self, origin):
290 # BVCI=0 and =1 are reserved, avoid them.
291 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)
292
Pau Espin Pedrol4676cbd2017-09-14 17:35:03 +0200293class NoResourceExn(log.Error):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200294 pass
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200295
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200296class Resources(dict):
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200297
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200298 def __init__(self, all_resources={}, do_copy=True):
299 if do_copy:
300 all_resources = copy.deepcopy(all_resources)
301 self.update(all_resources)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200302
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200303 def drop(self, reserved, fail_if_not_found=True):
304 # protect from modifying reserved because we're the same object
305 if reserved is self:
306 raise RuntimeError('Refusing to drop a list of resources from itself.'
307 ' This is probably a bug where a list of Resources()'
308 ' should have been copied but is passed as-is.'
309 ' use Resources.clear() instead.')
310
311 for key, reserved_list in reserved.items():
312 my_list = self.get(key) or []
313
314 if my_list is reserved_list:
315 self.pop(key)
316 continue
317
318 for reserved_item in reserved_list:
319 found = False
320 reserved_hash = reserved_item.get(HASH_KEY)
321 if not reserved_hash:
322 raise RuntimeError('Resources.drop() only works with hashed items')
323
324 for i in range(len(my_list)):
325 my_item = my_list[i]
326 my_hash = my_item.get(HASH_KEY)
327 if not my_hash:
328 raise RuntimeError('Resources.drop() only works with hashed items')
329 if my_hash == reserved_hash:
330 found = True
331 my_list.pop(i)
332 break
333
334 if fail_if_not_found and not found:
335 raise RuntimeError('Asked to drop resource from a pool, but the'
336 ' resource was not found: %s = %r' % (key, reserved_item))
337
338 if not my_list:
339 self.pop(key)
340 return self
341
342 def without(self, reserved):
343 return Resources(self).drop(reserved)
344
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200345 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 +0200346 '''
347 Pass a dict of resource requirements, e.g.:
348 want = {
349 'bts': [ {'type': 'osmo-bts-sysmo',}, {} ],
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200350 'modem': [ {}, {}, {} ]
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200351 }
352 This function tries to find a combination from the available resources that
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200353 matches these requirements. The return value is a dict (wrapped in a Resources class)
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200354 that contains the matching resources in the order of 'want' dict: in above
355 example, the returned dict would have a 'bts' list with the first item being
356 a sysmoBTS, the second item being any other available BTS.
357
358 If skip_if_marked is passed, any resource that contains this key is skipped.
359 E.g. if a BTS has the USED_KEY set like
360 reserved_resources = { 'bts' : {..., '_used': True} }
361 then this may be skipped by passing skip_if_marked='_used'
362 (or rather skip_if_marked=USED_KEY).
363
364 If do_copy is True, the returned dict is a deep copy and does not share
365 lists with any other Resources dict.
366
367 If raise_if_missing is False, this will return an empty item for any
368 resource that had no match, instead of immediately raising an exception.
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200369
370 This function expects input dictionaries whose contents have already
371 been replicated based on its the 'times' attributes. See
372 config.replicate_times() for more details.
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200373 '''
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200374 matches = {}
Neels Hofmeyr17c139e2017-04-12 02:42:02 +0200375 for key, want_list in sorted(want.items()): # sorted for deterministic test results
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200376 # here we have a resource of a given type, e.g. 'bts', with a list
377 # containing as many BTSes as the caller wants to reserve/use. Each
378 # list item contains specifics for the particular BTS.
Neels Hofmeyr2a1a1fa2017-05-29 01:36:21 +0200379 my_list = self.get(key, [])
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200380
Neels Hofmeyrf9e86932017-06-06 19:47:40 +0200381 if log_label:
382 for_origin.log(log_label, len(want_list), 'x', key, '(candidates: %d)'%len(my_list))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200383
384 # Try to avoid a less constrained item snatching away a resource
385 # from a more detailed constrained requirement.
386
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200387 # first record all matches, so that each requested item has a list
388 # of all available resources that match it. Some resources may
389 # appear for multiple requested items. Store matching indexes.
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200390 all_matches = []
391 for want_item in want_list:
392 item_match_list = []
393 for i in range(len(my_list)):
394 my_item = my_list[i]
395 if skip_if_marked and my_item.get(skip_if_marked):
396 continue
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200397 if item_matches(my_item, want_item):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200398 item_match_list.append(i)
399 if not item_match_list:
Neels Hofmeyr9b907702017-05-06 23:20:33 +0200400 if raise_if_missing:
401 raise NoResourceExn('No matching resource available for %s = %r'
402 % (key, want_item))
403 else:
404 # this one failed... see below
405 all_matches = []
406 break
407
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200408 all_matches.append( item_match_list )
409
410 if not all_matches:
Neels Hofmeyr9b907702017-05-06 23:20:33 +0200411 # ...this one failed. Makes no sense to solve resource
412 # allocations, return an empty list for this key to mark
413 # failure.
414 matches[key] = []
415 continue
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200416
417 # figure out who gets what
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200418 try:
419 solution = solve(all_matches)
420 except NotSolvable:
421 # instead of a cryptic error message, raise an exception that
422 # conveys meaning to the user.
423 raise NoResourceExn('Could not resolve request to reserve resources: '
424 '%d x %s with requirements: %r' % (len(want_list), key, want_list))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200425 picked = [ my_list[i] for i in solution if i is not None ]
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200426 for_origin.dbg('Picked', config.tostr(picked))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200427 matches[key] = picked
428
429 return Resources(matches, do_copy=do_copy)
430
431 def set_hashes(self):
432 for key, item_list in self.items():
433 for item in item_list:
434 item[HASH_KEY] = util.hash_obj(item, HASH_KEY, RESERVED_KEY, USED_KEY)
435
436 def add(self, more):
437 if more is self:
438 raise RuntimeError('adding a list of resources to itself?')
439 config.add(self, copy.deepcopy(more))
440
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200441 def mark_reserved_by(self, origin_id):
442 for key, item_list in self.items():
443 for item in item_list:
444 item[RESERVED_KEY] = origin_id
445
446
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200447class NotSolvable(Exception):
448 pass
449
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200450def solve(all_matches):
451 '''
452 all_matches shall be a list of index-lists.
453 all_matches[i] is the list of indexes that item i can use.
454 Return a solution so that each i gets a different index.
455 solve([ [0, 1, 2],
456 [0],
457 [0, 2] ]) == [1, 0, 2]
458 '''
459
460 def all_differ(l):
461 return len(set(l)) == len(l)
462
463 def search_in_permutations(fixed=[]):
464 idx = len(fixed)
465 for i in range(len(all_matches[idx])):
466 val = all_matches[idx][i]
467 # don't add a val that's already in the list
468 if val in fixed:
469 continue
470 l = list(fixed)
471 l.append(val)
472 if len(l) == len(all_matches):
473 # found a solution
474 return l
475 # not at the end yet, add next digit
476 r = search_in_permutations(l)
477 if r:
478 # nested search_in_permutations() call found a solution
479 return r
480 # this entire branch yielded no solution
481 return None
482
483 if not all_matches:
484 raise RuntimeError('Cannot solve: no candidates')
485
486 solution = search_in_permutations()
487 if not solution:
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200488 raise NotSolvable('The requested resource requirements are not solvable %r'
489 % all_matches)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200490 return solution
491
492
493def contains_hash(list_of_dicts, a_hash):
494 for d in list_of_dicts:
495 if d.get(HASH_KEY) == a_hash:
496 return True
497 return False
498
499def item_matches(item, wanted_item, ignore_keys=None):
500 if is_dict(wanted_item):
501 # match up two dicts
502 if not isinstance(item, dict):
503 return False
504 for key, wanted_val in wanted_item.items():
505 if ignore_keys and key in ignore_keys:
506 continue
507 if not item_matches(item.get(key), wanted_val, ignore_keys=ignore_keys):
508 return False
509 return True
510
511 if is_list(wanted_item):
Pau Espin Pedrol4e36f7c2017-08-28 13:29:28 +0200512 if not is_list(item):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200513 return False
Pau Espin Pedrol58475512017-09-14 15:33:15 +0200514 # Validate that all elements in both lists are of the same type:
515 t = util.list_validate_same_elem_type(wanted_item + item)
516 if t is None:
517 return True # both lists are empty, return
518 # For lists of complex objects, we expect them to be sorted lists:
519 if t in (dict, list, tuple):
520 for i in range(max(len(wanted_item), len(item))):
521 log.ctx(idx=i)
522 subitem = item[i] if i < len(item) else util.empty_instance_type(t)
523 wanted_subitem = wanted_item[i] if i < len(wanted_item) else util.empty_instance_type(t)
524 if not item_matches(subitem, wanted_subitem, ignore_keys=ignore_keys):
525 return False
526 else: # for lists of basic elements, we handle them as unsorted sets:
527 for val in wanted_item:
528 if val not in item:
529 return False
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200530 return True
531
532 return item == wanted_item
533
534
535class ReservedResources(log.Origin):
536 '''
537 After all resources have been figured out, this is the API that a test case
538 gets to interact with resources. From those resources that have been
539 reserved for it, it can pick some to mark them as currently in use.
540 Functions like nitb() provide a resource by automatically picking its
541 dependencies from so far unused (but reserved) resource.
542 '''
543
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200544 def __init__(self, resources_pool, origin, reserved, modifiers):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200545 self.resources_pool = resources_pool
546 self.origin = origin
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200547 self.reserved_original = reserved
548 self.reserved = copy.deepcopy(self.reserved_original)
549 config.overlay(self.reserved, modifiers)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200550
551 def __repr__(self):
552 return 'resources(%s)=%s' % (self.origin.name(), pprint.pformat(self.reserved))
553
554 def get(self, kind, specifics=None):
555 if specifics is None:
556 specifics = {}
557 self.dbg('requesting use of', kind, specifics=specifics)
558 want = { kind: [specifics] }
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200559 available_dict = self.reserved.find(self.origin, want, skip_if_marked=USED_KEY,
560 do_copy=False, raise_if_missing=False,
561 log_label='Using')
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200562 available = available_dict.get(kind)
563 self.dbg(available=len(available))
564 if not available:
Neels Hofmeyrf9e86932017-06-06 19:47:40 +0200565 # cook up a detailed error message for the current situation
566 kind_reserved = self.reserved.get(kind, [])
567 used_count = len([r for r in kind_reserved if USED_KEY in r])
568 matching = self.reserved.find(self.origin, want, raise_if_missing=False, log_label=None).get(kind, [])
569 if not matching:
570 msg = 'none of the reserved resources matches requirements %r' % specifics
571 elif not (used_count < len(kind_reserved)):
572 msg = 'suite.conf reserved only %d x %r.' % (len(kind_reserved), kind)
573 else:
574 msg = ('No unused resource left that matches the requirements;'
575 ' Of reserved %d x %r, %d match the requirements, but all are already in use;'
576 ' Requirements: %r'
577 % (len(kind_reserved), kind, len(matching), specifics))
578 raise NoResourceExn('When trying to use instance nr %d of %r: %s' % (used_count + 1, kind, msg))
579
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200580 pick = available[0]
581 self.dbg(using=pick)
582 assert not pick.get(USED_KEY)
583 pick[USED_KEY] = True
584 return copy.deepcopy(pick)
585
586 def put(self, item):
587 if not item.get(USED_KEY):
588 raise RuntimeError('Can only put() a resource that is used: %r' % item)
589 hash_to_put = item.get(HASH_KEY)
590 if not hash_to_put:
591 raise RuntimeError('Can only put() a resource that has a hash marker: %r' % item)
592 for key, item_list in self.reserved.items():
593 my_list = self.get(key)
594 for my_item in my_list:
595 if hash_to_put == my_item.get(HASH_KEY):
596 my_item.pop(USED_KEY)
597
598 def put_all(self):
Pau Espin Pedrol1dd29552017-06-13 18:07:57 +0200599 if not self.reserved:
600 return
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200601 for key, item_list in self.reserved.items():
Pau Espin Pedrol1dd29552017-06-13 18:07:57 +0200602 for item in item_list:
603 item.pop(USED_KEY, None)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200604
605 def free(self):
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200606 if self.reserved_original:
607 self.resources_pool.free(self.origin, self.reserved_original)
608 self.reserved_original = None
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200609
Neels Hofmeyr2d1d5612017-05-22 20:02:41 +0200610 def counts(self):
611 counts = {}
612 for key in self.reserved.keys():
613 counts[key] = self.count(key)
614 return counts
615
616 def count(self, key):
617 return len(self.reserved.get(key) or [])
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200618
619# vim: expandtab tabstop=4 shiftwidth=4