xref: /aosp_15_r20/external/autotest/autotest_lib/client/common_lib/config_vars.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
1# Lint as: python2, python3
2# Copyright (c) 2021 The Chromium Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5"""
6Functions to load config variables from JSON with transformation.
7
8* The config is a key-value dictionary.
9* If the value is a list, then the list constitutes a list of conditions
10  to check.
11* A condition is a key-value dictionary where the key is an external variable
12  name and the value is a case-insensitive regexp to match. If multiple
13  variables used, they all must match for the condition to succeed.
14* A special key "value" is the value to assign if condition succeeds.
15* The first matching condition wins.
16* Condition with zero external vars always succeeds - it should be the last in
17  the list as a last resort case.
18* If none of conditions match, it's an error.
19* The value, in turn, can be a nested list of conditions.
20* If the value is a boolean, the condition checks for the presence or absence
21  of an external variable.
22
23Example:
24    Python source:
25        config = TransformJsonFile(
26                                    "config.json",
27                                    extvars={
28                                        "board": "board1",
29                                        "model": "model1",
30                                    })
31        # config -> {
32        #               "cuj_username": "user",
33        #               "private_key": "SECRET",
34        #               "some_var": "val for board1",
35        #               "some_var2": "default val2",
36        #           }
37
38        config = TransformJsonFile(
39                                    "config.json",
40                                    extvars={
41                                        "board": "board2",
42                                        "model": "model2",
43                                    })
44        # config -> {
45        #               "cuj_username": "user",
46        #               "private_key": "SECRET",
47        #               "some_var": "val for board2",
48        #               "some_var2": "val2 for board2 model2",
49        #           }
50
51    config.json:
52        {
53            "cuj_username": "user",
54            "private_key": "SECRET",
55            "some_var": [
56                {
57                    "board": "board1.*",
58                    "value": "val for board1",
59                },
60                {
61                    "board": "board2.*",
62                    "value": "val for board2",
63                },
64                {
65                    "value": "default val",
66                }
67            ],
68            "some_var2": [
69                {
70                    "board": "board2.*",
71                    "model": "model2.*",
72                    "value": "val2 for board2 model2",
73                },
74                {
75                    "value": "default val2",
76                }
77            ],
78        }
79
80See more examples in config_vars_unittest.py
81
82"""
83
84# Lint as: python2, python3
85# pylint: disable=missing-docstring,bad-indentation
86from __future__ import absolute_import
87from __future__ import division
88from __future__ import print_function
89
90import json
91import logging
92import re
93
94try:
95    unicode
96except NameError:
97    unicode = str
98
99VERBOSE = False
100
101
102class ConfigTransformError(ValueError):
103    pass
104
105
106def TransformConfig(data, extvars):
107    """Transforms data loaded from JSON to config variables.
108
109    Args:
110        data (dict): input data dictionary from JSON parser
111        extvars (dict): external variables dictionary
112
113    Returns:
114        dict: config variables
115
116    Raises:
117        ConfigTransformError: transformation error
118        're' errors
119    """
120    if not isinstance(data, dict):
121        _Error('Top level configuration object must be a dictionary but got ' +
122               data.__class__.__name__)
123
124    return {key: _GetVal(key, val, extvars) for key, val in data.items()}
125
126
127def TransformJsonText(text, extvars):
128    """Transforms JSON text to config variables.
129
130    Args:
131        text (str): JSON input
132        extvars (dict): external variables dictionary
133
134    Returns:
135        dict: config variables
136
137    Raises:
138        ConfigTransformError: transformation error
139        're' errors
140        'json' errors
141    """
142    data = json.loads(text)
143    return TransformConfig(data, extvars)
144
145
146def TransformJsonFile(file_name, extvars):
147    """Transforms JSON file to config variables.
148
149    Args:
150        file_name (str): JSON file name
151        extvars (dict): external variables dictionary
152
153    Returns:
154        dict: config variables
155
156    Raises:
157        ConfigTransformError: transformation error
158        're' errors
159        'json' errors
160        IO errors
161    """
162    with open(file_name, 'r') as f:
163        data = json.load(f)
164    return TransformConfig(data, extvars)
165
166
167def _GetVal(key, val, extvars):
168    """Calculates and returns the config variable value.
169
170    Args:
171        key (str): key for error reporting
172        val (str | list): variable value or conditions list
173        extvars (dict): external variables dictionary
174
175    Returns:
176        str: resolved variable value
177
178    Raises:
179        ConfigTransformError: transformation error
180    """
181    if (isinstance(val, str) or isinstance(val, unicode)
182                or isinstance(val, int) or isinstance(val, float)):
183        return val
184
185    if not isinstance(val, list):
186        _Error('Conditions must be an array but got ' + val.__class__.__name__,
187               json.dumps(val), key)
188
189    for cond in val:
190        if not isinstance(cond, dict):
191            _Error(
192                    'Condition must be a dictionary but got ' +
193                    cond.__class__.__name__, json.dumps(cond), key)
194        if 'value' not in cond:
195            _Error('Missing mandatory "value" key from condition',
196                   json.dumps(cond), key)
197
198        for cond_key, cond_val in cond.items():
199            if cond_key == 'value':
200                continue
201
202            if isinstance(cond_val, bool):
203                # Boolean value -> check if variable exists
204                if (cond_key in extvars) == cond_val:
205                    continue
206                else:
207                    break
208
209            if cond_key not in extvars:
210                logging.warning('Unknown external var: %s', cond_key)
211                break
212            if re.search(cond_val, extvars[cond_key], re.I) is None:
213                break
214        else:
215            return _GetVal(key, cond['value'], extvars)
216
217    _Error('Condition did not match any external vars',
218           json.dumps(val, indent=4) + '\nvars: ' + extvars.__str__(), key)
219
220
221def _Error(text, extra='', key=''):
222    """Reports and raises an error.
223
224    Args:
225        text (str): Error text
226        extra (str, optional): potentially sensitive error text for verbose output
227        key (str): key for error reporting or empty string if none
228
229    Raises:
230        ConfigTransformError: error
231    """
232    if key:
233        text = key + ': ' + text
234    if VERBOSE and extra:
235        text += ':\n' + extra
236    logging.error('%s', text)
237    raise ConfigTransformError(text)
238