blob: a536e18142a087dabf98adb8fdf056b10e02a34c [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
Holger Hans Peter Freytherd2db10d2018-12-06 18:34:53 +000029from . import bts_sysmo, bts_osmotrx, bts_osmovirtual, bts_octphy, bts_nanobts
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +020030
Neels Hofmeyr3531a192017-03-28 14:30:28 +020031from .util import is_dict, is_list
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +020032
Neels Hofmeyr3531a192017-03-28 14:30:28 +020033HASH_KEY = '_hash'
34RESERVED_KEY = '_reserved_by'
35USED_KEY = '_used'
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +020036
Neels Hofmeyr3531a192017-03-28 14:30:28 +020037RESOURCES_CONF = 'resources.conf'
Neels Hofmeyr3531a192017-03-28 14:30:28 +020038RESERVED_RESOURCES_FILE = 'reserved_resources.state'
39
Neels Hofmeyr76d81032017-05-18 18:35:32 +020040R_IP_ADDRESS = 'ip_address'
Neels Hofmeyr3531a192017-03-28 14:30:28 +020041R_BTS = 'bts'
42R_ARFCN = 'arfcn'
43R_MODEM = 'modem'
Pau Espin Pedrolbc1ed882018-05-17 16:59:58 +020044R_OSMOCON = 'osmocon_phone'
45R_ALL = (R_IP_ADDRESS, R_BTS, R_ARFCN, R_MODEM, R_OSMOCON)
Neels Hofmeyr3531a192017-03-28 14:30:28 +020046
47RESOURCES_SCHEMA = {
Neels Hofmeyr76d81032017-05-18 18:35:32 +020048 'ip_address[].addr': schema.IPV4,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020049 'bts[].label': schema.STR,
50 'bts[].type': schema.STR,
Pau Espin Pedrolfa9a6d32017-09-12 15:13:21 +020051 'bts[].ipa_unit_id': schema.UINT,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020052 'bts[].addr': schema.IPV4,
53 'bts[].band': schema.BAND,
Pau Espin Pedrolce35d912017-11-23 11:01:24 +010054 'bts[].direct_pcu': schema.BOOL_STR,
Pau Espin Pedrol57497a62017-08-28 14:21:15 +020055 'bts[].ciphers[]': schema.CIPHER,
Pau Espin Pedrol722e94e2018-08-22 11:01:32 +020056 'bts[].channel_allocator': schema.CHAN_ALLOCATOR,
Pau Espin Pedrol4f23ab52018-10-29 11:30:00 +010057 'bts[].gprs_mode': schema.GPRS_MODE,
Pau Espin Pedrol39df7f42018-05-07 13:49:33 +020058 'bts[].num_trx': schema.UINT,
59 'bts[].max_trx': schema.UINT,
Pau Espin Pedrolf6a07122018-07-27 16:24:29 +020060 'bts[].trx_list[].addr': schema.IPV4,
Your Name44af3412017-04-13 03:11:59 +020061 'bts[].trx_list[].hw_addr': schema.HWADDR,
62 'bts[].trx_list[].net_device': schema.STR,
Pau Espin Pedrolb26f32a2017-09-14 13:52:28 +020063 'bts[].trx_list[].nominal_power': schema.UINT,
64 'bts[].trx_list[].max_power_red': schema.UINT,
Pau Espin Pedrolc9b63762018-05-07 01:57:01 +020065 'bts[].trx_list[].timeslot_list[].phys_chan_config': schema.PHY_CHAN,
Pau Espin Pedrolf6a07122018-07-27 16:24:29 +020066 'bts[].trx_list[].power_supply.type': schema.STR,
67 'bts[].trx_list[].power_supply.device': schema.STR,
68 'bts[].trx_list[].power_supply.port': schema.STR,
Pau Espin Pedrol0d455042018-08-27 17:07:41 +020069 'bts[].osmo_trx.launch_trx': schema.BOOL_STR,
70 'bts[].osmo_trx.type': schema.STR,
71 'bts[].osmo_trx.clock_reference': schema.OSMO_TRX_CLOCK_REF,
72 'bts[].osmo_trx.trx_ip': schema.IPV4,
Pau Espin Pedrola9006df2018-10-01 12:26:39 +020073 'bts[].osmo_trx.remote_user': schema.STR,
Pau Espin Pedrol8cfa10f2018-11-06 12:05:19 +010074 'bts[].osmo_trx.dev_args': schema.STR,
Pau Espin Pedrol94eab262018-09-17 20:25:55 +020075 'bts[].osmo_trx.multi_arfcn': schema.BOOL_STR,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020076 'arfcn[].arfcn': schema.INT,
77 'arfcn[].band': schema.BAND,
Holger Hans Peter Freyther93a89422019-02-27 06:36:36 +000078 'modem[].type': schema.STR,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020079 '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,
Holger Hans Peter Freytherd2db10d2018-12-06 18:34:53 +0000103 'osmo-bts-virtual': bts_osmovirtual.OsmoBtsVirtual,
Pau Espin Pedrol1b28a582018-03-08 21:01:26 +0100104 'nanobts': bts_nanobts.NanoBts,
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200105 }
106
107def register_bts_type(name, clazz):
108 KNOWN_BTS_TYPES[name] = clazz
109
110class ResourcesPool(log.Origin):
111 _remember_to_free = None
112 _registered_exit_handler = False
113
114 def __init__(self):
115 self.config_path = config.get_config_file(RESOURCES_CONF)
116 self.state_dir = config.get_state_dir()
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200117 super().__init__(log.C_CNF, conf=self.config_path, state=self.state_dir.path)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200118 self.read_conf()
119
120 def read_conf(self):
121 self.all_resources = Resources(config.read(self.config_path, RESOURCES_SCHEMA))
122 self.all_resources.set_hashes()
123
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200124 def reserve(self, origin, want, modifiers):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200125 '''
126 attempt to reserve the resources specified in the dict 'want' for
127 'origin'. Obtain a lock on the resources lock dir, verify that all
128 wanted resources are available, and if yes mark them as reserved.
129
130 On success, return a reservation object which can be used to release
131 the reservation. The reservation will be freed automatically on program
132 exit, if not yet done manually.
133
134 'origin' should be an Origin() instance.
135
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200136 'want' is a dict matching RESOURCES_SCHEMA, used to specify what to
137 reserve.
138
139 'modifiers' is a dict matching RESOURCES_SCHEMA, it is overlaid on top
140 of 'want'.
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200141
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200142 If an entry has no attribute set, any of the resources may be
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200143 reserved without further limitations.
144
145 ResourcesPool may also be selected with narrowed down constraints.
Neels Hofmeyr76d81032017-05-18 18:35:32 +0200146 This would reserve one IP address, two modems, one BTS of type
Neels Hofmeyr391afe32017-05-18 19:22:12 +0200147 sysmo and one of type trx, plus 2 ARFCNs in the 1800 band:
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200148
149 {
Neels Hofmeyr76d81032017-05-18 18:35:32 +0200150 'ip_address': [ {} ],
Neels Hofmeyr391afe32017-05-18 19:22:12 +0200151 'bts': [ { 'type': 'sysmo' }, { 'type': 'trx' } ],
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200152 'arfcn': [ { 'band': 'GSM-1800' }, { 'band': 'GSM-1800' } ],
153 'modem': [ {}, {} ],
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200154 }
155 '''
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200156 schema.validate(want, RESOURCES_SCHEMA)
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200157 schema.validate(modifiers, RESOURCES_SCHEMA)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200158
159 origin_id = origin.origin_id()
160
161 with self.state_dir.lock(origin_id):
162 rrfile_path = self.state_dir.mk_parentdir(RESERVED_RESOURCES_FILE)
163 reserved = Resources(config.read(rrfile_path, if_missing_return={}))
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200164 to_be_reserved = self.all_resources.without(reserved).find(origin, want)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200165
166 to_be_reserved.mark_reserved_by(origin_id)
167
168 reserved.add(to_be_reserved)
169 config.write(rrfile_path, reserved)
170
171 self.remember_to_free(to_be_reserved)
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200172 return ReservedResources(self, origin, to_be_reserved, modifiers)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200173
174 def free(self, origin, to_be_freed):
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200175 log.ctx(origin)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200176 with self.state_dir.lock(origin.origin_id()):
177 rrfile_path = self.state_dir.mk_parentdir(RESERVED_RESOURCES_FILE)
178 reserved = Resources(config.read(rrfile_path, if_missing_return={}))
179 reserved.drop(to_be_freed)
180 config.write(rrfile_path, reserved)
181 self.forget_freed(to_be_freed)
182
183 def register_exit_handler(self):
184 if self._registered_exit_handler:
185 return
186 atexit.register(self.clean_up_registered_resources)
187 self._registered_exit_handler = True
188
189 def unregister_exit_handler(self):
190 if not self._registered_exit_handler:
191 return
192 atexit.unregister(self.clean_up_registered_resources)
193 self._registered_exit_handler = False
194
195 def clean_up_registered_resources(self):
196 if not self._remember_to_free:
197 return
198 self.free(log.Origin('atexit.clean_up_registered_resources()'),
199 self._remember_to_free)
200
201 def remember_to_free(self, to_be_reserved):
202 self.register_exit_handler()
203 if not self._remember_to_free:
204 self._remember_to_free = Resources()
205 self._remember_to_free.add(to_be_reserved)
206
207 def forget_freed(self, freed):
208 if freed is self._remember_to_free:
209 self._remember_to_free.clear()
210 else:
211 self._remember_to_free.drop(freed)
212 if not self._remember_to_free:
213 self.unregister_exit_handler()
214
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100215 def next_persistent_value(self, token, first_val, validate_func, inc_func, origin):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200216 origin_id = origin.origin_id()
217
218 with self.state_dir.lock(origin_id):
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100219 token_path = self.state_dir.child('last_used_%s.state' % token)
220 log.ctx(token_path)
221 last_value = first_val
222 if os.path.exists(token_path):
223 if not os.path.isfile(token_path):
224 raise RuntimeError('path should be a file but is not: %r' % token_path)
225 with open(token_path, 'r') as f:
226 last_value = f.read().strip()
227 validate_func(last_value)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200228
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100229 next_value = inc_func(last_value)
230 with open(token_path, 'w') as f:
231 f.write(next_value)
232 return next_value
233
234 def next_msisdn(self, origin):
235 return self.next_persistent_value('msisdn', '1000', schema.msisdn, util.msisdn_inc, origin)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200236
Pau Espin Pedrol5e0c2512017-11-06 18:40:23 +0100237 def next_lac(self, origin):
Pau Espin Pedrolf7f06362017-11-28 12:56:35 +0100238 # LAC=0 has special meaning (MS detached), avoid it
239 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 +0200240
Pau Espin Pedrol8a3a7b52017-11-28 15:50:02 +0100241 def next_rac(self, origin):
242 return self.next_persistent_value('rac', '1', schema.uint8, lambda x: str((int(x)+1) % pow(2,8) or 1), origin)
243
Pau Espin Pedrol4ccce7c2017-11-07 11:13:20 +0100244 def next_cellid(self, origin):
245 return self.next_persistent_value('cellid', '1', schema.uint16, lambda x: str((int(x)+1) % pow(2,16)), origin)
246
Pau Espin Pedrol8a3a7b52017-11-28 15:50:02 +0100247 def next_bvci(self, origin):
248 # BVCI=0 and =1 are reserved, avoid them.
249 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)
250
Pau Espin Pedrol4676cbd2017-09-14 17:35:03 +0200251class NoResourceExn(log.Error):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200252 pass
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200253
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200254class Resources(dict):
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200255
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200256 def __init__(self, all_resources={}, do_copy=True):
257 if do_copy:
258 all_resources = copy.deepcopy(all_resources)
259 self.update(all_resources)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200260
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200261 def drop(self, reserved, fail_if_not_found=True):
262 # protect from modifying reserved because we're the same object
263 if reserved is self:
264 raise RuntimeError('Refusing to drop a list of resources from itself.'
265 ' This is probably a bug where a list of Resources()'
266 ' should have been copied but is passed as-is.'
267 ' use Resources.clear() instead.')
268
269 for key, reserved_list in reserved.items():
270 my_list = self.get(key) or []
271
272 if my_list is reserved_list:
273 self.pop(key)
274 continue
275
276 for reserved_item in reserved_list:
277 found = False
278 reserved_hash = reserved_item.get(HASH_KEY)
279 if not reserved_hash:
280 raise RuntimeError('Resources.drop() only works with hashed items')
281
282 for i in range(len(my_list)):
283 my_item = my_list[i]
284 my_hash = my_item.get(HASH_KEY)
285 if not my_hash:
286 raise RuntimeError('Resources.drop() only works with hashed items')
287 if my_hash == reserved_hash:
288 found = True
289 my_list.pop(i)
290 break
291
292 if fail_if_not_found and not found:
293 raise RuntimeError('Asked to drop resource from a pool, but the'
294 ' resource was not found: %s = %r' % (key, reserved_item))
295
296 if not my_list:
297 self.pop(key)
298 return self
299
300 def without(self, reserved):
301 return Resources(self).drop(reserved)
302
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200303 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 +0200304 '''
305 Pass a dict of resource requirements, e.g.:
306 want = {
307 'bts': [ {'type': 'osmo-bts-sysmo',}, {} ],
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200308 'modem': [ {}, {}, {} ]
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200309 }
310 This function tries to find a combination from the available resources that
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200311 matches these requirements. The return value is a dict (wrapped in a Resources class)
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200312 that contains the matching resources in the order of 'want' dict: in above
313 example, the returned dict would have a 'bts' list with the first item being
314 a sysmoBTS, the second item being any other available BTS.
315
316 If skip_if_marked is passed, any resource that contains this key is skipped.
317 E.g. if a BTS has the USED_KEY set like
318 reserved_resources = { 'bts' : {..., '_used': True} }
319 then this may be skipped by passing skip_if_marked='_used'
320 (or rather skip_if_marked=USED_KEY).
321
322 If do_copy is True, the returned dict is a deep copy and does not share
323 lists with any other Resources dict.
324
325 If raise_if_missing is False, this will return an empty item for any
326 resource that had no match, instead of immediately raising an exception.
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200327
328 This function expects input dictionaries whose contents have already
329 been replicated based on its the 'times' attributes. See
330 config.replicate_times() for more details.
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200331 '''
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200332 matches = {}
Neels Hofmeyr17c139e2017-04-12 02:42:02 +0200333 for key, want_list in sorted(want.items()): # sorted for deterministic test results
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200334 # here we have a resource of a given type, e.g. 'bts', with a list
335 # containing as many BTSes as the caller wants to reserve/use. Each
336 # list item contains specifics for the particular BTS.
Neels Hofmeyr2a1a1fa2017-05-29 01:36:21 +0200337 my_list = self.get(key, [])
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200338
Neels Hofmeyrf9e86932017-06-06 19:47:40 +0200339 if log_label:
340 for_origin.log(log_label, len(want_list), 'x', key, '(candidates: %d)'%len(my_list))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200341
342 # Try to avoid a less constrained item snatching away a resource
343 # from a more detailed constrained requirement.
344
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200345 # first record all matches, so that each requested item has a list
346 # of all available resources that match it. Some resources may
347 # appear for multiple requested items. Store matching indexes.
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200348 all_matches = []
349 for want_item in want_list:
350 item_match_list = []
351 for i in range(len(my_list)):
352 my_item = my_list[i]
353 if skip_if_marked and my_item.get(skip_if_marked):
354 continue
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200355 if item_matches(my_item, want_item):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200356 item_match_list.append(i)
357 if not item_match_list:
Neels Hofmeyr9b907702017-05-06 23:20:33 +0200358 if raise_if_missing:
359 raise NoResourceExn('No matching resource available for %s = %r'
360 % (key, want_item))
361 else:
362 # this one failed... see below
363 all_matches = []
364 break
365
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200366 all_matches.append( item_match_list )
367
368 if not all_matches:
Neels Hofmeyr9b907702017-05-06 23:20:33 +0200369 # ...this one failed. Makes no sense to solve resource
370 # allocations, return an empty list for this key to mark
371 # failure.
372 matches[key] = []
373 continue
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200374
375 # figure out who gets what
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200376 try:
377 solution = solve(all_matches)
378 except NotSolvable:
379 # instead of a cryptic error message, raise an exception that
380 # conveys meaning to the user.
381 raise NoResourceExn('Could not resolve request to reserve resources: '
382 '%d x %s with requirements: %r' % (len(want_list), key, want_list))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200383 picked = [ my_list[i] for i in solution if i is not None ]
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200384 for_origin.dbg('Picked', config.tostr(picked))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200385 matches[key] = picked
386
387 return Resources(matches, do_copy=do_copy)
388
389 def set_hashes(self):
390 for key, item_list in self.items():
391 for item in item_list:
392 item[HASH_KEY] = util.hash_obj(item, HASH_KEY, RESERVED_KEY, USED_KEY)
393
394 def add(self, more):
395 if more is self:
396 raise RuntimeError('adding a list of resources to itself?')
397 config.add(self, copy.deepcopy(more))
398
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200399 def mark_reserved_by(self, origin_id):
400 for key, item_list in self.items():
401 for item in item_list:
402 item[RESERVED_KEY] = origin_id
403
404
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200405class NotSolvable(Exception):
406 pass
407
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200408def solve(all_matches):
409 '''
410 all_matches shall be a list of index-lists.
411 all_matches[i] is the list of indexes that item i can use.
412 Return a solution so that each i gets a different index.
413 solve([ [0, 1, 2],
414 [0],
415 [0, 2] ]) == [1, 0, 2]
416 '''
417
418 def all_differ(l):
419 return len(set(l)) == len(l)
420
421 def search_in_permutations(fixed=[]):
422 idx = len(fixed)
423 for i in range(len(all_matches[idx])):
424 val = all_matches[idx][i]
425 # don't add a val that's already in the list
426 if val in fixed:
427 continue
428 l = list(fixed)
429 l.append(val)
430 if len(l) == len(all_matches):
431 # found a solution
432 return l
433 # not at the end yet, add next digit
434 r = search_in_permutations(l)
435 if r:
436 # nested search_in_permutations() call found a solution
437 return r
438 # this entire branch yielded no solution
439 return None
440
441 if not all_matches:
442 raise RuntimeError('Cannot solve: no candidates')
443
444 solution = search_in_permutations()
445 if not solution:
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200446 raise NotSolvable('The requested resource requirements are not solvable %r'
447 % all_matches)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200448 return solution
449
450
451def contains_hash(list_of_dicts, a_hash):
452 for d in list_of_dicts:
453 if d.get(HASH_KEY) == a_hash:
454 return True
455 return False
456
457def item_matches(item, wanted_item, ignore_keys=None):
458 if is_dict(wanted_item):
459 # match up two dicts
460 if not isinstance(item, dict):
461 return False
462 for key, wanted_val in wanted_item.items():
463 if ignore_keys and key in ignore_keys:
464 continue
465 if not item_matches(item.get(key), wanted_val, ignore_keys=ignore_keys):
466 return False
467 return True
468
469 if is_list(wanted_item):
Pau Espin Pedrol4e36f7c2017-08-28 13:29:28 +0200470 if not is_list(item):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200471 return False
Pau Espin Pedrol58475512017-09-14 15:33:15 +0200472 # Validate that all elements in both lists are of the same type:
473 t = util.list_validate_same_elem_type(wanted_item + item)
474 if t is None:
475 return True # both lists are empty, return
476 # For lists of complex objects, we expect them to be sorted lists:
477 if t in (dict, list, tuple):
478 for i in range(max(len(wanted_item), len(item))):
479 log.ctx(idx=i)
480 subitem = item[i] if i < len(item) else util.empty_instance_type(t)
481 wanted_subitem = wanted_item[i] if i < len(wanted_item) else util.empty_instance_type(t)
482 if not item_matches(subitem, wanted_subitem, ignore_keys=ignore_keys):
483 return False
484 else: # for lists of basic elements, we handle them as unsorted sets:
485 for val in wanted_item:
486 if val not in item:
487 return False
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200488 return True
489
490 return item == wanted_item
491
492
493class ReservedResources(log.Origin):
494 '''
495 After all resources have been figured out, this is the API that a test case
496 gets to interact with resources. From those resources that have been
497 reserved for it, it can pick some to mark them as currently in use.
498 Functions like nitb() provide a resource by automatically picking its
499 dependencies from so far unused (but reserved) resource.
500 '''
501
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200502 def __init__(self, resources_pool, origin, reserved, modifiers):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200503 self.resources_pool = resources_pool
504 self.origin = origin
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200505 self.reserved_original = reserved
506 self.reserved = copy.deepcopy(self.reserved_original)
507 config.overlay(self.reserved, modifiers)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200508
509 def __repr__(self):
510 return 'resources(%s)=%s' % (self.origin.name(), pprint.pformat(self.reserved))
511
512 def get(self, kind, specifics=None):
513 if specifics is None:
514 specifics = {}
515 self.dbg('requesting use of', kind, specifics=specifics)
516 want = { kind: [specifics] }
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200517 available_dict = self.reserved.find(self.origin, want, skip_if_marked=USED_KEY,
518 do_copy=False, raise_if_missing=False,
519 log_label='Using')
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200520 available = available_dict.get(kind)
521 self.dbg(available=len(available))
522 if not available:
Neels Hofmeyrf9e86932017-06-06 19:47:40 +0200523 # cook up a detailed error message for the current situation
524 kind_reserved = self.reserved.get(kind, [])
525 used_count = len([r for r in kind_reserved if USED_KEY in r])
526 matching = self.reserved.find(self.origin, want, raise_if_missing=False, log_label=None).get(kind, [])
527 if not matching:
528 msg = 'none of the reserved resources matches requirements %r' % specifics
529 elif not (used_count < len(kind_reserved)):
530 msg = 'suite.conf reserved only %d x %r.' % (len(kind_reserved), kind)
531 else:
532 msg = ('No unused resource left that matches the requirements;'
533 ' Of reserved %d x %r, %d match the requirements, but all are already in use;'
534 ' Requirements: %r'
535 % (len(kind_reserved), kind, len(matching), specifics))
536 raise NoResourceExn('When trying to use instance nr %d of %r: %s' % (used_count + 1, kind, msg))
537
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200538 pick = available[0]
539 self.dbg(using=pick)
540 assert not pick.get(USED_KEY)
541 pick[USED_KEY] = True
542 return copy.deepcopy(pick)
543
544 def put(self, item):
545 if not item.get(USED_KEY):
546 raise RuntimeError('Can only put() a resource that is used: %r' % item)
547 hash_to_put = item.get(HASH_KEY)
548 if not hash_to_put:
549 raise RuntimeError('Can only put() a resource that has a hash marker: %r' % item)
550 for key, item_list in self.reserved.items():
551 my_list = self.get(key)
552 for my_item in my_list:
553 if hash_to_put == my_item.get(HASH_KEY):
554 my_item.pop(USED_KEY)
555
556 def put_all(self):
Pau Espin Pedrol1dd29552017-06-13 18:07:57 +0200557 if not self.reserved:
558 return
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200559 for key, item_list in self.reserved.items():
Pau Espin Pedrol1dd29552017-06-13 18:07:57 +0200560 for item in item_list:
561 item.pop(USED_KEY, None)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200562
563 def free(self):
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200564 if self.reserved_original:
565 self.resources_pool.free(self.origin, self.reserved_original)
566 self.reserved_original = None
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200567
Neels Hofmeyr2d1d5612017-05-22 20:02:41 +0200568 def counts(self):
569 counts = {}
570 for key in self.reserved.keys():
571 counts[key] = self.count(key)
572 return counts
573
574 def count(self, key):
575 return len(self.reserved.get(key) or [])
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200576
577# vim: expandtab tabstop=4 shiftwidth=4