blob: e8ca8591f3b2ceee1b30efb349c9bc37e303f104 [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,
Pau Espin Pedrola9a2fe22020-02-13 19:29:55 +010092 'enb[].rf_dev_type': schema.STR,
93 'enb[].rf_dev_args': schema.STR,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020094 'arfcn[].arfcn': schema.INT,
95 'arfcn[].band': schema.BAND,
Holger Hans Peter Freyther93a89422019-02-27 06:36:36 +000096 'modem[].type': schema.STR,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020097 'modem[].label': schema.STR,
98 'modem[].path': schema.STR,
99 'modem[].imsi': schema.IMSI,
100 'modem[].ki': schema.KI,
Pau Espin Pedrol713ce2c2017-08-24 16:57:17 +0200101 'modem[].auth_algo': schema.AUTH_ALGO,
Pau Espin Pedrolc8b0f932020-02-11 17:45:26 +0100102 'modem[].remote_user': schema.STR,
103 'modem[].addr': schema.IPV4,
Pau Espin Pedrol57497a62017-08-28 14:21:15 +0200104 'modem[].ciphers[]': schema.CIPHER,
Pau Espin Pedrolac18fd32017-08-31 18:49:47 +0200105 'modem[].features[]': schema.MODEM_FEATURE,
Pau Espin Pedrola9a2fe22020-02-13 19:29:55 +0100106 'modem[].rf_dev_type': schema.STR,
107 'modem[].rf_dev_args': schema.STR,
Pau Espin Pedrolbc1ed882018-05-17 16:59:58 +0200108 'osmocon_phone[].serial_device': schema.STR,
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200109 }
110
111WANT_SCHEMA = util.dict_add(
Pau Espin Pedrolead79ac2017-09-12 15:19:18 +0200112 dict([('%s[].times' % r, schema.TIMES) for r in R_ALL]),
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200113 RESOURCES_SCHEMA)
114
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200115CONF_SCHEMA = util.dict_add(
Pau Espin Pedrol5dc24592018-08-27 12:53:41 +0200116 { 'defaults.timeout': schema.STR,
117 'config.bsc.net.codec_list[]': schema.CODEC },
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200118 dict([('resources.%s' % key, val) for key, val in WANT_SCHEMA.items()]),
119 dict([('modifiers.%s' % key, val) for key, val in WANT_SCHEMA.items()]))
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200120
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200121KNOWN_BTS_TYPES = {
Your Name44af3412017-04-13 03:11:59 +0200122 'osmo-bts-sysmo': bts_sysmo.SysmoBts,
123 'osmo-bts-trx': bts_osmotrx.OsmoBtsTrx,
Pau Espin Pedrolfeb66e72019-03-04 18:37:04 +0100124 'osmo-bts-oc2g': bts_oc2g.OsmoBtsOC2G,
Pau Espin Pedroldaed4472017-09-15 14:11:35 +0200125 'osmo-bts-octphy': bts_octphy.OsmoBtsOctphy,
Holger Hans Peter Freytherd2db10d2018-12-06 18:34:53 +0000126 'osmo-bts-virtual': bts_osmovirtual.OsmoBtsVirtual,
Pau Espin Pedrol1b28a582018-03-08 21:01:26 +0100127 'nanobts': bts_nanobts.NanoBts,
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200128 }
129
Pau Espin Pedrolc8b0f932020-02-11 17:45:26 +0100130KNOWN_ENB_TYPES = {
131 'srsenb': srs_enb.srsENB,
132}
Holger Hans Peter Freyther74243012019-02-27 08:18:38 +0000133
134KNOWN_MS_TYPES = {
135 # Map None to ofono for forward compability
136 None: modem.Modem,
137 'ofono': modem.Modem,
Holger Hans Peter Freyther10501cc2019-02-25 03:20:16 +0000138 'osmo-mobile': ms_osmo_mobile.MSOsmoMobile,
Pau Espin Pedrolc8b0f932020-02-11 17:45:26 +0100139 'srsue': srs_ue.srsUE,
Holger Hans Peter Freyther74243012019-02-27 08:18:38 +0000140}
141
142
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200143def register_bts_type(name, clazz):
144 KNOWN_BTS_TYPES[name] = clazz
145
146class ResourcesPool(log.Origin):
147 _remember_to_free = None
148 _registered_exit_handler = False
149
150 def __init__(self):
151 self.config_path = config.get_config_file(RESOURCES_CONF)
152 self.state_dir = config.get_state_dir()
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200153 super().__init__(log.C_CNF, conf=self.config_path, state=self.state_dir.path)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200154 self.read_conf()
155
156 def read_conf(self):
157 self.all_resources = Resources(config.read(self.config_path, RESOURCES_SCHEMA))
158 self.all_resources.set_hashes()
159
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200160 def reserve(self, origin, want, modifiers):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200161 '''
162 attempt to reserve the resources specified in the dict 'want' for
163 'origin'. Obtain a lock on the resources lock dir, verify that all
164 wanted resources are available, and if yes mark them as reserved.
165
166 On success, return a reservation object which can be used to release
167 the reservation. The reservation will be freed automatically on program
168 exit, if not yet done manually.
169
170 'origin' should be an Origin() instance.
171
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200172 'want' is a dict matching RESOURCES_SCHEMA, used to specify what to
173 reserve.
174
175 'modifiers' is a dict matching RESOURCES_SCHEMA, it is overlaid on top
176 of 'want'.
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200177
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200178 If an entry has no attribute set, any of the resources may be
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200179 reserved without further limitations.
180
181 ResourcesPool may also be selected with narrowed down constraints.
Neels Hofmeyr76d81032017-05-18 18:35:32 +0200182 This would reserve one IP address, two modems, one BTS of type
Neels Hofmeyr391afe32017-05-18 19:22:12 +0200183 sysmo and one of type trx, plus 2 ARFCNs in the 1800 band:
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200184
185 {
Neels Hofmeyr76d81032017-05-18 18:35:32 +0200186 'ip_address': [ {} ],
Neels Hofmeyr391afe32017-05-18 19:22:12 +0200187 'bts': [ { 'type': 'sysmo' }, { 'type': 'trx' } ],
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200188 'arfcn': [ { 'band': 'GSM-1800' }, { 'band': 'GSM-1800' } ],
189 'modem': [ {}, {} ],
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200190 }
191 '''
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200192 schema.validate(want, RESOURCES_SCHEMA)
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200193 schema.validate(modifiers, RESOURCES_SCHEMA)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200194
195 origin_id = origin.origin_id()
196
197 with self.state_dir.lock(origin_id):
198 rrfile_path = self.state_dir.mk_parentdir(RESERVED_RESOURCES_FILE)
199 reserved = Resources(config.read(rrfile_path, if_missing_return={}))
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200200 to_be_reserved = self.all_resources.without(reserved).find(origin, want)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200201
202 to_be_reserved.mark_reserved_by(origin_id)
203
204 reserved.add(to_be_reserved)
205 config.write(rrfile_path, reserved)
206
207 self.remember_to_free(to_be_reserved)
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200208 return ReservedResources(self, origin, to_be_reserved, modifiers)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200209
210 def free(self, origin, to_be_freed):
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200211 log.ctx(origin)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200212 with self.state_dir.lock(origin.origin_id()):
213 rrfile_path = self.state_dir.mk_parentdir(RESERVED_RESOURCES_FILE)
214 reserved = Resources(config.read(rrfile_path, if_missing_return={}))
215 reserved.drop(to_be_freed)
216 config.write(rrfile_path, reserved)
217 self.forget_freed(to_be_freed)
218
219 def register_exit_handler(self):
220 if self._registered_exit_handler:
221 return
222 atexit.register(self.clean_up_registered_resources)
223 self._registered_exit_handler = True
224
225 def unregister_exit_handler(self):
226 if not self._registered_exit_handler:
227 return
228 atexit.unregister(self.clean_up_registered_resources)
229 self._registered_exit_handler = False
230
231 def clean_up_registered_resources(self):
232 if not self._remember_to_free:
233 return
234 self.free(log.Origin('atexit.clean_up_registered_resources()'),
235 self._remember_to_free)
236
237 def remember_to_free(self, to_be_reserved):
238 self.register_exit_handler()
239 if not self._remember_to_free:
240 self._remember_to_free = Resources()
241 self._remember_to_free.add(to_be_reserved)
242
243 def forget_freed(self, freed):
244 if freed is self._remember_to_free:
245 self._remember_to_free.clear()
246 else:
247 self._remember_to_free.drop(freed)
248 if not self._remember_to_free:
249 self.unregister_exit_handler()
250
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100251 def next_persistent_value(self, token, first_val, validate_func, inc_func, origin):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200252 origin_id = origin.origin_id()
253
254 with self.state_dir.lock(origin_id):
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100255 token_path = self.state_dir.child('last_used_%s.state' % token)
256 log.ctx(token_path)
257 last_value = first_val
258 if os.path.exists(token_path):
259 if not os.path.isfile(token_path):
260 raise RuntimeError('path should be a file but is not: %r' % token_path)
261 with open(token_path, 'r') as f:
262 last_value = f.read().strip()
263 validate_func(last_value)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200264
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100265 next_value = inc_func(last_value)
266 with open(token_path, 'w') as f:
267 f.write(next_value)
268 return next_value
269
270 def next_msisdn(self, origin):
271 return self.next_persistent_value('msisdn', '1000', schema.msisdn, util.msisdn_inc, origin)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200272
Pau Espin Pedrol5e0c2512017-11-06 18:40:23 +0100273 def next_lac(self, origin):
Pau Espin Pedrolf7f06362017-11-28 12:56:35 +0100274 # LAC=0 has special meaning (MS detached), avoid it
275 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 +0200276
Pau Espin Pedrol8a3a7b52017-11-28 15:50:02 +0100277 def next_rac(self, origin):
278 return self.next_persistent_value('rac', '1', schema.uint8, lambda x: str((int(x)+1) % pow(2,8) or 1), origin)
279
Pau Espin Pedrol4ccce7c2017-11-07 11:13:20 +0100280 def next_cellid(self, origin):
281 return self.next_persistent_value('cellid', '1', schema.uint16, lambda x: str((int(x)+1) % pow(2,16)), origin)
282
Pau Espin Pedrol8a3a7b52017-11-28 15:50:02 +0100283 def next_bvci(self, origin):
284 # BVCI=0 and =1 are reserved, avoid them.
285 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)
286
Pau Espin Pedrol4676cbd2017-09-14 17:35:03 +0200287class NoResourceExn(log.Error):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200288 pass
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200289
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200290class Resources(dict):
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200291
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200292 def __init__(self, all_resources={}, do_copy=True):
293 if do_copy:
294 all_resources = copy.deepcopy(all_resources)
295 self.update(all_resources)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200296
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200297 def drop(self, reserved, fail_if_not_found=True):
298 # protect from modifying reserved because we're the same object
299 if reserved is self:
300 raise RuntimeError('Refusing to drop a list of resources from itself.'
301 ' This is probably a bug where a list of Resources()'
302 ' should have been copied but is passed as-is.'
303 ' use Resources.clear() instead.')
304
305 for key, reserved_list in reserved.items():
306 my_list = self.get(key) or []
307
308 if my_list is reserved_list:
309 self.pop(key)
310 continue
311
312 for reserved_item in reserved_list:
313 found = False
314 reserved_hash = reserved_item.get(HASH_KEY)
315 if not reserved_hash:
316 raise RuntimeError('Resources.drop() only works with hashed items')
317
318 for i in range(len(my_list)):
319 my_item = my_list[i]
320 my_hash = my_item.get(HASH_KEY)
321 if not my_hash:
322 raise RuntimeError('Resources.drop() only works with hashed items')
323 if my_hash == reserved_hash:
324 found = True
325 my_list.pop(i)
326 break
327
328 if fail_if_not_found and not found:
329 raise RuntimeError('Asked to drop resource from a pool, but the'
330 ' resource was not found: %s = %r' % (key, reserved_item))
331
332 if not my_list:
333 self.pop(key)
334 return self
335
336 def without(self, reserved):
337 return Resources(self).drop(reserved)
338
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200339 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 +0200340 '''
341 Pass a dict of resource requirements, e.g.:
342 want = {
343 'bts': [ {'type': 'osmo-bts-sysmo',}, {} ],
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200344 'modem': [ {}, {}, {} ]
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200345 }
346 This function tries to find a combination from the available resources that
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200347 matches these requirements. The return value is a dict (wrapped in a Resources class)
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200348 that contains the matching resources in the order of 'want' dict: in above
349 example, the returned dict would have a 'bts' list with the first item being
350 a sysmoBTS, the second item being any other available BTS.
351
352 If skip_if_marked is passed, any resource that contains this key is skipped.
353 E.g. if a BTS has the USED_KEY set like
354 reserved_resources = { 'bts' : {..., '_used': True} }
355 then this may be skipped by passing skip_if_marked='_used'
356 (or rather skip_if_marked=USED_KEY).
357
358 If do_copy is True, the returned dict is a deep copy and does not share
359 lists with any other Resources dict.
360
361 If raise_if_missing is False, this will return an empty item for any
362 resource that had no match, instead of immediately raising an exception.
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200363
364 This function expects input dictionaries whose contents have already
365 been replicated based on its the 'times' attributes. See
366 config.replicate_times() for more details.
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200367 '''
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200368 matches = {}
Neels Hofmeyr17c139e2017-04-12 02:42:02 +0200369 for key, want_list in sorted(want.items()): # sorted for deterministic test results
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200370 # here we have a resource of a given type, e.g. 'bts', with a list
371 # containing as many BTSes as the caller wants to reserve/use. Each
372 # list item contains specifics for the particular BTS.
Neels Hofmeyr2a1a1fa2017-05-29 01:36:21 +0200373 my_list = self.get(key, [])
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200374
Neels Hofmeyrf9e86932017-06-06 19:47:40 +0200375 if log_label:
376 for_origin.log(log_label, len(want_list), 'x', key, '(candidates: %d)'%len(my_list))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200377
378 # Try to avoid a less constrained item snatching away a resource
379 # from a more detailed constrained requirement.
380
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200381 # first record all matches, so that each requested item has a list
382 # of all available resources that match it. Some resources may
383 # appear for multiple requested items. Store matching indexes.
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200384 all_matches = []
385 for want_item in want_list:
386 item_match_list = []
387 for i in range(len(my_list)):
388 my_item = my_list[i]
389 if skip_if_marked and my_item.get(skip_if_marked):
390 continue
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200391 if item_matches(my_item, want_item):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200392 item_match_list.append(i)
393 if not item_match_list:
Neels Hofmeyr9b907702017-05-06 23:20:33 +0200394 if raise_if_missing:
395 raise NoResourceExn('No matching resource available for %s = %r'
396 % (key, want_item))
397 else:
398 # this one failed... see below
399 all_matches = []
400 break
401
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200402 all_matches.append( item_match_list )
403
404 if not all_matches:
Neels Hofmeyr9b907702017-05-06 23:20:33 +0200405 # ...this one failed. Makes no sense to solve resource
406 # allocations, return an empty list for this key to mark
407 # failure.
408 matches[key] = []
409 continue
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200410
411 # figure out who gets what
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200412 try:
413 solution = solve(all_matches)
414 except NotSolvable:
415 # instead of a cryptic error message, raise an exception that
416 # conveys meaning to the user.
417 raise NoResourceExn('Could not resolve request to reserve resources: '
418 '%d x %s with requirements: %r' % (len(want_list), key, want_list))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200419 picked = [ my_list[i] for i in solution if i is not None ]
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200420 for_origin.dbg('Picked', config.tostr(picked))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200421 matches[key] = picked
422
423 return Resources(matches, do_copy=do_copy)
424
425 def set_hashes(self):
426 for key, item_list in self.items():
427 for item in item_list:
428 item[HASH_KEY] = util.hash_obj(item, HASH_KEY, RESERVED_KEY, USED_KEY)
429
430 def add(self, more):
431 if more is self:
432 raise RuntimeError('adding a list of resources to itself?')
433 config.add(self, copy.deepcopy(more))
434
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200435 def mark_reserved_by(self, origin_id):
436 for key, item_list in self.items():
437 for item in item_list:
438 item[RESERVED_KEY] = origin_id
439
440
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200441class NotSolvable(Exception):
442 pass
443
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200444def solve(all_matches):
445 '''
446 all_matches shall be a list of index-lists.
447 all_matches[i] is the list of indexes that item i can use.
448 Return a solution so that each i gets a different index.
449 solve([ [0, 1, 2],
450 [0],
451 [0, 2] ]) == [1, 0, 2]
452 '''
453
454 def all_differ(l):
455 return len(set(l)) == len(l)
456
457 def search_in_permutations(fixed=[]):
458 idx = len(fixed)
459 for i in range(len(all_matches[idx])):
460 val = all_matches[idx][i]
461 # don't add a val that's already in the list
462 if val in fixed:
463 continue
464 l = list(fixed)
465 l.append(val)
466 if len(l) == len(all_matches):
467 # found a solution
468 return l
469 # not at the end yet, add next digit
470 r = search_in_permutations(l)
471 if r:
472 # nested search_in_permutations() call found a solution
473 return r
474 # this entire branch yielded no solution
475 return None
476
477 if not all_matches:
478 raise RuntimeError('Cannot solve: no candidates')
479
480 solution = search_in_permutations()
481 if not solution:
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200482 raise NotSolvable('The requested resource requirements are not solvable %r'
483 % all_matches)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200484 return solution
485
486
487def contains_hash(list_of_dicts, a_hash):
488 for d in list_of_dicts:
489 if d.get(HASH_KEY) == a_hash:
490 return True
491 return False
492
493def item_matches(item, wanted_item, ignore_keys=None):
494 if is_dict(wanted_item):
495 # match up two dicts
496 if not isinstance(item, dict):
497 return False
498 for key, wanted_val in wanted_item.items():
499 if ignore_keys and key in ignore_keys:
500 continue
501 if not item_matches(item.get(key), wanted_val, ignore_keys=ignore_keys):
502 return False
503 return True
504
505 if is_list(wanted_item):
Pau Espin Pedrol4e36f7c2017-08-28 13:29:28 +0200506 if not is_list(item):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200507 return False
Pau Espin Pedrol58475512017-09-14 15:33:15 +0200508 # Validate that all elements in both lists are of the same type:
509 t = util.list_validate_same_elem_type(wanted_item + item)
510 if t is None:
511 return True # both lists are empty, return
512 # For lists of complex objects, we expect them to be sorted lists:
513 if t in (dict, list, tuple):
514 for i in range(max(len(wanted_item), len(item))):
515 log.ctx(idx=i)
516 subitem = item[i] if i < len(item) else util.empty_instance_type(t)
517 wanted_subitem = wanted_item[i] if i < len(wanted_item) else util.empty_instance_type(t)
518 if not item_matches(subitem, wanted_subitem, ignore_keys=ignore_keys):
519 return False
520 else: # for lists of basic elements, we handle them as unsorted sets:
521 for val in wanted_item:
522 if val not in item:
523 return False
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200524 return True
525
526 return item == wanted_item
527
528
529class ReservedResources(log.Origin):
530 '''
531 After all resources have been figured out, this is the API that a test case
532 gets to interact with resources. From those resources that have been
533 reserved for it, it can pick some to mark them as currently in use.
534 Functions like nitb() provide a resource by automatically picking its
535 dependencies from so far unused (but reserved) resource.
536 '''
537
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200538 def __init__(self, resources_pool, origin, reserved, modifiers):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200539 self.resources_pool = resources_pool
540 self.origin = origin
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200541 self.reserved_original = reserved
542 self.reserved = copy.deepcopy(self.reserved_original)
543 config.overlay(self.reserved, modifiers)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200544
545 def __repr__(self):
546 return 'resources(%s)=%s' % (self.origin.name(), pprint.pformat(self.reserved))
547
548 def get(self, kind, specifics=None):
549 if specifics is None:
550 specifics = {}
551 self.dbg('requesting use of', kind, specifics=specifics)
552 want = { kind: [specifics] }
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200553 available_dict = self.reserved.find(self.origin, want, skip_if_marked=USED_KEY,
554 do_copy=False, raise_if_missing=False,
555 log_label='Using')
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200556 available = available_dict.get(kind)
557 self.dbg(available=len(available))
558 if not available:
Neels Hofmeyrf9e86932017-06-06 19:47:40 +0200559 # cook up a detailed error message for the current situation
560 kind_reserved = self.reserved.get(kind, [])
561 used_count = len([r for r in kind_reserved if USED_KEY in r])
562 matching = self.reserved.find(self.origin, want, raise_if_missing=False, log_label=None).get(kind, [])
563 if not matching:
564 msg = 'none of the reserved resources matches requirements %r' % specifics
565 elif not (used_count < len(kind_reserved)):
566 msg = 'suite.conf reserved only %d x %r.' % (len(kind_reserved), kind)
567 else:
568 msg = ('No unused resource left that matches the requirements;'
569 ' Of reserved %d x %r, %d match the requirements, but all are already in use;'
570 ' Requirements: %r'
571 % (len(kind_reserved), kind, len(matching), specifics))
572 raise NoResourceExn('When trying to use instance nr %d of %r: %s' % (used_count + 1, kind, msg))
573
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200574 pick = available[0]
575 self.dbg(using=pick)
576 assert not pick.get(USED_KEY)
577 pick[USED_KEY] = True
578 return copy.deepcopy(pick)
579
580 def put(self, item):
581 if not item.get(USED_KEY):
582 raise RuntimeError('Can only put() a resource that is used: %r' % item)
583 hash_to_put = item.get(HASH_KEY)
584 if not hash_to_put:
585 raise RuntimeError('Can only put() a resource that has a hash marker: %r' % item)
586 for key, item_list in self.reserved.items():
587 my_list = self.get(key)
588 for my_item in my_list:
589 if hash_to_put == my_item.get(HASH_KEY):
590 my_item.pop(USED_KEY)
591
592 def put_all(self):
Pau Espin Pedrol1dd29552017-06-13 18:07:57 +0200593 if not self.reserved:
594 return
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200595 for key, item_list in self.reserved.items():
Pau Espin Pedrol1dd29552017-06-13 18:07:57 +0200596 for item in item_list:
597 item.pop(USED_KEY, None)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200598
599 def free(self):
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200600 if self.reserved_original:
601 self.resources_pool.free(self.origin, self.reserved_original)
602 self.reserved_original = None
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200603
Neels Hofmeyr2d1d5612017-05-22 20:02:41 +0200604 def counts(self):
605 counts = {}
606 for key in self.reserved.keys():
607 counts[key] = self.count(key)
608 return counts
609
610 def count(self, key):
611 return len(self.reserved.get(key) or [])
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200612
613# vim: expandtab tabstop=4 shiftwidth=4