blob: e043d87cb8727de40a09ab38fb060ddba4015ae6 [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,
Pau Espin Pedrol94eab262018-09-17 20:25:55 +020075 'bts[].osmo_trx.multi_arfcn': schema.BOOL_STR,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020076 'arfcn[].arfcn': schema.INT,
77 'arfcn[].band': schema.BAND,
78 'modem[].label': schema.STR,
79 'modem[].path': schema.STR,
80 'modem[].imsi': schema.IMSI,
81 'modem[].ki': schema.KI,
Pau Espin Pedrol713ce2c2017-08-24 16:57:17 +020082 'modem[].auth_algo': schema.AUTH_ALGO,
Pau Espin Pedrol57497a62017-08-28 14:21:15 +020083 'modem[].ciphers[]': schema.CIPHER,
Pau Espin Pedrolac18fd32017-08-31 18:49:47 +020084 'modem[].features[]': schema.MODEM_FEATURE,
Pau Espin Pedrolbc1ed882018-05-17 16:59:58 +020085 'osmocon_phone[].serial_device': schema.STR,
Neels Hofmeyr3531a192017-03-28 14:30:28 +020086 }
87
88WANT_SCHEMA = util.dict_add(
Pau Espin Pedrolead79ac2017-09-12 15:19:18 +020089 dict([('%s[].times' % r, schema.TIMES) for r in R_ALL]),
Neels Hofmeyr3531a192017-03-28 14:30:28 +020090 RESOURCES_SCHEMA)
91
Pau Espin Pedrol0b302792017-09-10 16:33:10 +020092CONF_SCHEMA = util.dict_add(
Pau Espin Pedrol5dc24592018-08-27 12:53:41 +020093 { 'defaults.timeout': schema.STR,
94 'config.bsc.net.codec_list[]': schema.CODEC },
Pau Espin Pedrolaab56922018-08-21 14:58:29 +020095 dict([('resources.%s' % key, val) for key, val in WANT_SCHEMA.items()]),
96 dict([('modifiers.%s' % key, val) for key, val in WANT_SCHEMA.items()]))
Pau Espin Pedrol0b302792017-09-10 16:33:10 +020097
Neels Hofmeyr3531a192017-03-28 14:30:28 +020098KNOWN_BTS_TYPES = {
Your Name44af3412017-04-13 03:11:59 +020099 'osmo-bts-sysmo': bts_sysmo.SysmoBts,
100 'osmo-bts-trx': bts_osmotrx.OsmoBtsTrx,
Pau Espin Pedroldaed4472017-09-15 14:11:35 +0200101 'osmo-bts-octphy': bts_octphy.OsmoBtsOctphy,
Pau Espin Pedrol1b28a582018-03-08 21:01:26 +0100102 'nanobts': bts_nanobts.NanoBts,
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200103 }
104
105def register_bts_type(name, clazz):
106 KNOWN_BTS_TYPES[name] = clazz
107
108class ResourcesPool(log.Origin):
109 _remember_to_free = None
110 _registered_exit_handler = False
111
112 def __init__(self):
113 self.config_path = config.get_config_file(RESOURCES_CONF)
114 self.state_dir = config.get_state_dir()
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200115 super().__init__(log.C_CNF, conf=self.config_path, state=self.state_dir.path)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200116 self.read_conf()
117
118 def read_conf(self):
119 self.all_resources = Resources(config.read(self.config_path, RESOURCES_SCHEMA))
120 self.all_resources.set_hashes()
121
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200122 def reserve(self, origin, want, modifiers):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200123 '''
124 attempt to reserve the resources specified in the dict 'want' for
125 'origin'. Obtain a lock on the resources lock dir, verify that all
126 wanted resources are available, and if yes mark them as reserved.
127
128 On success, return a reservation object which can be used to release
129 the reservation. The reservation will be freed automatically on program
130 exit, if not yet done manually.
131
132 'origin' should be an Origin() instance.
133
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200134 'want' is a dict matching RESOURCES_SCHEMA, used to specify what to
135 reserve.
136
137 'modifiers' is a dict matching RESOURCES_SCHEMA, it is overlaid on top
138 of 'want'.
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200139
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200140 If an entry has no attribute set, any of the resources may be
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200141 reserved without further limitations.
142
143 ResourcesPool may also be selected with narrowed down constraints.
Neels Hofmeyr76d81032017-05-18 18:35:32 +0200144 This would reserve one IP address, two modems, one BTS of type
Neels Hofmeyr391afe32017-05-18 19:22:12 +0200145 sysmo and one of type trx, plus 2 ARFCNs in the 1800 band:
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200146
147 {
Neels Hofmeyr76d81032017-05-18 18:35:32 +0200148 'ip_address': [ {} ],
Neels Hofmeyr391afe32017-05-18 19:22:12 +0200149 'bts': [ { 'type': 'sysmo' }, { 'type': 'trx' } ],
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200150 'arfcn': [ { 'band': 'GSM-1800' }, { 'band': 'GSM-1800' } ],
151 'modem': [ {}, {} ],
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200152 }
153 '''
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200154 schema.validate(want, RESOURCES_SCHEMA)
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200155 schema.validate(modifiers, RESOURCES_SCHEMA)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200156
157 origin_id = origin.origin_id()
158
159 with self.state_dir.lock(origin_id):
160 rrfile_path = self.state_dir.mk_parentdir(RESERVED_RESOURCES_FILE)
161 reserved = Resources(config.read(rrfile_path, if_missing_return={}))
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200162 to_be_reserved = self.all_resources.without(reserved).find(origin, want)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200163
164 to_be_reserved.mark_reserved_by(origin_id)
165
166 reserved.add(to_be_reserved)
167 config.write(rrfile_path, reserved)
168
169 self.remember_to_free(to_be_reserved)
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200170 return ReservedResources(self, origin, to_be_reserved, modifiers)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200171
172 def free(self, origin, to_be_freed):
Neels Hofmeyr1a7a3f02017-06-10 01:18:27 +0200173 log.ctx(origin)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200174 with self.state_dir.lock(origin.origin_id()):
175 rrfile_path = self.state_dir.mk_parentdir(RESERVED_RESOURCES_FILE)
176 reserved = Resources(config.read(rrfile_path, if_missing_return={}))
177 reserved.drop(to_be_freed)
178 config.write(rrfile_path, reserved)
179 self.forget_freed(to_be_freed)
180
181 def register_exit_handler(self):
182 if self._registered_exit_handler:
183 return
184 atexit.register(self.clean_up_registered_resources)
185 self._registered_exit_handler = True
186
187 def unregister_exit_handler(self):
188 if not self._registered_exit_handler:
189 return
190 atexit.unregister(self.clean_up_registered_resources)
191 self._registered_exit_handler = False
192
193 def clean_up_registered_resources(self):
194 if not self._remember_to_free:
195 return
196 self.free(log.Origin('atexit.clean_up_registered_resources()'),
197 self._remember_to_free)
198
199 def remember_to_free(self, to_be_reserved):
200 self.register_exit_handler()
201 if not self._remember_to_free:
202 self._remember_to_free = Resources()
203 self._remember_to_free.add(to_be_reserved)
204
205 def forget_freed(self, freed):
206 if freed is self._remember_to_free:
207 self._remember_to_free.clear()
208 else:
209 self._remember_to_free.drop(freed)
210 if not self._remember_to_free:
211 self.unregister_exit_handler()
212
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100213 def next_persistent_value(self, token, first_val, validate_func, inc_func, origin):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200214 origin_id = origin.origin_id()
215
216 with self.state_dir.lock(origin_id):
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100217 token_path = self.state_dir.child('last_used_%s.state' % token)
218 log.ctx(token_path)
219 last_value = first_val
220 if os.path.exists(token_path):
221 if not os.path.isfile(token_path):
222 raise RuntimeError('path should be a file but is not: %r' % token_path)
223 with open(token_path, 'r') as f:
224 last_value = f.read().strip()
225 validate_func(last_value)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200226
Pau Espin Pedrol96d6b6c2017-11-06 18:09:09 +0100227 next_value = inc_func(last_value)
228 with open(token_path, 'w') as f:
229 f.write(next_value)
230 return next_value
231
232 def next_msisdn(self, origin):
233 return self.next_persistent_value('msisdn', '1000', schema.msisdn, util.msisdn_inc, origin)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200234
Pau Espin Pedrol5e0c2512017-11-06 18:40:23 +0100235 def next_lac(self, origin):
Pau Espin Pedrolf7f06362017-11-28 12:56:35 +0100236 # LAC=0 has special meaning (MS detached), avoid it
237 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 +0200238
Pau Espin Pedrol8a3a7b52017-11-28 15:50:02 +0100239 def next_rac(self, origin):
240 return self.next_persistent_value('rac', '1', schema.uint8, lambda x: str((int(x)+1) % pow(2,8) or 1), origin)
241
Pau Espin Pedrol4ccce7c2017-11-07 11:13:20 +0100242 def next_cellid(self, origin):
243 return self.next_persistent_value('cellid', '1', schema.uint16, lambda x: str((int(x)+1) % pow(2,16)), origin)
244
Pau Espin Pedrol8a3a7b52017-11-28 15:50:02 +0100245 def next_bvci(self, origin):
246 # BVCI=0 and =1 are reserved, avoid them.
247 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)
248
Pau Espin Pedrol4676cbd2017-09-14 17:35:03 +0200249class NoResourceExn(log.Error):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200250 pass
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200251
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200252class Resources(dict):
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200253
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200254 def __init__(self, all_resources={}, do_copy=True):
255 if do_copy:
256 all_resources = copy.deepcopy(all_resources)
257 self.update(all_resources)
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200258
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200259 def drop(self, reserved, fail_if_not_found=True):
260 # protect from modifying reserved because we're the same object
261 if reserved is self:
262 raise RuntimeError('Refusing to drop a list of resources from itself.'
263 ' This is probably a bug where a list of Resources()'
264 ' should have been copied but is passed as-is.'
265 ' use Resources.clear() instead.')
266
267 for key, reserved_list in reserved.items():
268 my_list = self.get(key) or []
269
270 if my_list is reserved_list:
271 self.pop(key)
272 continue
273
274 for reserved_item in reserved_list:
275 found = False
276 reserved_hash = reserved_item.get(HASH_KEY)
277 if not reserved_hash:
278 raise RuntimeError('Resources.drop() only works with hashed items')
279
280 for i in range(len(my_list)):
281 my_item = my_list[i]
282 my_hash = my_item.get(HASH_KEY)
283 if not my_hash:
284 raise RuntimeError('Resources.drop() only works with hashed items')
285 if my_hash == reserved_hash:
286 found = True
287 my_list.pop(i)
288 break
289
290 if fail_if_not_found and not found:
291 raise RuntimeError('Asked to drop resource from a pool, but the'
292 ' resource was not found: %s = %r' % (key, reserved_item))
293
294 if not my_list:
295 self.pop(key)
296 return self
297
298 def without(self, reserved):
299 return Resources(self).drop(reserved)
300
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200301 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 +0200302 '''
303 Pass a dict of resource requirements, e.g.:
304 want = {
305 'bts': [ {'type': 'osmo-bts-sysmo',}, {} ],
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200306 'modem': [ {}, {}, {} ]
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200307 }
308 This function tries to find a combination from the available resources that
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200309 matches these requirements. The return value is a dict (wrapped in a Resources class)
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200310 that contains the matching resources in the order of 'want' dict: in above
311 example, the returned dict would have a 'bts' list with the first item being
312 a sysmoBTS, the second item being any other available BTS.
313
314 If skip_if_marked is passed, any resource that contains this key is skipped.
315 E.g. if a BTS has the USED_KEY set like
316 reserved_resources = { 'bts' : {..., '_used': True} }
317 then this may be skipped by passing skip_if_marked='_used'
318 (or rather skip_if_marked=USED_KEY).
319
320 If do_copy is True, the returned dict is a deep copy and does not share
321 lists with any other Resources dict.
322
323 If raise_if_missing is False, this will return an empty item for any
324 resource that had no match, instead of immediately raising an exception.
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200325
326 This function expects input dictionaries whose contents have already
327 been replicated based on its the 'times' attributes. See
328 config.replicate_times() for more details.
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200329 '''
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200330 matches = {}
Neels Hofmeyr17c139e2017-04-12 02:42:02 +0200331 for key, want_list in sorted(want.items()): # sorted for deterministic test results
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200332 # here we have a resource of a given type, e.g. 'bts', with a list
333 # containing as many BTSes as the caller wants to reserve/use. Each
334 # list item contains specifics for the particular BTS.
Neels Hofmeyr2a1a1fa2017-05-29 01:36:21 +0200335 my_list = self.get(key, [])
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200336
Neels Hofmeyrf9e86932017-06-06 19:47:40 +0200337 if log_label:
338 for_origin.log(log_label, len(want_list), 'x', key, '(candidates: %d)'%len(my_list))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200339
340 # Try to avoid a less constrained item snatching away a resource
341 # from a more detailed constrained requirement.
342
Neels Hofmeyr2fade332017-05-06 23:18:23 +0200343 # first record all matches, so that each requested item has a list
344 # of all available resources that match it. Some resources may
345 # appear for multiple requested items. Store matching indexes.
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200346 all_matches = []
347 for want_item in want_list:
348 item_match_list = []
349 for i in range(len(my_list)):
350 my_item = my_list[i]
351 if skip_if_marked and my_item.get(skip_if_marked):
352 continue
Pau Espin Pedrol0b302792017-09-10 16:33:10 +0200353 if item_matches(my_item, want_item):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200354 item_match_list.append(i)
355 if not item_match_list:
Neels Hofmeyr9b907702017-05-06 23:20:33 +0200356 if raise_if_missing:
357 raise NoResourceExn('No matching resource available for %s = %r'
358 % (key, want_item))
359 else:
360 # this one failed... see below
361 all_matches = []
362 break
363
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200364 all_matches.append( item_match_list )
365
366 if not all_matches:
Neels Hofmeyr9b907702017-05-06 23:20:33 +0200367 # ...this one failed. Makes no sense to solve resource
368 # allocations, return an empty list for this key to mark
369 # failure.
370 matches[key] = []
371 continue
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200372
373 # figure out who gets what
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200374 try:
375 solution = solve(all_matches)
376 except NotSolvable:
377 # instead of a cryptic error message, raise an exception that
378 # conveys meaning to the user.
379 raise NoResourceExn('Could not resolve request to reserve resources: '
380 '%d x %s with requirements: %r' % (len(want_list), key, want_list))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200381 picked = [ my_list[i] for i in solution if i is not None ]
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200382 for_origin.dbg('Picked', config.tostr(picked))
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200383 matches[key] = picked
384
385 return Resources(matches, do_copy=do_copy)
386
387 def set_hashes(self):
388 for key, item_list in self.items():
389 for item in item_list:
390 item[HASH_KEY] = util.hash_obj(item, HASH_KEY, RESERVED_KEY, USED_KEY)
391
392 def add(self, more):
393 if more is self:
394 raise RuntimeError('adding a list of resources to itself?')
395 config.add(self, copy.deepcopy(more))
396
397 def combine(self, more_rules):
398 if more_rules is self:
399 raise RuntimeError('combining a list of resource rules with itself?')
400 config.combine(self, copy.deepcopy(more))
401
402 def mark_reserved_by(self, origin_id):
403 for key, item_list in self.items():
404 for item in item_list:
405 item[RESERVED_KEY] = origin_id
406
407
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200408class NotSolvable(Exception):
409 pass
410
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200411def solve(all_matches):
412 '''
413 all_matches shall be a list of index-lists.
414 all_matches[i] is the list of indexes that item i can use.
415 Return a solution so that each i gets a different index.
416 solve([ [0, 1, 2],
417 [0],
418 [0, 2] ]) == [1, 0, 2]
419 '''
420
421 def all_differ(l):
422 return len(set(l)) == len(l)
423
424 def search_in_permutations(fixed=[]):
425 idx = len(fixed)
426 for i in range(len(all_matches[idx])):
427 val = all_matches[idx][i]
428 # don't add a val that's already in the list
429 if val in fixed:
430 continue
431 l = list(fixed)
432 l.append(val)
433 if len(l) == len(all_matches):
434 # found a solution
435 return l
436 # not at the end yet, add next digit
437 r = search_in_permutations(l)
438 if r:
439 # nested search_in_permutations() call found a solution
440 return r
441 # this entire branch yielded no solution
442 return None
443
444 if not all_matches:
445 raise RuntimeError('Cannot solve: no candidates')
446
447 solution = search_in_permutations()
448 if not solution:
Neels Hofmeyra8a05a22017-06-06 19:47:40 +0200449 raise NotSolvable('The requested resource requirements are not solvable %r'
450 % all_matches)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200451 return solution
452
453
454def contains_hash(list_of_dicts, a_hash):
455 for d in list_of_dicts:
456 if d.get(HASH_KEY) == a_hash:
457 return True
458 return False
459
460def item_matches(item, wanted_item, ignore_keys=None):
461 if is_dict(wanted_item):
462 # match up two dicts
463 if not isinstance(item, dict):
464 return False
465 for key, wanted_val in wanted_item.items():
466 if ignore_keys and key in ignore_keys:
467 continue
468 if not item_matches(item.get(key), wanted_val, ignore_keys=ignore_keys):
469 return False
470 return True
471
472 if is_list(wanted_item):
Pau Espin Pedrol4e36f7c2017-08-28 13:29:28 +0200473 if not is_list(item):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200474 return False
Pau Espin Pedrol58475512017-09-14 15:33:15 +0200475 # Validate that all elements in both lists are of the same type:
476 t = util.list_validate_same_elem_type(wanted_item + item)
477 if t is None:
478 return True # both lists are empty, return
479 # For lists of complex objects, we expect them to be sorted lists:
480 if t in (dict, list, tuple):
481 for i in range(max(len(wanted_item), len(item))):
482 log.ctx(idx=i)
483 subitem = item[i] if i < len(item) else util.empty_instance_type(t)
484 wanted_subitem = wanted_item[i] if i < len(wanted_item) else util.empty_instance_type(t)
485 if not item_matches(subitem, wanted_subitem, ignore_keys=ignore_keys):
486 return False
487 else: # for lists of basic elements, we handle them as unsorted sets:
488 for val in wanted_item:
489 if val not in item:
490 return False
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200491 return True
492
493 return item == wanted_item
494
495
496class ReservedResources(log.Origin):
497 '''
498 After all resources have been figured out, this is the API that a test case
499 gets to interact with resources. From those resources that have been
500 reserved for it, it can pick some to mark them as currently in use.
501 Functions like nitb() provide a resource by automatically picking its
502 dependencies from so far unused (but reserved) resource.
503 '''
504
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200505 def __init__(self, resources_pool, origin, reserved, modifiers):
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200506 self.resources_pool = resources_pool
507 self.origin = origin
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200508 self.reserved_original = reserved
509 self.reserved = copy.deepcopy(self.reserved_original)
510 config.overlay(self.reserved, modifiers)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200511
512 def __repr__(self):
513 return 'resources(%s)=%s' % (self.origin.name(), pprint.pformat(self.reserved))
514
515 def get(self, kind, specifics=None):
516 if specifics is None:
517 specifics = {}
518 self.dbg('requesting use of', kind, specifics=specifics)
519 want = { kind: [specifics] }
Neels Hofmeyrcccbe592017-05-07 01:16:07 +0200520 available_dict = self.reserved.find(self.origin, want, skip_if_marked=USED_KEY,
521 do_copy=False, raise_if_missing=False,
522 log_label='Using')
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200523 available = available_dict.get(kind)
524 self.dbg(available=len(available))
525 if not available:
Neels Hofmeyrf9e86932017-06-06 19:47:40 +0200526 # cook up a detailed error message for the current situation
527 kind_reserved = self.reserved.get(kind, [])
528 used_count = len([r for r in kind_reserved if USED_KEY in r])
529 matching = self.reserved.find(self.origin, want, raise_if_missing=False, log_label=None).get(kind, [])
530 if not matching:
531 msg = 'none of the reserved resources matches requirements %r' % specifics
532 elif not (used_count < len(kind_reserved)):
533 msg = 'suite.conf reserved only %d x %r.' % (len(kind_reserved), kind)
534 else:
535 msg = ('No unused resource left that matches the requirements;'
536 ' Of reserved %d x %r, %d match the requirements, but all are already in use;'
537 ' Requirements: %r'
538 % (len(kind_reserved), kind, len(matching), specifics))
539 raise NoResourceExn('When trying to use instance nr %d of %r: %s' % (used_count + 1, kind, msg))
540
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200541 pick = available[0]
542 self.dbg(using=pick)
543 assert not pick.get(USED_KEY)
544 pick[USED_KEY] = True
545 return copy.deepcopy(pick)
546
547 def put(self, item):
548 if not item.get(USED_KEY):
549 raise RuntimeError('Can only put() a resource that is used: %r' % item)
550 hash_to_put = item.get(HASH_KEY)
551 if not hash_to_put:
552 raise RuntimeError('Can only put() a resource that has a hash marker: %r' % item)
553 for key, item_list in self.reserved.items():
554 my_list = self.get(key)
555 for my_item in my_list:
556 if hash_to_put == my_item.get(HASH_KEY):
557 my_item.pop(USED_KEY)
558
559 def put_all(self):
Pau Espin Pedrol1dd29552017-06-13 18:07:57 +0200560 if not self.reserved:
561 return
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200562 for key, item_list in self.reserved.items():
Pau Espin Pedrol1dd29552017-06-13 18:07:57 +0200563 for item in item_list:
564 item.pop(USED_KEY, None)
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200565
566 def free(self):
Pau Espin Pedrolaab56922018-08-21 14:58:29 +0200567 if self.reserved_original:
568 self.resources_pool.free(self.origin, self.reserved_original)
569 self.reserved_original = None
Neels Hofmeyr3531a192017-03-28 14:30:28 +0200570
Neels Hofmeyr2d1d5612017-05-22 20:02:41 +0200571 def counts(self):
572 counts = {}
573 for key in self.reserved.keys():
574 counts[key] = self.count(key)
575 return counts
576
577 def count(self, key):
578 return len(self.reserved.get(key) or [])
Neels Hofmeyrdae3d3c2017-03-28 12:16:58 +0200579
580# vim: expandtab tabstop=4 shiftwidth=4