blob: 0804591c2147d8e54de25da54b916c9fc89932c4 [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 +020040RESOURCES_CONF = 'resources.conf'
Neels Hofmeyr3531a192017-03-28 14:30:28 +020041RESERVED_RESOURCES_FILE = 'reserved_resources.state'
42
Neels Hofmeyr76d81032017-05-18 18:35:32 +020043R_IP_ADDRESS = 'ip_address'
Pau Espin Pedrol116a2c42020-02-11 17:41:13 +010044R_RUN_NODE = 'run_node'
Neels Hofmeyr3531a192017-03-28 14:30:28 +020045R_BTS = 'bts'
46R_ARFCN = 'arfcn'
47R_MODEM = 'modem'
Pau Espin Pedrolbc1ed882018-05-17 16:59:58 +020048R_OSMOCON = 'osmocon_phone'
Pau Espin Pedrolc8b0f932020-02-11 17:45:26 +010049R_ENB = 'enb'
50R_ALL = (R_IP_ADDRESS, R_RUN_NODE, R_BTS, R_ARFCN, R_MODEM, R_OSMOCON, R_ENB)
Neels Hofmeyr3531a192017-03-28 14:30:28 +020051
52RESOURCES_SCHEMA = {
Neels Hofmeyr76d81032017-05-18 18:35:32 +020053 'ip_address[].addr': schema.IPV4,
Pau Espin Pedrol116a2c42020-02-11 17:41:13 +010054 'run_node[].run_type': schema.STR,
55 'run_node[].run_addr': schema.IPV4,
56 'run_node[].ssh_user': schema.STR,
57 'run_node[].ssh_addr': schema.IPV4,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020058 'bts[].label': schema.STR,
59 'bts[].type': schema.STR,
Pau Espin Pedrolfa9a6d32017-09-12 15:13:21 +020060 'bts[].ipa_unit_id': schema.UINT,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020061 'bts[].addr': schema.IPV4,
62 'bts[].band': schema.BAND,
Pau Espin Pedrolce35d912017-11-23 11:01:24 +010063 'bts[].direct_pcu': schema.BOOL_STR,
Pau Espin Pedrol57497a62017-08-28 14:21:15 +020064 'bts[].ciphers[]': schema.CIPHER,
Pau Espin Pedrol722e94e2018-08-22 11:01:32 +020065 'bts[].channel_allocator': schema.CHAN_ALLOCATOR,
Pau Espin Pedrol4f23ab52018-10-29 11:30:00 +010066 'bts[].gprs_mode': schema.GPRS_MODE,
Pau Espin Pedrol39df7f42018-05-07 13:49:33 +020067 'bts[].num_trx': schema.UINT,
68 'bts[].max_trx': schema.UINT,
Pau Espin Pedrolf6a07122018-07-27 16:24:29 +020069 'bts[].trx_list[].addr': schema.IPV4,
Your Name44af3412017-04-13 03:11:59 +020070 'bts[].trx_list[].hw_addr': schema.HWADDR,
71 'bts[].trx_list[].net_device': schema.STR,
Pau Espin Pedrolb26f32a2017-09-14 13:52:28 +020072 'bts[].trx_list[].nominal_power': schema.UINT,
73 'bts[].trx_list[].max_power_red': schema.UINT,
Pau Espin Pedrolc9b63762018-05-07 01:57:01 +020074 'bts[].trx_list[].timeslot_list[].phys_chan_config': schema.PHY_CHAN,
Pau Espin Pedrolf6a07122018-07-27 16:24:29 +020075 'bts[].trx_list[].power_supply.type': schema.STR,
76 'bts[].trx_list[].power_supply.device': schema.STR,
77 'bts[].trx_list[].power_supply.port': schema.STR,
Pau Espin Pedrol0d455042018-08-27 17:07:41 +020078 'bts[].osmo_trx.launch_trx': schema.BOOL_STR,
79 'bts[].osmo_trx.type': schema.STR,
80 'bts[].osmo_trx.clock_reference': schema.OSMO_TRX_CLOCK_REF,
81 'bts[].osmo_trx.trx_ip': schema.IPV4,
Pau Espin Pedrola9006df2018-10-01 12:26:39 +020082 'bts[].osmo_trx.remote_user': schema.STR,
Pau Espin Pedrol8cfa10f2018-11-06 12:05:19 +010083 'bts[].osmo_trx.dev_args': schema.STR,
Pau Espin Pedrol94eab262018-09-17 20:25:55 +020084 'bts[].osmo_trx.multi_arfcn': schema.BOOL_STR,
Pau Espin Pedrol0cde25f2019-07-24 19:55:08 +020085 'bts[].osmo_trx.max_trxd_version': schema.UINT,
Pau Espin Pedrolc18c5b82019-11-26 14:24:24 +010086 'bts[].osmo_trx.channels[].rx_path': schema.STR,
87 'bts[].osmo_trx.channels[].tx_path': schema.STR,
Pau Espin Pedrolc8b0f932020-02-11 17:45:26 +010088 'enb[].label': schema.STR,
89 'enb[].type': schema.STR,
90 'enb[].remote_user': schema.STR,
91 'enb[].addr': schema.IPV4,
92 'enb[].band': schema.BAND,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020093 'arfcn[].arfcn': schema.INT,
94 'arfcn[].band': schema.BAND,
Holger Hans Peter Freyther93a89422019-02-27 06:36:36 +000095 'modem[].type': schema.STR,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020096 'modem[].label': schema.STR,
97 'modem[].path': schema.STR,
98 'modem[].imsi': schema.IMSI,
99 'modem[].ki': schema.KI,
Pau Espin Pedrol713ce2c2017-08-24 16:57:17 +0200100 'modem[].auth_algo': schema.AUTH_ALGO,
Pau Espin Pedrolc8b0f932020-02-11 17:45:26 +0100101 'modem[].remote_user': schema.STR,
102 'modem[].addr': schema.IPV4,
Pau Espin Pedrol57497a62017-08-28 14:21:15 +0200103 'modem[].ciphers[]': schema.CIPHER,
Pau Espin Pedrolac18fd32017-08-31 18:49:47 +0200104 'modem[].features[]': schema.MODEM_FEATURE,
Pau Espin Pedrolbc1ed882018-05-17 16:59:58 +0200105 'osmocon_phone[].serial_device': schema.STR,
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200106 }
107
108WANT_SCHEMA = util.dict_add(
Pau Espin Pedrolead79ac2017-09-12 15:19:18 +0200109 dict([('%s[].times' % r, schema.TIMES) for r in R_ALL]),
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200110 RESOURCES_SCHEMA)
111
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200112CONF_SCHEMA = util.dict_add(
Pau Espin Pedrol5dc24592018-08-27 12:53:41 +0200113 { 'defaults.timeout': schema.STR,
114 'config.bsc.net.codec_list[]': schema.CODEC },
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200115 dict([('resources.%s' % key, val) for key, val in WANT_SCHEMA.items()]),
116 dict([('modifiers.%s' % key, val) for key, val in WANT_SCHEMA.items()]))
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200117
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200118KNOWN_BTS_TYPES = {
Your Name44af3412017-04-13 03:11:59 +0200119 'osmo-bts-sysmo': bts_sysmo.SysmoBts,
120 'osmo-bts-trx': bts_osmotrx.OsmoBtsTrx,
Pau Espin Pedrolfeb66e72019-03-04 18:37:04 +0100121 'osmo-bts-oc2g': bts_oc2g.OsmoBtsOC2G,
Pau Espin Pedroldaed4472017-09-15 14:11:35 +0200122 'osmo-bts-octphy': bts_octphy.OsmoBtsOctphy,
Holger Hans Peter Freytherd2db10d2018-12-06 18:34:53 +0000123 'osmo-bts-virtual': bts_osmovirtual.OsmoBtsVirtual,
Pau Espin Pedrol1b28a582018-03-08 21:01:26 +0100124 'nanobts': bts_nanobts.NanoBts,
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200125 }
126
Pau Espin Pedrolc8b0f932020-02-11 17:45:26 +0100127KNOWN_ENB_TYPES = {
128 'srsenb': srs_enb.srsENB,
129}
Holger Hans Peter Freyther74243012019-02-27 08:18:38 +0000130
131KNOWN_MS_TYPES = {
132 # Map None to ofono for forward compability
133 None: modem.Modem,
134 'ofono': modem.Modem,
Holger Hans Peter Freyther10501cc2019-02-25 03:20:16 +0000135 'osmo-mobile': ms_osmo_mobile.MSOsmoMobile,
Pau Espin Pedrolc8b0f932020-02-11 17:45:26 +0100136 'srsue': srs_ue.srsUE,
Holger Hans Peter Freyther74243012019-02-27 08:18:38 +0000137}
138
139
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200140def register_bts_type(name, clazz):
141 KNOWN_BTS_TYPES[name] = clazz
142
143class ResourcesPool(log.Origin):
144 _remember_to_free = None
145 _registered_exit_handler = False
146
147 def __init__(self):
148 self.config_path = config.get_config_file(RESOURCES_CONF)
149 self.state_dir = config.get_state_dir()
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200150 super().__init__(log.C_CNF, conf=self.config_path, state=self.state_dir.path)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200151 self.read_conf()
152
153 def read_conf(self):
154 self.all_resources = Resources(config.read(self.config_path, RESOURCES_SCHEMA))
155 self.all_resources.set_hashes()
156
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200157 def reserve(self, origin, want, modifiers):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200158 '''
159 attempt to reserve the resources specified in the dict 'want' for
160 'origin'. Obtain a lock on the resources lock dir, verify that all
161 wanted resources are available, and if yes mark them as reserved.
162
163 On success, return a reservation object which can be used to release
164 the reservation. The reservation will be freed automatically on program
165 exit, if not yet done manually.
166
167 'origin' should be an Origin() instance.
168
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200169 'want' is a dict matching RESOURCES_SCHEMA, used to specify what to
170 reserve.
171
172 'modifiers' is a dict matching RESOURCES_SCHEMA, it is overlaid on top
173 of 'want'.
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200174
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200175 If an entry has no attribute set, any of the resources may be
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200176 reserved without further limitations.
177
178 ResourcesPool may also be selected with narrowed down constraints.
Neels Hofmeyr76d81032017-05-18 18:35:32 +0200179 This would reserve one IP address, two modems, one BTS of type
Neels Hofmeyr391afe32017-05-18 19:22:12 +0200180 sysmo and one of type trx, plus 2 ARFCNs in the 1800 band:
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200181
182 {
Neels Hofmeyr76d81032017-05-18 18:35:32 +0200183 'ip_address': [ {} ],
Neels Hofmeyr391afe32017-05-18 19:22:12 +0200184 'bts': [ { 'type': 'sysmo' }, { 'type': 'trx' } ],
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200185 'arfcn': [ { 'band': 'GSM-1800' }, { 'band': 'GSM-1800' } ],
186 'modem': [ {}, {} ],
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200187 }
188 '''
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200189 schema.validate(want, RESOURCES_SCHEMA)
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200190 schema.validate(modifiers, RESOURCES_SCHEMA)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200191
192 origin_id = origin.origin_id()
193
194 with self.state_dir.lock(origin_id):
195 rrfile_path = self.state_dir.mk_parentdir(RESERVED_RESOURCES_FILE)
196 reserved = Resources(config.read(rrfile_path, if_missing_return={}))
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200197 to_be_reserved = self.all_resources.without(reserved).find(origin, want)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200198
199 to_be_reserved.mark_reserved_by(origin_id)
200
201 reserved.add(to_be_reserved)
202 config.write(rrfile_path, reserved)
203
204 self.remember_to_free(to_be_reserved)
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200205 return ReservedResources(self, origin, to_be_reserved, modifiers)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200206
207 def free(self, origin, to_be_freed):
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200208 log.ctx(origin)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200209 with self.state_dir.lock(origin.origin_id()):
210 rrfile_path = self.state_dir.mk_parentdir(RESERVED_RESOURCES_FILE)
211 reserved = Resources(config.read(rrfile_path, if_missing_return={}))
212 reserved.drop(to_be_freed)
213 config.write(rrfile_path, reserved)
214 self.forget_freed(to_be_freed)
215
216 def register_exit_handler(self):
217 if self._registered_exit_handler:
218 return
219 atexit.register(self.clean_up_registered_resources)
220 self._registered_exit_handler = True
221
222 def unregister_exit_handler(self):
223 if not self._registered_exit_handler:
224 return
225 atexit.unregister(self.clean_up_registered_resources)
226 self._registered_exit_handler = False
227
228 def clean_up_registered_resources(self):
229 if not self._remember_to_free:
230 return
231 self.free(log.Origin('atexit.clean_up_registered_resources()'),
232 self._remember_to_free)
233
234 def remember_to_free(self, to_be_reserved):
235 self.register_exit_handler()
236 if not self._remember_to_free:
237 self._remember_to_free = Resources()
238 self._remember_to_free.add(to_be_reserved)
239
240 def forget_freed(self, freed):
241 if freed is self._remember_to_free:
242 self._remember_to_free.clear()
243 else:
244 self._remember_to_free.drop(freed)
245 if not self._remember_to_free:
246 self.unregister_exit_handler()
247
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100248 def next_persistent_value(self, token, first_val, validate_func, inc_func, origin):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200249 origin_id = origin.origin_id()
250
251 with self.state_dir.lock(origin_id):
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100252 token_path = self.state_dir.child('last_used_%s.state' % token)
253 log.ctx(token_path)
254 last_value = first_val
255 if os.path.exists(token_path):
256 if not os.path.isfile(token_path):
257 raise RuntimeError('path should be a file but is not: %r' % token_path)
258 with open(token_path, 'r') as f:
259 last_value = f.read().strip()
260 validate_func(last_value)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200261
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100262 next_value = inc_func(last_value)
263 with open(token_path, 'w') as f:
264 f.write(next_value)
265 return next_value
266
267 def next_msisdn(self, origin):
268 return self.next_persistent_value('msisdn', '1000', schema.msisdn, util.msisdn_inc, origin)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200269
Pau Espin Pedrol5e0c2512017-11-06 18:40:23 +0100270 def next_lac(self, origin):
Pau Espin Pedrolf7f06362017-11-28 12:56:35 +0100271 # LAC=0 has special meaning (MS detached), avoid it
272 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 +0200273
Pau Espin Pedrol8a3a7b52017-11-28 15:50:02 +0100274 def next_rac(self, origin):
275 return self.next_persistent_value('rac', '1', schema.uint8, lambda x: str((int(x)+1) % pow(2,8) or 1), origin)
276
Pau Espin Pedrol4ccce7c2017-11-07 11:13:20 +0100277 def next_cellid(self, origin):
278 return self.next_persistent_value('cellid', '1', schema.uint16, lambda x: str((int(x)+1) % pow(2,16)), origin)
279
Pau Espin Pedrol8a3a7b52017-11-28 15:50:02 +0100280 def next_bvci(self, origin):
281 # BVCI=0 and =1 are reserved, avoid them.
282 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)
283
Pau Espin Pedrol4676cbd2017-09-14 17:35:03 +0200284class NoResourceExn(log.Error):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200285 pass
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200286
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200287class Resources(dict):
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200288
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200289 def __init__(self, all_resources={}, do_copy=True):
290 if do_copy:
291 all_resources = copy.deepcopy(all_resources)
292 self.update(all_resources)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200293
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200294 def drop(self, reserved, fail_if_not_found=True):
295 # protect from modifying reserved because we're the same object
296 if reserved is self:
297 raise RuntimeError('Refusing to drop a list of resources from itself.'
298 ' This is probably a bug where a list of Resources()'
299 ' should have been copied but is passed as-is.'
300 ' use Resources.clear() instead.')
301
302 for key, reserved_list in reserved.items():
303 my_list = self.get(key) or []
304
305 if my_list is reserved_list:
306 self.pop(key)
307 continue
308
309 for reserved_item in reserved_list:
310 found = False
311 reserved_hash = reserved_item.get(HASH_KEY)
312 if not reserved_hash:
313 raise RuntimeError('Resources.drop() only works with hashed items')
314
315 for i in range(len(my_list)):
316 my_item = my_list[i]
317 my_hash = my_item.get(HASH_KEY)
318 if not my_hash:
319 raise RuntimeError('Resources.drop() only works with hashed items')
320 if my_hash == reserved_hash:
321 found = True
322 my_list.pop(i)
323 break
324
325 if fail_if_not_found and not found:
326 raise RuntimeError('Asked to drop resource from a pool, but the'
327 ' resource was not found: %s = %r' % (key, reserved_item))
328
329 if not my_list:
330 self.pop(key)
331 return self
332
333 def without(self, reserved):
334 return Resources(self).drop(reserved)
335
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200336 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 +0200337 '''
338 Pass a dict of resource requirements, e.g.:
339 want = {
340 'bts': [ {'type': 'osmo-bts-sysmo',}, {} ],
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200341 'modem': [ {}, {}, {} ]
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200342 }
343 This function tries to find a combination from the available resources that
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200344 matches these requirements. The return value is a dict (wrapped in a Resources class)
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200345 that contains the matching resources in the order of 'want' dict: in above
346 example, the returned dict would have a 'bts' list with the first item being
347 a sysmoBTS, the second item being any other available BTS.
348
349 If skip_if_marked is passed, any resource that contains this key is skipped.
350 E.g. if a BTS has the USED_KEY set like
351 reserved_resources = { 'bts' : {..., '_used': True} }
352 then this may be skipped by passing skip_if_marked='_used'
353 (or rather skip_if_marked=USED_KEY).
354
355 If do_copy is True, the returned dict is a deep copy and does not share
356 lists with any other Resources dict.
357
358 If raise_if_missing is False, this will return an empty item for any
359 resource that had no match, instead of immediately raising an exception.
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200360
361 This function expects input dictionaries whose contents have already
362 been replicated based on its the 'times' attributes. See
363 config.replicate_times() for more details.
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200364 '''
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200365 matches = {}
Neels Hofmeyr17c139e2017-04-12 02:42:02 +0200366 for key, want_list in sorted(want.items()): # sorted for deterministic test results
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200367 # here we have a resource of a given type, e.g. 'bts', with a list
368 # containing as many BTSes as the caller wants to reserve/use. Each
369 # list item contains specifics for the particular BTS.
Neels Hofmeyr2a1a1fa2017-05-29 01:36:21 +0200370 my_list = self.get(key, [])
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200371
Neels Hofmeyrf9e86932017-06-06 19:47:40 +0200372 if log_label:
373 for_origin.log(log_label, len(want_list), 'x', key, '(candidates: %d)'%len(my_list))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200374
375 # Try to avoid a less constrained item snatching away a resource
376 # from a more detailed constrained requirement.
377
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200378 # first record all matches, so that each requested item has a list
379 # of all available resources that match it. Some resources may
380 # appear for multiple requested items. Store matching indexes.
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200381 all_matches = []
382 for want_item in want_list:
383 item_match_list = []
384 for i in range(len(my_list)):
385 my_item = my_list[i]
386 if skip_if_marked and my_item.get(skip_if_marked):
387 continue
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200388 if item_matches(my_item, want_item):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200389 item_match_list.append(i)
390 if not item_match_list:
Neels Hofmeyr9b907702017-05-06 23:20:33 +0200391 if raise_if_missing:
392 raise NoResourceExn('No matching resource available for %s = %r'
393 % (key, want_item))
394 else:
395 # this one failed... see below
396 all_matches = []
397 break
398
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200399 all_matches.append( item_match_list )
400
401 if not all_matches:
Neels Hofmeyr9b907702017-05-06 23:20:33 +0200402 # ...this one failed. Makes no sense to solve resource
403 # allocations, return an empty list for this key to mark
404 # failure.
405 matches[key] = []
406 continue
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200407
408 # figure out who gets what
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200409 try:
410 solution = solve(all_matches)
411 except NotSolvable:
412 # instead of a cryptic error message, raise an exception that
413 # conveys meaning to the user.
414 raise NoResourceExn('Could not resolve request to reserve resources: '
415 '%d x %s with requirements: %r' % (len(want_list), key, want_list))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200416 picked = [ my_list[i] for i in solution if i is not None ]
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200417 for_origin.dbg('Picked', config.tostr(picked))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200418 matches[key] = picked
419
420 return Resources(matches, do_copy=do_copy)
421
422 def set_hashes(self):
423 for key, item_list in self.items():
424 for item in item_list:
425 item[HASH_KEY] = util.hash_obj(item, HASH_KEY, RESERVED_KEY, USED_KEY)
426
427 def add(self, more):
428 if more is self:
429 raise RuntimeError('adding a list of resources to itself?')
430 config.add(self, copy.deepcopy(more))
431
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200432 def mark_reserved_by(self, origin_id):
433 for key, item_list in self.items():
434 for item in item_list:
435 item[RESERVED_KEY] = origin_id
436
437
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200438class NotSolvable(Exception):
439 pass
440
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200441def solve(all_matches):
442 '''
443 all_matches shall be a list of index-lists.
444 all_matches[i] is the list of indexes that item i can use.
445 Return a solution so that each i gets a different index.
446 solve([ [0, 1, 2],
447 [0],
448 [0, 2] ]) == [1, 0, 2]
449 '''
450
451 def all_differ(l):
452 return len(set(l)) == len(l)
453
454 def search_in_permutations(fixed=[]):
455 idx = len(fixed)
456 for i in range(len(all_matches[idx])):
457 val = all_matches[idx][i]
458 # don't add a val that's already in the list
459 if val in fixed:
460 continue
461 l = list(fixed)
462 l.append(val)
463 if len(l) == len(all_matches):
464 # found a solution
465 return l
466 # not at the end yet, add next digit
467 r = search_in_permutations(l)
468 if r:
469 # nested search_in_permutations() call found a solution
470 return r
471 # this entire branch yielded no solution
472 return None
473
474 if not all_matches:
475 raise RuntimeError('Cannot solve: no candidates')
476
477 solution = search_in_permutations()
478 if not solution:
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200479 raise NotSolvable('The requested resource requirements are not solvable %r'
480 % all_matches)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200481 return solution
482
483
484def contains_hash(list_of_dicts, a_hash):
485 for d in list_of_dicts:
486 if d.get(HASH_KEY) == a_hash:
487 return True
488 return False
489
490def item_matches(item, wanted_item, ignore_keys=None):
491 if is_dict(wanted_item):
492 # match up two dicts
493 if not isinstance(item, dict):
494 return False
495 for key, wanted_val in wanted_item.items():
496 if ignore_keys and key in ignore_keys:
497 continue
498 if not item_matches(item.get(key), wanted_val, ignore_keys=ignore_keys):
499 return False
500 return True
501
502 if is_list(wanted_item):
Pau Espin Pedrol4e36f7c2017-08-28 13:29:28 +0200503 if not is_list(item):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200504 return False
Pau Espin Pedrol58475512017-09-14 15:33:15 +0200505 # Validate that all elements in both lists are of the same type:
506 t = util.list_validate_same_elem_type(wanted_item + item)
507 if t is None:
508 return True # both lists are empty, return
509 # For lists of complex objects, we expect them to be sorted lists:
510 if t in (dict, list, tuple):
511 for i in range(max(len(wanted_item), len(item))):
512 log.ctx(idx=i)
513 subitem = item[i] if i < len(item) else util.empty_instance_type(t)
514 wanted_subitem = wanted_item[i] if i < len(wanted_item) else util.empty_instance_type(t)
515 if not item_matches(subitem, wanted_subitem, ignore_keys=ignore_keys):
516 return False
517 else: # for lists of basic elements, we handle them as unsorted sets:
518 for val in wanted_item:
519 if val not in item:
520 return False
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200521 return True
522
523 return item == wanted_item
524
525
526class ReservedResources(log.Origin):
527 '''
528 After all resources have been figured out, this is the API that a test case
529 gets to interact with resources. From those resources that have been
530 reserved for it, it can pick some to mark them as currently in use.
531 Functions like nitb() provide a resource by automatically picking its
532 dependencies from so far unused (but reserved) resource.
533 '''
534
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200535 def __init__(self, resources_pool, origin, reserved, modifiers):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200536 self.resources_pool = resources_pool
537 self.origin = origin
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200538 self.reserved_original = reserved
539 self.reserved = copy.deepcopy(self.reserved_original)
540 config.overlay(self.reserved, modifiers)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200541
542 def __repr__(self):
543 return 'resources(%s)=%s' % (self.origin.name(), pprint.pformat(self.reserved))
544
545 def get(self, kind, specifics=None):
546 if specifics is None:
547 specifics = {}
548 self.dbg('requesting use of', kind, specifics=specifics)
549 want = { kind: [specifics] }
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200550 available_dict = self.reserved.find(self.origin, want, skip_if_marked=USED_KEY,
551 do_copy=False, raise_if_missing=False,
552 log_label='Using')
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200553 available = available_dict.get(kind)
554 self.dbg(available=len(available))
555 if not available:
Neels Hofmeyrf9e86932017-06-06 19:47:40 +0200556 # cook up a detailed error message for the current situation
557 kind_reserved = self.reserved.get(kind, [])
558 used_count = len([r for r in kind_reserved if USED_KEY in r])
559 matching = self.reserved.find(self.origin, want, raise_if_missing=False, log_label=None).get(kind, [])
560 if not matching:
561 msg = 'none of the reserved resources matches requirements %r' % specifics
562 elif not (used_count < len(kind_reserved)):
563 msg = 'suite.conf reserved only %d x %r.' % (len(kind_reserved), kind)
564 else:
565 msg = ('No unused resource left that matches the requirements;'
566 ' Of reserved %d x %r, %d match the requirements, but all are already in use;'
567 ' Requirements: %r'
568 % (len(kind_reserved), kind, len(matching), specifics))
569 raise NoResourceExn('When trying to use instance nr %d of %r: %s' % (used_count + 1, kind, msg))
570
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200571 pick = available[0]
572 self.dbg(using=pick)
573 assert not pick.get(USED_KEY)
574 pick[USED_KEY] = True
575 return copy.deepcopy(pick)
576
577 def put(self, item):
578 if not item.get(USED_KEY):
579 raise RuntimeError('Can only put() a resource that is used: %r' % item)
580 hash_to_put = item.get(HASH_KEY)
581 if not hash_to_put:
582 raise RuntimeError('Can only put() a resource that has a hash marker: %r' % item)
583 for key, item_list in self.reserved.items():
584 my_list = self.get(key)
585 for my_item in my_list:
586 if hash_to_put == my_item.get(HASH_KEY):
587 my_item.pop(USED_KEY)
588
589 def put_all(self):
Pau Espin Pedrol1dd29552017-06-13 18:07:57 +0200590 if not self.reserved:
591 return
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200592 for key, item_list in self.reserved.items():
Pau Espin Pedrol1dd29552017-06-13 18:07:57 +0200593 for item in item_list:
594 item.pop(USED_KEY, None)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200595
596 def free(self):
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200597 if self.reserved_original:
598 self.resources_pool.free(self.origin, self.reserved_original)
599 self.reserved_original = None
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200600
Neels Hofmeyr2d1d5612017-05-22 20:02:41 +0200601 def counts(self):
602 counts = {}
603 for key in self.reserved.keys():
604 counts[key] = self.count(key)
605 return counts
606
607 def count(self, key):
608 return len(self.reserved.get(key) or [])
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200609
610# vim: expandtab tabstop=4 shiftwidth=4