blob: 689f9767c2d78728cc02b075bc46f554cf62b4c8 [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,
Pau Espin Pedrol57497a62017-08-28 14:21:15 +020062 'bts[].ciphers[]': schema.CIPHER,
Your Name44af3412017-04-13 03:11:59 +020063 'bts[].trx_list[].hw_addr': schema.HWADDR,
64 'bts[].trx_list[].net_device': schema.STR,
Pau Espin Pedrolb26f32a2017-09-14 13:52:28 +020065 'bts[].trx_list[].nominal_power': schema.UINT,
66 'bts[].trx_list[].max_power_red': schema.UINT,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020067 'arfcn[].arfcn': schema.INT,
68 'arfcn[].band': schema.BAND,
69 'modem[].label': schema.STR,
70 'modem[].path': schema.STR,
71 'modem[].imsi': schema.IMSI,
72 'modem[].ki': schema.KI,
Pau Espin Pedrol713ce2c2017-08-24 16:57:17 +020073 'modem[].auth_algo': schema.AUTH_ALGO,
Pau Espin Pedrol57497a62017-08-28 14:21:15 +020074 'modem[].ciphers[]': schema.CIPHER,
Pau Espin Pedrolac18fd32017-08-31 18:49:47 +020075 'modem[].features[]': schema.MODEM_FEATURE,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020076 }
77
78WANT_SCHEMA = util.dict_add(
Pau Espin Pedrolead79ac2017-09-12 15:19:18 +020079 dict([('%s[].times' % r, schema.TIMES) for r in R_ALL]),
Neels Hofmeyr3531a192017-03-28 14:30:28 +020080 RESOURCES_SCHEMA)
81
Pau Espin Pedrol0b302792017-09-10 16:33:10 +020082CONF_SCHEMA = util.dict_add(
83 { 'defaults.timeout': schema.STR },
84 dict([('resources.%s' % key, val) for key, val in WANT_SCHEMA.items()]))
85
Neels Hofmeyr3531a192017-03-28 14:30:28 +020086KNOWN_BTS_TYPES = {
Your Name44af3412017-04-13 03:11:59 +020087 'osmo-bts-sysmo': bts_sysmo.SysmoBts,
88 'osmo-bts-trx': bts_osmotrx.OsmoBtsTrx,
Pau Espin Pedroldaed4472017-09-15 14:11:35 +020089 'osmo-bts-octphy': bts_octphy.OsmoBtsOctphy,
Pau Espin Pedrol1b28a582018-03-08 21:01:26 +010090 'nanobts': bts_nanobts.NanoBts,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020091 }
92
93def register_bts_type(name, clazz):
94 KNOWN_BTS_TYPES[name] = clazz
95
96class ResourcesPool(log.Origin):
97 _remember_to_free = None
98 _registered_exit_handler = False
99
100 def __init__(self):
101 self.config_path = config.get_config_file(RESOURCES_CONF)
102 self.state_dir = config.get_state_dir()
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200103 super().__init__(log.C_CNF, conf=self.config_path, state=self.state_dir.path)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200104 self.read_conf()
105
106 def read_conf(self):
107 self.all_resources = Resources(config.read(self.config_path, RESOURCES_SCHEMA))
108 self.all_resources.set_hashes()
109
110 def reserve(self, origin, want):
111 '''
112 attempt to reserve the resources specified in the dict 'want' for
113 'origin'. Obtain a lock on the resources lock dir, verify that all
114 wanted resources are available, and if yes mark them as reserved.
115
116 On success, return a reservation object which can be used to release
117 the reservation. The reservation will be freed automatically on program
118 exit, if not yet done manually.
119
120 'origin' should be an Origin() instance.
121
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200122 'want' is a dict matching RESOURCES_SCHEMA.
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200123
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200124 If an entry has no attribute set, any of the resources may be
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200125 reserved without further limitations.
126
127 ResourcesPool may also be selected with narrowed down constraints.
Neels Hofmeyr76d81032017-05-18 18:35:32 +0200128 This would reserve one IP address, two modems, one BTS of type
Neels Hofmeyr391afe32017-05-18 19:22:12 +0200129 sysmo and one of type trx, plus 2 ARFCNs in the 1800 band:
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200130
131 {
Neels Hofmeyr76d81032017-05-18 18:35:32 +0200132 'ip_address': [ {} ],
Neels Hofmeyr391afe32017-05-18 19:22:12 +0200133 'bts': [ { 'type': 'sysmo' }, { 'type': 'trx' } ],
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200134 'arfcn': [ { 'band': 'GSM-1800' }, { 'band': 'GSM-1800' } ],
135 'modem': [ {}, {} ],
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200136 }
137 '''
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200138 schema.validate(want, RESOURCES_SCHEMA)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200139
140 origin_id = origin.origin_id()
141
142 with self.state_dir.lock(origin_id):
143 rrfile_path = self.state_dir.mk_parentdir(RESERVED_RESOURCES_FILE)
144 reserved = Resources(config.read(rrfile_path, if_missing_return={}))
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200145 to_be_reserved = self.all_resources.without(reserved).find(origin, want)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200146
147 to_be_reserved.mark_reserved_by(origin_id)
148
149 reserved.add(to_be_reserved)
150 config.write(rrfile_path, reserved)
151
152 self.remember_to_free(to_be_reserved)
153 return ReservedResources(self, origin, to_be_reserved)
154
155 def free(self, origin, to_be_freed):
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200156 log.ctx(origin)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200157 with self.state_dir.lock(origin.origin_id()):
158 rrfile_path = self.state_dir.mk_parentdir(RESERVED_RESOURCES_FILE)
159 reserved = Resources(config.read(rrfile_path, if_missing_return={}))
160 reserved.drop(to_be_freed)
161 config.write(rrfile_path, reserved)
162 self.forget_freed(to_be_freed)
163
164 def register_exit_handler(self):
165 if self._registered_exit_handler:
166 return
167 atexit.register(self.clean_up_registered_resources)
168 self._registered_exit_handler = True
169
170 def unregister_exit_handler(self):
171 if not self._registered_exit_handler:
172 return
173 atexit.unregister(self.clean_up_registered_resources)
174 self._registered_exit_handler = False
175
176 def clean_up_registered_resources(self):
177 if not self._remember_to_free:
178 return
179 self.free(log.Origin('atexit.clean_up_registered_resources()'),
180 self._remember_to_free)
181
182 def remember_to_free(self, to_be_reserved):
183 self.register_exit_handler()
184 if not self._remember_to_free:
185 self._remember_to_free = Resources()
186 self._remember_to_free.add(to_be_reserved)
187
188 def forget_freed(self, freed):
189 if freed is self._remember_to_free:
190 self._remember_to_free.clear()
191 else:
192 self._remember_to_free.drop(freed)
193 if not self._remember_to_free:
194 self.unregister_exit_handler()
195
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100196 def next_persistent_value(self, token, first_val, validate_func, inc_func, origin):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200197 origin_id = origin.origin_id()
198
199 with self.state_dir.lock(origin_id):
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100200 token_path = self.state_dir.child('last_used_%s.state' % token)
201 log.ctx(token_path)
202 last_value = first_val
203 if os.path.exists(token_path):
204 if not os.path.isfile(token_path):
205 raise RuntimeError('path should be a file but is not: %r' % token_path)
206 with open(token_path, 'r') as f:
207 last_value = f.read().strip()
208 validate_func(last_value)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200209
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100210 next_value = inc_func(last_value)
211 with open(token_path, 'w') as f:
212 f.write(next_value)
213 return next_value
214
215 def next_msisdn(self, origin):
216 return self.next_persistent_value('msisdn', '1000', schema.msisdn, util.msisdn_inc, origin)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200217
Pau Espin Pedrol5e0c2512017-11-06 18:40:23 +0100218 def next_lac(self, origin):
Pau Espin Pedrolf7f06362017-11-28 12:56:35 +0100219 # LAC=0 has special meaning (MS detached), avoid it
220 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 +0200221
Pau Espin Pedrol8a3a7b52017-11-28 15:50:02 +0100222 def next_rac(self, origin):
223 return self.next_persistent_value('rac', '1', schema.uint8, lambda x: str((int(x)+1) % pow(2,8) or 1), origin)
224
Pau Espin Pedrol4ccce7c2017-11-07 11:13:20 +0100225 def next_cellid(self, origin):
226 return self.next_persistent_value('cellid', '1', schema.uint16, lambda x: str((int(x)+1) % pow(2,16)), origin)
227
Pau Espin Pedrol8a3a7b52017-11-28 15:50:02 +0100228 def next_bvci(self, origin):
229 # BVCI=0 and =1 are reserved, avoid them.
230 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)
231
Pau Espin Pedrol4676cbd2017-09-14 17:35:03 +0200232class NoResourceExn(log.Error):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200233 pass
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200234
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200235class Resources(dict):
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200236
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200237 def __init__(self, all_resources={}, do_copy=True):
238 if do_copy:
239 all_resources = copy.deepcopy(all_resources)
240 self.update(all_resources)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200241
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200242 def drop(self, reserved, fail_if_not_found=True):
243 # protect from modifying reserved because we're the same object
244 if reserved is self:
245 raise RuntimeError('Refusing to drop a list of resources from itself.'
246 ' This is probably a bug where a list of Resources()'
247 ' should have been copied but is passed as-is.'
248 ' use Resources.clear() instead.')
249
250 for key, reserved_list in reserved.items():
251 my_list = self.get(key) or []
252
253 if my_list is reserved_list:
254 self.pop(key)
255 continue
256
257 for reserved_item in reserved_list:
258 found = False
259 reserved_hash = reserved_item.get(HASH_KEY)
260 if not reserved_hash:
261 raise RuntimeError('Resources.drop() only works with hashed items')
262
263 for i in range(len(my_list)):
264 my_item = my_list[i]
265 my_hash = my_item.get(HASH_KEY)
266 if not my_hash:
267 raise RuntimeError('Resources.drop() only works with hashed items')
268 if my_hash == reserved_hash:
269 found = True
270 my_list.pop(i)
271 break
272
273 if fail_if_not_found and not found:
274 raise RuntimeError('Asked to drop resource from a pool, but the'
275 ' resource was not found: %s = %r' % (key, reserved_item))
276
277 if not my_list:
278 self.pop(key)
279 return self
280
281 def without(self, reserved):
282 return Resources(self).drop(reserved)
283
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200284 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 +0200285 '''
286 Pass a dict of resource requirements, e.g.:
287 want = {
288 'bts': [ {'type': 'osmo-bts-sysmo',}, {} ],
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200289 'modem': [ {}, {}, {} ]
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200290 }
291 This function tries to find a combination from the available resources that
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200292 matches these requirements. The return value is a dict (wrapped in a Resources class)
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200293 that contains the matching resources in the order of 'want' dict: in above
294 example, the returned dict would have a 'bts' list with the first item being
295 a sysmoBTS, the second item being any other available BTS.
296
297 If skip_if_marked is passed, any resource that contains this key is skipped.
298 E.g. if a BTS has the USED_KEY set like
299 reserved_resources = { 'bts' : {..., '_used': True} }
300 then this may be skipped by passing skip_if_marked='_used'
301 (or rather skip_if_marked=USED_KEY).
302
303 If do_copy is True, the returned dict is a deep copy and does not share
304 lists with any other Resources dict.
305
306 If raise_if_missing is False, this will return an empty item for any
307 resource that had no match, instead of immediately raising an exception.
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200308
309 This function expects input dictionaries whose contents have already
310 been replicated based on its the 'times' attributes. See
311 config.replicate_times() for more details.
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200312 '''
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200313 matches = {}
Neels Hofmeyr17c139e2017-04-12 02:42:02 +0200314 for key, want_list in sorted(want.items()): # sorted for deterministic test results
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200315 # here we have a resource of a given type, e.g. 'bts', with a list
316 # containing as many BTSes as the caller wants to reserve/use. Each
317 # list item contains specifics for the particular BTS.
Neels Hofmeyr2a1a1fa2017-05-29 01:36:21 +0200318 my_list = self.get(key, [])
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200319
Neels Hofmeyrf9e86932017-06-06 19:47:40 +0200320 if log_label:
321 for_origin.log(log_label, len(want_list), 'x', key, '(candidates: %d)'%len(my_list))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200322
323 # Try to avoid a less constrained item snatching away a resource
324 # from a more detailed constrained requirement.
325
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200326 # first record all matches, so that each requested item has a list
327 # of all available resources that match it. Some resources may
328 # appear for multiple requested items. Store matching indexes.
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200329 all_matches = []
330 for want_item in want_list:
331 item_match_list = []
332 for i in range(len(my_list)):
333 my_item = my_list[i]
334 if skip_if_marked and my_item.get(skip_if_marked):
335 continue
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200336 if item_matches(my_item, want_item):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200337 item_match_list.append(i)
338 if not item_match_list:
Neels Hofmeyr9b907702017-05-06 23:20:33 +0200339 if raise_if_missing:
340 raise NoResourceExn('No matching resource available for %s = %r'
341 % (key, want_item))
342 else:
343 # this one failed... see below
344 all_matches = []
345 break
346
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200347 all_matches.append( item_match_list )
348
349 if not all_matches:
Neels Hofmeyr9b907702017-05-06 23:20:33 +0200350 # ...this one failed. Makes no sense to solve resource
351 # allocations, return an empty list for this key to mark
352 # failure.
353 matches[key] = []
354 continue
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200355
356 # figure out who gets what
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200357 try:
358 solution = solve(all_matches)
359 except NotSolvable:
360 # instead of a cryptic error message, raise an exception that
361 # conveys meaning to the user.
362 raise NoResourceExn('Could not resolve request to reserve resources: '
363 '%d x %s with requirements: %r' % (len(want_list), key, want_list))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200364 picked = [ my_list[i] for i in solution if i is not None ]
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200365 for_origin.dbg('Picked', config.tostr(picked))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200366 matches[key] = picked
367
368 return Resources(matches, do_copy=do_copy)
369
370 def set_hashes(self):
371 for key, item_list in self.items():
372 for item in item_list:
373 item[HASH_KEY] = util.hash_obj(item, HASH_KEY, RESERVED_KEY, USED_KEY)
374
375 def add(self, more):
376 if more is self:
377 raise RuntimeError('adding a list of resources to itself?')
378 config.add(self, copy.deepcopy(more))
379
380 def combine(self, more_rules):
381 if more_rules is self:
382 raise RuntimeError('combining a list of resource rules with itself?')
383 config.combine(self, copy.deepcopy(more))
384
385 def mark_reserved_by(self, origin_id):
386 for key, item_list in self.items():
387 for item in item_list:
388 item[RESERVED_KEY] = origin_id
389
390
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200391class NotSolvable(Exception):
392 pass
393
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200394def solve(all_matches):
395 '''
396 all_matches shall be a list of index-lists.
397 all_matches[i] is the list of indexes that item i can use.
398 Return a solution so that each i gets a different index.
399 solve([ [0, 1, 2],
400 [0],
401 [0, 2] ]) == [1, 0, 2]
402 '''
403
404 def all_differ(l):
405 return len(set(l)) == len(l)
406
407 def search_in_permutations(fixed=[]):
408 idx = len(fixed)
409 for i in range(len(all_matches[idx])):
410 val = all_matches[idx][i]
411 # don't add a val that's already in the list
412 if val in fixed:
413 continue
414 l = list(fixed)
415 l.append(val)
416 if len(l) == len(all_matches):
417 # found a solution
418 return l
419 # not at the end yet, add next digit
420 r = search_in_permutations(l)
421 if r:
422 # nested search_in_permutations() call found a solution
423 return r
424 # this entire branch yielded no solution
425 return None
426
427 if not all_matches:
428 raise RuntimeError('Cannot solve: no candidates')
429
430 solution = search_in_permutations()
431 if not solution:
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200432 raise NotSolvable('The requested resource requirements are not solvable %r'
433 % all_matches)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200434 return solution
435
436
437def contains_hash(list_of_dicts, a_hash):
438 for d in list_of_dicts:
439 if d.get(HASH_KEY) == a_hash:
440 return True
441 return False
442
443def item_matches(item, wanted_item, ignore_keys=None):
444 if is_dict(wanted_item):
445 # match up two dicts
446 if not isinstance(item, dict):
447 return False
448 for key, wanted_val in wanted_item.items():
449 if ignore_keys and key in ignore_keys:
450 continue
451 if not item_matches(item.get(key), wanted_val, ignore_keys=ignore_keys):
452 return False
453 return True
454
455 if is_list(wanted_item):
Pau Espin Pedrol4e36f7c2017-08-28 13:29:28 +0200456 if not is_list(item):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200457 return False
Pau Espin Pedrol58475512017-09-14 15:33:15 +0200458 # Validate that all elements in both lists are of the same type:
459 t = util.list_validate_same_elem_type(wanted_item + item)
460 if t is None:
461 return True # both lists are empty, return
462 # For lists of complex objects, we expect them to be sorted lists:
463 if t in (dict, list, tuple):
464 for i in range(max(len(wanted_item), len(item))):
465 log.ctx(idx=i)
466 subitem = item[i] if i < len(item) else util.empty_instance_type(t)
467 wanted_subitem = wanted_item[i] if i < len(wanted_item) else util.empty_instance_type(t)
468 if not item_matches(subitem, wanted_subitem, ignore_keys=ignore_keys):
469 return False
470 else: # for lists of basic elements, we handle them as unsorted sets:
471 for val in wanted_item:
472 if val not in item:
473 return False
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200474 return True
475
476 return item == wanted_item
477
478
479class ReservedResources(log.Origin):
480 '''
481 After all resources have been figured out, this is the API that a test case
482 gets to interact with resources. From those resources that have been
483 reserved for it, it can pick some to mark them as currently in use.
484 Functions like nitb() provide a resource by automatically picking its
485 dependencies from so far unused (but reserved) resource.
486 '''
487
488 def __init__(self, resources_pool, origin, reserved):
489 self.resources_pool = resources_pool
490 self.origin = origin
491 self.reserved = reserved
492
493 def __repr__(self):
494 return 'resources(%s)=%s' % (self.origin.name(), pprint.pformat(self.reserved))
495
496 def get(self, kind, specifics=None):
497 if specifics is None:
498 specifics = {}
499 self.dbg('requesting use of', kind, specifics=specifics)
500 want = { kind: [specifics] }
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200501 available_dict = self.reserved.find(self.origin, want, skip_if_marked=USED_KEY,
502 do_copy=False, raise_if_missing=False,
503 log_label='Using')
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200504 available = available_dict.get(kind)
505 self.dbg(available=len(available))
506 if not available:
Neels Hofmeyrf9e86932017-06-06 19:47:40 +0200507 # cook up a detailed error message for the current situation
508 kind_reserved = self.reserved.get(kind, [])
509 used_count = len([r for r in kind_reserved if USED_KEY in r])
510 matching = self.reserved.find(self.origin, want, raise_if_missing=False, log_label=None).get(kind, [])
511 if not matching:
512 msg = 'none of the reserved resources matches requirements %r' % specifics
513 elif not (used_count < len(kind_reserved)):
514 msg = 'suite.conf reserved only %d x %r.' % (len(kind_reserved), kind)
515 else:
516 msg = ('No unused resource left that matches the requirements;'
517 ' Of reserved %d x %r, %d match the requirements, but all are already in use;'
518 ' Requirements: %r'
519 % (len(kind_reserved), kind, len(matching), specifics))
520 raise NoResourceExn('When trying to use instance nr %d of %r: %s' % (used_count + 1, kind, msg))
521
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200522 pick = available[0]
523 self.dbg(using=pick)
524 assert not pick.get(USED_KEY)
525 pick[USED_KEY] = True
526 return copy.deepcopy(pick)
527
528 def put(self, item):
529 if not item.get(USED_KEY):
530 raise RuntimeError('Can only put() a resource that is used: %r' % item)
531 hash_to_put = item.get(HASH_KEY)
532 if not hash_to_put:
533 raise RuntimeError('Can only put() a resource that has a hash marker: %r' % item)
534 for key, item_list in self.reserved.items():
535 my_list = self.get(key)
536 for my_item in my_list:
537 if hash_to_put == my_item.get(HASH_KEY):
538 my_item.pop(USED_KEY)
539
540 def put_all(self):
Pau Espin Pedrol1dd29552017-06-13 18:07:57 +0200541 if not self.reserved:
542 return
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200543 for key, item_list in self.reserved.items():
Pau Espin Pedrol1dd29552017-06-13 18:07:57 +0200544 for item in item_list:
545 item.pop(USED_KEY, None)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200546
547 def free(self):
Neels Hofmeyred4e5282017-05-29 02:53:54 +0200548 if self.reserved:
549 self.resources_pool.free(self.origin, self.reserved)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200550 self.reserved = None
551
Neels Hofmeyr2d1d5612017-05-22 20:02:41 +0200552 def counts(self):
553 counts = {}
554 for key in self.reserved.keys():
555 counts[key] = self.count(key)
556 return counts
557
558 def count(self, key):
559 return len(self.reserved.get(key) or [])
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200560
561# vim: expandtab tabstop=4 shiftwidth=4