blob: 4f48dc4cea313fdc4799eabdac41762b82581006 [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 time
22import copy
23import atexit
24import pprint
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +020025
26from . import log
27from . import config
Neels Hofmeyr3531a192017-03-28 14:30:28 +020028from . import util
29from . import schema
Pau Espin Pedrol6cdd2fd2017-11-07 11:57:42 +010030from . import modem
Neels Hofmeyr3531a192017-03-28 14:30:28 +020031from . import osmo_nitb
Holger Hans Peter Freytherd2db10d2018-12-06 18:34:53 +000032from . import bts_sysmo, bts_osmotrx, bts_osmovirtual, bts_octphy, bts_nanobts
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'
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'
48R_ALL = (R_IP_ADDRESS, R_BTS, R_ARFCN, R_MODEM, R_OSMOCON)
Neels Hofmeyr3531a192017-03-28 14:30:28 +020049
50RESOURCES_SCHEMA = {
Neels Hofmeyr76d81032017-05-18 18:35:32 +020051 'ip_address[].addr': schema.IPV4,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020052 'bts[].label': schema.STR,
53 'bts[].type': schema.STR,
Pau Espin Pedrolfa9a6d32017-09-12 15:13:21 +020054 'bts[].ipa_unit_id': schema.UINT,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020055 'bts[].addr': schema.IPV4,
56 'bts[].band': schema.BAND,
Pau Espin Pedrolce35d912017-11-23 11:01:24 +010057 'bts[].direct_pcu': schema.BOOL_STR,
Pau Espin Pedrol57497a62017-08-28 14:21:15 +020058 'bts[].ciphers[]': schema.CIPHER,
Pau Espin Pedrol722e94e2018-08-22 11:01:32 +020059 'bts[].channel_allocator': schema.CHAN_ALLOCATOR,
Pau Espin Pedrol4f23ab52018-10-29 11:30:00 +010060 'bts[].gprs_mode': schema.GPRS_MODE,
Pau Espin Pedrol39df7f42018-05-07 13:49:33 +020061 'bts[].num_trx': schema.UINT,
62 'bts[].max_trx': schema.UINT,
Pau Espin Pedrolf6a07122018-07-27 16:24:29 +020063 'bts[].trx_list[].addr': schema.IPV4,
Your Name44af3412017-04-13 03:11:59 +020064 'bts[].trx_list[].hw_addr': schema.HWADDR,
65 'bts[].trx_list[].net_device': schema.STR,
Pau Espin Pedrolb26f32a2017-09-14 13:52:28 +020066 'bts[].trx_list[].nominal_power': schema.UINT,
67 'bts[].trx_list[].max_power_red': schema.UINT,
Pau Espin Pedrolc9b63762018-05-07 01:57:01 +020068 'bts[].trx_list[].timeslot_list[].phys_chan_config': schema.PHY_CHAN,
Pau Espin Pedrolf6a07122018-07-27 16:24:29 +020069 'bts[].trx_list[].power_supply.type': schema.STR,
70 'bts[].trx_list[].power_supply.device': schema.STR,
71 'bts[].trx_list[].power_supply.port': schema.STR,
Pau Espin Pedrol0d455042018-08-27 17:07:41 +020072 'bts[].osmo_trx.launch_trx': schema.BOOL_STR,
73 'bts[].osmo_trx.type': schema.STR,
74 'bts[].osmo_trx.clock_reference': schema.OSMO_TRX_CLOCK_REF,
75 'bts[].osmo_trx.trx_ip': schema.IPV4,
Pau Espin Pedrola9006df2018-10-01 12:26:39 +020076 'bts[].osmo_trx.remote_user': schema.STR,
Pau Espin Pedrol8cfa10f2018-11-06 12:05:19 +010077 'bts[].osmo_trx.dev_args': schema.STR,
Pau Espin Pedrol94eab262018-09-17 20:25:55 +020078 'bts[].osmo_trx.multi_arfcn': schema.BOOL_STR,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020079 'arfcn[].arfcn': schema.INT,
80 'arfcn[].band': schema.BAND,
81 'modem[].label': schema.STR,
82 'modem[].path': schema.STR,
83 'modem[].imsi': schema.IMSI,
84 'modem[].ki': schema.KI,
Pau Espin Pedrol713ce2c2017-08-24 16:57:17 +020085 'modem[].auth_algo': schema.AUTH_ALGO,
Pau Espin Pedrol57497a62017-08-28 14:21:15 +020086 'modem[].ciphers[]': schema.CIPHER,
Pau Espin Pedrolac18fd32017-08-31 18:49:47 +020087 'modem[].features[]': schema.MODEM_FEATURE,
Pau Espin Pedrolbc1ed882018-05-17 16:59:58 +020088 'osmocon_phone[].serial_device': schema.STR,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020089 }
90
91WANT_SCHEMA = util.dict_add(
Pau Espin Pedrolead79ac2017-09-12 15:19:18 +020092 dict([('%s[].times' % r, schema.TIMES) for r in R_ALL]),
Neels Hofmeyr3531a192017-03-28 14:30:28 +020093 RESOURCES_SCHEMA)
94
Pau Espin Pedrol0b302792017-09-10 16:33:10 +020095CONF_SCHEMA = util.dict_add(
Pau Espin Pedrol5dc24592018-08-27 12:53:41 +020096 { 'defaults.timeout': schema.STR,
97 'config.bsc.net.codec_list[]': schema.CODEC },
Pau Espin Pedrolaab56922018-08-21 14:58:29 +020098 dict([('resources.%s' % key, val) for key, val in WANT_SCHEMA.items()]),
99 dict([('modifiers.%s' % key, val) for key, val in WANT_SCHEMA.items()]))
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200100
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200101KNOWN_BTS_TYPES = {
Your Name44af3412017-04-13 03:11:59 +0200102 'osmo-bts-sysmo': bts_sysmo.SysmoBts,
103 'osmo-bts-trx': bts_osmotrx.OsmoBtsTrx,
Pau Espin Pedroldaed4472017-09-15 14:11:35 +0200104 'osmo-bts-octphy': bts_octphy.OsmoBtsOctphy,
Holger Hans Peter Freytherd2db10d2018-12-06 18:34:53 +0000105 'osmo-bts-virtual': bts_osmovirtual.OsmoBtsVirtual,
Pau Espin Pedrol1b28a582018-03-08 21:01:26 +0100106 'nanobts': bts_nanobts.NanoBts,
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200107 }
108
109def register_bts_type(name, clazz):
110 KNOWN_BTS_TYPES[name] = clazz
111
112class ResourcesPool(log.Origin):
113 _remember_to_free = None
114 _registered_exit_handler = False
115
116 def __init__(self):
117 self.config_path = config.get_config_file(RESOURCES_CONF)
118 self.state_dir = config.get_state_dir()
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200119 super().__init__(log.C_CNF, conf=self.config_path, state=self.state_dir.path)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200120 self.read_conf()
121
122 def read_conf(self):
123 self.all_resources = Resources(config.read(self.config_path, RESOURCES_SCHEMA))
124 self.all_resources.set_hashes()
125
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200126 def reserve(self, origin, want, modifiers):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200127 '''
128 attempt to reserve the resources specified in the dict 'want' for
129 'origin'. Obtain a lock on the resources lock dir, verify that all
130 wanted resources are available, and if yes mark them as reserved.
131
132 On success, return a reservation object which can be used to release
133 the reservation. The reservation will be freed automatically on program
134 exit, if not yet done manually.
135
136 'origin' should be an Origin() instance.
137
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200138 'want' is a dict matching RESOURCES_SCHEMA, used to specify what to
139 reserve.
140
141 'modifiers' is a dict matching RESOURCES_SCHEMA, it is overlaid on top
142 of 'want'.
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200143
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200144 If an entry has no attribute set, any of the resources may be
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200145 reserved without further limitations.
146
147 ResourcesPool may also be selected with narrowed down constraints.
Neels Hofmeyr76d81032017-05-18 18:35:32 +0200148 This would reserve one IP address, two modems, one BTS of type
Neels Hofmeyr391afe32017-05-18 19:22:12 +0200149 sysmo and one of type trx, plus 2 ARFCNs in the 1800 band:
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200150
151 {
Neels Hofmeyr76d81032017-05-18 18:35:32 +0200152 'ip_address': [ {} ],
Neels Hofmeyr391afe32017-05-18 19:22:12 +0200153 'bts': [ { 'type': 'sysmo' }, { 'type': 'trx' } ],
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200154 'arfcn': [ { 'band': 'GSM-1800' }, { 'band': 'GSM-1800' } ],
155 'modem': [ {}, {} ],
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200156 }
157 '''
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200158 schema.validate(want, RESOURCES_SCHEMA)
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200159 schema.validate(modifiers, RESOURCES_SCHEMA)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200160
161 origin_id = origin.origin_id()
162
163 with self.state_dir.lock(origin_id):
164 rrfile_path = self.state_dir.mk_parentdir(RESERVED_RESOURCES_FILE)
165 reserved = Resources(config.read(rrfile_path, if_missing_return={}))
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200166 to_be_reserved = self.all_resources.without(reserved).find(origin, want)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200167
168 to_be_reserved.mark_reserved_by(origin_id)
169
170 reserved.add(to_be_reserved)
171 config.write(rrfile_path, reserved)
172
173 self.remember_to_free(to_be_reserved)
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200174 return ReservedResources(self, origin, to_be_reserved, modifiers)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200175
176 def free(self, origin, to_be_freed):
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200177 log.ctx(origin)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200178 with self.state_dir.lock(origin.origin_id()):
179 rrfile_path = self.state_dir.mk_parentdir(RESERVED_RESOURCES_FILE)
180 reserved = Resources(config.read(rrfile_path, if_missing_return={}))
181 reserved.drop(to_be_freed)
182 config.write(rrfile_path, reserved)
183 self.forget_freed(to_be_freed)
184
185 def register_exit_handler(self):
186 if self._registered_exit_handler:
187 return
188 atexit.register(self.clean_up_registered_resources)
189 self._registered_exit_handler = True
190
191 def unregister_exit_handler(self):
192 if not self._registered_exit_handler:
193 return
194 atexit.unregister(self.clean_up_registered_resources)
195 self._registered_exit_handler = False
196
197 def clean_up_registered_resources(self):
198 if not self._remember_to_free:
199 return
200 self.free(log.Origin('atexit.clean_up_registered_resources()'),
201 self._remember_to_free)
202
203 def remember_to_free(self, to_be_reserved):
204 self.register_exit_handler()
205 if not self._remember_to_free:
206 self._remember_to_free = Resources()
207 self._remember_to_free.add(to_be_reserved)
208
209 def forget_freed(self, freed):
210 if freed is self._remember_to_free:
211 self._remember_to_free.clear()
212 else:
213 self._remember_to_free.drop(freed)
214 if not self._remember_to_free:
215 self.unregister_exit_handler()
216
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100217 def next_persistent_value(self, token, first_val, validate_func, inc_func, origin):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200218 origin_id = origin.origin_id()
219
220 with self.state_dir.lock(origin_id):
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100221 token_path = self.state_dir.child('last_used_%s.state' % token)
222 log.ctx(token_path)
223 last_value = first_val
224 if os.path.exists(token_path):
225 if not os.path.isfile(token_path):
226 raise RuntimeError('path should be a file but is not: %r' % token_path)
227 with open(token_path, 'r') as f:
228 last_value = f.read().strip()
229 validate_func(last_value)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200230
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100231 next_value = inc_func(last_value)
232 with open(token_path, 'w') as f:
233 f.write(next_value)
234 return next_value
235
236 def next_msisdn(self, origin):
237 return self.next_persistent_value('msisdn', '1000', schema.msisdn, util.msisdn_inc, origin)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200238
Pau Espin Pedrol5e0c2512017-11-06 18:40:23 +0100239 def next_lac(self, origin):
Pau Espin Pedrolf7f06362017-11-28 12:56:35 +0100240 # LAC=0 has special meaning (MS detached), avoid it
241 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 +0200242
Pau Espin Pedrol8a3a7b52017-11-28 15:50:02 +0100243 def next_rac(self, origin):
244 return self.next_persistent_value('rac', '1', schema.uint8, lambda x: str((int(x)+1) % pow(2,8) or 1), origin)
245
Pau Espin Pedrol4ccce7c2017-11-07 11:13:20 +0100246 def next_cellid(self, origin):
247 return self.next_persistent_value('cellid', '1', schema.uint16, lambda x: str((int(x)+1) % pow(2,16)), origin)
248
Pau Espin Pedrol8a3a7b52017-11-28 15:50:02 +0100249 def next_bvci(self, origin):
250 # BVCI=0 and =1 are reserved, avoid them.
251 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)
252
Pau Espin Pedrol4676cbd2017-09-14 17:35:03 +0200253class NoResourceExn(log.Error):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200254 pass
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200255
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200256class Resources(dict):
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200257
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200258 def __init__(self, all_resources={}, do_copy=True):
259 if do_copy:
260 all_resources = copy.deepcopy(all_resources)
261 self.update(all_resources)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200262
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200263 def drop(self, reserved, fail_if_not_found=True):
264 # protect from modifying reserved because we're the same object
265 if reserved is self:
266 raise RuntimeError('Refusing to drop a list of resources from itself.'
267 ' This is probably a bug where a list of Resources()'
268 ' should have been copied but is passed as-is.'
269 ' use Resources.clear() instead.')
270
271 for key, reserved_list in reserved.items():
272 my_list = self.get(key) or []
273
274 if my_list is reserved_list:
275 self.pop(key)
276 continue
277
278 for reserved_item in reserved_list:
279 found = False
280 reserved_hash = reserved_item.get(HASH_KEY)
281 if not reserved_hash:
282 raise RuntimeError('Resources.drop() only works with hashed items')
283
284 for i in range(len(my_list)):
285 my_item = my_list[i]
286 my_hash = my_item.get(HASH_KEY)
287 if not my_hash:
288 raise RuntimeError('Resources.drop() only works with hashed items')
289 if my_hash == reserved_hash:
290 found = True
291 my_list.pop(i)
292 break
293
294 if fail_if_not_found and not found:
295 raise RuntimeError('Asked to drop resource from a pool, but the'
296 ' resource was not found: %s = %r' % (key, reserved_item))
297
298 if not my_list:
299 self.pop(key)
300 return self
301
302 def without(self, reserved):
303 return Resources(self).drop(reserved)
304
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200305 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 +0200306 '''
307 Pass a dict of resource requirements, e.g.:
308 want = {
309 'bts': [ {'type': 'osmo-bts-sysmo',}, {} ],
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200310 'modem': [ {}, {}, {} ]
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200311 }
312 This function tries to find a combination from the available resources that
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200313 matches these requirements. The return value is a dict (wrapped in a Resources class)
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200314 that contains the matching resources in the order of 'want' dict: in above
315 example, the returned dict would have a 'bts' list with the first item being
316 a sysmoBTS, the second item being any other available BTS.
317
318 If skip_if_marked is passed, any resource that contains this key is skipped.
319 E.g. if a BTS has the USED_KEY set like
320 reserved_resources = { 'bts' : {..., '_used': True} }
321 then this may be skipped by passing skip_if_marked='_used'
322 (or rather skip_if_marked=USED_KEY).
323
324 If do_copy is True, the returned dict is a deep copy and does not share
325 lists with any other Resources dict.
326
327 If raise_if_missing is False, this will return an empty item for any
328 resource that had no match, instead of immediately raising an exception.
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200329
330 This function expects input dictionaries whose contents have already
331 been replicated based on its the 'times' attributes. See
332 config.replicate_times() for more details.
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200333 '''
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200334 matches = {}
Neels Hofmeyr17c139e2017-04-12 02:42:02 +0200335 for key, want_list in sorted(want.items()): # sorted for deterministic test results
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200336 # here we have a resource of a given type, e.g. 'bts', with a list
337 # containing as many BTSes as the caller wants to reserve/use. Each
338 # list item contains specifics for the particular BTS.
Neels Hofmeyr2a1a1fa2017-05-29 01:36:21 +0200339 my_list = self.get(key, [])
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200340
Neels Hofmeyrf9e86932017-06-06 19:47:40 +0200341 if log_label:
342 for_origin.log(log_label, len(want_list), 'x', key, '(candidates: %d)'%len(my_list))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200343
344 # Try to avoid a less constrained item snatching away a resource
345 # from a more detailed constrained requirement.
346
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200347 # first record all matches, so that each requested item has a list
348 # of all available resources that match it. Some resources may
349 # appear for multiple requested items. Store matching indexes.
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200350 all_matches = []
351 for want_item in want_list:
352 item_match_list = []
353 for i in range(len(my_list)):
354 my_item = my_list[i]
355 if skip_if_marked and my_item.get(skip_if_marked):
356 continue
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200357 if item_matches(my_item, want_item):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200358 item_match_list.append(i)
359 if not item_match_list:
Neels Hofmeyr9b907702017-05-06 23:20:33 +0200360 if raise_if_missing:
361 raise NoResourceExn('No matching resource available for %s = %r'
362 % (key, want_item))
363 else:
364 # this one failed... see below
365 all_matches = []
366 break
367
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200368 all_matches.append( item_match_list )
369
370 if not all_matches:
Neels Hofmeyr9b907702017-05-06 23:20:33 +0200371 # ...this one failed. Makes no sense to solve resource
372 # allocations, return an empty list for this key to mark
373 # failure.
374 matches[key] = []
375 continue
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200376
377 # figure out who gets what
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200378 try:
379 solution = solve(all_matches)
380 except NotSolvable:
381 # instead of a cryptic error message, raise an exception that
382 # conveys meaning to the user.
383 raise NoResourceExn('Could not resolve request to reserve resources: '
384 '%d x %s with requirements: %r' % (len(want_list), key, want_list))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200385 picked = [ my_list[i] for i in solution if i is not None ]
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200386 for_origin.dbg('Picked', config.tostr(picked))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200387 matches[key] = picked
388
389 return Resources(matches, do_copy=do_copy)
390
391 def set_hashes(self):
392 for key, item_list in self.items():
393 for item in item_list:
394 item[HASH_KEY] = util.hash_obj(item, HASH_KEY, RESERVED_KEY, USED_KEY)
395
396 def add(self, more):
397 if more is self:
398 raise RuntimeError('adding a list of resources to itself?')
399 config.add(self, copy.deepcopy(more))
400
401 def combine(self, more_rules):
402 if more_rules is self:
403 raise RuntimeError('combining a list of resource rules with itself?')
404 config.combine(self, copy.deepcopy(more))
405
406 def mark_reserved_by(self, origin_id):
407 for key, item_list in self.items():
408 for item in item_list:
409 item[RESERVED_KEY] = origin_id
410
411
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200412class NotSolvable(Exception):
413 pass
414
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200415def solve(all_matches):
416 '''
417 all_matches shall be a list of index-lists.
418 all_matches[i] is the list of indexes that item i can use.
419 Return a solution so that each i gets a different index.
420 solve([ [0, 1, 2],
421 [0],
422 [0, 2] ]) == [1, 0, 2]
423 '''
424
425 def all_differ(l):
426 return len(set(l)) == len(l)
427
428 def search_in_permutations(fixed=[]):
429 idx = len(fixed)
430 for i in range(len(all_matches[idx])):
431 val = all_matches[idx][i]
432 # don't add a val that's already in the list
433 if val in fixed:
434 continue
435 l = list(fixed)
436 l.append(val)
437 if len(l) == len(all_matches):
438 # found a solution
439 return l
440 # not at the end yet, add next digit
441 r = search_in_permutations(l)
442 if r:
443 # nested search_in_permutations() call found a solution
444 return r
445 # this entire branch yielded no solution
446 return None
447
448 if not all_matches:
449 raise RuntimeError('Cannot solve: no candidates')
450
451 solution = search_in_permutations()
452 if not solution:
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200453 raise NotSolvable('The requested resource requirements are not solvable %r'
454 % all_matches)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200455 return solution
456
457
458def contains_hash(list_of_dicts, a_hash):
459 for d in list_of_dicts:
460 if d.get(HASH_KEY) == a_hash:
461 return True
462 return False
463
464def item_matches(item, wanted_item, ignore_keys=None):
465 if is_dict(wanted_item):
466 # match up two dicts
467 if not isinstance(item, dict):
468 return False
469 for key, wanted_val in wanted_item.items():
470 if ignore_keys and key in ignore_keys:
471 continue
472 if not item_matches(item.get(key), wanted_val, ignore_keys=ignore_keys):
473 return False
474 return True
475
476 if is_list(wanted_item):
Pau Espin Pedrol4e36f7c2017-08-28 13:29:28 +0200477 if not is_list(item):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200478 return False
Pau Espin Pedrol58475512017-09-14 15:33:15 +0200479 # Validate that all elements in both lists are of the same type:
480 t = util.list_validate_same_elem_type(wanted_item + item)
481 if t is None:
482 return True # both lists are empty, return
483 # For lists of complex objects, we expect them to be sorted lists:
484 if t in (dict, list, tuple):
485 for i in range(max(len(wanted_item), len(item))):
486 log.ctx(idx=i)
487 subitem = item[i] if i < len(item) else util.empty_instance_type(t)
488 wanted_subitem = wanted_item[i] if i < len(wanted_item) else util.empty_instance_type(t)
489 if not item_matches(subitem, wanted_subitem, ignore_keys=ignore_keys):
490 return False
491 else: # for lists of basic elements, we handle them as unsorted sets:
492 for val in wanted_item:
493 if val not in item:
494 return False
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200495 return True
496
497 return item == wanted_item
498
499
500class ReservedResources(log.Origin):
501 '''
502 After all resources have been figured out, this is the API that a test case
503 gets to interact with resources. From those resources that have been
504 reserved for it, it can pick some to mark them as currently in use.
505 Functions like nitb() provide a resource by automatically picking its
506 dependencies from so far unused (but reserved) resource.
507 '''
508
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200509 def __init__(self, resources_pool, origin, reserved, modifiers):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200510 self.resources_pool = resources_pool
511 self.origin = origin
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200512 self.reserved_original = reserved
513 self.reserved = copy.deepcopy(self.reserved_original)
514 config.overlay(self.reserved, modifiers)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200515
516 def __repr__(self):
517 return 'resources(%s)=%s' % (self.origin.name(), pprint.pformat(self.reserved))
518
519 def get(self, kind, specifics=None):
520 if specifics is None:
521 specifics = {}
522 self.dbg('requesting use of', kind, specifics=specifics)
523 want = { kind: [specifics] }
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200524 available_dict = self.reserved.find(self.origin, want, skip_if_marked=USED_KEY,
525 do_copy=False, raise_if_missing=False,
526 log_label='Using')
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200527 available = available_dict.get(kind)
528 self.dbg(available=len(available))
529 if not available:
Neels Hofmeyrf9e86932017-06-06 19:47:40 +0200530 # cook up a detailed error message for the current situation
531 kind_reserved = self.reserved.get(kind, [])
532 used_count = len([r for r in kind_reserved if USED_KEY in r])
533 matching = self.reserved.find(self.origin, want, raise_if_missing=False, log_label=None).get(kind, [])
534 if not matching:
535 msg = 'none of the reserved resources matches requirements %r' % specifics
536 elif not (used_count < len(kind_reserved)):
537 msg = 'suite.conf reserved only %d x %r.' % (len(kind_reserved), kind)
538 else:
539 msg = ('No unused resource left that matches the requirements;'
540 ' Of reserved %d x %r, %d match the requirements, but all are already in use;'
541 ' Requirements: %r'
542 % (len(kind_reserved), kind, len(matching), specifics))
543 raise NoResourceExn('When trying to use instance nr %d of %r: %s' % (used_count + 1, kind, msg))
544
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200545 pick = available[0]
546 self.dbg(using=pick)
547 assert not pick.get(USED_KEY)
548 pick[USED_KEY] = True
549 return copy.deepcopy(pick)
550
551 def put(self, item):
552 if not item.get(USED_KEY):
553 raise RuntimeError('Can only put() a resource that is used: %r' % item)
554 hash_to_put = item.get(HASH_KEY)
555 if not hash_to_put:
556 raise RuntimeError('Can only put() a resource that has a hash marker: %r' % item)
557 for key, item_list in self.reserved.items():
558 my_list = self.get(key)
559 for my_item in my_list:
560 if hash_to_put == my_item.get(HASH_KEY):
561 my_item.pop(USED_KEY)
562
563 def put_all(self):
Pau Espin Pedrol1dd29552017-06-13 18:07:57 +0200564 if not self.reserved:
565 return
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200566 for key, item_list in self.reserved.items():
Pau Espin Pedrol1dd29552017-06-13 18:07:57 +0200567 for item in item_list:
568 item.pop(USED_KEY, None)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200569
570 def free(self):
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200571 if self.reserved_original:
572 self.resources_pool.free(self.origin, self.reserved_original)
573 self.reserved_original = None
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200574
Neels Hofmeyr2d1d5612017-05-22 20:02:41 +0200575 def counts(self):
576 counts = {}
577 for key in self.reserved.keys():
578 counts[key] = self.count(key)
579 return counts
580
581 def count(self, key):
582 return len(self.reserved.get(key) or [])
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200583
584# vim: expandtab tabstop=4 shiftwidth=4