blob: 9ca5665a718a0cb6165addc1205d6e4fb2731cda [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 Pedrolce35d912017-11-23 11:01:24 +010057 'bts[].direct_pcu': schema.BOOL_STR,
Pau Espin Pedrol57497a62017-08-28 14:21:15 +020058 'bts[].ciphers[]': schema.CIPHER,
Pau Espin Pedrol722e94e2018-08-22 11:01:32 +020059 'bts[].channel_allocator': schema.CHAN_ALLOCATOR,
Pau Espin Pedrol39df7f42018-05-07 13:49:33 +020060 'bts[].num_trx': schema.UINT,
61 'bts[].max_trx': schema.UINT,
Pau Espin Pedrolf6a07122018-07-27 16:24:29 +020062 'bts[].trx_list[].addr': schema.IPV4,
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,
Pau Espin Pedrolc9b63762018-05-07 01:57:01 +020067 'bts[].trx_list[].timeslot_list[].phys_chan_config': schema.PHY_CHAN,
Pau Espin Pedrolf6a07122018-07-27 16:24:29 +020068 'bts[].trx_list[].power_supply.type': schema.STR,
69 'bts[].trx_list[].power_supply.device': schema.STR,
70 'bts[].trx_list[].power_supply.port': schema.STR,
Pau Espin Pedrol0d455042018-08-27 17:07:41 +020071 'bts[].osmo_trx.launch_trx': schema.BOOL_STR,
72 'bts[].osmo_trx.type': schema.STR,
73 'bts[].osmo_trx.clock_reference': schema.OSMO_TRX_CLOCK_REF,
74 'bts[].osmo_trx.trx_ip': schema.IPV4,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020075 'arfcn[].arfcn': schema.INT,
76 'arfcn[].band': schema.BAND,
77 'modem[].label': schema.STR,
78 'modem[].path': schema.STR,
79 'modem[].imsi': schema.IMSI,
80 'modem[].ki': schema.KI,
Pau Espin Pedrol713ce2c2017-08-24 16:57:17 +020081 'modem[].auth_algo': schema.AUTH_ALGO,
Pau Espin Pedrol57497a62017-08-28 14:21:15 +020082 'modem[].ciphers[]': schema.CIPHER,
Pau Espin Pedrolac18fd32017-08-31 18:49:47 +020083 'modem[].features[]': schema.MODEM_FEATURE,
Pau Espin Pedrolbc1ed882018-05-17 16:59:58 +020084 'osmocon_phone[].serial_device': schema.STR,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020085 }
86
87WANT_SCHEMA = util.dict_add(
Pau Espin Pedrolead79ac2017-09-12 15:19:18 +020088 dict([('%s[].times' % r, schema.TIMES) for r in R_ALL]),
Neels Hofmeyr3531a192017-03-28 14:30:28 +020089 RESOURCES_SCHEMA)
90
Pau Espin Pedrol0b302792017-09-10 16:33:10 +020091CONF_SCHEMA = util.dict_add(
Pau Espin Pedrol5dc24592018-08-27 12:53:41 +020092 { 'defaults.timeout': schema.STR,
93 'config.bsc.net.codec_list[]': schema.CODEC },
Pau Espin Pedrolaab56922018-08-21 14:58:29 +020094 dict([('resources.%s' % key, val) for key, val in WANT_SCHEMA.items()]),
95 dict([('modifiers.%s' % key, val) for key, val in WANT_SCHEMA.items()]))
Pau Espin Pedrol0b302792017-09-10 16:33:10 +020096
Neels Hofmeyr3531a192017-03-28 14:30:28 +020097KNOWN_BTS_TYPES = {
Your Name44af3412017-04-13 03:11:59 +020098 'osmo-bts-sysmo': bts_sysmo.SysmoBts,
99 'osmo-bts-trx': bts_osmotrx.OsmoBtsTrx,
Pau Espin Pedroldaed4472017-09-15 14:11:35 +0200100 'osmo-bts-octphy': bts_octphy.OsmoBtsOctphy,
Pau Espin Pedrol1b28a582018-03-08 21:01:26 +0100101 'nanobts': bts_nanobts.NanoBts,
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200102 }
103
104def register_bts_type(name, clazz):
105 KNOWN_BTS_TYPES[name] = clazz
106
107class ResourcesPool(log.Origin):
108 _remember_to_free = None
109 _registered_exit_handler = False
110
111 def __init__(self):
112 self.config_path = config.get_config_file(RESOURCES_CONF)
113 self.state_dir = config.get_state_dir()
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200114 super().__init__(log.C_CNF, conf=self.config_path, state=self.state_dir.path)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200115 self.read_conf()
116
117 def read_conf(self):
118 self.all_resources = Resources(config.read(self.config_path, RESOURCES_SCHEMA))
119 self.all_resources.set_hashes()
120
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200121 def reserve(self, origin, want, modifiers):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200122 '''
123 attempt to reserve the resources specified in the dict 'want' for
124 'origin'. Obtain a lock on the resources lock dir, verify that all
125 wanted resources are available, and if yes mark them as reserved.
126
127 On success, return a reservation object which can be used to release
128 the reservation. The reservation will be freed automatically on program
129 exit, if not yet done manually.
130
131 'origin' should be an Origin() instance.
132
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200133 'want' is a dict matching RESOURCES_SCHEMA, used to specify what to
134 reserve.
135
136 'modifiers' is a dict matching RESOURCES_SCHEMA, it is overlaid on top
137 of 'want'.
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200138
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200139 If an entry has no attribute set, any of the resources may be
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200140 reserved without further limitations.
141
142 ResourcesPool may also be selected with narrowed down constraints.
Neels Hofmeyr76d81032017-05-18 18:35:32 +0200143 This would reserve one IP address, two modems, one BTS of type
Neels Hofmeyr391afe32017-05-18 19:22:12 +0200144 sysmo and one of type trx, plus 2 ARFCNs in the 1800 band:
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200145
146 {
Neels Hofmeyr76d81032017-05-18 18:35:32 +0200147 'ip_address': [ {} ],
Neels Hofmeyr391afe32017-05-18 19:22:12 +0200148 'bts': [ { 'type': 'sysmo' }, { 'type': 'trx' } ],
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200149 'arfcn': [ { 'band': 'GSM-1800' }, { 'band': 'GSM-1800' } ],
150 'modem': [ {}, {} ],
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200151 }
152 '''
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200153 schema.validate(want, RESOURCES_SCHEMA)
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200154 schema.validate(modifiers, RESOURCES_SCHEMA)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200155
156 origin_id = origin.origin_id()
157
158 with self.state_dir.lock(origin_id):
159 rrfile_path = self.state_dir.mk_parentdir(RESERVED_RESOURCES_FILE)
160 reserved = Resources(config.read(rrfile_path, if_missing_return={}))
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200161 to_be_reserved = self.all_resources.without(reserved).find(origin, want)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200162
163 to_be_reserved.mark_reserved_by(origin_id)
164
165 reserved.add(to_be_reserved)
166 config.write(rrfile_path, reserved)
167
168 self.remember_to_free(to_be_reserved)
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200169 return ReservedResources(self, origin, to_be_reserved, modifiers)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200170
171 def free(self, origin, to_be_freed):
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200172 log.ctx(origin)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200173 with self.state_dir.lock(origin.origin_id()):
174 rrfile_path = self.state_dir.mk_parentdir(RESERVED_RESOURCES_FILE)
175 reserved = Resources(config.read(rrfile_path, if_missing_return={}))
176 reserved.drop(to_be_freed)
177 config.write(rrfile_path, reserved)
178 self.forget_freed(to_be_freed)
179
180 def register_exit_handler(self):
181 if self._registered_exit_handler:
182 return
183 atexit.register(self.clean_up_registered_resources)
184 self._registered_exit_handler = True
185
186 def unregister_exit_handler(self):
187 if not self._registered_exit_handler:
188 return
189 atexit.unregister(self.clean_up_registered_resources)
190 self._registered_exit_handler = False
191
192 def clean_up_registered_resources(self):
193 if not self._remember_to_free:
194 return
195 self.free(log.Origin('atexit.clean_up_registered_resources()'),
196 self._remember_to_free)
197
198 def remember_to_free(self, to_be_reserved):
199 self.register_exit_handler()
200 if not self._remember_to_free:
201 self._remember_to_free = Resources()
202 self._remember_to_free.add(to_be_reserved)
203
204 def forget_freed(self, freed):
205 if freed is self._remember_to_free:
206 self._remember_to_free.clear()
207 else:
208 self._remember_to_free.drop(freed)
209 if not self._remember_to_free:
210 self.unregister_exit_handler()
211
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100212 def next_persistent_value(self, token, first_val, validate_func, inc_func, origin):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200213 origin_id = origin.origin_id()
214
215 with self.state_dir.lock(origin_id):
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100216 token_path = self.state_dir.child('last_used_%s.state' % token)
217 log.ctx(token_path)
218 last_value = first_val
219 if os.path.exists(token_path):
220 if not os.path.isfile(token_path):
221 raise RuntimeError('path should be a file but is not: %r' % token_path)
222 with open(token_path, 'r') as f:
223 last_value = f.read().strip()
224 validate_func(last_value)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200225
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100226 next_value = inc_func(last_value)
227 with open(token_path, 'w') as f:
228 f.write(next_value)
229 return next_value
230
231 def next_msisdn(self, origin):
232 return self.next_persistent_value('msisdn', '1000', schema.msisdn, util.msisdn_inc, origin)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200233
Pau Espin Pedrol5e0c2512017-11-06 18:40:23 +0100234 def next_lac(self, origin):
Pau Espin Pedrolf7f06362017-11-28 12:56:35 +0100235 # LAC=0 has special meaning (MS detached), avoid it
236 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 +0200237
Pau Espin Pedrol8a3a7b52017-11-28 15:50:02 +0100238 def next_rac(self, origin):
239 return self.next_persistent_value('rac', '1', schema.uint8, lambda x: str((int(x)+1) % pow(2,8) or 1), origin)
240
Pau Espin Pedrol4ccce7c2017-11-07 11:13:20 +0100241 def next_cellid(self, origin):
242 return self.next_persistent_value('cellid', '1', schema.uint16, lambda x: str((int(x)+1) % pow(2,16)), origin)
243
Pau Espin Pedrol8a3a7b52017-11-28 15:50:02 +0100244 def next_bvci(self, origin):
245 # BVCI=0 and =1 are reserved, avoid them.
246 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)
247
Pau Espin Pedrol4676cbd2017-09-14 17:35:03 +0200248class NoResourceExn(log.Error):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200249 pass
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200250
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200251class Resources(dict):
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200252
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200253 def __init__(self, all_resources={}, do_copy=True):
254 if do_copy:
255 all_resources = copy.deepcopy(all_resources)
256 self.update(all_resources)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200257
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200258 def drop(self, reserved, fail_if_not_found=True):
259 # protect from modifying reserved because we're the same object
260 if reserved is self:
261 raise RuntimeError('Refusing to drop a list of resources from itself.'
262 ' This is probably a bug where a list of Resources()'
263 ' should have been copied but is passed as-is.'
264 ' use Resources.clear() instead.')
265
266 for key, reserved_list in reserved.items():
267 my_list = self.get(key) or []
268
269 if my_list is reserved_list:
270 self.pop(key)
271 continue
272
273 for reserved_item in reserved_list:
274 found = False
275 reserved_hash = reserved_item.get(HASH_KEY)
276 if not reserved_hash:
277 raise RuntimeError('Resources.drop() only works with hashed items')
278
279 for i in range(len(my_list)):
280 my_item = my_list[i]
281 my_hash = my_item.get(HASH_KEY)
282 if not my_hash:
283 raise RuntimeError('Resources.drop() only works with hashed items')
284 if my_hash == reserved_hash:
285 found = True
286 my_list.pop(i)
287 break
288
289 if fail_if_not_found and not found:
290 raise RuntimeError('Asked to drop resource from a pool, but the'
291 ' resource was not found: %s = %r' % (key, reserved_item))
292
293 if not my_list:
294 self.pop(key)
295 return self
296
297 def without(self, reserved):
298 return Resources(self).drop(reserved)
299
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200300 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 +0200301 '''
302 Pass a dict of resource requirements, e.g.:
303 want = {
304 'bts': [ {'type': 'osmo-bts-sysmo',}, {} ],
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200305 'modem': [ {}, {}, {} ]
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200306 }
307 This function tries to find a combination from the available resources that
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200308 matches these requirements. The return value is a dict (wrapped in a Resources class)
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200309 that contains the matching resources in the order of 'want' dict: in above
310 example, the returned dict would have a 'bts' list with the first item being
311 a sysmoBTS, the second item being any other available BTS.
312
313 If skip_if_marked is passed, any resource that contains this key is skipped.
314 E.g. if a BTS has the USED_KEY set like
315 reserved_resources = { 'bts' : {..., '_used': True} }
316 then this may be skipped by passing skip_if_marked='_used'
317 (or rather skip_if_marked=USED_KEY).
318
319 If do_copy is True, the returned dict is a deep copy and does not share
320 lists with any other Resources dict.
321
322 If raise_if_missing is False, this will return an empty item for any
323 resource that had no match, instead of immediately raising an exception.
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200324
325 This function expects input dictionaries whose contents have already
326 been replicated based on its the 'times' attributes. See
327 config.replicate_times() for more details.
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200328 '''
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200329 matches = {}
Neels Hofmeyr17c139e2017-04-12 02:42:02 +0200330 for key, want_list in sorted(want.items()): # sorted for deterministic test results
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200331 # here we have a resource of a given type, e.g. 'bts', with a list
332 # containing as many BTSes as the caller wants to reserve/use. Each
333 # list item contains specifics for the particular BTS.
Neels Hofmeyr2a1a1fa2017-05-29 01:36:21 +0200334 my_list = self.get(key, [])
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200335
Neels Hofmeyrf9e86932017-06-06 19:47:40 +0200336 if log_label:
337 for_origin.log(log_label, len(want_list), 'x', key, '(candidates: %d)'%len(my_list))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200338
339 # Try to avoid a less constrained item snatching away a resource
340 # from a more detailed constrained requirement.
341
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200342 # first record all matches, so that each requested item has a list
343 # of all available resources that match it. Some resources may
344 # appear for multiple requested items. Store matching indexes.
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200345 all_matches = []
346 for want_item in want_list:
347 item_match_list = []
348 for i in range(len(my_list)):
349 my_item = my_list[i]
350 if skip_if_marked and my_item.get(skip_if_marked):
351 continue
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200352 if item_matches(my_item, want_item):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200353 item_match_list.append(i)
354 if not item_match_list:
Neels Hofmeyr9b907702017-05-06 23:20:33 +0200355 if raise_if_missing:
356 raise NoResourceExn('No matching resource available for %s = %r'
357 % (key, want_item))
358 else:
359 # this one failed... see below
360 all_matches = []
361 break
362
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200363 all_matches.append( item_match_list )
364
365 if not all_matches:
Neels Hofmeyr9b907702017-05-06 23:20:33 +0200366 # ...this one failed. Makes no sense to solve resource
367 # allocations, return an empty list for this key to mark
368 # failure.
369 matches[key] = []
370 continue
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200371
372 # figure out who gets what
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200373 try:
374 solution = solve(all_matches)
375 except NotSolvable:
376 # instead of a cryptic error message, raise an exception that
377 # conveys meaning to the user.
378 raise NoResourceExn('Could not resolve request to reserve resources: '
379 '%d x %s with requirements: %r' % (len(want_list), key, want_list))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200380 picked = [ my_list[i] for i in solution if i is not None ]
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200381 for_origin.dbg('Picked', config.tostr(picked))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200382 matches[key] = picked
383
384 return Resources(matches, do_copy=do_copy)
385
386 def set_hashes(self):
387 for key, item_list in self.items():
388 for item in item_list:
389 item[HASH_KEY] = util.hash_obj(item, HASH_KEY, RESERVED_KEY, USED_KEY)
390
391 def add(self, more):
392 if more is self:
393 raise RuntimeError('adding a list of resources to itself?')
394 config.add(self, copy.deepcopy(more))
395
396 def combine(self, more_rules):
397 if more_rules is self:
398 raise RuntimeError('combining a list of resource rules with itself?')
399 config.combine(self, copy.deepcopy(more))
400
401 def mark_reserved_by(self, origin_id):
402 for key, item_list in self.items():
403 for item in item_list:
404 item[RESERVED_KEY] = origin_id
405
406
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200407class NotSolvable(Exception):
408 pass
409
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200410def solve(all_matches):
411 '''
412 all_matches shall be a list of index-lists.
413 all_matches[i] is the list of indexes that item i can use.
414 Return a solution so that each i gets a different index.
415 solve([ [0, 1, 2],
416 [0],
417 [0, 2] ]) == [1, 0, 2]
418 '''
419
420 def all_differ(l):
421 return len(set(l)) == len(l)
422
423 def search_in_permutations(fixed=[]):
424 idx = len(fixed)
425 for i in range(len(all_matches[idx])):
426 val = all_matches[idx][i]
427 # don't add a val that's already in the list
428 if val in fixed:
429 continue
430 l = list(fixed)
431 l.append(val)
432 if len(l) == len(all_matches):
433 # found a solution
434 return l
435 # not at the end yet, add next digit
436 r = search_in_permutations(l)
437 if r:
438 # nested search_in_permutations() call found a solution
439 return r
440 # this entire branch yielded no solution
441 return None
442
443 if not all_matches:
444 raise RuntimeError('Cannot solve: no candidates')
445
446 solution = search_in_permutations()
447 if not solution:
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200448 raise NotSolvable('The requested resource requirements are not solvable %r'
449 % all_matches)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200450 return solution
451
452
453def contains_hash(list_of_dicts, a_hash):
454 for d in list_of_dicts:
455 if d.get(HASH_KEY) == a_hash:
456 return True
457 return False
458
459def item_matches(item, wanted_item, ignore_keys=None):
460 if is_dict(wanted_item):
461 # match up two dicts
462 if not isinstance(item, dict):
463 return False
464 for key, wanted_val in wanted_item.items():
465 if ignore_keys and key in ignore_keys:
466 continue
467 if not item_matches(item.get(key), wanted_val, ignore_keys=ignore_keys):
468 return False
469 return True
470
471 if is_list(wanted_item):
Pau Espin Pedrol4e36f7c2017-08-28 13:29:28 +0200472 if not is_list(item):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200473 return False
Pau Espin Pedrol58475512017-09-14 15:33:15 +0200474 # Validate that all elements in both lists are of the same type:
475 t = util.list_validate_same_elem_type(wanted_item + item)
476 if t is None:
477 return True # both lists are empty, return
478 # For lists of complex objects, we expect them to be sorted lists:
479 if t in (dict, list, tuple):
480 for i in range(max(len(wanted_item), len(item))):
481 log.ctx(idx=i)
482 subitem = item[i] if i < len(item) else util.empty_instance_type(t)
483 wanted_subitem = wanted_item[i] if i < len(wanted_item) else util.empty_instance_type(t)
484 if not item_matches(subitem, wanted_subitem, ignore_keys=ignore_keys):
485 return False
486 else: # for lists of basic elements, we handle them as unsorted sets:
487 for val in wanted_item:
488 if val not in item:
489 return False
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200490 return True
491
492 return item == wanted_item
493
494
495class ReservedResources(log.Origin):
496 '''
497 After all resources have been figured out, this is the API that a test case
498 gets to interact with resources. From those resources that have been
499 reserved for it, it can pick some to mark them as currently in use.
500 Functions like nitb() provide a resource by automatically picking its
501 dependencies from so far unused (but reserved) resource.
502 '''
503
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200504 def __init__(self, resources_pool, origin, reserved, modifiers):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200505 self.resources_pool = resources_pool
506 self.origin = origin
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200507 self.reserved_original = reserved
508 self.reserved = copy.deepcopy(self.reserved_original)
509 config.overlay(self.reserved, modifiers)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200510
511 def __repr__(self):
512 return 'resources(%s)=%s' % (self.origin.name(), pprint.pformat(self.reserved))
513
514 def get(self, kind, specifics=None):
515 if specifics is None:
516 specifics = {}
517 self.dbg('requesting use of', kind, specifics=specifics)
518 want = { kind: [specifics] }
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200519 available_dict = self.reserved.find(self.origin, want, skip_if_marked=USED_KEY,
520 do_copy=False, raise_if_missing=False,
521 log_label='Using')
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200522 available = available_dict.get(kind)
523 self.dbg(available=len(available))
524 if not available:
Neels Hofmeyrf9e86932017-06-06 19:47:40 +0200525 # cook up a detailed error message for the current situation
526 kind_reserved = self.reserved.get(kind, [])
527 used_count = len([r for r in kind_reserved if USED_KEY in r])
528 matching = self.reserved.find(self.origin, want, raise_if_missing=False, log_label=None).get(kind, [])
529 if not matching:
530 msg = 'none of the reserved resources matches requirements %r' % specifics
531 elif not (used_count < len(kind_reserved)):
532 msg = 'suite.conf reserved only %d x %r.' % (len(kind_reserved), kind)
533 else:
534 msg = ('No unused resource left that matches the requirements;'
535 ' Of reserved %d x %r, %d match the requirements, but all are already in use;'
536 ' Requirements: %r'
537 % (len(kind_reserved), kind, len(matching), specifics))
538 raise NoResourceExn('When trying to use instance nr %d of %r: %s' % (used_count + 1, kind, msg))
539
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200540 pick = available[0]
541 self.dbg(using=pick)
542 assert not pick.get(USED_KEY)
543 pick[USED_KEY] = True
544 return copy.deepcopy(pick)
545
546 def put(self, item):
547 if not item.get(USED_KEY):
548 raise RuntimeError('Can only put() a resource that is used: %r' % item)
549 hash_to_put = item.get(HASH_KEY)
550 if not hash_to_put:
551 raise RuntimeError('Can only put() a resource that has a hash marker: %r' % item)
552 for key, item_list in self.reserved.items():
553 my_list = self.get(key)
554 for my_item in my_list:
555 if hash_to_put == my_item.get(HASH_KEY):
556 my_item.pop(USED_KEY)
557
558 def put_all(self):
Pau Espin Pedrol1dd29552017-06-13 18:07:57 +0200559 if not self.reserved:
560 return
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200561 for key, item_list in self.reserved.items():
Pau Espin Pedrol1dd29552017-06-13 18:07:57 +0200562 for item in item_list:
563 item.pop(USED_KEY, None)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200564
565 def free(self):
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200566 if self.reserved_original:
567 self.resources_pool.free(self.origin, self.reserved_original)
568 self.reserved_original = None
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200569
Neels Hofmeyr2d1d5612017-05-22 20:02:41 +0200570 def counts(self):
571 counts = {}
572 for key in self.reserved.keys():
573 counts[key] = self.count(key)
574 return counts
575
576 def count(self, key):
577 return len(self.reserved.get(key) or [])
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200578
579# vim: expandtab tabstop=4 shiftwidth=4