blob: 4acf0f6dc4cc4235a3b34f0b0616bedb56388b42 [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 Pedroldaed4472017-09-15 14:11:35 +020032from . import bts_sysmo, bts_osmotrx, bts_octphy
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'
Neels Hofmeyr76d81032017-05-18 18:35:32 +020047R_ALL = (R_IP_ADDRESS, R_BTS, R_ARFCN, R_MODEM)
Neels Hofmeyr3531a192017-03-28 14:30:28 +020048
49RESOURCES_SCHEMA = {
Neels Hofmeyr76d81032017-05-18 18:35:32 +020050 'ip_address[].addr': schema.IPV4,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020051 'bts[].label': schema.STR,
52 'bts[].type': schema.STR,
Pau Espin Pedrolfa9a6d32017-09-12 15:13:21 +020053 'bts[].ipa_unit_id': schema.UINT,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020054 'bts[].addr': schema.IPV4,
55 'bts[].band': schema.BAND,
Pau Espin Pedrol404e1502017-08-22 11:17:43 +020056 'bts[].trx_remote_ip': schema.IPV4,
57 'bts[].launch_trx': schema.BOOL_STR,
Pau Espin Pedrolce35d912017-11-23 11:01:24 +010058 'bts[].direct_pcu': schema.BOOL_STR,
Pau Espin Pedrol57497a62017-08-28 14:21:15 +020059 'bts[].ciphers[]': schema.CIPHER,
Your Name44af3412017-04-13 03:11:59 +020060 'bts[].trx_list[].hw_addr': schema.HWADDR,
61 'bts[].trx_list[].net_device': schema.STR,
Pau Espin Pedrolb26f32a2017-09-14 13:52:28 +020062 'bts[].trx_list[].nominal_power': schema.UINT,
63 'bts[].trx_list[].max_power_red': schema.UINT,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020064 'arfcn[].arfcn': schema.INT,
65 'arfcn[].band': schema.BAND,
66 'modem[].label': schema.STR,
67 'modem[].path': schema.STR,
68 'modem[].imsi': schema.IMSI,
69 'modem[].ki': schema.KI,
Pau Espin Pedrol713ce2c2017-08-24 16:57:17 +020070 'modem[].auth_algo': schema.AUTH_ALGO,
Pau Espin Pedrol57497a62017-08-28 14:21:15 +020071 'modem[].ciphers[]': schema.CIPHER,
Pau Espin Pedrolac18fd32017-08-31 18:49:47 +020072 'modem[].features[]': schema.MODEM_FEATURE,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020073 }
74
75WANT_SCHEMA = util.dict_add(
Pau Espin Pedrolead79ac2017-09-12 15:19:18 +020076 dict([('%s[].times' % r, schema.TIMES) for r in R_ALL]),
Neels Hofmeyr3531a192017-03-28 14:30:28 +020077 RESOURCES_SCHEMA)
78
Pau Espin Pedrol0b302792017-09-10 16:33:10 +020079CONF_SCHEMA = util.dict_add(
80 { 'defaults.timeout': schema.STR },
81 dict([('resources.%s' % key, val) for key, val in WANT_SCHEMA.items()]))
82
Neels Hofmeyr3531a192017-03-28 14:30:28 +020083KNOWN_BTS_TYPES = {
Your Name44af3412017-04-13 03:11:59 +020084 'osmo-bts-sysmo': bts_sysmo.SysmoBts,
85 'osmo-bts-trx': bts_osmotrx.OsmoBtsTrx,
Pau Espin Pedroldaed4472017-09-15 14:11:35 +020086 'osmo-bts-octphy': bts_octphy.OsmoBtsOctphy,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020087 }
88
89def register_bts_type(name, clazz):
90 KNOWN_BTS_TYPES[name] = clazz
91
92class ResourcesPool(log.Origin):
93 _remember_to_free = None
94 _registered_exit_handler = False
95
96 def __init__(self):
97 self.config_path = config.get_config_file(RESOURCES_CONF)
98 self.state_dir = config.get_state_dir()
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +020099 super().__init__(log.C_CNF, conf=self.config_path, state=self.state_dir.path)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200100 self.read_conf()
101
102 def read_conf(self):
103 self.all_resources = Resources(config.read(self.config_path, RESOURCES_SCHEMA))
104 self.all_resources.set_hashes()
105
106 def reserve(self, origin, want):
107 '''
108 attempt to reserve the resources specified in the dict 'want' for
109 'origin'. Obtain a lock on the resources lock dir, verify that all
110 wanted resources are available, and if yes mark them as reserved.
111
112 On success, return a reservation object which can be used to release
113 the reservation. The reservation will be freed automatically on program
114 exit, if not yet done manually.
115
116 'origin' should be an Origin() instance.
117
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200118 'want' is a dict matching RESOURCES_SCHEMA.
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200119
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200120 If an entry has no attribute set, any of the resources may be
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200121 reserved without further limitations.
122
123 ResourcesPool may also be selected with narrowed down constraints.
Neels Hofmeyr76d81032017-05-18 18:35:32 +0200124 This would reserve one IP address, two modems, one BTS of type
Neels Hofmeyr391afe32017-05-18 19:22:12 +0200125 sysmo and one of type trx, plus 2 ARFCNs in the 1800 band:
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200126
127 {
Neels Hofmeyr76d81032017-05-18 18:35:32 +0200128 'ip_address': [ {} ],
Neels Hofmeyr391afe32017-05-18 19:22:12 +0200129 'bts': [ { 'type': 'sysmo' }, { 'type': 'trx' } ],
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200130 'arfcn': [ { 'band': 'GSM-1800' }, { 'band': 'GSM-1800' } ],
131 'modem': [ {}, {} ],
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200132 }
133 '''
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200134 schema.validate(want, RESOURCES_SCHEMA)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200135
136 origin_id = origin.origin_id()
137
138 with self.state_dir.lock(origin_id):
139 rrfile_path = self.state_dir.mk_parentdir(RESERVED_RESOURCES_FILE)
140 reserved = Resources(config.read(rrfile_path, if_missing_return={}))
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200141 to_be_reserved = self.all_resources.without(reserved).find(origin, want)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200142
143 to_be_reserved.mark_reserved_by(origin_id)
144
145 reserved.add(to_be_reserved)
146 config.write(rrfile_path, reserved)
147
148 self.remember_to_free(to_be_reserved)
149 return ReservedResources(self, origin, to_be_reserved)
150
151 def free(self, origin, to_be_freed):
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200152 log.ctx(origin)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200153 with self.state_dir.lock(origin.origin_id()):
154 rrfile_path = self.state_dir.mk_parentdir(RESERVED_RESOURCES_FILE)
155 reserved = Resources(config.read(rrfile_path, if_missing_return={}))
156 reserved.drop(to_be_freed)
157 config.write(rrfile_path, reserved)
158 self.forget_freed(to_be_freed)
159
160 def register_exit_handler(self):
161 if self._registered_exit_handler:
162 return
163 atexit.register(self.clean_up_registered_resources)
164 self._registered_exit_handler = True
165
166 def unregister_exit_handler(self):
167 if not self._registered_exit_handler:
168 return
169 atexit.unregister(self.clean_up_registered_resources)
170 self._registered_exit_handler = False
171
172 def clean_up_registered_resources(self):
173 if not self._remember_to_free:
174 return
175 self.free(log.Origin('atexit.clean_up_registered_resources()'),
176 self._remember_to_free)
177
178 def remember_to_free(self, to_be_reserved):
179 self.register_exit_handler()
180 if not self._remember_to_free:
181 self._remember_to_free = Resources()
182 self._remember_to_free.add(to_be_reserved)
183
184 def forget_freed(self, freed):
185 if freed is self._remember_to_free:
186 self._remember_to_free.clear()
187 else:
188 self._remember_to_free.drop(freed)
189 if not self._remember_to_free:
190 self.unregister_exit_handler()
191
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100192 def next_persistent_value(self, token, first_val, validate_func, inc_func, origin):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200193 origin_id = origin.origin_id()
194
195 with self.state_dir.lock(origin_id):
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100196 token_path = self.state_dir.child('last_used_%s.state' % token)
197 log.ctx(token_path)
198 last_value = first_val
199 if os.path.exists(token_path):
200 if not os.path.isfile(token_path):
201 raise RuntimeError('path should be a file but is not: %r' % token_path)
202 with open(token_path, 'r') as f:
203 last_value = f.read().strip()
204 validate_func(last_value)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200205
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100206 next_value = inc_func(last_value)
207 with open(token_path, 'w') as f:
208 f.write(next_value)
209 return next_value
210
211 def next_msisdn(self, origin):
212 return self.next_persistent_value('msisdn', '1000', schema.msisdn, util.msisdn_inc, origin)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200213
Pau Espin Pedrol5e0c2512017-11-06 18:40:23 +0100214 def next_lac(self, origin):
Pau Espin Pedrolf7f06362017-11-28 12:56:35 +0100215 # LAC=0 has special meaning (MS detached), avoid it
216 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 +0200217
Pau Espin Pedrol8a3a7b52017-11-28 15:50:02 +0100218 def next_rac(self, origin):
219 return self.next_persistent_value('rac', '1', schema.uint8, lambda x: str((int(x)+1) % pow(2,8) or 1), origin)
220
Pau Espin Pedrol4ccce7c2017-11-07 11:13:20 +0100221 def next_cellid(self, origin):
222 return self.next_persistent_value('cellid', '1', schema.uint16, lambda x: str((int(x)+1) % pow(2,16)), origin)
223
Pau Espin Pedrol8a3a7b52017-11-28 15:50:02 +0100224 def next_bvci(self, origin):
225 # BVCI=0 and =1 are reserved, avoid them.
226 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)
227
Pau Espin Pedrol4676cbd2017-09-14 17:35:03 +0200228class NoResourceExn(log.Error):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200229 pass
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200230
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200231class Resources(dict):
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200232
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200233 def __init__(self, all_resources={}, do_copy=True):
234 if do_copy:
235 all_resources = copy.deepcopy(all_resources)
236 self.update(all_resources)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200237
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200238 def drop(self, reserved, fail_if_not_found=True):
239 # protect from modifying reserved because we're the same object
240 if reserved is self:
241 raise RuntimeError('Refusing to drop a list of resources from itself.'
242 ' This is probably a bug where a list of Resources()'
243 ' should have been copied but is passed as-is.'
244 ' use Resources.clear() instead.')
245
246 for key, reserved_list in reserved.items():
247 my_list = self.get(key) or []
248
249 if my_list is reserved_list:
250 self.pop(key)
251 continue
252
253 for reserved_item in reserved_list:
254 found = False
255 reserved_hash = reserved_item.get(HASH_KEY)
256 if not reserved_hash:
257 raise RuntimeError('Resources.drop() only works with hashed items')
258
259 for i in range(len(my_list)):
260 my_item = my_list[i]
261 my_hash = my_item.get(HASH_KEY)
262 if not my_hash:
263 raise RuntimeError('Resources.drop() only works with hashed items')
264 if my_hash == reserved_hash:
265 found = True
266 my_list.pop(i)
267 break
268
269 if fail_if_not_found and not found:
270 raise RuntimeError('Asked to drop resource from a pool, but the'
271 ' resource was not found: %s = %r' % (key, reserved_item))
272
273 if not my_list:
274 self.pop(key)
275 return self
276
277 def without(self, reserved):
278 return Resources(self).drop(reserved)
279
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200280 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 +0200281 '''
282 Pass a dict of resource requirements, e.g.:
283 want = {
284 'bts': [ {'type': 'osmo-bts-sysmo',}, {} ],
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200285 'modem': [ {}, {}, {} ]
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200286 }
287 This function tries to find a combination from the available resources that
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200288 matches these requirements. The return value is a dict (wrapped in a Resources class)
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200289 that contains the matching resources in the order of 'want' dict: in above
290 example, the returned dict would have a 'bts' list with the first item being
291 a sysmoBTS, the second item being any other available BTS.
292
293 If skip_if_marked is passed, any resource that contains this key is skipped.
294 E.g. if a BTS has the USED_KEY set like
295 reserved_resources = { 'bts' : {..., '_used': True} }
296 then this may be skipped by passing skip_if_marked='_used'
297 (or rather skip_if_marked=USED_KEY).
298
299 If do_copy is True, the returned dict is a deep copy and does not share
300 lists with any other Resources dict.
301
302 If raise_if_missing is False, this will return an empty item for any
303 resource that had no match, instead of immediately raising an exception.
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200304
305 This function expects input dictionaries whose contents have already
306 been replicated based on its the 'times' attributes. See
307 config.replicate_times() for more details.
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200308 '''
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200309 matches = {}
Neels Hofmeyr17c139e2017-04-12 02:42:02 +0200310 for key, want_list in sorted(want.items()): # sorted for deterministic test results
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200311 # here we have a resource of a given type, e.g. 'bts', with a list
312 # containing as many BTSes as the caller wants to reserve/use. Each
313 # list item contains specifics for the particular BTS.
Neels Hofmeyr2a1a1fa2017-05-29 01:36:21 +0200314 my_list = self.get(key, [])
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200315
Neels Hofmeyrf9e86932017-06-06 19:47:40 +0200316 if log_label:
317 for_origin.log(log_label, len(want_list), 'x', key, '(candidates: %d)'%len(my_list))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200318
319 # Try to avoid a less constrained item snatching away a resource
320 # from a more detailed constrained requirement.
321
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200322 # first record all matches, so that each requested item has a list
323 # of all available resources that match it. Some resources may
324 # appear for multiple requested items. Store matching indexes.
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200325 all_matches = []
326 for want_item in want_list:
327 item_match_list = []
328 for i in range(len(my_list)):
329 my_item = my_list[i]
330 if skip_if_marked and my_item.get(skip_if_marked):
331 continue
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200332 if item_matches(my_item, want_item):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200333 item_match_list.append(i)
334 if not item_match_list:
Neels Hofmeyr9b907702017-05-06 23:20:33 +0200335 if raise_if_missing:
336 raise NoResourceExn('No matching resource available for %s = %r'
337 % (key, want_item))
338 else:
339 # this one failed... see below
340 all_matches = []
341 break
342
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200343 all_matches.append( item_match_list )
344
345 if not all_matches:
Neels Hofmeyr9b907702017-05-06 23:20:33 +0200346 # ...this one failed. Makes no sense to solve resource
347 # allocations, return an empty list for this key to mark
348 # failure.
349 matches[key] = []
350 continue
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200351
352 # figure out who gets what
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200353 try:
354 solution = solve(all_matches)
355 except NotSolvable:
356 # instead of a cryptic error message, raise an exception that
357 # conveys meaning to the user.
358 raise NoResourceExn('Could not resolve request to reserve resources: '
359 '%d x %s with requirements: %r' % (len(want_list), key, want_list))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200360 picked = [ my_list[i] for i in solution if i is not None ]
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200361 for_origin.dbg('Picked', config.tostr(picked))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200362 matches[key] = picked
363
364 return Resources(matches, do_copy=do_copy)
365
366 def set_hashes(self):
367 for key, item_list in self.items():
368 for item in item_list:
369 item[HASH_KEY] = util.hash_obj(item, HASH_KEY, RESERVED_KEY, USED_KEY)
370
371 def add(self, more):
372 if more is self:
373 raise RuntimeError('adding a list of resources to itself?')
374 config.add(self, copy.deepcopy(more))
375
376 def combine(self, more_rules):
377 if more_rules is self:
378 raise RuntimeError('combining a list of resource rules with itself?')
379 config.combine(self, copy.deepcopy(more))
380
381 def mark_reserved_by(self, origin_id):
382 for key, item_list in self.items():
383 for item in item_list:
384 item[RESERVED_KEY] = origin_id
385
386
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200387class NotSolvable(Exception):
388 pass
389
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200390def solve(all_matches):
391 '''
392 all_matches shall be a list of index-lists.
393 all_matches[i] is the list of indexes that item i can use.
394 Return a solution so that each i gets a different index.
395 solve([ [0, 1, 2],
396 [0],
397 [0, 2] ]) == [1, 0, 2]
398 '''
399
400 def all_differ(l):
401 return len(set(l)) == len(l)
402
403 def search_in_permutations(fixed=[]):
404 idx = len(fixed)
405 for i in range(len(all_matches[idx])):
406 val = all_matches[idx][i]
407 # don't add a val that's already in the list
408 if val in fixed:
409 continue
410 l = list(fixed)
411 l.append(val)
412 if len(l) == len(all_matches):
413 # found a solution
414 return l
415 # not at the end yet, add next digit
416 r = search_in_permutations(l)
417 if r:
418 # nested search_in_permutations() call found a solution
419 return r
420 # this entire branch yielded no solution
421 return None
422
423 if not all_matches:
424 raise RuntimeError('Cannot solve: no candidates')
425
426 solution = search_in_permutations()
427 if not solution:
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200428 raise NotSolvable('The requested resource requirements are not solvable %r'
429 % all_matches)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200430 return solution
431
432
433def contains_hash(list_of_dicts, a_hash):
434 for d in list_of_dicts:
435 if d.get(HASH_KEY) == a_hash:
436 return True
437 return False
438
439def item_matches(item, wanted_item, ignore_keys=None):
440 if is_dict(wanted_item):
441 # match up two dicts
442 if not isinstance(item, dict):
443 return False
444 for key, wanted_val in wanted_item.items():
445 if ignore_keys and key in ignore_keys:
446 continue
447 if not item_matches(item.get(key), wanted_val, ignore_keys=ignore_keys):
448 return False
449 return True
450
451 if is_list(wanted_item):
Pau Espin Pedrol4e36f7c2017-08-28 13:29:28 +0200452 if not is_list(item):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200453 return False
Pau Espin Pedrol58475512017-09-14 15:33:15 +0200454 # Validate that all elements in both lists are of the same type:
455 t = util.list_validate_same_elem_type(wanted_item + item)
456 if t is None:
457 return True # both lists are empty, return
458 # For lists of complex objects, we expect them to be sorted lists:
459 if t in (dict, list, tuple):
460 for i in range(max(len(wanted_item), len(item))):
461 log.ctx(idx=i)
462 subitem = item[i] if i < len(item) else util.empty_instance_type(t)
463 wanted_subitem = wanted_item[i] if i < len(wanted_item) else util.empty_instance_type(t)
464 if not item_matches(subitem, wanted_subitem, ignore_keys=ignore_keys):
465 return False
466 else: # for lists of basic elements, we handle them as unsorted sets:
467 for val in wanted_item:
468 if val not in item:
469 return False
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200470 return True
471
472 return item == wanted_item
473
474
475class ReservedResources(log.Origin):
476 '''
477 After all resources have been figured out, this is the API that a test case
478 gets to interact with resources. From those resources that have been
479 reserved for it, it can pick some to mark them as currently in use.
480 Functions like nitb() provide a resource by automatically picking its
481 dependencies from so far unused (but reserved) resource.
482 '''
483
484 def __init__(self, resources_pool, origin, reserved):
485 self.resources_pool = resources_pool
486 self.origin = origin
487 self.reserved = reserved
488
489 def __repr__(self):
490 return 'resources(%s)=%s' % (self.origin.name(), pprint.pformat(self.reserved))
491
492 def get(self, kind, specifics=None):
493 if specifics is None:
494 specifics = {}
495 self.dbg('requesting use of', kind, specifics=specifics)
496 want = { kind: [specifics] }
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200497 available_dict = self.reserved.find(self.origin, want, skip_if_marked=USED_KEY,
498 do_copy=False, raise_if_missing=False,
499 log_label='Using')
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200500 available = available_dict.get(kind)
501 self.dbg(available=len(available))
502 if not available:
Neels Hofmeyrf9e86932017-06-06 19:47:40 +0200503 # cook up a detailed error message for the current situation
504 kind_reserved = self.reserved.get(kind, [])
505 used_count = len([r for r in kind_reserved if USED_KEY in r])
506 matching = self.reserved.find(self.origin, want, raise_if_missing=False, log_label=None).get(kind, [])
507 if not matching:
508 msg = 'none of the reserved resources matches requirements %r' % specifics
509 elif not (used_count < len(kind_reserved)):
510 msg = 'suite.conf reserved only %d x %r.' % (len(kind_reserved), kind)
511 else:
512 msg = ('No unused resource left that matches the requirements;'
513 ' Of reserved %d x %r, %d match the requirements, but all are already in use;'
514 ' Requirements: %r'
515 % (len(kind_reserved), kind, len(matching), specifics))
516 raise NoResourceExn('When trying to use instance nr %d of %r: %s' % (used_count + 1, kind, msg))
517
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200518 pick = available[0]
519 self.dbg(using=pick)
520 assert not pick.get(USED_KEY)
521 pick[USED_KEY] = True
522 return copy.deepcopy(pick)
523
524 def put(self, item):
525 if not item.get(USED_KEY):
526 raise RuntimeError('Can only put() a resource that is used: %r' % item)
527 hash_to_put = item.get(HASH_KEY)
528 if not hash_to_put:
529 raise RuntimeError('Can only put() a resource that has a hash marker: %r' % item)
530 for key, item_list in self.reserved.items():
531 my_list = self.get(key)
532 for my_item in my_list:
533 if hash_to_put == my_item.get(HASH_KEY):
534 my_item.pop(USED_KEY)
535
536 def put_all(self):
Pau Espin Pedrol1dd29552017-06-13 18:07:57 +0200537 if not self.reserved:
538 return
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200539 for key, item_list in self.reserved.items():
Pau Espin Pedrol1dd29552017-06-13 18:07:57 +0200540 for item in item_list:
541 item.pop(USED_KEY, None)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200542
543 def free(self):
Neels Hofmeyred4e5282017-05-29 02:53:54 +0200544 if self.reserved:
545 self.resources_pool.free(self.origin, self.reserved)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200546 self.reserved = None
547
Neels Hofmeyr2d1d5612017-05-22 20:02:41 +0200548 def counts(self):
549 counts = {}
550 for key in self.reserved.keys():
551 counts[key] = self.count(key)
552 return counts
553
554 def count(self, key):
555 return len(self.reserved.get(key) or [])
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200556
557# vim: expandtab tabstop=4 shiftwidth=4