blob: 29ede839d11762d3ad85dba8d102d2f3376e228e [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'
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 Pedrol1b28a582018-03-08 21:01:26 +010059 'bts[].power_supply.type': schema.STR,
60 'bts[].power_supply.device': schema.STR,
61 'bts[].power_supply.port': schema.STR,
62 'bts[].net_device': schema.STR,
Pau Espin Pedrol57497a62017-08-28 14:21:15 +020063 'bts[].ciphers[]': schema.CIPHER,
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,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020068 'arfcn[].arfcn': schema.INT,
69 'arfcn[].band': schema.BAND,
70 'modem[].label': schema.STR,
71 'modem[].path': schema.STR,
72 'modem[].imsi': schema.IMSI,
73 'modem[].ki': schema.KI,
Pau Espin Pedrol713ce2c2017-08-24 16:57:17 +020074 'modem[].auth_algo': schema.AUTH_ALGO,
Pau Espin Pedrol57497a62017-08-28 14:21:15 +020075 'modem[].ciphers[]': schema.CIPHER,
Pau Espin Pedrolac18fd32017-08-31 18:49:47 +020076 'modem[].features[]': schema.MODEM_FEATURE,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020077 }
78
79WANT_SCHEMA = util.dict_add(
Pau Espin Pedrolead79ac2017-09-12 15:19:18 +020080 dict([('%s[].times' % r, schema.TIMES) for r in R_ALL]),
Neels Hofmeyr3531a192017-03-28 14:30:28 +020081 RESOURCES_SCHEMA)
82
Pau Espin Pedrol0b302792017-09-10 16:33:10 +020083CONF_SCHEMA = util.dict_add(
84 { 'defaults.timeout': schema.STR },
85 dict([('resources.%s' % key, val) for key, val in WANT_SCHEMA.items()]))
86
Neels Hofmeyr3531a192017-03-28 14:30:28 +020087KNOWN_BTS_TYPES = {
Your Name44af3412017-04-13 03:11:59 +020088 'osmo-bts-sysmo': bts_sysmo.SysmoBts,
89 'osmo-bts-trx': bts_osmotrx.OsmoBtsTrx,
Pau Espin Pedroldaed4472017-09-15 14:11:35 +020090 'osmo-bts-octphy': bts_octphy.OsmoBtsOctphy,
Pau Espin Pedrol1b28a582018-03-08 21:01:26 +010091 'nanobts': bts_nanobts.NanoBts,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020092 }
93
94def register_bts_type(name, clazz):
95 KNOWN_BTS_TYPES[name] = clazz
96
97class ResourcesPool(log.Origin):
98 _remember_to_free = None
99 _registered_exit_handler = False
100
101 def __init__(self):
102 self.config_path = config.get_config_file(RESOURCES_CONF)
103 self.state_dir = config.get_state_dir()
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200104 super().__init__(log.C_CNF, conf=self.config_path, state=self.state_dir.path)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200105 self.read_conf()
106
107 def read_conf(self):
108 self.all_resources = Resources(config.read(self.config_path, RESOURCES_SCHEMA))
109 self.all_resources.set_hashes()
110
111 def reserve(self, origin, want):
112 '''
113 attempt to reserve the resources specified in the dict 'want' for
114 'origin'. Obtain a lock on the resources lock dir, verify that all
115 wanted resources are available, and if yes mark them as reserved.
116
117 On success, return a reservation object which can be used to release
118 the reservation. The reservation will be freed automatically on program
119 exit, if not yet done manually.
120
121 'origin' should be an Origin() instance.
122
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200123 'want' is a dict matching RESOURCES_SCHEMA.
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200124
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200125 If an entry has no attribute set, any of the resources may be
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200126 reserved without further limitations.
127
128 ResourcesPool may also be selected with narrowed down constraints.
Neels Hofmeyr76d81032017-05-18 18:35:32 +0200129 This would reserve one IP address, two modems, one BTS of type
Neels Hofmeyr391afe32017-05-18 19:22:12 +0200130 sysmo and one of type trx, plus 2 ARFCNs in the 1800 band:
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200131
132 {
Neels Hofmeyr76d81032017-05-18 18:35:32 +0200133 'ip_address': [ {} ],
Neels Hofmeyr391afe32017-05-18 19:22:12 +0200134 'bts': [ { 'type': 'sysmo' }, { 'type': 'trx' } ],
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200135 'arfcn': [ { 'band': 'GSM-1800' }, { 'band': 'GSM-1800' } ],
136 'modem': [ {}, {} ],
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200137 }
138 '''
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200139 schema.validate(want, RESOURCES_SCHEMA)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200140
141 origin_id = origin.origin_id()
142
143 with self.state_dir.lock(origin_id):
144 rrfile_path = self.state_dir.mk_parentdir(RESERVED_RESOURCES_FILE)
145 reserved = Resources(config.read(rrfile_path, if_missing_return={}))
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200146 to_be_reserved = self.all_resources.without(reserved).find(origin, want)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200147
148 to_be_reserved.mark_reserved_by(origin_id)
149
150 reserved.add(to_be_reserved)
151 config.write(rrfile_path, reserved)
152
153 self.remember_to_free(to_be_reserved)
154 return ReservedResources(self, origin, to_be_reserved)
155
156 def free(self, origin, to_be_freed):
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200157 log.ctx(origin)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200158 with self.state_dir.lock(origin.origin_id()):
159 rrfile_path = self.state_dir.mk_parentdir(RESERVED_RESOURCES_FILE)
160 reserved = Resources(config.read(rrfile_path, if_missing_return={}))
161 reserved.drop(to_be_freed)
162 config.write(rrfile_path, reserved)
163 self.forget_freed(to_be_freed)
164
165 def register_exit_handler(self):
166 if self._registered_exit_handler:
167 return
168 atexit.register(self.clean_up_registered_resources)
169 self._registered_exit_handler = True
170
171 def unregister_exit_handler(self):
172 if not self._registered_exit_handler:
173 return
174 atexit.unregister(self.clean_up_registered_resources)
175 self._registered_exit_handler = False
176
177 def clean_up_registered_resources(self):
178 if not self._remember_to_free:
179 return
180 self.free(log.Origin('atexit.clean_up_registered_resources()'),
181 self._remember_to_free)
182
183 def remember_to_free(self, to_be_reserved):
184 self.register_exit_handler()
185 if not self._remember_to_free:
186 self._remember_to_free = Resources()
187 self._remember_to_free.add(to_be_reserved)
188
189 def forget_freed(self, freed):
190 if freed is self._remember_to_free:
191 self._remember_to_free.clear()
192 else:
193 self._remember_to_free.drop(freed)
194 if not self._remember_to_free:
195 self.unregister_exit_handler()
196
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100197 def next_persistent_value(self, token, first_val, validate_func, inc_func, origin):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200198 origin_id = origin.origin_id()
199
200 with self.state_dir.lock(origin_id):
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100201 token_path = self.state_dir.child('last_used_%s.state' % token)
202 log.ctx(token_path)
203 last_value = first_val
204 if os.path.exists(token_path):
205 if not os.path.isfile(token_path):
206 raise RuntimeError('path should be a file but is not: %r' % token_path)
207 with open(token_path, 'r') as f:
208 last_value = f.read().strip()
209 validate_func(last_value)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200210
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100211 next_value = inc_func(last_value)
212 with open(token_path, 'w') as f:
213 f.write(next_value)
214 return next_value
215
216 def next_msisdn(self, origin):
217 return self.next_persistent_value('msisdn', '1000', schema.msisdn, util.msisdn_inc, origin)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200218
Pau Espin Pedrol5e0c2512017-11-06 18:40:23 +0100219 def next_lac(self, origin):
Pau Espin Pedrolf7f06362017-11-28 12:56:35 +0100220 # LAC=0 has special meaning (MS detached), avoid it
221 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 +0200222
Pau Espin Pedrol8a3a7b52017-11-28 15:50:02 +0100223 def next_rac(self, origin):
224 return self.next_persistent_value('rac', '1', schema.uint8, lambda x: str((int(x)+1) % pow(2,8) or 1), origin)
225
Pau Espin Pedrol4ccce7c2017-11-07 11:13:20 +0100226 def next_cellid(self, origin):
227 return self.next_persistent_value('cellid', '1', schema.uint16, lambda x: str((int(x)+1) % pow(2,16)), origin)
228
Pau Espin Pedrol8a3a7b52017-11-28 15:50:02 +0100229 def next_bvci(self, origin):
230 # BVCI=0 and =1 are reserved, avoid them.
231 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)
232
Pau Espin Pedrol4676cbd2017-09-14 17:35:03 +0200233class NoResourceExn(log.Error):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200234 pass
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200235
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200236class Resources(dict):
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200237
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200238 def __init__(self, all_resources={}, do_copy=True):
239 if do_copy:
240 all_resources = copy.deepcopy(all_resources)
241 self.update(all_resources)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200242
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200243 def drop(self, reserved, fail_if_not_found=True):
244 # protect from modifying reserved because we're the same object
245 if reserved is self:
246 raise RuntimeError('Refusing to drop a list of resources from itself.'
247 ' This is probably a bug where a list of Resources()'
248 ' should have been copied but is passed as-is.'
249 ' use Resources.clear() instead.')
250
251 for key, reserved_list in reserved.items():
252 my_list = self.get(key) or []
253
254 if my_list is reserved_list:
255 self.pop(key)
256 continue
257
258 for reserved_item in reserved_list:
259 found = False
260 reserved_hash = reserved_item.get(HASH_KEY)
261 if not reserved_hash:
262 raise RuntimeError('Resources.drop() only works with hashed items')
263
264 for i in range(len(my_list)):
265 my_item = my_list[i]
266 my_hash = my_item.get(HASH_KEY)
267 if not my_hash:
268 raise RuntimeError('Resources.drop() only works with hashed items')
269 if my_hash == reserved_hash:
270 found = True
271 my_list.pop(i)
272 break
273
274 if fail_if_not_found and not found:
275 raise RuntimeError('Asked to drop resource from a pool, but the'
276 ' resource was not found: %s = %r' % (key, reserved_item))
277
278 if not my_list:
279 self.pop(key)
280 return self
281
282 def without(self, reserved):
283 return Resources(self).drop(reserved)
284
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200285 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 +0200286 '''
287 Pass a dict of resource requirements, e.g.:
288 want = {
289 'bts': [ {'type': 'osmo-bts-sysmo',}, {} ],
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200290 'modem': [ {}, {}, {} ]
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200291 }
292 This function tries to find a combination from the available resources that
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200293 matches these requirements. The return value is a dict (wrapped in a Resources class)
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200294 that contains the matching resources in the order of 'want' dict: in above
295 example, the returned dict would have a 'bts' list with the first item being
296 a sysmoBTS, the second item being any other available BTS.
297
298 If skip_if_marked is passed, any resource that contains this key is skipped.
299 E.g. if a BTS has the USED_KEY set like
300 reserved_resources = { 'bts' : {..., '_used': True} }
301 then this may be skipped by passing skip_if_marked='_used'
302 (or rather skip_if_marked=USED_KEY).
303
304 If do_copy is True, the returned dict is a deep copy and does not share
305 lists with any other Resources dict.
306
307 If raise_if_missing is False, this will return an empty item for any
308 resource that had no match, instead of immediately raising an exception.
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200309
310 This function expects input dictionaries whose contents have already
311 been replicated based on its the 'times' attributes. See
312 config.replicate_times() for more details.
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200313 '''
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200314 matches = {}
Neels Hofmeyr17c139e2017-04-12 02:42:02 +0200315 for key, want_list in sorted(want.items()): # sorted for deterministic test results
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200316 # here we have a resource of a given type, e.g. 'bts', with a list
317 # containing as many BTSes as the caller wants to reserve/use. Each
318 # list item contains specifics for the particular BTS.
Neels Hofmeyr2a1a1fa2017-05-29 01:36:21 +0200319 my_list = self.get(key, [])
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200320
Neels Hofmeyrf9e86932017-06-06 19:47:40 +0200321 if log_label:
322 for_origin.log(log_label, len(want_list), 'x', key, '(candidates: %d)'%len(my_list))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200323
324 # Try to avoid a less constrained item snatching away a resource
325 # from a more detailed constrained requirement.
326
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200327 # first record all matches, so that each requested item has a list
328 # of all available resources that match it. Some resources may
329 # appear for multiple requested items. Store matching indexes.
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200330 all_matches = []
331 for want_item in want_list:
332 item_match_list = []
333 for i in range(len(my_list)):
334 my_item = my_list[i]
335 if skip_if_marked and my_item.get(skip_if_marked):
336 continue
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200337 if item_matches(my_item, want_item):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200338 item_match_list.append(i)
339 if not item_match_list:
Neels Hofmeyr9b907702017-05-06 23:20:33 +0200340 if raise_if_missing:
341 raise NoResourceExn('No matching resource available for %s = %r'
342 % (key, want_item))
343 else:
344 # this one failed... see below
345 all_matches = []
346 break
347
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200348 all_matches.append( item_match_list )
349
350 if not all_matches:
Neels Hofmeyr9b907702017-05-06 23:20:33 +0200351 # ...this one failed. Makes no sense to solve resource
352 # allocations, return an empty list for this key to mark
353 # failure.
354 matches[key] = []
355 continue
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200356
357 # figure out who gets what
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200358 try:
359 solution = solve(all_matches)
360 except NotSolvable:
361 # instead of a cryptic error message, raise an exception that
362 # conveys meaning to the user.
363 raise NoResourceExn('Could not resolve request to reserve resources: '
364 '%d x %s with requirements: %r' % (len(want_list), key, want_list))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200365 picked = [ my_list[i] for i in solution if i is not None ]
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200366 for_origin.dbg('Picked', config.tostr(picked))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200367 matches[key] = picked
368
369 return Resources(matches, do_copy=do_copy)
370
371 def set_hashes(self):
372 for key, item_list in self.items():
373 for item in item_list:
374 item[HASH_KEY] = util.hash_obj(item, HASH_KEY, RESERVED_KEY, USED_KEY)
375
376 def add(self, more):
377 if more is self:
378 raise RuntimeError('adding a list of resources to itself?')
379 config.add(self, copy.deepcopy(more))
380
381 def combine(self, more_rules):
382 if more_rules is self:
383 raise RuntimeError('combining a list of resource rules with itself?')
384 config.combine(self, copy.deepcopy(more))
385
386 def mark_reserved_by(self, origin_id):
387 for key, item_list in self.items():
388 for item in item_list:
389 item[RESERVED_KEY] = origin_id
390
391
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200392class NotSolvable(Exception):
393 pass
394
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200395def solve(all_matches):
396 '''
397 all_matches shall be a list of index-lists.
398 all_matches[i] is the list of indexes that item i can use.
399 Return a solution so that each i gets a different index.
400 solve([ [0, 1, 2],
401 [0],
402 [0, 2] ]) == [1, 0, 2]
403 '''
404
405 def all_differ(l):
406 return len(set(l)) == len(l)
407
408 def search_in_permutations(fixed=[]):
409 idx = len(fixed)
410 for i in range(len(all_matches[idx])):
411 val = all_matches[idx][i]
412 # don't add a val that's already in the list
413 if val in fixed:
414 continue
415 l = list(fixed)
416 l.append(val)
417 if len(l) == len(all_matches):
418 # found a solution
419 return l
420 # not at the end yet, add next digit
421 r = search_in_permutations(l)
422 if r:
423 # nested search_in_permutations() call found a solution
424 return r
425 # this entire branch yielded no solution
426 return None
427
428 if not all_matches:
429 raise RuntimeError('Cannot solve: no candidates')
430
431 solution = search_in_permutations()
432 if not solution:
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200433 raise NotSolvable('The requested resource requirements are not solvable %r'
434 % all_matches)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200435 return solution
436
437
438def contains_hash(list_of_dicts, a_hash):
439 for d in list_of_dicts:
440 if d.get(HASH_KEY) == a_hash:
441 return True
442 return False
443
444def item_matches(item, wanted_item, ignore_keys=None):
445 if is_dict(wanted_item):
446 # match up two dicts
447 if not isinstance(item, dict):
448 return False
449 for key, wanted_val in wanted_item.items():
450 if ignore_keys and key in ignore_keys:
451 continue
452 if not item_matches(item.get(key), wanted_val, ignore_keys=ignore_keys):
453 return False
454 return True
455
456 if is_list(wanted_item):
Pau Espin Pedrol4e36f7c2017-08-28 13:29:28 +0200457 if not is_list(item):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200458 return False
Pau Espin Pedrol58475512017-09-14 15:33:15 +0200459 # Validate that all elements in both lists are of the same type:
460 t = util.list_validate_same_elem_type(wanted_item + item)
461 if t is None:
462 return True # both lists are empty, return
463 # For lists of complex objects, we expect them to be sorted lists:
464 if t in (dict, list, tuple):
465 for i in range(max(len(wanted_item), len(item))):
466 log.ctx(idx=i)
467 subitem = item[i] if i < len(item) else util.empty_instance_type(t)
468 wanted_subitem = wanted_item[i] if i < len(wanted_item) else util.empty_instance_type(t)
469 if not item_matches(subitem, wanted_subitem, ignore_keys=ignore_keys):
470 return False
471 else: # for lists of basic elements, we handle them as unsorted sets:
472 for val in wanted_item:
473 if val not in item:
474 return False
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200475 return True
476
477 return item == wanted_item
478
479
480class ReservedResources(log.Origin):
481 '''
482 After all resources have been figured out, this is the API that a test case
483 gets to interact with resources. From those resources that have been
484 reserved for it, it can pick some to mark them as currently in use.
485 Functions like nitb() provide a resource by automatically picking its
486 dependencies from so far unused (but reserved) resource.
487 '''
488
489 def __init__(self, resources_pool, origin, reserved):
490 self.resources_pool = resources_pool
491 self.origin = origin
492 self.reserved = reserved
493
494 def __repr__(self):
495 return 'resources(%s)=%s' % (self.origin.name(), pprint.pformat(self.reserved))
496
497 def get(self, kind, specifics=None):
498 if specifics is None:
499 specifics = {}
500 self.dbg('requesting use of', kind, specifics=specifics)
501 want = { kind: [specifics] }
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200502 available_dict = self.reserved.find(self.origin, want, skip_if_marked=USED_KEY,
503 do_copy=False, raise_if_missing=False,
504 log_label='Using')
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200505 available = available_dict.get(kind)
506 self.dbg(available=len(available))
507 if not available:
Neels Hofmeyrf9e86932017-06-06 19:47:40 +0200508 # cook up a detailed error message for the current situation
509 kind_reserved = self.reserved.get(kind, [])
510 used_count = len([r for r in kind_reserved if USED_KEY in r])
511 matching = self.reserved.find(self.origin, want, raise_if_missing=False, log_label=None).get(kind, [])
512 if not matching:
513 msg = 'none of the reserved resources matches requirements %r' % specifics
514 elif not (used_count < len(kind_reserved)):
515 msg = 'suite.conf reserved only %d x %r.' % (len(kind_reserved), kind)
516 else:
517 msg = ('No unused resource left that matches the requirements;'
518 ' Of reserved %d x %r, %d match the requirements, but all are already in use;'
519 ' Requirements: %r'
520 % (len(kind_reserved), kind, len(matching), specifics))
521 raise NoResourceExn('When trying to use instance nr %d of %r: %s' % (used_count + 1, kind, msg))
522
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200523 pick = available[0]
524 self.dbg(using=pick)
525 assert not pick.get(USED_KEY)
526 pick[USED_KEY] = True
527 return copy.deepcopy(pick)
528
529 def put(self, item):
530 if not item.get(USED_KEY):
531 raise RuntimeError('Can only put() a resource that is used: %r' % item)
532 hash_to_put = item.get(HASH_KEY)
533 if not hash_to_put:
534 raise RuntimeError('Can only put() a resource that has a hash marker: %r' % item)
535 for key, item_list in self.reserved.items():
536 my_list = self.get(key)
537 for my_item in my_list:
538 if hash_to_put == my_item.get(HASH_KEY):
539 my_item.pop(USED_KEY)
540
541 def put_all(self):
Pau Espin Pedrol1dd29552017-06-13 18:07:57 +0200542 if not self.reserved:
543 return
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200544 for key, item_list in self.reserved.items():
Pau Espin Pedrol1dd29552017-06-13 18:07:57 +0200545 for item in item_list:
546 item.pop(USED_KEY, None)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200547
548 def free(self):
Neels Hofmeyred4e5282017-05-29 02:53:54 +0200549 if self.reserved:
550 self.resources_pool.free(self.origin, self.reserved)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200551 self.reserved = None
552
Neels Hofmeyr2d1d5612017-05-22 20:02:41 +0200553 def counts(self):
554 counts = {}
555 for key in self.reserved.keys():
556 counts[key] = self.count(key)
557 return counts
558
559 def count(self, key):
560 return len(self.reserved.get(key) or [])
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200561
562# vim: expandtab tabstop=4 shiftwidth=4