blob: e71f4cd51e4197cab39e0959b8d9c223ad0e3d40 [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 Pedrol4f23ab52018-10-29 11:30:00 +010060 'bts[].gprs_mode': schema.GPRS_MODE,
Pau Espin Pedrol39df7f42018-05-07 13:49:33 +020061 'bts[].num_trx': schema.UINT,
62 'bts[].max_trx': schema.UINT,
Pau Espin Pedrolf6a07122018-07-27 16:24:29 +020063 'bts[].trx_list[].addr': schema.IPV4,
Your Name44af3412017-04-13 03:11:59 +020064 'bts[].trx_list[].hw_addr': schema.HWADDR,
65 'bts[].trx_list[].net_device': schema.STR,
Pau Espin Pedrolb26f32a2017-09-14 13:52:28 +020066 'bts[].trx_list[].nominal_power': schema.UINT,
67 'bts[].trx_list[].max_power_red': schema.UINT,
Pau Espin Pedrolc9b63762018-05-07 01:57:01 +020068 'bts[].trx_list[].timeslot_list[].phys_chan_config': schema.PHY_CHAN,
Pau Espin Pedrolf6a07122018-07-27 16:24:29 +020069 'bts[].trx_list[].power_supply.type': schema.STR,
70 'bts[].trx_list[].power_supply.device': schema.STR,
71 'bts[].trx_list[].power_supply.port': schema.STR,
Pau Espin Pedrol0d455042018-08-27 17:07:41 +020072 'bts[].osmo_trx.launch_trx': schema.BOOL_STR,
73 'bts[].osmo_trx.type': schema.STR,
74 'bts[].osmo_trx.clock_reference': schema.OSMO_TRX_CLOCK_REF,
75 'bts[].osmo_trx.trx_ip': schema.IPV4,
Pau Espin Pedrola9006df2018-10-01 12:26:39 +020076 'bts[].osmo_trx.remote_user': schema.STR,
Pau Espin Pedrol8cfa10f2018-11-06 12:05:19 +010077 'bts[].osmo_trx.dev_args': schema.STR,
Pau Espin Pedrol94eab262018-09-17 20:25:55 +020078 'bts[].osmo_trx.multi_arfcn': schema.BOOL_STR,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020079 'arfcn[].arfcn': schema.INT,
80 'arfcn[].band': schema.BAND,
81 'modem[].label': schema.STR,
82 'modem[].path': schema.STR,
83 'modem[].imsi': schema.IMSI,
84 'modem[].ki': schema.KI,
Pau Espin Pedrol713ce2c2017-08-24 16:57:17 +020085 'modem[].auth_algo': schema.AUTH_ALGO,
Pau Espin Pedrol57497a62017-08-28 14:21:15 +020086 'modem[].ciphers[]': schema.CIPHER,
Pau Espin Pedrolac18fd32017-08-31 18:49:47 +020087 'modem[].features[]': schema.MODEM_FEATURE,
Pau Espin Pedrolbc1ed882018-05-17 16:59:58 +020088 'osmocon_phone[].serial_device': schema.STR,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020089 }
90
91WANT_SCHEMA = util.dict_add(
Pau Espin Pedrolead79ac2017-09-12 15:19:18 +020092 dict([('%s[].times' % r, schema.TIMES) for r in R_ALL]),
Neels Hofmeyr3531a192017-03-28 14:30:28 +020093 RESOURCES_SCHEMA)
94
Pau Espin Pedrol0b302792017-09-10 16:33:10 +020095CONF_SCHEMA = util.dict_add(
Pau Espin Pedrol5dc24592018-08-27 12:53:41 +020096 { 'defaults.timeout': schema.STR,
97 'config.bsc.net.codec_list[]': schema.CODEC },
Pau Espin Pedrolaab56922018-08-21 14:58:29 +020098 dict([('resources.%s' % key, val) for key, val in WANT_SCHEMA.items()]),
99 dict([('modifiers.%s' % key, val) for key, val in WANT_SCHEMA.items()]))
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200100
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200101KNOWN_BTS_TYPES = {
Your Name44af3412017-04-13 03:11:59 +0200102 'osmo-bts-sysmo': bts_sysmo.SysmoBts,
103 'osmo-bts-trx': bts_osmotrx.OsmoBtsTrx,
Pau Espin Pedroldaed4472017-09-15 14:11:35 +0200104 'osmo-bts-octphy': bts_octphy.OsmoBtsOctphy,
Pau Espin Pedrol1b28a582018-03-08 21:01:26 +0100105 'nanobts': bts_nanobts.NanoBts,
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200106 }
107
108def register_bts_type(name, clazz):
109 KNOWN_BTS_TYPES[name] = clazz
110
111class ResourcesPool(log.Origin):
112 _remember_to_free = None
113 _registered_exit_handler = False
114
115 def __init__(self):
116 self.config_path = config.get_config_file(RESOURCES_CONF)
117 self.state_dir = config.get_state_dir()
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200118 super().__init__(log.C_CNF, conf=self.config_path, state=self.state_dir.path)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200119 self.read_conf()
120
121 def read_conf(self):
122 self.all_resources = Resources(config.read(self.config_path, RESOURCES_SCHEMA))
123 self.all_resources.set_hashes()
124
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200125 def reserve(self, origin, want, modifiers):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200126 '''
127 attempt to reserve the resources specified in the dict 'want' for
128 'origin'. Obtain a lock on the resources lock dir, verify that all
129 wanted resources are available, and if yes mark them as reserved.
130
131 On success, return a reservation object which can be used to release
132 the reservation. The reservation will be freed automatically on program
133 exit, if not yet done manually.
134
135 'origin' should be an Origin() instance.
136
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200137 'want' is a dict matching RESOURCES_SCHEMA, used to specify what to
138 reserve.
139
140 'modifiers' is a dict matching RESOURCES_SCHEMA, it is overlaid on top
141 of 'want'.
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200142
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200143 If an entry has no attribute set, any of the resources may be
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200144 reserved without further limitations.
145
146 ResourcesPool may also be selected with narrowed down constraints.
Neels Hofmeyr76d81032017-05-18 18:35:32 +0200147 This would reserve one IP address, two modems, one BTS of type
Neels Hofmeyr391afe32017-05-18 19:22:12 +0200148 sysmo and one of type trx, plus 2 ARFCNs in the 1800 band:
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200149
150 {
Neels Hofmeyr76d81032017-05-18 18:35:32 +0200151 'ip_address': [ {} ],
Neels Hofmeyr391afe32017-05-18 19:22:12 +0200152 'bts': [ { 'type': 'sysmo' }, { 'type': 'trx' } ],
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200153 'arfcn': [ { 'band': 'GSM-1800' }, { 'band': 'GSM-1800' } ],
154 'modem': [ {}, {} ],
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200155 }
156 '''
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200157 schema.validate(want, RESOURCES_SCHEMA)
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200158 schema.validate(modifiers, RESOURCES_SCHEMA)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200159
160 origin_id = origin.origin_id()
161
162 with self.state_dir.lock(origin_id):
163 rrfile_path = self.state_dir.mk_parentdir(RESERVED_RESOURCES_FILE)
164 reserved = Resources(config.read(rrfile_path, if_missing_return={}))
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200165 to_be_reserved = self.all_resources.without(reserved).find(origin, want)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200166
167 to_be_reserved.mark_reserved_by(origin_id)
168
169 reserved.add(to_be_reserved)
170 config.write(rrfile_path, reserved)
171
172 self.remember_to_free(to_be_reserved)
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200173 return ReservedResources(self, origin, to_be_reserved, modifiers)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200174
175 def free(self, origin, to_be_freed):
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200176 log.ctx(origin)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200177 with self.state_dir.lock(origin.origin_id()):
178 rrfile_path = self.state_dir.mk_parentdir(RESERVED_RESOURCES_FILE)
179 reserved = Resources(config.read(rrfile_path, if_missing_return={}))
180 reserved.drop(to_be_freed)
181 config.write(rrfile_path, reserved)
182 self.forget_freed(to_be_freed)
183
184 def register_exit_handler(self):
185 if self._registered_exit_handler:
186 return
187 atexit.register(self.clean_up_registered_resources)
188 self._registered_exit_handler = True
189
190 def unregister_exit_handler(self):
191 if not self._registered_exit_handler:
192 return
193 atexit.unregister(self.clean_up_registered_resources)
194 self._registered_exit_handler = False
195
196 def clean_up_registered_resources(self):
197 if not self._remember_to_free:
198 return
199 self.free(log.Origin('atexit.clean_up_registered_resources()'),
200 self._remember_to_free)
201
202 def remember_to_free(self, to_be_reserved):
203 self.register_exit_handler()
204 if not self._remember_to_free:
205 self._remember_to_free = Resources()
206 self._remember_to_free.add(to_be_reserved)
207
208 def forget_freed(self, freed):
209 if freed is self._remember_to_free:
210 self._remember_to_free.clear()
211 else:
212 self._remember_to_free.drop(freed)
213 if not self._remember_to_free:
214 self.unregister_exit_handler()
215
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100216 def next_persistent_value(self, token, first_val, validate_func, inc_func, origin):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200217 origin_id = origin.origin_id()
218
219 with self.state_dir.lock(origin_id):
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100220 token_path = self.state_dir.child('last_used_%s.state' % token)
221 log.ctx(token_path)
222 last_value = first_val
223 if os.path.exists(token_path):
224 if not os.path.isfile(token_path):
225 raise RuntimeError('path should be a file but is not: %r' % token_path)
226 with open(token_path, 'r') as f:
227 last_value = f.read().strip()
228 validate_func(last_value)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200229
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100230 next_value = inc_func(last_value)
231 with open(token_path, 'w') as f:
232 f.write(next_value)
233 return next_value
234
235 def next_msisdn(self, origin):
236 return self.next_persistent_value('msisdn', '1000', schema.msisdn, util.msisdn_inc, origin)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200237
Pau Espin Pedrol5e0c2512017-11-06 18:40:23 +0100238 def next_lac(self, origin):
Pau Espin Pedrolf7f06362017-11-28 12:56:35 +0100239 # LAC=0 has special meaning (MS detached), avoid it
240 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 +0200241
Pau Espin Pedrol8a3a7b52017-11-28 15:50:02 +0100242 def next_rac(self, origin):
243 return self.next_persistent_value('rac', '1', schema.uint8, lambda x: str((int(x)+1) % pow(2,8) or 1), origin)
244
Pau Espin Pedrol4ccce7c2017-11-07 11:13:20 +0100245 def next_cellid(self, origin):
246 return self.next_persistent_value('cellid', '1', schema.uint16, lambda x: str((int(x)+1) % pow(2,16)), origin)
247
Pau Espin Pedrol8a3a7b52017-11-28 15:50:02 +0100248 def next_bvci(self, origin):
249 # BVCI=0 and =1 are reserved, avoid them.
250 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)
251
Pau Espin Pedrol4676cbd2017-09-14 17:35:03 +0200252class NoResourceExn(log.Error):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200253 pass
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200254
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200255class Resources(dict):
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200256
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200257 def __init__(self, all_resources={}, do_copy=True):
258 if do_copy:
259 all_resources = copy.deepcopy(all_resources)
260 self.update(all_resources)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200261
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200262 def drop(self, reserved, fail_if_not_found=True):
263 # protect from modifying reserved because we're the same object
264 if reserved is self:
265 raise RuntimeError('Refusing to drop a list of resources from itself.'
266 ' This is probably a bug where a list of Resources()'
267 ' should have been copied but is passed as-is.'
268 ' use Resources.clear() instead.')
269
270 for key, reserved_list in reserved.items():
271 my_list = self.get(key) or []
272
273 if my_list is reserved_list:
274 self.pop(key)
275 continue
276
277 for reserved_item in reserved_list:
278 found = False
279 reserved_hash = reserved_item.get(HASH_KEY)
280 if not reserved_hash:
281 raise RuntimeError('Resources.drop() only works with hashed items')
282
283 for i in range(len(my_list)):
284 my_item = my_list[i]
285 my_hash = my_item.get(HASH_KEY)
286 if not my_hash:
287 raise RuntimeError('Resources.drop() only works with hashed items')
288 if my_hash == reserved_hash:
289 found = True
290 my_list.pop(i)
291 break
292
293 if fail_if_not_found and not found:
294 raise RuntimeError('Asked to drop resource from a pool, but the'
295 ' resource was not found: %s = %r' % (key, reserved_item))
296
297 if not my_list:
298 self.pop(key)
299 return self
300
301 def without(self, reserved):
302 return Resources(self).drop(reserved)
303
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200304 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 +0200305 '''
306 Pass a dict of resource requirements, e.g.:
307 want = {
308 'bts': [ {'type': 'osmo-bts-sysmo',}, {} ],
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200309 'modem': [ {}, {}, {} ]
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200310 }
311 This function tries to find a combination from the available resources that
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200312 matches these requirements. The return value is a dict (wrapped in a Resources class)
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200313 that contains the matching resources in the order of 'want' dict: in above
314 example, the returned dict would have a 'bts' list with the first item being
315 a sysmoBTS, the second item being any other available BTS.
316
317 If skip_if_marked is passed, any resource that contains this key is skipped.
318 E.g. if a BTS has the USED_KEY set like
319 reserved_resources = { 'bts' : {..., '_used': True} }
320 then this may be skipped by passing skip_if_marked='_used'
321 (or rather skip_if_marked=USED_KEY).
322
323 If do_copy is True, the returned dict is a deep copy and does not share
324 lists with any other Resources dict.
325
326 If raise_if_missing is False, this will return an empty item for any
327 resource that had no match, instead of immediately raising an exception.
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200328
329 This function expects input dictionaries whose contents have already
330 been replicated based on its the 'times' attributes. See
331 config.replicate_times() for more details.
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200332 '''
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200333 matches = {}
Neels Hofmeyr17c139e2017-04-12 02:42:02 +0200334 for key, want_list in sorted(want.items()): # sorted for deterministic test results
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200335 # here we have a resource of a given type, e.g. 'bts', with a list
336 # containing as many BTSes as the caller wants to reserve/use. Each
337 # list item contains specifics for the particular BTS.
Neels Hofmeyr2a1a1fa2017-05-29 01:36:21 +0200338 my_list = self.get(key, [])
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200339
Neels Hofmeyrf9e86932017-06-06 19:47:40 +0200340 if log_label:
341 for_origin.log(log_label, len(want_list), 'x', key, '(candidates: %d)'%len(my_list))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200342
343 # Try to avoid a less constrained item snatching away a resource
344 # from a more detailed constrained requirement.
345
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200346 # first record all matches, so that each requested item has a list
347 # of all available resources that match it. Some resources may
348 # appear for multiple requested items. Store matching indexes.
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200349 all_matches = []
350 for want_item in want_list:
351 item_match_list = []
352 for i in range(len(my_list)):
353 my_item = my_list[i]
354 if skip_if_marked and my_item.get(skip_if_marked):
355 continue
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200356 if item_matches(my_item, want_item):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200357 item_match_list.append(i)
358 if not item_match_list:
Neels Hofmeyr9b907702017-05-06 23:20:33 +0200359 if raise_if_missing:
360 raise NoResourceExn('No matching resource available for %s = %r'
361 % (key, want_item))
362 else:
363 # this one failed... see below
364 all_matches = []
365 break
366
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200367 all_matches.append( item_match_list )
368
369 if not all_matches:
Neels Hofmeyr9b907702017-05-06 23:20:33 +0200370 # ...this one failed. Makes no sense to solve resource
371 # allocations, return an empty list for this key to mark
372 # failure.
373 matches[key] = []
374 continue
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200375
376 # figure out who gets what
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200377 try:
378 solution = solve(all_matches)
379 except NotSolvable:
380 # instead of a cryptic error message, raise an exception that
381 # conveys meaning to the user.
382 raise NoResourceExn('Could not resolve request to reserve resources: '
383 '%d x %s with requirements: %r' % (len(want_list), key, want_list))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200384 picked = [ my_list[i] for i in solution if i is not None ]
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200385 for_origin.dbg('Picked', config.tostr(picked))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200386 matches[key] = picked
387
388 return Resources(matches, do_copy=do_copy)
389
390 def set_hashes(self):
391 for key, item_list in self.items():
392 for item in item_list:
393 item[HASH_KEY] = util.hash_obj(item, HASH_KEY, RESERVED_KEY, USED_KEY)
394
395 def add(self, more):
396 if more is self:
397 raise RuntimeError('adding a list of resources to itself?')
398 config.add(self, copy.deepcopy(more))
399
400 def combine(self, more_rules):
401 if more_rules is self:
402 raise RuntimeError('combining a list of resource rules with itself?')
403 config.combine(self, copy.deepcopy(more))
404
405 def mark_reserved_by(self, origin_id):
406 for key, item_list in self.items():
407 for item in item_list:
408 item[RESERVED_KEY] = origin_id
409
410
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200411class NotSolvable(Exception):
412 pass
413
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200414def solve(all_matches):
415 '''
416 all_matches shall be a list of index-lists.
417 all_matches[i] is the list of indexes that item i can use.
418 Return a solution so that each i gets a different index.
419 solve([ [0, 1, 2],
420 [0],
421 [0, 2] ]) == [1, 0, 2]
422 '''
423
424 def all_differ(l):
425 return len(set(l)) == len(l)
426
427 def search_in_permutations(fixed=[]):
428 idx = len(fixed)
429 for i in range(len(all_matches[idx])):
430 val = all_matches[idx][i]
431 # don't add a val that's already in the list
432 if val in fixed:
433 continue
434 l = list(fixed)
435 l.append(val)
436 if len(l) == len(all_matches):
437 # found a solution
438 return l
439 # not at the end yet, add next digit
440 r = search_in_permutations(l)
441 if r:
442 # nested search_in_permutations() call found a solution
443 return r
444 # this entire branch yielded no solution
445 return None
446
447 if not all_matches:
448 raise RuntimeError('Cannot solve: no candidates')
449
450 solution = search_in_permutations()
451 if not solution:
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200452 raise NotSolvable('The requested resource requirements are not solvable %r'
453 % all_matches)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200454 return solution
455
456
457def contains_hash(list_of_dicts, a_hash):
458 for d in list_of_dicts:
459 if d.get(HASH_KEY) == a_hash:
460 return True
461 return False
462
463def item_matches(item, wanted_item, ignore_keys=None):
464 if is_dict(wanted_item):
465 # match up two dicts
466 if not isinstance(item, dict):
467 return False
468 for key, wanted_val in wanted_item.items():
469 if ignore_keys and key in ignore_keys:
470 continue
471 if not item_matches(item.get(key), wanted_val, ignore_keys=ignore_keys):
472 return False
473 return True
474
475 if is_list(wanted_item):
Pau Espin Pedrol4e36f7c2017-08-28 13:29:28 +0200476 if not is_list(item):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200477 return False
Pau Espin Pedrol58475512017-09-14 15:33:15 +0200478 # Validate that all elements in both lists are of the same type:
479 t = util.list_validate_same_elem_type(wanted_item + item)
480 if t is None:
481 return True # both lists are empty, return
482 # For lists of complex objects, we expect them to be sorted lists:
483 if t in (dict, list, tuple):
484 for i in range(max(len(wanted_item), len(item))):
485 log.ctx(idx=i)
486 subitem = item[i] if i < len(item) else util.empty_instance_type(t)
487 wanted_subitem = wanted_item[i] if i < len(wanted_item) else util.empty_instance_type(t)
488 if not item_matches(subitem, wanted_subitem, ignore_keys=ignore_keys):
489 return False
490 else: # for lists of basic elements, we handle them as unsorted sets:
491 for val in wanted_item:
492 if val not in item:
493 return False
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200494 return True
495
496 return item == wanted_item
497
498
499class ReservedResources(log.Origin):
500 '''
501 After all resources have been figured out, this is the API that a test case
502 gets to interact with resources. From those resources that have been
503 reserved for it, it can pick some to mark them as currently in use.
504 Functions like nitb() provide a resource by automatically picking its
505 dependencies from so far unused (but reserved) resource.
506 '''
507
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200508 def __init__(self, resources_pool, origin, reserved, modifiers):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200509 self.resources_pool = resources_pool
510 self.origin = origin
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200511 self.reserved_original = reserved
512 self.reserved = copy.deepcopy(self.reserved_original)
513 config.overlay(self.reserved, modifiers)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200514
515 def __repr__(self):
516 return 'resources(%s)=%s' % (self.origin.name(), pprint.pformat(self.reserved))
517
518 def get(self, kind, specifics=None):
519 if specifics is None:
520 specifics = {}
521 self.dbg('requesting use of', kind, specifics=specifics)
522 want = { kind: [specifics] }
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200523 available_dict = self.reserved.find(self.origin, want, skip_if_marked=USED_KEY,
524 do_copy=False, raise_if_missing=False,
525 log_label='Using')
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200526 available = available_dict.get(kind)
527 self.dbg(available=len(available))
528 if not available:
Neels Hofmeyrf9e86932017-06-06 19:47:40 +0200529 # cook up a detailed error message for the current situation
530 kind_reserved = self.reserved.get(kind, [])
531 used_count = len([r for r in kind_reserved if USED_KEY in r])
532 matching = self.reserved.find(self.origin, want, raise_if_missing=False, log_label=None).get(kind, [])
533 if not matching:
534 msg = 'none of the reserved resources matches requirements %r' % specifics
535 elif not (used_count < len(kind_reserved)):
536 msg = 'suite.conf reserved only %d x %r.' % (len(kind_reserved), kind)
537 else:
538 msg = ('No unused resource left that matches the requirements;'
539 ' Of reserved %d x %r, %d match the requirements, but all are already in use;'
540 ' Requirements: %r'
541 % (len(kind_reserved), kind, len(matching), specifics))
542 raise NoResourceExn('When trying to use instance nr %d of %r: %s' % (used_count + 1, kind, msg))
543
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200544 pick = available[0]
545 self.dbg(using=pick)
546 assert not pick.get(USED_KEY)
547 pick[USED_KEY] = True
548 return copy.deepcopy(pick)
549
550 def put(self, item):
551 if not item.get(USED_KEY):
552 raise RuntimeError('Can only put() a resource that is used: %r' % item)
553 hash_to_put = item.get(HASH_KEY)
554 if not hash_to_put:
555 raise RuntimeError('Can only put() a resource that has a hash marker: %r' % item)
556 for key, item_list in self.reserved.items():
557 my_list = self.get(key)
558 for my_item in my_list:
559 if hash_to_put == my_item.get(HASH_KEY):
560 my_item.pop(USED_KEY)
561
562 def put_all(self):
Pau Espin Pedrol1dd29552017-06-13 18:07:57 +0200563 if not self.reserved:
564 return
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200565 for key, item_list in self.reserved.items():
Pau Espin Pedrol1dd29552017-06-13 18:07:57 +0200566 for item in item_list:
567 item.pop(USED_KEY, None)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200568
569 def free(self):
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200570 if self.reserved_original:
571 self.resources_pool.free(self.origin, self.reserved_original)
572 self.reserved_original = None
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200573
Neels Hofmeyr2d1d5612017-05-22 20:02:41 +0200574 def counts(self):
575 counts = {}
576 for key in self.reserved.keys():
577 counts[key] = self.count(key)
578 return counts
579
580 def count(self, key):
581 return len(self.reserved.get(key) or [])
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200582
583# vim: expandtab tabstop=4 shiftwidth=4