blob: dca8090c38868cbcc05f6d60001fd3749ba3f809 [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 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,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020072 'arfcn[].arfcn': schema.INT,
73 'arfcn[].band': schema.BAND,
74 'modem[].label': schema.STR,
75 'modem[].path': schema.STR,
76 'modem[].imsi': schema.IMSI,
77 'modem[].ki': schema.KI,
Pau Espin Pedrol713ce2c2017-08-24 16:57:17 +020078 'modem[].auth_algo': schema.AUTH_ALGO,
Pau Espin Pedrol57497a62017-08-28 14:21:15 +020079 'modem[].ciphers[]': schema.CIPHER,
Pau Espin Pedrolac18fd32017-08-31 18:49:47 +020080 'modem[].features[]': schema.MODEM_FEATURE,
Pau Espin Pedrolbc1ed882018-05-17 16:59:58 +020081 'osmocon_phone[].serial_device': schema.STR,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020082 }
83
84WANT_SCHEMA = util.dict_add(
Pau Espin Pedrolead79ac2017-09-12 15:19:18 +020085 dict([('%s[].times' % r, schema.TIMES) for r in R_ALL]),
Neels Hofmeyr3531a192017-03-28 14:30:28 +020086 RESOURCES_SCHEMA)
87
Pau Espin Pedrol0b302792017-09-10 16:33:10 +020088CONF_SCHEMA = util.dict_add(
89 { 'defaults.timeout': schema.STR },
90 dict([('resources.%s' % key, val) for key, val in WANT_SCHEMA.items()]))
91
Neels Hofmeyr3531a192017-03-28 14:30:28 +020092KNOWN_BTS_TYPES = {
Your Name44af3412017-04-13 03:11:59 +020093 'osmo-bts-sysmo': bts_sysmo.SysmoBts,
94 'osmo-bts-trx': bts_osmotrx.OsmoBtsTrx,
Pau Espin Pedroldaed4472017-09-15 14:11:35 +020095 'osmo-bts-octphy': bts_octphy.OsmoBtsOctphy,
Pau Espin Pedrol1b28a582018-03-08 21:01:26 +010096 'nanobts': bts_nanobts.NanoBts,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020097 }
98
99def register_bts_type(name, clazz):
100 KNOWN_BTS_TYPES[name] = clazz
101
102class ResourcesPool(log.Origin):
103 _remember_to_free = None
104 _registered_exit_handler = False
105
106 def __init__(self):
107 self.config_path = config.get_config_file(RESOURCES_CONF)
108 self.state_dir = config.get_state_dir()
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200109 super().__init__(log.C_CNF, conf=self.config_path, state=self.state_dir.path)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200110 self.read_conf()
111
112 def read_conf(self):
113 self.all_resources = Resources(config.read(self.config_path, RESOURCES_SCHEMA))
114 self.all_resources.set_hashes()
115
116 def reserve(self, origin, want):
117 '''
118 attempt to reserve the resources specified in the dict 'want' for
119 'origin'. Obtain a lock on the resources lock dir, verify that all
120 wanted resources are available, and if yes mark them as reserved.
121
122 On success, return a reservation object which can be used to release
123 the reservation. The reservation will be freed automatically on program
124 exit, if not yet done manually.
125
126 'origin' should be an Origin() instance.
127
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200128 'want' is a dict matching RESOURCES_SCHEMA.
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200129
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200130 If an entry has no attribute set, any of the resources may be
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200131 reserved without further limitations.
132
133 ResourcesPool may also be selected with narrowed down constraints.
Neels Hofmeyr76d81032017-05-18 18:35:32 +0200134 This would reserve one IP address, two modems, one BTS of type
Neels Hofmeyr391afe32017-05-18 19:22:12 +0200135 sysmo and one of type trx, plus 2 ARFCNs in the 1800 band:
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200136
137 {
Neels Hofmeyr76d81032017-05-18 18:35:32 +0200138 'ip_address': [ {} ],
Neels Hofmeyr391afe32017-05-18 19:22:12 +0200139 'bts': [ { 'type': 'sysmo' }, { 'type': 'trx' } ],
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200140 'arfcn': [ { 'band': 'GSM-1800' }, { 'band': 'GSM-1800' } ],
141 'modem': [ {}, {} ],
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200142 }
143 '''
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200144 schema.validate(want, RESOURCES_SCHEMA)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200145
146 origin_id = origin.origin_id()
147
148 with self.state_dir.lock(origin_id):
149 rrfile_path = self.state_dir.mk_parentdir(RESERVED_RESOURCES_FILE)
150 reserved = Resources(config.read(rrfile_path, if_missing_return={}))
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200151 to_be_reserved = self.all_resources.without(reserved).find(origin, want)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200152
153 to_be_reserved.mark_reserved_by(origin_id)
154
155 reserved.add(to_be_reserved)
156 config.write(rrfile_path, reserved)
157
158 self.remember_to_free(to_be_reserved)
159 return ReservedResources(self, origin, to_be_reserved)
160
161 def free(self, origin, to_be_freed):
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200162 log.ctx(origin)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200163 with self.state_dir.lock(origin.origin_id()):
164 rrfile_path = self.state_dir.mk_parentdir(RESERVED_RESOURCES_FILE)
165 reserved = Resources(config.read(rrfile_path, if_missing_return={}))
166 reserved.drop(to_be_freed)
167 config.write(rrfile_path, reserved)
168 self.forget_freed(to_be_freed)
169
170 def register_exit_handler(self):
171 if self._registered_exit_handler:
172 return
173 atexit.register(self.clean_up_registered_resources)
174 self._registered_exit_handler = True
175
176 def unregister_exit_handler(self):
177 if not self._registered_exit_handler:
178 return
179 atexit.unregister(self.clean_up_registered_resources)
180 self._registered_exit_handler = False
181
182 def clean_up_registered_resources(self):
183 if not self._remember_to_free:
184 return
185 self.free(log.Origin('atexit.clean_up_registered_resources()'),
186 self._remember_to_free)
187
188 def remember_to_free(self, to_be_reserved):
189 self.register_exit_handler()
190 if not self._remember_to_free:
191 self._remember_to_free = Resources()
192 self._remember_to_free.add(to_be_reserved)
193
194 def forget_freed(self, freed):
195 if freed is self._remember_to_free:
196 self._remember_to_free.clear()
197 else:
198 self._remember_to_free.drop(freed)
199 if not self._remember_to_free:
200 self.unregister_exit_handler()
201
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100202 def next_persistent_value(self, token, first_val, validate_func, inc_func, origin):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200203 origin_id = origin.origin_id()
204
205 with self.state_dir.lock(origin_id):
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100206 token_path = self.state_dir.child('last_used_%s.state' % token)
207 log.ctx(token_path)
208 last_value = first_val
209 if os.path.exists(token_path):
210 if not os.path.isfile(token_path):
211 raise RuntimeError('path should be a file but is not: %r' % token_path)
212 with open(token_path, 'r') as f:
213 last_value = f.read().strip()
214 validate_func(last_value)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200215
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100216 next_value = inc_func(last_value)
217 with open(token_path, 'w') as f:
218 f.write(next_value)
219 return next_value
220
221 def next_msisdn(self, origin):
222 return self.next_persistent_value('msisdn', '1000', schema.msisdn, util.msisdn_inc, origin)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200223
Pau Espin Pedrol5e0c2512017-11-06 18:40:23 +0100224 def next_lac(self, origin):
Pau Espin Pedrolf7f06362017-11-28 12:56:35 +0100225 # LAC=0 has special meaning (MS detached), avoid it
226 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 +0200227
Pau Espin Pedrol8a3a7b52017-11-28 15:50:02 +0100228 def next_rac(self, origin):
229 return self.next_persistent_value('rac', '1', schema.uint8, lambda x: str((int(x)+1) % pow(2,8) or 1), origin)
230
Pau Espin Pedrol4ccce7c2017-11-07 11:13:20 +0100231 def next_cellid(self, origin):
232 return self.next_persistent_value('cellid', '1', schema.uint16, lambda x: str((int(x)+1) % pow(2,16)), origin)
233
Pau Espin Pedrol8a3a7b52017-11-28 15:50:02 +0100234 def next_bvci(self, origin):
235 # BVCI=0 and =1 are reserved, avoid them.
236 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)
237
Pau Espin Pedrol4676cbd2017-09-14 17:35:03 +0200238class NoResourceExn(log.Error):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200239 pass
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200240
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200241class Resources(dict):
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200242
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200243 def __init__(self, all_resources={}, do_copy=True):
244 if do_copy:
245 all_resources = copy.deepcopy(all_resources)
246 self.update(all_resources)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200247
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200248 def drop(self, reserved, fail_if_not_found=True):
249 # protect from modifying reserved because we're the same object
250 if reserved is self:
251 raise RuntimeError('Refusing to drop a list of resources from itself.'
252 ' This is probably a bug where a list of Resources()'
253 ' should have been copied but is passed as-is.'
254 ' use Resources.clear() instead.')
255
256 for key, reserved_list in reserved.items():
257 my_list = self.get(key) or []
258
259 if my_list is reserved_list:
260 self.pop(key)
261 continue
262
263 for reserved_item in reserved_list:
264 found = False
265 reserved_hash = reserved_item.get(HASH_KEY)
266 if not reserved_hash:
267 raise RuntimeError('Resources.drop() only works with hashed items')
268
269 for i in range(len(my_list)):
270 my_item = my_list[i]
271 my_hash = my_item.get(HASH_KEY)
272 if not my_hash:
273 raise RuntimeError('Resources.drop() only works with hashed items')
274 if my_hash == reserved_hash:
275 found = True
276 my_list.pop(i)
277 break
278
279 if fail_if_not_found and not found:
280 raise RuntimeError('Asked to drop resource from a pool, but the'
281 ' resource was not found: %s = %r' % (key, reserved_item))
282
283 if not my_list:
284 self.pop(key)
285 return self
286
287 def without(self, reserved):
288 return Resources(self).drop(reserved)
289
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200290 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 +0200291 '''
292 Pass a dict of resource requirements, e.g.:
293 want = {
294 'bts': [ {'type': 'osmo-bts-sysmo',}, {} ],
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200295 'modem': [ {}, {}, {} ]
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200296 }
297 This function tries to find a combination from the available resources that
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200298 matches these requirements. The return value is a dict (wrapped in a Resources class)
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200299 that contains the matching resources in the order of 'want' dict: in above
300 example, the returned dict would have a 'bts' list with the first item being
301 a sysmoBTS, the second item being any other available BTS.
302
303 If skip_if_marked is passed, any resource that contains this key is skipped.
304 E.g. if a BTS has the USED_KEY set like
305 reserved_resources = { 'bts' : {..., '_used': True} }
306 then this may be skipped by passing skip_if_marked='_used'
307 (or rather skip_if_marked=USED_KEY).
308
309 If do_copy is True, the returned dict is a deep copy and does not share
310 lists with any other Resources dict.
311
312 If raise_if_missing is False, this will return an empty item for any
313 resource that had no match, instead of immediately raising an exception.
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200314
315 This function expects input dictionaries whose contents have already
316 been replicated based on its the 'times' attributes. See
317 config.replicate_times() for more details.
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200318 '''
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200319 matches = {}
Neels Hofmeyr17c139e2017-04-12 02:42:02 +0200320 for key, want_list in sorted(want.items()): # sorted for deterministic test results
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200321 # here we have a resource of a given type, e.g. 'bts', with a list
322 # containing as many BTSes as the caller wants to reserve/use. Each
323 # list item contains specifics for the particular BTS.
Neels Hofmeyr2a1a1fa2017-05-29 01:36:21 +0200324 my_list = self.get(key, [])
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200325
Neels Hofmeyrf9e86932017-06-06 19:47:40 +0200326 if log_label:
327 for_origin.log(log_label, len(want_list), 'x', key, '(candidates: %d)'%len(my_list))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200328
329 # Try to avoid a less constrained item snatching away a resource
330 # from a more detailed constrained requirement.
331
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200332 # first record all matches, so that each requested item has a list
333 # of all available resources that match it. Some resources may
334 # appear for multiple requested items. Store matching indexes.
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200335 all_matches = []
336 for want_item in want_list:
337 item_match_list = []
338 for i in range(len(my_list)):
339 my_item = my_list[i]
340 if skip_if_marked and my_item.get(skip_if_marked):
341 continue
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200342 if item_matches(my_item, want_item):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200343 item_match_list.append(i)
344 if not item_match_list:
Neels Hofmeyr9b907702017-05-06 23:20:33 +0200345 if raise_if_missing:
346 raise NoResourceExn('No matching resource available for %s = %r'
347 % (key, want_item))
348 else:
349 # this one failed... see below
350 all_matches = []
351 break
352
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200353 all_matches.append( item_match_list )
354
355 if not all_matches:
Neels Hofmeyr9b907702017-05-06 23:20:33 +0200356 # ...this one failed. Makes no sense to solve resource
357 # allocations, return an empty list for this key to mark
358 # failure.
359 matches[key] = []
360 continue
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200361
362 # figure out who gets what
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200363 try:
364 solution = solve(all_matches)
365 except NotSolvable:
366 # instead of a cryptic error message, raise an exception that
367 # conveys meaning to the user.
368 raise NoResourceExn('Could not resolve request to reserve resources: '
369 '%d x %s with requirements: %r' % (len(want_list), key, want_list))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200370 picked = [ my_list[i] for i in solution if i is not None ]
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200371 for_origin.dbg('Picked', config.tostr(picked))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200372 matches[key] = picked
373
374 return Resources(matches, do_copy=do_copy)
375
376 def set_hashes(self):
377 for key, item_list in self.items():
378 for item in item_list:
379 item[HASH_KEY] = util.hash_obj(item, HASH_KEY, RESERVED_KEY, USED_KEY)
380
381 def add(self, more):
382 if more is self:
383 raise RuntimeError('adding a list of resources to itself?')
384 config.add(self, copy.deepcopy(more))
385
386 def combine(self, more_rules):
387 if more_rules is self:
388 raise RuntimeError('combining a list of resource rules with itself?')
389 config.combine(self, copy.deepcopy(more))
390
391 def mark_reserved_by(self, origin_id):
392 for key, item_list in self.items():
393 for item in item_list:
394 item[RESERVED_KEY] = origin_id
395
396
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200397class NotSolvable(Exception):
398 pass
399
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200400def solve(all_matches):
401 '''
402 all_matches shall be a list of index-lists.
403 all_matches[i] is the list of indexes that item i can use.
404 Return a solution so that each i gets a different index.
405 solve([ [0, 1, 2],
406 [0],
407 [0, 2] ]) == [1, 0, 2]
408 '''
409
410 def all_differ(l):
411 return len(set(l)) == len(l)
412
413 def search_in_permutations(fixed=[]):
414 idx = len(fixed)
415 for i in range(len(all_matches[idx])):
416 val = all_matches[idx][i]
417 # don't add a val that's already in the list
418 if val in fixed:
419 continue
420 l = list(fixed)
421 l.append(val)
422 if len(l) == len(all_matches):
423 # found a solution
424 return l
425 # not at the end yet, add next digit
426 r = search_in_permutations(l)
427 if r:
428 # nested search_in_permutations() call found a solution
429 return r
430 # this entire branch yielded no solution
431 return None
432
433 if not all_matches:
434 raise RuntimeError('Cannot solve: no candidates')
435
436 solution = search_in_permutations()
437 if not solution:
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200438 raise NotSolvable('The requested resource requirements are not solvable %r'
439 % all_matches)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200440 return solution
441
442
443def contains_hash(list_of_dicts, a_hash):
444 for d in list_of_dicts:
445 if d.get(HASH_KEY) == a_hash:
446 return True
447 return False
448
449def item_matches(item, wanted_item, ignore_keys=None):
450 if is_dict(wanted_item):
451 # match up two dicts
452 if not isinstance(item, dict):
453 return False
454 for key, wanted_val in wanted_item.items():
455 if ignore_keys and key in ignore_keys:
456 continue
457 if not item_matches(item.get(key), wanted_val, ignore_keys=ignore_keys):
458 return False
459 return True
460
461 if is_list(wanted_item):
Pau Espin Pedrol4e36f7c2017-08-28 13:29:28 +0200462 if not is_list(item):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200463 return False
Pau Espin Pedrol58475512017-09-14 15:33:15 +0200464 # Validate that all elements in both lists are of the same type:
465 t = util.list_validate_same_elem_type(wanted_item + item)
466 if t is None:
467 return True # both lists are empty, return
468 # For lists of complex objects, we expect them to be sorted lists:
469 if t in (dict, list, tuple):
470 for i in range(max(len(wanted_item), len(item))):
471 log.ctx(idx=i)
472 subitem = item[i] if i < len(item) else util.empty_instance_type(t)
473 wanted_subitem = wanted_item[i] if i < len(wanted_item) else util.empty_instance_type(t)
474 if not item_matches(subitem, wanted_subitem, ignore_keys=ignore_keys):
475 return False
476 else: # for lists of basic elements, we handle them as unsorted sets:
477 for val in wanted_item:
478 if val not in item:
479 return False
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200480 return True
481
482 return item == wanted_item
483
484
485class ReservedResources(log.Origin):
486 '''
487 After all resources have been figured out, this is the API that a test case
488 gets to interact with resources. From those resources that have been
489 reserved for it, it can pick some to mark them as currently in use.
490 Functions like nitb() provide a resource by automatically picking its
491 dependencies from so far unused (but reserved) resource.
492 '''
493
494 def __init__(self, resources_pool, origin, reserved):
495 self.resources_pool = resources_pool
496 self.origin = origin
497 self.reserved = reserved
498
499 def __repr__(self):
500 return 'resources(%s)=%s' % (self.origin.name(), pprint.pformat(self.reserved))
501
502 def get(self, kind, specifics=None):
503 if specifics is None:
504 specifics = {}
505 self.dbg('requesting use of', kind, specifics=specifics)
506 want = { kind: [specifics] }
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200507 available_dict = self.reserved.find(self.origin, want, skip_if_marked=USED_KEY,
508 do_copy=False, raise_if_missing=False,
509 log_label='Using')
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200510 available = available_dict.get(kind)
511 self.dbg(available=len(available))
512 if not available:
Neels Hofmeyrf9e86932017-06-06 19:47:40 +0200513 # cook up a detailed error message for the current situation
514 kind_reserved = self.reserved.get(kind, [])
515 used_count = len([r for r in kind_reserved if USED_KEY in r])
516 matching = self.reserved.find(self.origin, want, raise_if_missing=False, log_label=None).get(kind, [])
517 if not matching:
518 msg = 'none of the reserved resources matches requirements %r' % specifics
519 elif not (used_count < len(kind_reserved)):
520 msg = 'suite.conf reserved only %d x %r.' % (len(kind_reserved), kind)
521 else:
522 msg = ('No unused resource left that matches the requirements;'
523 ' Of reserved %d x %r, %d match the requirements, but all are already in use;'
524 ' Requirements: %r'
525 % (len(kind_reserved), kind, len(matching), specifics))
526 raise NoResourceExn('When trying to use instance nr %d of %r: %s' % (used_count + 1, kind, msg))
527
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200528 pick = available[0]
529 self.dbg(using=pick)
530 assert not pick.get(USED_KEY)
531 pick[USED_KEY] = True
532 return copy.deepcopy(pick)
533
534 def put(self, item):
535 if not item.get(USED_KEY):
536 raise RuntimeError('Can only put() a resource that is used: %r' % item)
537 hash_to_put = item.get(HASH_KEY)
538 if not hash_to_put:
539 raise RuntimeError('Can only put() a resource that has a hash marker: %r' % item)
540 for key, item_list in self.reserved.items():
541 my_list = self.get(key)
542 for my_item in my_list:
543 if hash_to_put == my_item.get(HASH_KEY):
544 my_item.pop(USED_KEY)
545
546 def put_all(self):
Pau Espin Pedrol1dd29552017-06-13 18:07:57 +0200547 if not self.reserved:
548 return
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200549 for key, item_list in self.reserved.items():
Pau Espin Pedrol1dd29552017-06-13 18:07:57 +0200550 for item in item_list:
551 item.pop(USED_KEY, None)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200552
553 def free(self):
Neels Hofmeyred4e5282017-05-29 02:53:54 +0200554 if self.reserved:
555 self.resources_pool.free(self.origin, self.reserved)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200556 self.reserved = None
557
Neels Hofmeyr2d1d5612017-05-22 20:02:41 +0200558 def counts(self):
559 counts = {}
560 for key in self.reserved.keys():
561 counts[key] = self.count(key)
562 return counts
563
564 def count(self, key):
565 return len(self.reserved.get(key) or [])
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200566
567# vim: expandtab tabstop=4 shiftwidth=4