xixuan | bea010f | 2017-03-27 10:10:19 -0700 | [diff] [blame] | 1 | # Copyright 2017 The Chromium OS Authors. All rights reserved. |
| 2 | # Use of this source code is governed by a BSD-style license that can be |
| 3 | # found in the LICENSE file. |
| 4 | |
| 5 | """Module of config file readers.""" |
Xixuan Wu | abbaa4c | 2017-08-23 17:31:49 -0700 | [diff] [blame] | 6 | # pylint: disable=invalid-name |
xixuan | bea010f | 2017-03-27 10:10:19 -0700 | [diff] [blame] | 7 | |
xixuan | bea010f | 2017-03-27 10:10:19 -0700 | [diff] [blame] | 8 | import collections |
Xixuan Wu | abbaa4c | 2017-08-23 17:31:49 -0700 | [diff] [blame] | 9 | import ConfigParser |
Xixuan Wu | 1e42c75 | 2019-03-21 13:41:49 -0700 | [diff] [blame] | 10 | import json |
Dhanya Ganesh | f239412 | 2020-09-22 19:44:21 +0000 | [diff] [blame] | 11 | import logging |
xixuan | bea010f | 2017-03-27 10:10:19 -0700 | [diff] [blame] | 12 | import re |
| 13 | |
Xixuan Wu | eefb21a | 2019-03-18 15:15:00 -0700 | [diff] [blame] | 14 | import build_utils |
xixuan | bea010f | 2017-03-27 10:10:19 -0700 | [diff] [blame] | 15 | |
xixuan | bea010f | 2017-03-27 10:10:19 -0700 | [diff] [blame] | 16 | |
Xixuan Wu | f4a4c88 | 2019-03-15 14:48:26 -0700 | [diff] [blame] | 17 | # Config readers. |
| 18 | Configs = collections.namedtuple( |
| 19 | 'Configs', |
| 20 | [ |
| 21 | 'lab_config', |
Xixuan Wu | f4a4c88 | 2019-03-15 14:48:26 -0700 | [diff] [blame] | 22 | ]) |
| 23 | |
Xixuan Wu | 26d06e0 | 2017-09-20 14:50:28 -0700 | [diff] [blame] | 24 | # The entries of android settings in lab config file. |
Xixuan Wu | 6fb1627 | 2017-10-19 13:16:00 -0700 | [diff] [blame] | 25 | ANDROID_SETTINGS = 'ANDROID' |
| 26 | |
| 27 | # The entries of CrOS settings in lab config file. |
| 28 | CROS_SETTINGS = 'CROS' |
Xixuan Wu | abbaa4c | 2017-08-23 17:31:49 -0700 | [diff] [blame] | 29 | |
Jack Neus | 8f0edb4 | 2022-03-17 20:21:39 +0000 | [diff] [blame] | 30 | # The entries of Rubik boards in rubik board config file. |
| 31 | RUBIK_SETTINGS = 'RUBIK' |
xixuan | bea010f | 2017-03-27 10:10:19 -0700 | [diff] [blame] | 32 | |
| 33 | def forgive_config_error(func): |
Xixuan Wu | abbaa4c | 2017-08-23 17:31:49 -0700 | [diff] [blame] | 34 | """A decorator making ConfigParser get*() functions return None on fail.""" |
| 35 | def wrapper(*args, **kwargs): |
| 36 | try: |
| 37 | return func(*args, **kwargs) |
| 38 | except ConfigParser.Error: |
| 39 | return None |
xixuan | bea010f | 2017-03-27 10:10:19 -0700 | [diff] [blame] | 40 | |
Xixuan Wu | abbaa4c | 2017-08-23 17:31:49 -0700 | [diff] [blame] | 41 | return wrapper |
xixuan | bea010f | 2017-03-27 10:10:19 -0700 | [diff] [blame] | 42 | |
| 43 | |
| 44 | class ConfigReader(ConfigParser.SafeConfigParser): |
Xixuan Wu | 0a8dbda | 2020-03-17 15:01:38 -0700 | [diff] [blame] | 45 | """A SafeConfigReader that returns None on any error in get*().""" |
Xixuan Wu | abbaa4c | 2017-08-23 17:31:49 -0700 | [diff] [blame] | 46 | |
| 47 | def __init__(self, filename): |
| 48 | """Initialize a config reader.""" |
| 49 | ConfigParser.SafeConfigParser.__init__(self) |
| 50 | self.read(filename) |
| 51 | |
| 52 | def read(self, filename): |
| 53 | """Read the file indicated by the given filename. |
| 54 | |
| 55 | Args: |
| 56 | filename: string for the file name to read. |
xixuan | bea010f | 2017-03-27 10:10:19 -0700 | [diff] [blame] | 57 | """ |
Xixuan Wu | abbaa4c | 2017-08-23 17:31:49 -0700 | [diff] [blame] | 58 | self.filename = filename |
| 59 | if filename is not None: |
| 60 | ConfigParser.SafeConfigParser.read(self, filename) |
xixuan | bea010f | 2017-03-27 10:10:19 -0700 | [diff] [blame] | 61 | |
Xixuan Wu | abbaa4c | 2017-08-23 17:31:49 -0700 | [diff] [blame] | 62 | @forgive_config_error |
| 63 | def getstring(self, section, option): |
| 64 | """Get string value for the given section and option.""" |
| 65 | return ConfigParser.SafeConfigParser.get(self, section, option) |
xixuan | bea010f | 2017-03-27 10:10:19 -0700 | [diff] [blame] | 66 | |
Xixuan Wu | abbaa4c | 2017-08-23 17:31:49 -0700 | [diff] [blame] | 67 | @forgive_config_error |
| 68 | def getint(self, section, option): |
| 69 | """Get int value for the given section and option.""" |
| 70 | return ConfigParser.SafeConfigParser.getint(self, section, option) |
xixuan | bea010f | 2017-03-27 10:10:19 -0700 | [diff] [blame] | 71 | |
Xixuan Wu | abbaa4c | 2017-08-23 17:31:49 -0700 | [diff] [blame] | 72 | @forgive_config_error |
| 73 | def getfloat(self, section, option): |
| 74 | """Get float value for the given section and option.""" |
| 75 | return ConfigParser.SafeConfigParser.getfloat(self, section, option) |
xixuan | bea010f | 2017-03-27 10:10:19 -0700 | [diff] [blame] | 76 | |
Xixuan Wu | abbaa4c | 2017-08-23 17:31:49 -0700 | [diff] [blame] | 77 | @forgive_config_error |
| 78 | def getboolean(self, section, option): |
| 79 | """Get boolean value for the given section and option.""" |
| 80 | return ConfigParser.SafeConfigParser.getboolean(self, section, option) |
xixuan | bea010f | 2017-03-27 10:10:19 -0700 | [diff] [blame] | 81 | |
| 82 | |
xixuan | bea010f | 2017-03-27 10:10:19 -0700 | [diff] [blame] | 83 | class LabConfig(object): |
Xixuan Wu | abbaa4c | 2017-08-23 17:31:49 -0700 | [diff] [blame] | 84 | """Class for reading lab config file for suite scheduler.""" |
xixuan | bea010f | 2017-03-27 10:10:19 -0700 | [diff] [blame] | 85 | |
Xixuan Wu | abbaa4c | 2017-08-23 17:31:49 -0700 | [diff] [blame] | 86 | def __init__(self, config_reader): |
| 87 | """Initialize a LabConfig to read lab info. |
xixuan | bea010f | 2017-03-27 10:10:19 -0700 | [diff] [blame] | 88 | |
Xixuan Wu | abbaa4c | 2017-08-23 17:31:49 -0700 | [diff] [blame] | 89 | Args: |
Xixuan Wu | f4a4c88 | 2019-03-15 14:48:26 -0700 | [diff] [blame] | 90 | config_reader: a ConfigReader to read lab configs. |
Xixuan Wu | abbaa4c | 2017-08-23 17:31:49 -0700 | [diff] [blame] | 91 | """ |
| 92 | self._config_reader = config_reader |
xixuan | bea010f | 2017-03-27 10:10:19 -0700 | [diff] [blame] | 93 | |
Xixuan Wu | abbaa4c | 2017-08-23 17:31:49 -0700 | [diff] [blame] | 94 | def get_android_board_list(self): |
| 95 | """Get the android board list from lab config file. |
xixuan | bea010f | 2017-03-27 10:10:19 -0700 | [diff] [blame] | 96 | |
Xixuan Wu | abbaa4c | 2017-08-23 17:31:49 -0700 | [diff] [blame] | 97 | Returns: |
| 98 | A set of Android boards: (android-angler, android-bullhead, ...) |
xixuan | bea010f | 2017-03-27 10:10:19 -0700 | [diff] [blame] | 99 | |
Xixuan Wu | abbaa4c | 2017-08-23 17:31:49 -0700 | [diff] [blame] | 100 | Raises: |
| 101 | ValueError: no android board list is found. |
| 102 | """ |
Dhanya Ganesh | c45d319 | 2020-08-06 20:52:12 +0000 | [diff] [blame] | 103 | return set() |
xixuan | bea010f | 2017-03-27 10:10:19 -0700 | [diff] [blame] | 104 | |
Xixuan Wu | 6fb1627 | 2017-10-19 13:16:00 -0700 | [diff] [blame] | 105 | def get_cros_board_list(self): |
| 106 | """Get the CrOS board list from lab config file. |
| 107 | |
| 108 | Returns: |
| 109 | A set of CrOS boards: (abox_edge, alex, ...) |
| 110 | |
| 111 | Raises: |
| 112 | ValueError: no CrOS board list is found. |
| 113 | """ |
| 114 | board_list = self._config_reader.getstring(CROS_SETTINGS, 'board_list') |
| 115 | if board_list is None: |
| 116 | raise ValueError('Cannot find CrOS board_list in lab config file.') |
| 117 | |
| 118 | android_board_list = self.get_android_board_list() |
| 119 | # Filter board like xxxx_1 to xxxx. |
| 120 | cros_board_list = set([re.match(r'(.*?)(?:-\d+)?$', board.strip()).group(1) |
| 121 | for board in board_list.split(',')]) |
Dhanya Ganesh | f239412 | 2020-09-22 19:44:21 +0000 | [diff] [blame] | 122 | logging.info('Found CROS boards in lab_config.ini: %s', str(cros_board_list)) |
Xixuan Wu | 6fb1627 | 2017-10-19 13:16:00 -0700 | [diff] [blame] | 123 | return cros_board_list - android_board_list |
| 124 | |
C Shapiro | 7f24a00 | 2017-12-05 14:25:09 -0700 | [diff] [blame] | 125 | def get_cros_model_map(self): |
| 126 | """Gets a map of CrOS boards onto a list of their corresponding models. |
| 127 | |
| 128 | Returns: |
| 129 | A map of CrOS board names onto a list of models |
| 130 | """ |
Garry Wang | 6ca42dd | 2022-02-28 21:54:19 -0800 | [diff] [blame] | 131 | return self._get_model_map(CROS_SETTINGS) |
| 132 | |
| 133 | def get_android_model_map(self): |
| 134 | """Gets a map of Android boards onto a list of their corresponding models. |
| 135 | |
| 136 | Returns: |
| 137 | A map of Android board names onto a list of models |
| 138 | """ |
| 139 | return self._get_model_map(ANDROID_SETTINGS) |
| 140 | |
| 141 | def _get_model_map(self, setting_name): |
C Shapiro | 7f24a00 | 2017-12-05 14:25:09 -0700 | [diff] [blame] | 142 | result = {} |
| 143 | if not self._config_reader: |
| 144 | return result |
| 145 | |
Garry Wang | 6ca42dd | 2022-02-28 21:54:19 -0800 | [diff] [blame] | 146 | encoded_map = self._config_reader.getstring(setting_name, 'model_list') |
C Shapiro | 7f24a00 | 2017-12-05 14:25:09 -0700 | [diff] [blame] | 147 | if encoded_map is None: |
| 148 | return result |
| 149 | |
Shijin Abraham | a8c7477 | 2020-01-24 09:39:25 -0800 | [diff] [blame] | 150 | # Models are read from boardName_modelName/familyName_boardName_modelName. |
C Shapiro | 7f24a00 | 2017-12-05 14:25:09 -0700 | [diff] [blame] | 151 | for full_model in encoded_map.split(','): |
Xixuan Wu | eefb21a | 2019-03-18 15:15:00 -0700 | [diff] [blame] | 152 | board_name, model_name = build_utils.parse_full_model(full_model) |
C Shapiro | 7f24a00 | 2017-12-05 14:25:09 -0700 | [diff] [blame] | 153 | if board_name in result: |
| 154 | result[board_name].append(model_name) |
| 155 | else: |
| 156 | result[board_name] = [model_name] |
| 157 | |
| 158 | return result |
| 159 | |
Jack Neus | 8f0edb4 | 2022-03-17 20:21:39 +0000 | [diff] [blame] | 160 | class RubikConfig(object): |
| 161 | """Class for reading the Rubik config file for suite scheduler.""" |
| 162 | |
| 163 | def __init__(self, config_reader): |
| 164 | """Initialize a RubikConfig to read rubik config. |
| 165 | |
| 166 | Args: |
| 167 | config_reader: a ConfigReader to read rubik config. |
| 168 | """ |
| 169 | self._config_reader = config_reader |
| 170 | |
Jack Neus | 2cbdc43 | 2022-06-24 20:21:30 +0000 | [diff] [blame] | 171 | def get_stable_rubik_milestones(self): |
| 172 | """Get stable rubik milestones from the Rubik config file. |
Jack Neus | 8f0edb4 | 2022-03-17 20:21:39 +0000 | [diff] [blame] | 173 | |
| 174 | Returns: |
Jack Neus | 2cbdc43 | 2022-06-24 20:21:30 +0000 | [diff] [blame] | 175 | A dict mapping build target (e.g. eve) to a stable rubik milestone. |
Jack Neus | 8f0edb4 | 2022-03-17 20:21:39 +0000 | [diff] [blame] | 176 | |
| 177 | Raises: |
| 178 | ValueError: no build target list is found. |
| 179 | """ |
Jack Neus | 2cbdc43 | 2022-06-24 20:21:30 +0000 | [diff] [blame] | 180 | data = self._config_reader.getstring(RUBIK_SETTINGS, 'stable_rubik_milestones') |
| 181 | if data is None: |
| 182 | raise ValueError('Cannot find stable_rubik_milestones in Rubik config file.') |
Jack Neus | 8f0edb4 | 2022-03-17 20:21:39 +0000 | [diff] [blame] | 183 | |
Jack Neus | 2cbdc43 | 2022-06-24 20:21:30 +0000 | [diff] [blame] | 184 | data = set([bt.strip() for bt in data.split(',')]) |
| 185 | mstone_map = {} |
| 186 | for entry in data: |
| 187 | entry = entry.split('=') |
| 188 | mstone_map[entry[0]] = int(entry[1]) |
| 189 | logging.info('Found stable rubik milestones targets in rubik_config.ini: %s', str(mstone_map)) |
| 190 | return mstone_map |
Jack Neus | 8f0edb4 | 2022-03-17 20:21:39 +0000 | [diff] [blame] | 191 | |
xixuan | bea010f | 2017-03-27 10:10:19 -0700 | [diff] [blame] | 192 | |
Xixuan Wu | 1e42c75 | 2019-03-21 13:41:49 -0700 | [diff] [blame] | 193 | def config_dump(res): |
| 194 | """Dump a config dict to a json string. |
| 195 | |
| 196 | Args: |
| 197 | res: A dict object to dump. |
| 198 | |
| 199 | Returns: |
| 200 | A json string. |
| 201 | """ |
| 202 | return json.dumps(res, |
| 203 | sort_keys=True, |
| 204 | indent=4, |
| 205 | separators=(',', ': ')) + '\n' |
| 206 | |
| 207 | |
| 208 | def _parse_model_list(models): |
| 209 | r"""Parse models of list. |
| 210 | |
| 211 | Args: |
| 212 | models: A string, read from lab or migration config file. |
| 213 | e.g. '\nreef,\nastronaut,\nrobo360' |
| 214 | Returns: |
| 215 | A set of string models, e.g. set(['reef', 'astronaut', 'robo360', ...]) |
| 216 | """ |
| 217 | return set([re.match(r'(.*?)(?:-\d+)?$', m.strip()).group(1) |
| 218 | for m in models.split(',')]) |
| 219 | |
| 220 | |
| 221 | def _parse_pool_list(l): |
Ned Nguyen | b0a377d | 2020-08-27 08:43:30 -0600 | [diff] [blame] | 222 | r"""Parse allowlist/denylist for a suite in migration config file. |
Xixuan Wu | 1e42c75 | 2019-03-21 13:41:49 -0700 | [diff] [blame] | 223 | |
| 224 | Args: |
| 225 | l: A string, read from migration config file entry |
Ned Nguyen | b0a377d | 2020-08-27 08:43:30 -0600 | [diff] [blame] | 226 | allowlist or denylist. e.g. '\ncq:reef,\nbvt:peppy...' |
Xixuan Wu | 1e42c75 | 2019-03-21 13:41:49 -0700 | [diff] [blame] | 227 | |
| 228 | Returns: |
Ned Nguyen | b0a377d | 2020-08-27 08:43:30 -0600 | [diff] [blame] | 229 | A list of allowlist/denylist <pool:model> string. |
Xixuan Wu | 1e42c75 | 2019-03-21 13:41:49 -0700 | [diff] [blame] | 230 | e.g. ['cq:reef', 'bvt:peppy', ...] |
| 231 | """ |
| 232 | if l is None or not l: |
| 233 | return [] |
| 234 | else: |
Xixuan Wu | 0d2ff81 | 2019-04-11 14:09:17 -0700 | [diff] [blame] | 235 | return list(set([w.strip() for w in l.split('\n') if w.strip()])) |
Xixuan Wu | 1e42c75 | 2019-03-21 13:41:49 -0700 | [diff] [blame] | 236 | |
| 237 | |
| 238 | def _get_models_by_pool(model_pool_list, pool): |
| 239 | """Get models from a model_pool_list by pool. |
| 240 | |
| 241 | Args: |
| 242 | model_pool_list: A list of model_pool string. e.g. |
Xixuan Wu | 0d2ff81 | 2019-04-11 14:09:17 -0700 | [diff] [blame] | 243 | [(cq, reef), (bvt, peppy), (bvt, link), ...] |
Xixuan Wu | 1e42c75 | 2019-03-21 13:41:49 -0700 | [diff] [blame] | 244 | pool: A string pool. e.g. 'bvt' |
| 245 | |
| 246 | Returns: |
| 247 | A list of string models searched by pool. e.g. |
| 248 | ['peppy', 'link', ...] |
| 249 | """ |
Xixuan Wu | 0d2ff81 | 2019-04-11 14:09:17 -0700 | [diff] [blame] | 250 | res = set([]) |
| 251 | for mp in model_pool_list: |
| 252 | p, m = _parse_pool_model_key(mp) |
| 253 | if p == pool: |
| 254 | res.add(m) |
Xixuan Wu | 1e42c75 | 2019-03-21 13:41:49 -0700 | [diff] [blame] | 255 | |
Xixuan Wu | 0d2ff81 | 2019-04-11 14:09:17 -0700 | [diff] [blame] | 256 | return list(res) |
Xixuan Wu | 1e42c75 | 2019-03-21 13:41:49 -0700 | [diff] [blame] | 257 | |
| 258 | |
| 259 | def suite_key(suite_name): |
| 260 | return 'suite:%s' % suite_name |
| 261 | |
| 262 | |
| 263 | def model_key(model_name): |
| 264 | return 'model:%s' % model_name |
| 265 | |
| 266 | |
| 267 | def pool_key(pool_name): |
| 268 | return 'pool:%s' % pool_name |
Xixuan Wu | 0d2ff81 | 2019-04-11 14:09:17 -0700 | [diff] [blame] | 269 | |
| 270 | |
| 271 | def _pool_model_key(pool_name, model_name=None): |
| 272 | if model_name is None: |
| 273 | return '(%s)' % pool_name |
| 274 | |
| 275 | return '(%s, %s)' % (pool_name, model_name) |
| 276 | |
| 277 | |
| 278 | def _parse_pool_model_key(model_pool): |
| 279 | return (model_pool[1:-1].split(',')[0].strip(), |
| 280 | model_pool[1:-1].split(',')[1].strip()) |