xref: /openbmc/phosphor-power/phosphor-regulators/tools/validate-regulators-config.py (revision e959754ba97354eb38e4b2bb59baf2012b53bcdb)
1#!/usr/bin/env python
2
3import argparse
4import json
5import sys
6import jsonschema
7
8r"""
9Validates the phosphor-regulators configuration file. Checks it against a JSON
10schema as well as doing some extra checks that can't be encoded in the schema.
11"""
12
13def handle_validation_error():
14    sys.exit("Validation failed.")
15
16def get_values(json_element, key, result = None):
17    r"""
18    Finds all occurrences of a key within the specified JSON element and its
19    children. Returns the associated values.
20    To search the entire configuration file, pass the root JSON element
21    json_element: JSON element within the config file.
22    key: key name.
23    result: list of values found with the specified key.
24    """
25
26    if result is None:
27        result = []
28    if type(json_element) is dict:
29        for json_key in json_element:
30            if json_key == key:
31                result.append(json_element[json_key])
32            elif type(json_element[json_key]) in (list, dict):
33                get_values(json_element[json_key], key, result)
34    elif type(json_element) is list:
35        for item in json_element:
36            if type(item) in (list, dict):
37                get_values(item, key, result)
38    return result
39
40def get_rule_ids(config_json):
41    r"""
42    Get all rule IDs in the configuration file.
43    config_json: Configuration file JSON
44    """
45    rule_ids = []
46    for rule in config_json.get('rules', {}):
47        rule_ids.append(rule['id'])
48    return rule_ids
49
50def get_device_ids(config_json):
51    r"""
52    Get all device IDs in the configuration file.
53    config_json: Configuration file JSON
54    """
55    device_ids = []
56    for chassis in config_json.get('chassis', {}):
57        for device in chassis.get('devices', {}):
58            device_ids.append(device['id'])
59    return device_ids
60
61def check_set_device_value_exists(config_json):
62    r"""
63    Check if a set_device action specifies a device ID that does not exist.
64    config_json: Configuration file JSON
65    """
66
67    device_ids = get_values(config_json, 'set_device')
68    valid_device_ids = get_device_ids(config_json)
69    for device_id in device_ids:
70        if device_id not in valid_device_ids:
71            sys.stderr.write("Error: Device ID does not exist.\n"+\
72            "Found set_device action that specifies invalid device ID "+\
73            device_id+'\n')
74            handle_validation_error()
75
76def check_run_rule_value_exists(config_json):
77    r"""
78    Check if any run_rule actions specify a rule ID that does not exist.
79    config_json: Configuration file JSON
80    """
81
82    rule_ids = get_values(config_json, 'run_rule')
83    valid_rule_ids = get_rule_ids(config_json)
84    for rule_id in rule_ids:
85        if rule_id not in valid_rule_ids:
86            sys.stderr.write("Error: Rule ID does not exist.\n"+\
87            "Found run_rule action that specifies invalid rule ID "+\
88            rule_id+'\n')
89            handle_validation_error()
90
91def check_infinite_loops_in_rule(config_json, rule_json, call_stack=[]):
92    r"""
93    Check if a 'run_rule' action in the specified rule causes an
94    infinite loop.
95    config_json: Configuration file JSON.
96    rule_json: A rule in the JSON config file.
97    call_stack: Current call stack of rules.
98    """
99
100    call_stack.append(rule_json['id'])
101    for action in rule_json.get('actions', {}):
102        if 'run_rule' in action:
103            run_rule_id = action['run_rule']
104            if run_rule_id in call_stack:
105                call_stack.append(run_rule_id)
106                sys.stderr.write(\
107               "Infinite loop caused by run_rule actions.\n"+\
108                str(call_stack)+'\n')
109                handle_validation_error()
110            else:
111                for rule in config_json.get('rules', {}):
112                    if rule['id'] == run_rule_id:
113                        check_infinite_loops_in_rule(\
114                        config_json, rule, call_stack)
115    call_stack.pop()
116
117def check_infinite_loops(config_json):
118    r"""
119    Check if rule in config file is called recursively, causing an
120    infinite loop.
121    config_json: Configuration file JSON
122    """
123
124    for rule in config_json.get('rules', {}):
125        check_infinite_loops_in_rule(config_json, rule)
126
127def check_duplicate_object_id(config_json):
128    r"""
129    Check that there aren't any JSON objects with the same 'id' property value.
130    config_json: Configuration file JSON
131    """
132
133    json_ids = get_values(config_json, 'id')
134    unique_ids = set()
135    for id in json_ids:
136        if id in unique_ids:
137           sys.stderr.write("Error: Duplicate ID.\n"+\
138           "Found multiple objects with the ID "+id+'\n')
139           handle_validation_error()
140        else:
141            unique_ids.add(id)
142
143def check_duplicate_rule_id(config_json):
144    r"""
145    Check that there aren't any "rule" elements with the same 'id' field.
146    config_json: Configuration file JSON
147    """
148    rule_ids = []
149    for rule in config_json.get('rules', {}):
150        rule_id = rule['id']
151        if rule_id in rule_ids:
152            sys.stderr.write("Error: Duplicate rule ID.\n"+\
153            "Found multiple rules with the ID "+rule_id+'\n')
154            handle_validation_error()
155        else:
156            rule_ids.append(rule_id)
157
158def check_duplicate_chassis_number(config_json):
159    r"""
160    Check that there aren't any "chassis" elements with the same 'number' field.
161    config_json: Configuration file JSON
162    """
163    numbers = []
164    for chassis in config_json.get('chassis', {}):
165        number = chassis['number']
166        if number in numbers:
167            sys.stderr.write("Error: Duplicate chassis number.\n"+\
168            "Found multiple chassis with the number "+str(number)+'\n')
169            handle_validation_error()
170        else:
171            numbers.append(number)
172
173def check_duplicate_device_id(config_json):
174    r"""
175    Check that there aren't any "devices" with the same 'id' field.
176    config_json: Configuration file JSON
177    """
178    device_ids = []
179    for chassis in config_json.get('chassis', {}):
180        for device in chassis.get('devices', {}):
181            device_id = device['id']
182            if device_id in device_ids:
183                sys.stderr.write("Error: Duplicate device ID.\n"+\
184                "Found multiple devices with the ID "+device_id+'\n')
185                handle_validation_error()
186            else:
187                device_ids.append(device_id)
188
189def check_duplicate_rail_id(config_json):
190    r"""
191    Check that there aren't any "rails" with the same 'id' field.
192    config_json: Configuration file JSON
193    """
194    rail_ids = []
195    for chassis in config_json.get('chassis', {}):
196        for device in chassis.get('devices', {}):
197            for rail in device.get('rails', {}):
198                rail_id = rail['id']
199                if rail_id in rail_ids:
200                    sys.stderr.write("Error: Duplicate rail ID.\n"+\
201                    "Found multiple rails with the ID "+rail_id+'\n')
202                    handle_validation_error()
203                else:
204                    rail_ids.append(rail_id)
205
206def check_for_duplicates(config_json):
207    r"""
208    Check for duplicate ID.
209    """
210    check_duplicate_rule_id(config_json)
211
212    check_duplicate_chassis_number(config_json)
213
214    check_duplicate_device_id(config_json)
215
216    check_duplicate_rail_id(config_json)
217
218    check_duplicate_object_id(config_json)
219
220def validate_schema(config, schema):
221    r"""
222    Validates the specified config file using the specified
223    schema file.
224
225    config:   Path of the file containing the config JSON
226    schema:   Path of the file containing the schema JSON
227    """
228
229    with open(config) as config_handle:
230        config_json = json.load(config_handle)
231
232        with open(schema) as schema_handle:
233            schema_json = json.load(schema_handle)
234
235            try:
236                jsonschema.validate(config_json, schema_json)
237            except jsonschema.ValidationError as e:
238                print(e)
239                handle_validation_error()
240
241    return config_json
242
243if __name__ == '__main__':
244
245    parser = argparse.ArgumentParser(
246        description='phosphor-regulators configuration file validator')
247
248    parser.add_argument('-s', '--schema-file', dest='schema_file',
249                        help='The phosphor-regulators schema file')
250
251    parser.add_argument('-c', '--configuration-file', dest='configuration_file',
252                        help='The phosphor-regulators configuration file')
253
254    args = parser.parse_args()
255
256    if not args.schema_file:
257        parser.print_help()
258        sys.exit("Error: Schema file is required.")
259    if not args.configuration_file:
260        parser.print_help()
261        sys.exit("Error: Configuration file is required.")
262
263    config_json = validate_schema(args.configuration_file, args.schema_file)
264
265    check_for_duplicates(config_json)
266
267    check_infinite_loops(config_json)
268
269    check_run_rule_value_exists(config_json)
270
271    check_set_device_value_exists(config_json)
272