blob: 3f2ddc7fa0c75f4b1c25498635429a32223cfebe [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
Pau Espin Pedrol1b28a582018-03-08 21:01:26 +010032from . import bts_sysmo, bts_osmotrx, 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 Pedrol39df7f42018-05-07 13:49:33 +020060 'bts[].num_trx': schema.UINT,
61 'bts[].max_trx': schema.UINT,
Pau Espin Pedrolf6a07122018-07-27 16:24:29 +020062 'bts[].trx_list[].addr': schema.IPV4,
Your Name44af3412017-04-13 03:11:59 +020063 'bts[].trx_list[].hw_addr': schema.HWADDR,
64 'bts[].trx_list[].net_device': schema.STR,
Pau Espin Pedrolb26f32a2017-09-14 13:52:28 +020065 'bts[].trx_list[].nominal_power': schema.UINT,
66 'bts[].trx_list[].max_power_red': schema.UINT,
Pau Espin Pedrolc9b63762018-05-07 01:57:01 +020067 'bts[].trx_list[].timeslot_list[].phys_chan_config': schema.PHY_CHAN,
Pau Espin Pedrolf6a07122018-07-27 16:24:29 +020068 'bts[].trx_list[].power_supply.type': schema.STR,
69 'bts[].trx_list[].power_supply.device': schema.STR,
70 'bts[].trx_list[].power_supply.port': schema.STR,
Pau Espin Pedrol0d455042018-08-27 17:07:41 +020071 'bts[].osmo_trx.launch_trx': schema.BOOL_STR,
72 'bts[].osmo_trx.type': schema.STR,
73 'bts[].osmo_trx.clock_reference': schema.OSMO_TRX_CLOCK_REF,
74 'bts[].osmo_trx.trx_ip': schema.IPV4,
Pau Espin Pedrola9006df2018-10-01 12:26:39 +020075 'bts[].osmo_trx.remote_user': schema.STR,
Pau Espin Pedrol94eab262018-09-17 20:25:55 +020076 'bts[].osmo_trx.multi_arfcn': schema.BOOL_STR,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020077 'arfcn[].arfcn': schema.INT,
78 'arfcn[].band': schema.BAND,
79 'modem[].label': schema.STR,
80 'modem[].path': schema.STR,
81 'modem[].imsi': schema.IMSI,
82 'modem[].ki': schema.KI,
Pau Espin Pedrol713ce2c2017-08-24 16:57:17 +020083 'modem[].auth_algo': schema.AUTH_ALGO,
Pau Espin Pedrol57497a62017-08-28 14:21:15 +020084 'modem[].ciphers[]': schema.CIPHER,
Pau Espin Pedrolac18fd32017-08-31 18:49:47 +020085 'modem[].features[]': schema.MODEM_FEATURE,
Pau Espin Pedrolbc1ed882018-05-17 16:59:58 +020086 'osmocon_phone[].serial_device': schema.STR,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020087 }
88
89WANT_SCHEMA = util.dict_add(
Pau Espin Pedrolead79ac2017-09-12 15:19:18 +020090 dict([('%s[].times' % r, schema.TIMES) for r in R_ALL]),
Neels Hofmeyr3531a192017-03-28 14:30:28 +020091 RESOURCES_SCHEMA)
92
Pau Espin Pedrol0b302792017-09-10 16:33:10 +020093CONF_SCHEMA = util.dict_add(
Pau Espin Pedrol5dc24592018-08-27 12:53:41 +020094 { 'defaults.timeout': schema.STR,
95 'config.bsc.net.codec_list[]': schema.CODEC },
Pau Espin Pedrolaab56922018-08-21 14:58:29 +020096 dict([('resources.%s' % key, val) for key, val in WANT_SCHEMA.items()]),
97 dict([('modifiers.%s' % key, val) for key, val in WANT_SCHEMA.items()]))
Pau Espin Pedrol0b302792017-09-10 16:33:10 +020098
Neels Hofmeyr3531a192017-03-28 14:30:28 +020099KNOWN_BTS_TYPES = {
Your Name44af3412017-04-13 03:11:59 +0200100 'osmo-bts-sysmo': bts_sysmo.SysmoBts,
101 'osmo-bts-trx': bts_osmotrx.OsmoBtsTrx,
Pau Espin Pedroldaed4472017-09-15 14:11:35 +0200102 'osmo-bts-octphy': bts_octphy.OsmoBtsOctphy,
Pau Espin Pedrol1b28a582018-03-08 21:01:26 +0100103 'nanobts': bts_nanobts.NanoBts,
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200104 }
105
106def register_bts_type(name, clazz):
107 KNOWN_BTS_TYPES[name] = clazz
108
109class ResourcesPool(log.Origin):
110 _remember_to_free = None
111 _registered_exit_handler = False
112
113 def __init__(self):
114 self.config_path = config.get_config_file(RESOURCES_CONF)
115 self.state_dir = config.get_state_dir()
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200116 super().__init__(log.C_CNF, conf=self.config_path, state=self.state_dir.path)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200117 self.read_conf()
118
119 def read_conf(self):
120 self.all_resources = Resources(config.read(self.config_path, RESOURCES_SCHEMA))
121 self.all_resources.set_hashes()
122
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200123 def reserve(self, origin, want, modifiers):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200124 '''
125 attempt to reserve the resources specified in the dict 'want' for
126 'origin'. Obtain a lock on the resources lock dir, verify that all
127 wanted resources are available, and if yes mark them as reserved.
128
129 On success, return a reservation object which can be used to release
130 the reservation. The reservation will be freed automatically on program
131 exit, if not yet done manually.
132
133 'origin' should be an Origin() instance.
134
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200135 'want' is a dict matching RESOURCES_SCHEMA, used to specify what to
136 reserve.
137
138 'modifiers' is a dict matching RESOURCES_SCHEMA, it is overlaid on top
139 of 'want'.
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200140
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200141 If an entry has no attribute set, any of the resources may be
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200142 reserved without further limitations.
143
144 ResourcesPool may also be selected with narrowed down constraints.
Neels Hofmeyr76d81032017-05-18 18:35:32 +0200145 This would reserve one IP address, two modems, one BTS of type
Neels Hofmeyr391afe32017-05-18 19:22:12 +0200146 sysmo and one of type trx, plus 2 ARFCNs in the 1800 band:
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200147
148 {
Neels Hofmeyr76d81032017-05-18 18:35:32 +0200149 'ip_address': [ {} ],
Neels Hofmeyr391afe32017-05-18 19:22:12 +0200150 'bts': [ { 'type': 'sysmo' }, { 'type': 'trx' } ],
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200151 'arfcn': [ { 'band': 'GSM-1800' }, { 'band': 'GSM-1800' } ],
152 'modem': [ {}, {} ],
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200153 }
154 '''
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200155 schema.validate(want, RESOURCES_SCHEMA)
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200156 schema.validate(modifiers, RESOURCES_SCHEMA)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200157
158 origin_id = origin.origin_id()
159
160 with self.state_dir.lock(origin_id):
161 rrfile_path = self.state_dir.mk_parentdir(RESERVED_RESOURCES_FILE)
162 reserved = Resources(config.read(rrfile_path, if_missing_return={}))
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200163 to_be_reserved = self.all_resources.without(reserved).find(origin, want)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200164
165 to_be_reserved.mark_reserved_by(origin_id)
166
167 reserved.add(to_be_reserved)
168 config.write(rrfile_path, reserved)
169
170 self.remember_to_free(to_be_reserved)
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200171 return ReservedResources(self, origin, to_be_reserved, modifiers)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200172
173 def free(self, origin, to_be_freed):
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200174 log.ctx(origin)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200175 with self.state_dir.lock(origin.origin_id()):
176 rrfile_path = self.state_dir.mk_parentdir(RESERVED_RESOURCES_FILE)
177 reserved = Resources(config.read(rrfile_path, if_missing_return={}))
178 reserved.drop(to_be_freed)
179 config.write(rrfile_path, reserved)
180 self.forget_freed(to_be_freed)
181
182 def register_exit_handler(self):
183 if self._registered_exit_handler:
184 return
185 atexit.register(self.clean_up_registered_resources)
186 self._registered_exit_handler = True
187
188 def unregister_exit_handler(self):
189 if not self._registered_exit_handler:
190 return
191 atexit.unregister(self.clean_up_registered_resources)
192 self._registered_exit_handler = False
193
194 def clean_up_registered_resources(self):
195 if not self._remember_to_free:
196 return
197 self.free(log.Origin('atexit.clean_up_registered_resources()'),
198 self._remember_to_free)
199
200 def remember_to_free(self, to_be_reserved):
201 self.register_exit_handler()
202 if not self._remember_to_free:
203 self._remember_to_free = Resources()
204 self._remember_to_free.add(to_be_reserved)
205
206 def forget_freed(self, freed):
207 if freed is self._remember_to_free:
208 self._remember_to_free.clear()
209 else:
210 self._remember_to_free.drop(freed)
211 if not self._remember_to_free:
212 self.unregister_exit_handler()
213
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100214 def next_persistent_value(self, token, first_val, validate_func, inc_func, origin):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200215 origin_id = origin.origin_id()
216
217 with self.state_dir.lock(origin_id):
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100218 token_path = self.state_dir.child('last_used_%s.state' % token)
219 log.ctx(token_path)
220 last_value = first_val
221 if os.path.exists(token_path):
222 if not os.path.isfile(token_path):
223 raise RuntimeError('path should be a file but is not: %r' % token_path)
224 with open(token_path, 'r') as f:
225 last_value = f.read().strip()
226 validate_func(last_value)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200227
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100228 next_value = inc_func(last_value)
229 with open(token_path, 'w') as f:
230 f.write(next_value)
231 return next_value
232
233 def next_msisdn(self, origin):
234 return self.next_persistent_value('msisdn', '1000', schema.msisdn, util.msisdn_inc, origin)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200235
Pau Espin Pedrol5e0c2512017-11-06 18:40:23 +0100236 def next_lac(self, origin):
Pau Espin Pedrolf7f06362017-11-28 12:56:35 +0100237 # LAC=0 has special meaning (MS detached), avoid it
238 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 +0200239
Pau Espin Pedrol8a3a7b52017-11-28 15:50:02 +0100240 def next_rac(self, origin):
241 return self.next_persistent_value('rac', '1', schema.uint8, lambda x: str((int(x)+1) % pow(2,8) or 1), origin)
242
Pau Espin Pedrol4ccce7c2017-11-07 11:13:20 +0100243 def next_cellid(self, origin):
244 return self.next_persistent_value('cellid', '1', schema.uint16, lambda x: str((int(x)+1) % pow(2,16)), origin)
245
Pau Espin Pedrol8a3a7b52017-11-28 15:50:02 +0100246 def next_bvci(self, origin):
247 # BVCI=0 and =1 are reserved, avoid them.
248 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)
249
Pau Espin Pedrol4676cbd2017-09-14 17:35:03 +0200250class NoResourceExn(log.Error):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200251 pass
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200252
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200253class Resources(dict):
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200254
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200255 def __init__(self, all_resources={}, do_copy=True):
256 if do_copy:
257 all_resources = copy.deepcopy(all_resources)
258 self.update(all_resources)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200259
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200260 def drop(self, reserved, fail_if_not_found=True):
261 # protect from modifying reserved because we're the same object
262 if reserved is self:
263 raise RuntimeError('Refusing to drop a list of resources from itself.'
264 ' This is probably a bug where a list of Resources()'
265 ' should have been copied but is passed as-is.'
266 ' use Resources.clear() instead.')
267
268 for key, reserved_list in reserved.items():
269 my_list = self.get(key) or []
270
271 if my_list is reserved_list:
272 self.pop(key)
273 continue
274
275 for reserved_item in reserved_list:
276 found = False
277 reserved_hash = reserved_item.get(HASH_KEY)
278 if not reserved_hash:
279 raise RuntimeError('Resources.drop() only works with hashed items')
280
281 for i in range(len(my_list)):
282 my_item = my_list[i]
283 my_hash = my_item.get(HASH_KEY)
284 if not my_hash:
285 raise RuntimeError('Resources.drop() only works with hashed items')
286 if my_hash == reserved_hash:
287 found = True
288 my_list.pop(i)
289 break
290
291 if fail_if_not_found and not found:
292 raise RuntimeError('Asked to drop resource from a pool, but the'
293 ' resource was not found: %s = %r' % (key, reserved_item))
294
295 if not my_list:
296 self.pop(key)
297 return self
298
299 def without(self, reserved):
300 return Resources(self).drop(reserved)
301
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200302 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 +0200303 '''
304 Pass a dict of resource requirements, e.g.:
305 want = {
306 'bts': [ {'type': 'osmo-bts-sysmo',}, {} ],
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200307 'modem': [ {}, {}, {} ]
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200308 }
309 This function tries to find a combination from the available resources that
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200310 matches these requirements. The return value is a dict (wrapped in a Resources class)
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200311 that contains the matching resources in the order of 'want' dict: in above
312 example, the returned dict would have a 'bts' list with the first item being
313 a sysmoBTS, the second item being any other available BTS.
314
315 If skip_if_marked is passed, any resource that contains this key is skipped.
316 E.g. if a BTS has the USED_KEY set like
317 reserved_resources = { 'bts' : {..., '_used': True} }
318 then this may be skipped by passing skip_if_marked='_used'
319 (or rather skip_if_marked=USED_KEY).
320
321 If do_copy is True, the returned dict is a deep copy and does not share
322 lists with any other Resources dict.
323
324 If raise_if_missing is False, this will return an empty item for any
325 resource that had no match, instead of immediately raising an exception.
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200326
327 This function expects input dictionaries whose contents have already
328 been replicated based on its the 'times' attributes. See
329 config.replicate_times() for more details.
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200330 '''
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200331 matches = {}
Neels Hofmeyr17c139e2017-04-12 02:42:02 +0200332 for key, want_list in sorted(want.items()): # sorted for deterministic test results
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200333 # here we have a resource of a given type, e.g. 'bts', with a list
334 # containing as many BTSes as the caller wants to reserve/use. Each
335 # list item contains specifics for the particular BTS.
Neels Hofmeyr2a1a1fa2017-05-29 01:36:21 +0200336 my_list = self.get(key, [])
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200337
Neels Hofmeyrf9e86932017-06-06 19:47:40 +0200338 if log_label:
339 for_origin.log(log_label, len(want_list), 'x', key, '(candidates: %d)'%len(my_list))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200340
341 # Try to avoid a less constrained item snatching away a resource
342 # from a more detailed constrained requirement.
343
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200344 # first record all matches, so that each requested item has a list
345 # of all available resources that match it. Some resources may
346 # appear for multiple requested items. Store matching indexes.
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200347 all_matches = []
348 for want_item in want_list:
349 item_match_list = []
350 for i in range(len(my_list)):
351 my_item = my_list[i]
352 if skip_if_marked and my_item.get(skip_if_marked):
353 continue
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200354 if item_matches(my_item, want_item):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200355 item_match_list.append(i)
356 if not item_match_list:
Neels Hofmeyr9b907702017-05-06 23:20:33 +0200357 if raise_if_missing:
358 raise NoResourceExn('No matching resource available for %s = %r'
359 % (key, want_item))
360 else:
361 # this one failed... see below
362 all_matches = []
363 break
364
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200365 all_matches.append( item_match_list )
366
367 if not all_matches:
Neels Hofmeyr9b907702017-05-06 23:20:33 +0200368 # ...this one failed. Makes no sense to solve resource
369 # allocations, return an empty list for this key to mark
370 # failure.
371 matches[key] = []
372 continue
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200373
374 # figure out who gets what
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200375 try:
376 solution = solve(all_matches)
377 except NotSolvable:
378 # instead of a cryptic error message, raise an exception that
379 # conveys meaning to the user.
380 raise NoResourceExn('Could not resolve request to reserve resources: '
381 '%d x %s with requirements: %r' % (len(want_list), key, want_list))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200382 picked = [ my_list[i] for i in solution if i is not None ]
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200383 for_origin.dbg('Picked', config.tostr(picked))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200384 matches[key] = picked
385
386 return Resources(matches, do_copy=do_copy)
387
388 def set_hashes(self):
389 for key, item_list in self.items():
390 for item in item_list:
391 item[HASH_KEY] = util.hash_obj(item, HASH_KEY, RESERVED_KEY, USED_KEY)
392
393 def add(self, more):
394 if more is self:
395 raise RuntimeError('adding a list of resources to itself?')
396 config.add(self, copy.deepcopy(more))
397
398 def combine(self, more_rules):
399 if more_rules is self:
400 raise RuntimeError('combining a list of resource rules with itself?')
401 config.combine(self, copy.deepcopy(more))
402
403 def mark_reserved_by(self, origin_id):
404 for key, item_list in self.items():
405 for item in item_list:
406 item[RESERVED_KEY] = origin_id
407
408
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200409class NotSolvable(Exception):
410 pass
411
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200412def solve(all_matches):
413 '''
414 all_matches shall be a list of index-lists.
415 all_matches[i] is the list of indexes that item i can use.
416 Return a solution so that each i gets a different index.
417 solve([ [0, 1, 2],
418 [0],
419 [0, 2] ]) == [1, 0, 2]
420 '''
421
422 def all_differ(l):
423 return len(set(l)) == len(l)
424
425 def search_in_permutations(fixed=[]):
426 idx = len(fixed)
427 for i in range(len(all_matches[idx])):
428 val = all_matches[idx][i]
429 # don't add a val that's already in the list
430 if val in fixed:
431 continue
432 l = list(fixed)
433 l.append(val)
434 if len(l) == len(all_matches):
435 # found a solution
436 return l
437 # not at the end yet, add next digit
438 r = search_in_permutations(l)
439 if r:
440 # nested search_in_permutations() call found a solution
441 return r
442 # this entire branch yielded no solution
443 return None
444
445 if not all_matches:
446 raise RuntimeError('Cannot solve: no candidates')
447
448 solution = search_in_permutations()
449 if not solution:
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200450 raise NotSolvable('The requested resource requirements are not solvable %r'
451 % all_matches)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200452 return solution
453
454
455def contains_hash(list_of_dicts, a_hash):
456 for d in list_of_dicts:
457 if d.get(HASH_KEY) == a_hash:
458 return True
459 return False
460
461def item_matches(item, wanted_item, ignore_keys=None):
462 if is_dict(wanted_item):
463 # match up two dicts
464 if not isinstance(item, dict):
465 return False
466 for key, wanted_val in wanted_item.items():
467 if ignore_keys and key in ignore_keys:
468 continue
469 if not item_matches(item.get(key), wanted_val, ignore_keys=ignore_keys):
470 return False
471 return True
472
473 if is_list(wanted_item):
Pau Espin Pedrol4e36f7c2017-08-28 13:29:28 +0200474 if not is_list(item):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200475 return False
Pau Espin Pedrol58475512017-09-14 15:33:15 +0200476 # Validate that all elements in both lists are of the same type:
477 t = util.list_validate_same_elem_type(wanted_item + item)
478 if t is None:
479 return True # both lists are empty, return
480 # For lists of complex objects, we expect them to be sorted lists:
481 if t in (dict, list, tuple):
482 for i in range(max(len(wanted_item), len(item))):
483 log.ctx(idx=i)
484 subitem = item[i] if i < len(item) else util.empty_instance_type(t)
485 wanted_subitem = wanted_item[i] if i < len(wanted_item) else util.empty_instance_type(t)
486 if not item_matches(subitem, wanted_subitem, ignore_keys=ignore_keys):
487 return False
488 else: # for lists of basic elements, we handle them as unsorted sets:
489 for val in wanted_item:
490 if val not in item:
491 return False
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200492 return True
493
494 return item == wanted_item
495
496
497class ReservedResources(log.Origin):
498 '''
499 After all resources have been figured out, this is the API that a test case
500 gets to interact with resources. From those resources that have been
501 reserved for it, it can pick some to mark them as currently in use.
502 Functions like nitb() provide a resource by automatically picking its
503 dependencies from so far unused (but reserved) resource.
504 '''
505
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200506 def __init__(self, resources_pool, origin, reserved, modifiers):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200507 self.resources_pool = resources_pool
508 self.origin = origin
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200509 self.reserved_original = reserved
510 self.reserved = copy.deepcopy(self.reserved_original)
511 config.overlay(self.reserved, modifiers)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200512
513 def __repr__(self):
514 return 'resources(%s)=%s' % (self.origin.name(), pprint.pformat(self.reserved))
515
516 def get(self, kind, specifics=None):
517 if specifics is None:
518 specifics = {}
519 self.dbg('requesting use of', kind, specifics=specifics)
520 want = { kind: [specifics] }
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200521 available_dict = self.reserved.find(self.origin, want, skip_if_marked=USED_KEY,
522 do_copy=False, raise_if_missing=False,
523 log_label='Using')
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200524 available = available_dict.get(kind)
525 self.dbg(available=len(available))
526 if not available:
Neels Hofmeyrf9e86932017-06-06 19:47:40 +0200527 # cook up a detailed error message for the current situation
528 kind_reserved = self.reserved.get(kind, [])
529 used_count = len([r for r in kind_reserved if USED_KEY in r])
530 matching = self.reserved.find(self.origin, want, raise_if_missing=False, log_label=None).get(kind, [])
531 if not matching:
532 msg = 'none of the reserved resources matches requirements %r' % specifics
533 elif not (used_count < len(kind_reserved)):
534 msg = 'suite.conf reserved only %d x %r.' % (len(kind_reserved), kind)
535 else:
536 msg = ('No unused resource left that matches the requirements;'
537 ' Of reserved %d x %r, %d match the requirements, but all are already in use;'
538 ' Requirements: %r'
539 % (len(kind_reserved), kind, len(matching), specifics))
540 raise NoResourceExn('When trying to use instance nr %d of %r: %s' % (used_count + 1, kind, msg))
541
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200542 pick = available[0]
543 self.dbg(using=pick)
544 assert not pick.get(USED_KEY)
545 pick[USED_KEY] = True
546 return copy.deepcopy(pick)
547
548 def put(self, item):
549 if not item.get(USED_KEY):
550 raise RuntimeError('Can only put() a resource that is used: %r' % item)
551 hash_to_put = item.get(HASH_KEY)
552 if not hash_to_put:
553 raise RuntimeError('Can only put() a resource that has a hash marker: %r' % item)
554 for key, item_list in self.reserved.items():
555 my_list = self.get(key)
556 for my_item in my_list:
557 if hash_to_put == my_item.get(HASH_KEY):
558 my_item.pop(USED_KEY)
559
560 def put_all(self):
Pau Espin Pedrol1dd29552017-06-13 18:07:57 +0200561 if not self.reserved:
562 return
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200563 for key, item_list in self.reserved.items():
Pau Espin Pedrol1dd29552017-06-13 18:07:57 +0200564 for item in item_list:
565 item.pop(USED_KEY, None)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200566
567 def free(self):
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200568 if self.reserved_original:
569 self.resources_pool.free(self.origin, self.reserved_original)
570 self.reserved_original = None
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200571
Neels Hofmeyr2d1d5612017-05-22 20:02:41 +0200572 def counts(self):
573 counts = {}
574 for key in self.reserved.keys():
575 counts[key] = self.count(key)
576 return counts
577
578 def count(self, key):
579 return len(self.reserved.get(key) or [])
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200580
581# vim: expandtab tabstop=4 shiftwidth=4