blob: e4e2f9d7b6a7c3d0757d6f5a45318d25e51327d9 [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 Pedrol404e1502017-08-22 11:17:43 +020057 'bts[].trx_remote_ip': schema.IPV4,
58 'bts[].launch_trx': schema.BOOL_STR,
Pau Espin Pedrolce35d912017-11-23 11:01:24 +010059 'bts[].direct_pcu': schema.BOOL_STR,
Pau Espin Pedrol57497a62017-08-28 14:21:15 +020060 'bts[].ciphers[]': schema.CIPHER,
Pau Espin Pedrol722e94e2018-08-22 11:01:32 +020061 'bts[].channel_allocator': schema.CHAN_ALLOCATOR,
Pau Espin Pedrol39df7f42018-05-07 13:49:33 +020062 'bts[].num_trx': schema.UINT,
63 'bts[].max_trx': schema.UINT,
Pau Espin Pedrolf6a07122018-07-27 16:24:29 +020064 'bts[].trx_list[].addr': schema.IPV4,
Your Name44af3412017-04-13 03:11:59 +020065 'bts[].trx_list[].hw_addr': schema.HWADDR,
66 'bts[].trx_list[].net_device': schema.STR,
Pau Espin Pedrolb26f32a2017-09-14 13:52:28 +020067 'bts[].trx_list[].nominal_power': schema.UINT,
68 'bts[].trx_list[].max_power_red': schema.UINT,
Pau Espin Pedrolc9b63762018-05-07 01:57:01 +020069 'bts[].trx_list[].timeslot_list[].phys_chan_config': schema.PHY_CHAN,
Pau Espin Pedrolf6a07122018-07-27 16:24:29 +020070 'bts[].trx_list[].power_supply.type': schema.STR,
71 'bts[].trx_list[].power_supply.device': schema.STR,
72 'bts[].trx_list[].power_supply.port': schema.STR,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020073 'arfcn[].arfcn': schema.INT,
74 'arfcn[].band': schema.BAND,
75 'modem[].label': schema.STR,
76 'modem[].path': schema.STR,
77 'modem[].imsi': schema.IMSI,
78 'modem[].ki': schema.KI,
Pau Espin Pedrol713ce2c2017-08-24 16:57:17 +020079 'modem[].auth_algo': schema.AUTH_ALGO,
Pau Espin Pedrol57497a62017-08-28 14:21:15 +020080 'modem[].ciphers[]': schema.CIPHER,
Pau Espin Pedrolac18fd32017-08-31 18:49:47 +020081 'modem[].features[]': schema.MODEM_FEATURE,
Pau Espin Pedrolbc1ed882018-05-17 16:59:58 +020082 'osmocon_phone[].serial_device': schema.STR,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020083 }
84
85WANT_SCHEMA = util.dict_add(
Pau Espin Pedrolead79ac2017-09-12 15:19:18 +020086 dict([('%s[].times' % r, schema.TIMES) for r in R_ALL]),
Neels Hofmeyr3531a192017-03-28 14:30:28 +020087 RESOURCES_SCHEMA)
88
Pau Espin Pedrol0b302792017-09-10 16:33:10 +020089CONF_SCHEMA = util.dict_add(
90 { 'defaults.timeout': schema.STR },
Pau Espin Pedrolaab56922018-08-21 14:58:29 +020091 dict([('resources.%s' % key, val) for key, val in WANT_SCHEMA.items()]),
92 dict([('modifiers.%s' % key, val) for key, val in WANT_SCHEMA.items()]))
Pau Espin Pedrol0b302792017-09-10 16:33:10 +020093
Neels Hofmeyr3531a192017-03-28 14:30:28 +020094KNOWN_BTS_TYPES = {
Your Name44af3412017-04-13 03:11:59 +020095 'osmo-bts-sysmo': bts_sysmo.SysmoBts,
96 'osmo-bts-trx': bts_osmotrx.OsmoBtsTrx,
Pau Espin Pedroldaed4472017-09-15 14:11:35 +020097 'osmo-bts-octphy': bts_octphy.OsmoBtsOctphy,
Pau Espin Pedrol1b28a582018-03-08 21:01:26 +010098 'nanobts': bts_nanobts.NanoBts,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020099 }
100
101def register_bts_type(name, clazz):
102 KNOWN_BTS_TYPES[name] = clazz
103
104class ResourcesPool(log.Origin):
105 _remember_to_free = None
106 _registered_exit_handler = False
107
108 def __init__(self):
109 self.config_path = config.get_config_file(RESOURCES_CONF)
110 self.state_dir = config.get_state_dir()
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200111 super().__init__(log.C_CNF, conf=self.config_path, state=self.state_dir.path)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200112 self.read_conf()
113
114 def read_conf(self):
115 self.all_resources = Resources(config.read(self.config_path, RESOURCES_SCHEMA))
116 self.all_resources.set_hashes()
117
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200118 def reserve(self, origin, want, modifiers):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200119 '''
120 attempt to reserve the resources specified in the dict 'want' for
121 'origin'. Obtain a lock on the resources lock dir, verify that all
122 wanted resources are available, and if yes mark them as reserved.
123
124 On success, return a reservation object which can be used to release
125 the reservation. The reservation will be freed automatically on program
126 exit, if not yet done manually.
127
128 'origin' should be an Origin() instance.
129
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200130 'want' is a dict matching RESOURCES_SCHEMA, used to specify what to
131 reserve.
132
133 'modifiers' is a dict matching RESOURCES_SCHEMA, it is overlaid on top
134 of 'want'.
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200135
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200136 If an entry has no attribute set, any of the resources may be
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200137 reserved without further limitations.
138
139 ResourcesPool may also be selected with narrowed down constraints.
Neels Hofmeyr76d81032017-05-18 18:35:32 +0200140 This would reserve one IP address, two modems, one BTS of type
Neels Hofmeyr391afe32017-05-18 19:22:12 +0200141 sysmo and one of type trx, plus 2 ARFCNs in the 1800 band:
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200142
143 {
Neels Hofmeyr76d81032017-05-18 18:35:32 +0200144 'ip_address': [ {} ],
Neels Hofmeyr391afe32017-05-18 19:22:12 +0200145 'bts': [ { 'type': 'sysmo' }, { 'type': 'trx' } ],
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200146 'arfcn': [ { 'band': 'GSM-1800' }, { 'band': 'GSM-1800' } ],
147 'modem': [ {}, {} ],
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200148 }
149 '''
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200150 schema.validate(want, RESOURCES_SCHEMA)
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200151 schema.validate(modifiers, RESOURCES_SCHEMA)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200152
153 origin_id = origin.origin_id()
154
155 with self.state_dir.lock(origin_id):
156 rrfile_path = self.state_dir.mk_parentdir(RESERVED_RESOURCES_FILE)
157 reserved = Resources(config.read(rrfile_path, if_missing_return={}))
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200158 to_be_reserved = self.all_resources.without(reserved).find(origin, want)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200159
160 to_be_reserved.mark_reserved_by(origin_id)
161
162 reserved.add(to_be_reserved)
163 config.write(rrfile_path, reserved)
164
165 self.remember_to_free(to_be_reserved)
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200166 return ReservedResources(self, origin, to_be_reserved, modifiers)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200167
168 def free(self, origin, to_be_freed):
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200169 log.ctx(origin)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200170 with self.state_dir.lock(origin.origin_id()):
171 rrfile_path = self.state_dir.mk_parentdir(RESERVED_RESOURCES_FILE)
172 reserved = Resources(config.read(rrfile_path, if_missing_return={}))
173 reserved.drop(to_be_freed)
174 config.write(rrfile_path, reserved)
175 self.forget_freed(to_be_freed)
176
177 def register_exit_handler(self):
178 if self._registered_exit_handler:
179 return
180 atexit.register(self.clean_up_registered_resources)
181 self._registered_exit_handler = True
182
183 def unregister_exit_handler(self):
184 if not self._registered_exit_handler:
185 return
186 atexit.unregister(self.clean_up_registered_resources)
187 self._registered_exit_handler = False
188
189 def clean_up_registered_resources(self):
190 if not self._remember_to_free:
191 return
192 self.free(log.Origin('atexit.clean_up_registered_resources()'),
193 self._remember_to_free)
194
195 def remember_to_free(self, to_be_reserved):
196 self.register_exit_handler()
197 if not self._remember_to_free:
198 self._remember_to_free = Resources()
199 self._remember_to_free.add(to_be_reserved)
200
201 def forget_freed(self, freed):
202 if freed is self._remember_to_free:
203 self._remember_to_free.clear()
204 else:
205 self._remember_to_free.drop(freed)
206 if not self._remember_to_free:
207 self.unregister_exit_handler()
208
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100209 def next_persistent_value(self, token, first_val, validate_func, inc_func, origin):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200210 origin_id = origin.origin_id()
211
212 with self.state_dir.lock(origin_id):
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100213 token_path = self.state_dir.child('last_used_%s.state' % token)
214 log.ctx(token_path)
215 last_value = first_val
216 if os.path.exists(token_path):
217 if not os.path.isfile(token_path):
218 raise RuntimeError('path should be a file but is not: %r' % token_path)
219 with open(token_path, 'r') as f:
220 last_value = f.read().strip()
221 validate_func(last_value)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200222
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100223 next_value = inc_func(last_value)
224 with open(token_path, 'w') as f:
225 f.write(next_value)
226 return next_value
227
228 def next_msisdn(self, origin):
229 return self.next_persistent_value('msisdn', '1000', schema.msisdn, util.msisdn_inc, origin)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200230
Pau Espin Pedrol5e0c2512017-11-06 18:40:23 +0100231 def next_lac(self, origin):
Pau Espin Pedrolf7f06362017-11-28 12:56:35 +0100232 # LAC=0 has special meaning (MS detached), avoid it
233 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 +0200234
Pau Espin Pedrol8a3a7b52017-11-28 15:50:02 +0100235 def next_rac(self, origin):
236 return self.next_persistent_value('rac', '1', schema.uint8, lambda x: str((int(x)+1) % pow(2,8) or 1), origin)
237
Pau Espin Pedrol4ccce7c2017-11-07 11:13:20 +0100238 def next_cellid(self, origin):
239 return self.next_persistent_value('cellid', '1', schema.uint16, lambda x: str((int(x)+1) % pow(2,16)), origin)
240
Pau Espin Pedrol8a3a7b52017-11-28 15:50:02 +0100241 def next_bvci(self, origin):
242 # BVCI=0 and =1 are reserved, avoid them.
243 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)
244
Pau Espin Pedrol4676cbd2017-09-14 17:35:03 +0200245class NoResourceExn(log.Error):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200246 pass
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200247
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200248class Resources(dict):
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200249
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200250 def __init__(self, all_resources={}, do_copy=True):
251 if do_copy:
252 all_resources = copy.deepcopy(all_resources)
253 self.update(all_resources)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200254
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200255 def drop(self, reserved, fail_if_not_found=True):
256 # protect from modifying reserved because we're the same object
257 if reserved is self:
258 raise RuntimeError('Refusing to drop a list of resources from itself.'
259 ' This is probably a bug where a list of Resources()'
260 ' should have been copied but is passed as-is.'
261 ' use Resources.clear() instead.')
262
263 for key, reserved_list in reserved.items():
264 my_list = self.get(key) or []
265
266 if my_list is reserved_list:
267 self.pop(key)
268 continue
269
270 for reserved_item in reserved_list:
271 found = False
272 reserved_hash = reserved_item.get(HASH_KEY)
273 if not reserved_hash:
274 raise RuntimeError('Resources.drop() only works with hashed items')
275
276 for i in range(len(my_list)):
277 my_item = my_list[i]
278 my_hash = my_item.get(HASH_KEY)
279 if not my_hash:
280 raise RuntimeError('Resources.drop() only works with hashed items')
281 if my_hash == reserved_hash:
282 found = True
283 my_list.pop(i)
284 break
285
286 if fail_if_not_found and not found:
287 raise RuntimeError('Asked to drop resource from a pool, but the'
288 ' resource was not found: %s = %r' % (key, reserved_item))
289
290 if not my_list:
291 self.pop(key)
292 return self
293
294 def without(self, reserved):
295 return Resources(self).drop(reserved)
296
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200297 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 +0200298 '''
299 Pass a dict of resource requirements, e.g.:
300 want = {
301 'bts': [ {'type': 'osmo-bts-sysmo',}, {} ],
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200302 'modem': [ {}, {}, {} ]
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200303 }
304 This function tries to find a combination from the available resources that
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200305 matches these requirements. The return value is a dict (wrapped in a Resources class)
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200306 that contains the matching resources in the order of 'want' dict: in above
307 example, the returned dict would have a 'bts' list with the first item being
308 a sysmoBTS, the second item being any other available BTS.
309
310 If skip_if_marked is passed, any resource that contains this key is skipped.
311 E.g. if a BTS has the USED_KEY set like
312 reserved_resources = { 'bts' : {..., '_used': True} }
313 then this may be skipped by passing skip_if_marked='_used'
314 (or rather skip_if_marked=USED_KEY).
315
316 If do_copy is True, the returned dict is a deep copy and does not share
317 lists with any other Resources dict.
318
319 If raise_if_missing is False, this will return an empty item for any
320 resource that had no match, instead of immediately raising an exception.
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200321
322 This function expects input dictionaries whose contents have already
323 been replicated based on its the 'times' attributes. See
324 config.replicate_times() for more details.
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200325 '''
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200326 matches = {}
Neels Hofmeyr17c139e2017-04-12 02:42:02 +0200327 for key, want_list in sorted(want.items()): # sorted for deterministic test results
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200328 # here we have a resource of a given type, e.g. 'bts', with a list
329 # containing as many BTSes as the caller wants to reserve/use. Each
330 # list item contains specifics for the particular BTS.
Neels Hofmeyr2a1a1fa2017-05-29 01:36:21 +0200331 my_list = self.get(key, [])
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200332
Neels Hofmeyrf9e86932017-06-06 19:47:40 +0200333 if log_label:
334 for_origin.log(log_label, len(want_list), 'x', key, '(candidates: %d)'%len(my_list))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200335
336 # Try to avoid a less constrained item snatching away a resource
337 # from a more detailed constrained requirement.
338
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200339 # first record all matches, so that each requested item has a list
340 # of all available resources that match it. Some resources may
341 # appear for multiple requested items. Store matching indexes.
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200342 all_matches = []
343 for want_item in want_list:
344 item_match_list = []
345 for i in range(len(my_list)):
346 my_item = my_list[i]
347 if skip_if_marked and my_item.get(skip_if_marked):
348 continue
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200349 if item_matches(my_item, want_item):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200350 item_match_list.append(i)
351 if not item_match_list:
Neels Hofmeyr9b907702017-05-06 23:20:33 +0200352 if raise_if_missing:
353 raise NoResourceExn('No matching resource available for %s = %r'
354 % (key, want_item))
355 else:
356 # this one failed... see below
357 all_matches = []
358 break
359
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200360 all_matches.append( item_match_list )
361
362 if not all_matches:
Neels Hofmeyr9b907702017-05-06 23:20:33 +0200363 # ...this one failed. Makes no sense to solve resource
364 # allocations, return an empty list for this key to mark
365 # failure.
366 matches[key] = []
367 continue
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200368
369 # figure out who gets what
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200370 try:
371 solution = solve(all_matches)
372 except NotSolvable:
373 # instead of a cryptic error message, raise an exception that
374 # conveys meaning to the user.
375 raise NoResourceExn('Could not resolve request to reserve resources: '
376 '%d x %s with requirements: %r' % (len(want_list), key, want_list))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200377 picked = [ my_list[i] for i in solution if i is not None ]
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200378 for_origin.dbg('Picked', config.tostr(picked))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200379 matches[key] = picked
380
381 return Resources(matches, do_copy=do_copy)
382
383 def set_hashes(self):
384 for key, item_list in self.items():
385 for item in item_list:
386 item[HASH_KEY] = util.hash_obj(item, HASH_KEY, RESERVED_KEY, USED_KEY)
387
388 def add(self, more):
389 if more is self:
390 raise RuntimeError('adding a list of resources to itself?')
391 config.add(self, copy.deepcopy(more))
392
393 def combine(self, more_rules):
394 if more_rules is self:
395 raise RuntimeError('combining a list of resource rules with itself?')
396 config.combine(self, copy.deepcopy(more))
397
398 def mark_reserved_by(self, origin_id):
399 for key, item_list in self.items():
400 for item in item_list:
401 item[RESERVED_KEY] = origin_id
402
403
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200404class NotSolvable(Exception):
405 pass
406
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200407def solve(all_matches):
408 '''
409 all_matches shall be a list of index-lists.
410 all_matches[i] is the list of indexes that item i can use.
411 Return a solution so that each i gets a different index.
412 solve([ [0, 1, 2],
413 [0],
414 [0, 2] ]) == [1, 0, 2]
415 '''
416
417 def all_differ(l):
418 return len(set(l)) == len(l)
419
420 def search_in_permutations(fixed=[]):
421 idx = len(fixed)
422 for i in range(len(all_matches[idx])):
423 val = all_matches[idx][i]
424 # don't add a val that's already in the list
425 if val in fixed:
426 continue
427 l = list(fixed)
428 l.append(val)
429 if len(l) == len(all_matches):
430 # found a solution
431 return l
432 # not at the end yet, add next digit
433 r = search_in_permutations(l)
434 if r:
435 # nested search_in_permutations() call found a solution
436 return r
437 # this entire branch yielded no solution
438 return None
439
440 if not all_matches:
441 raise RuntimeError('Cannot solve: no candidates')
442
443 solution = search_in_permutations()
444 if not solution:
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200445 raise NotSolvable('The requested resource requirements are not solvable %r'
446 % all_matches)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200447 return solution
448
449
450def contains_hash(list_of_dicts, a_hash):
451 for d in list_of_dicts:
452 if d.get(HASH_KEY) == a_hash:
453 return True
454 return False
455
456def item_matches(item, wanted_item, ignore_keys=None):
457 if is_dict(wanted_item):
458 # match up two dicts
459 if not isinstance(item, dict):
460 return False
461 for key, wanted_val in wanted_item.items():
462 if ignore_keys and key in ignore_keys:
463 continue
464 if not item_matches(item.get(key), wanted_val, ignore_keys=ignore_keys):
465 return False
466 return True
467
468 if is_list(wanted_item):
Pau Espin Pedrol4e36f7c2017-08-28 13:29:28 +0200469 if not is_list(item):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200470 return False
Pau Espin Pedrol58475512017-09-14 15:33:15 +0200471 # Validate that all elements in both lists are of the same type:
472 t = util.list_validate_same_elem_type(wanted_item + item)
473 if t is None:
474 return True # both lists are empty, return
475 # For lists of complex objects, we expect them to be sorted lists:
476 if t in (dict, list, tuple):
477 for i in range(max(len(wanted_item), len(item))):
478 log.ctx(idx=i)
479 subitem = item[i] if i < len(item) else util.empty_instance_type(t)
480 wanted_subitem = wanted_item[i] if i < len(wanted_item) else util.empty_instance_type(t)
481 if not item_matches(subitem, wanted_subitem, ignore_keys=ignore_keys):
482 return False
483 else: # for lists of basic elements, we handle them as unsorted sets:
484 for val in wanted_item:
485 if val not in item:
486 return False
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200487 return True
488
489 return item == wanted_item
490
491
492class ReservedResources(log.Origin):
493 '''
494 After all resources have been figured out, this is the API that a test case
495 gets to interact with resources. From those resources that have been
496 reserved for it, it can pick some to mark them as currently in use.
497 Functions like nitb() provide a resource by automatically picking its
498 dependencies from so far unused (but reserved) resource.
499 '''
500
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200501 def __init__(self, resources_pool, origin, reserved, modifiers):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200502 self.resources_pool = resources_pool
503 self.origin = origin
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200504 self.reserved_original = reserved
505 self.reserved = copy.deepcopy(self.reserved_original)
506 config.overlay(self.reserved, modifiers)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200507
508 def __repr__(self):
509 return 'resources(%s)=%s' % (self.origin.name(), pprint.pformat(self.reserved))
510
511 def get(self, kind, specifics=None):
512 if specifics is None:
513 specifics = {}
514 self.dbg('requesting use of', kind, specifics=specifics)
515 want = { kind: [specifics] }
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200516 available_dict = self.reserved.find(self.origin, want, skip_if_marked=USED_KEY,
517 do_copy=False, raise_if_missing=False,
518 log_label='Using')
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200519 available = available_dict.get(kind)
520 self.dbg(available=len(available))
521 if not available:
Neels Hofmeyrf9e86932017-06-06 19:47:40 +0200522 # cook up a detailed error message for the current situation
523 kind_reserved = self.reserved.get(kind, [])
524 used_count = len([r for r in kind_reserved if USED_KEY in r])
525 matching = self.reserved.find(self.origin, want, raise_if_missing=False, log_label=None).get(kind, [])
526 if not matching:
527 msg = 'none of the reserved resources matches requirements %r' % specifics
528 elif not (used_count < len(kind_reserved)):
529 msg = 'suite.conf reserved only %d x %r.' % (len(kind_reserved), kind)
530 else:
531 msg = ('No unused resource left that matches the requirements;'
532 ' Of reserved %d x %r, %d match the requirements, but all are already in use;'
533 ' Requirements: %r'
534 % (len(kind_reserved), kind, len(matching), specifics))
535 raise NoResourceExn('When trying to use instance nr %d of %r: %s' % (used_count + 1, kind, msg))
536
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200537 pick = available[0]
538 self.dbg(using=pick)
539 assert not pick.get(USED_KEY)
540 pick[USED_KEY] = True
541 return copy.deepcopy(pick)
542
543 def put(self, item):
544 if not item.get(USED_KEY):
545 raise RuntimeError('Can only put() a resource that is used: %r' % item)
546 hash_to_put = item.get(HASH_KEY)
547 if not hash_to_put:
548 raise RuntimeError('Can only put() a resource that has a hash marker: %r' % item)
549 for key, item_list in self.reserved.items():
550 my_list = self.get(key)
551 for my_item in my_list:
552 if hash_to_put == my_item.get(HASH_KEY):
553 my_item.pop(USED_KEY)
554
555 def put_all(self):
Pau Espin Pedrol1dd29552017-06-13 18:07:57 +0200556 if not self.reserved:
557 return
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200558 for key, item_list in self.reserved.items():
Pau Espin Pedrol1dd29552017-06-13 18:07:57 +0200559 for item in item_list:
560 item.pop(USED_KEY, None)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200561
562 def free(self):
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200563 if self.reserved_original:
564 self.resources_pool.free(self.origin, self.reserved_original)
565 self.reserved_original = None
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200566
Neels Hofmeyr2d1d5612017-05-22 20:02:41 +0200567 def counts(self):
568 counts = {}
569 for key in self.reserved.keys():
570 counts[key] = self.count(key)
571 return counts
572
573 def count(self, key):
574 return len(self.reserved.get(key) or [])
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200575
576# vim: expandtab tabstop=4 shiftwidth=4