xref: /openbmc/phosphor-power/phosphor-regulators/tools/validate-regulators-config.py (revision a93243d154dd7f611ee871331f6839040bcd49a3)
143082e70SLei YU#!/usr/bin/env python3
25cc01280SBob King
35cc01280SBob Kingimport argparse
45cc01280SBob Kingimport json
5b7552f0cSBob Kingimport os
6b7552f0cSBob Kingimport sys
75cc01280SBob King
8*a93243d1SPatrick Williamsimport jsonschema
9*a93243d1SPatrick Williams
105cc01280SBob Kingr"""
115cc01280SBob KingValidates the phosphor-regulators configuration file. Checks it against a JSON
125cc01280SBob Kingschema as well as doing some extra checks that can't be encoded in the schema.
135cc01280SBob King"""
145cc01280SBob King
15*a93243d1SPatrick Williams
1695b796a4SBob Kingdef handle_validation_error():
1795b796a4SBob King    sys.exit("Validation failed.")
1895b796a4SBob King
19*a93243d1SPatrick Williams
2023dd60baSBob Kingdef get_values(json_element, key, result=None):
2123dd60baSBob King    r"""
2223dd60baSBob King    Finds all occurrences of a key within the specified JSON element and its
2323dd60baSBob King    children. Returns the associated values.
2423dd60baSBob King    To search the entire configuration file, pass the root JSON element
2523dd60baSBob King    json_element: JSON element within the config file.
2623dd60baSBob King    key: key name.
2723dd60baSBob King    result: list of values found with the specified key.
2823dd60baSBob King    """
2923dd60baSBob King
3023dd60baSBob King    if result is None:
3123dd60baSBob King        result = []
3223dd60baSBob King    if type(json_element) is dict:
3323dd60baSBob King        for json_key in json_element:
3423dd60baSBob King            if json_key == key:
3523dd60baSBob King                result.append(json_element[json_key])
3623dd60baSBob King            elif type(json_element[json_key]) in (list, dict):
3723dd60baSBob King                get_values(json_element[json_key], key, result)
3823dd60baSBob King    elif type(json_element) is list:
3923dd60baSBob King        for item in json_element:
4023dd60baSBob King            if type(item) in (list, dict):
4123dd60baSBob King                get_values(item, key, result)
4223dd60baSBob King    return result
4323dd60baSBob King
44*a93243d1SPatrick Williams
4523dd60baSBob Kingdef get_rule_ids(config_json):
4623dd60baSBob King    r"""
4723dd60baSBob King    Get all rule IDs in the configuration file.
4823dd60baSBob King    config_json: Configuration file JSON
4923dd60baSBob King    """
5023dd60baSBob King    rule_ids = []
51*a93243d1SPatrick Williams    for rule in config_json.get("rules", {}):
52*a93243d1SPatrick Williams        rule_ids.append(rule["id"])
5323dd60baSBob King    return rule_ids
5423dd60baSBob King
55*a93243d1SPatrick Williams
56e959754bSBob Kingdef get_device_ids(config_json):
57e959754bSBob King    r"""
58e959754bSBob King    Get all device IDs in the configuration file.
59e959754bSBob King    config_json: Configuration file JSON
60e959754bSBob King    """
61e959754bSBob King    device_ids = []
62*a93243d1SPatrick Williams    for chassis in config_json.get("chassis", {}):
63*a93243d1SPatrick Williams        for device in chassis.get("devices", {}):
64*a93243d1SPatrick Williams            device_ids.append(device["id"])
65e959754bSBob King    return device_ids
66e959754bSBob King
67*a93243d1SPatrick Williams
68a533d700SBob Kingdef check_number_of_elements_in_masks(config_json):
69a533d700SBob King    r"""
70a533d700SBob King    Check if the number of bit masks in the 'masks' property matches the number
71a533d700SBob King    of byte values in the 'values' property.
72a533d700SBob King    config_json: Configuration file JSON
73a533d700SBob King    """
74a533d700SBob King
75*a93243d1SPatrick Williams    i2c_write_bytes = get_values(config_json, "i2c_write_bytes")
76*a93243d1SPatrick Williams    i2c_compare_bytes = get_values(config_json, "i2c_compare_bytes")
77a533d700SBob King
78a533d700SBob King    for object in i2c_write_bytes:
79*a93243d1SPatrick Williams        if "masks" in object:
80*a93243d1SPatrick Williams            if len(object.get("masks", [])) != len(object.get("values", [])):
81*a93243d1SPatrick Williams                sys.stderr.write(
82*a93243d1SPatrick Williams                    "Error: Invalid i2c_write_bytes action.\n"
83*a93243d1SPatrick Williams                    + "The masks array must have the same size as the values"
84*a93243d1SPatrick Williams                    + " array. masks: "
85*a93243d1SPatrick Williams                    + str(object.get("masks", []))
86*a93243d1SPatrick Williams                    + ", values: "
87*a93243d1SPatrick Williams                    + str(object.get("values", []))
88*a93243d1SPatrick Williams                    + ".\n"
89*a93243d1SPatrick Williams                )
90a533d700SBob King                handle_validation_error()
91a533d700SBob King
92a533d700SBob King    for object in i2c_compare_bytes:
93*a93243d1SPatrick Williams        if "masks" in object:
94*a93243d1SPatrick Williams            if len(object.get("masks", [])) != len(object.get("values", [])):
95*a93243d1SPatrick Williams                sys.stderr.write(
96*a93243d1SPatrick Williams                    "Error: Invalid i2c_compare_bytes action.\n"
97*a93243d1SPatrick Williams                    + "The masks array must have the same size as the values "
98*a93243d1SPatrick Williams                    + "array. masks: "
99*a93243d1SPatrick Williams                    + str(object.get("masks", []))
100*a93243d1SPatrick Williams                    + ", values: "
101*a93243d1SPatrick Williams                    + str(object.get("values", []))
102*a93243d1SPatrick Williams                    + ".\n"
103*a93243d1SPatrick Williams                )
104a533d700SBob King                handle_validation_error()
105a533d700SBob King
106*a93243d1SPatrick Williams
1079146df2bSBob Kingdef check_rule_id_exists(config_json):
1089146df2bSBob King    r"""
1099146df2bSBob King    Check if a rule_id property specifies a rule ID that does not exist.
1109146df2bSBob King    config_json: Configuration file JSON
1119146df2bSBob King    """
1129146df2bSBob King
113*a93243d1SPatrick Williams    rule_ids = get_values(config_json, "rule_id")
1149146df2bSBob King    valid_rule_ids = get_rule_ids(config_json)
1159146df2bSBob King    for rule_id in rule_ids:
1169146df2bSBob King        if rule_id not in valid_rule_ids:
117*a93243d1SPatrick Williams            sys.stderr.write(
118*a93243d1SPatrick Williams                "Error: Rule ID does not exist.\n"
119*a93243d1SPatrick Williams                + "Found rule_id value that specifies invalid rule ID "
120*a93243d1SPatrick Williams                + rule_id
121*a93243d1SPatrick Williams                + "\n"
122*a93243d1SPatrick Williams            )
1239146df2bSBob King            handle_validation_error()
1249146df2bSBob King
125*a93243d1SPatrick Williams
1265d4a9c78SShawn McCarneydef check_device_id_exists(config_json):
1275d4a9c78SShawn McCarney    r"""
1285d4a9c78SShawn McCarney    Check if a device_id property specifies a device ID that does not exist.
1295d4a9c78SShawn McCarney    config_json: Configuration file JSON
1305d4a9c78SShawn McCarney    """
1315d4a9c78SShawn McCarney
132*a93243d1SPatrick Williams    device_ids = get_values(config_json, "device_id")
1335d4a9c78SShawn McCarney    valid_device_ids = get_device_ids(config_json)
1345d4a9c78SShawn McCarney    for device_id in device_ids:
1355d4a9c78SShawn McCarney        if device_id not in valid_device_ids:
136*a93243d1SPatrick Williams            sys.stderr.write(
137*a93243d1SPatrick Williams                "Error: Device ID does not exist.\n"
138*a93243d1SPatrick Williams                + "Found device_id value that specifies invalid device ID "
139*a93243d1SPatrick Williams                + device_id
140*a93243d1SPatrick Williams                + "\n"
141*a93243d1SPatrick Williams            )
1425d4a9c78SShawn McCarney            handle_validation_error()
1435d4a9c78SShawn McCarney
144*a93243d1SPatrick Williams
145e959754bSBob Kingdef check_set_device_value_exists(config_json):
146e959754bSBob King    r"""
147e959754bSBob King    Check if a set_device action specifies a device ID that does not exist.
148e959754bSBob King    config_json: Configuration file JSON
149e959754bSBob King    """
150e959754bSBob King
151*a93243d1SPatrick Williams    device_ids = get_values(config_json, "set_device")
152e959754bSBob King    valid_device_ids = get_device_ids(config_json)
153e959754bSBob King    for device_id in device_ids:
154e959754bSBob King        if device_id not in valid_device_ids:
155*a93243d1SPatrick Williams            sys.stderr.write(
156*a93243d1SPatrick Williams                "Error: Device ID does not exist.\n"
157*a93243d1SPatrick Williams                + "Found set_device action that specifies invalid device ID "
158*a93243d1SPatrick Williams                + device_id
159*a93243d1SPatrick Williams                + "\n"
160*a93243d1SPatrick Williams            )
161e959754bSBob King            handle_validation_error()
162e959754bSBob King
163*a93243d1SPatrick Williams
16423dd60baSBob Kingdef check_run_rule_value_exists(config_json):
16523dd60baSBob King    r"""
16623dd60baSBob King    Check if any run_rule actions specify a rule ID that does not exist.
16723dd60baSBob King    config_json: Configuration file JSON
16823dd60baSBob King    """
16923dd60baSBob King
170*a93243d1SPatrick Williams    rule_ids = get_values(config_json, "run_rule")
17123dd60baSBob King    valid_rule_ids = get_rule_ids(config_json)
17223dd60baSBob King    for rule_id in rule_ids:
17323dd60baSBob King        if rule_id not in valid_rule_ids:
174*a93243d1SPatrick Williams            sys.stderr.write(
175*a93243d1SPatrick Williams                "Error: Rule ID does not exist.\n"
176*a93243d1SPatrick Williams                + "Found run_rule action that specifies invalid rule ID "
177*a93243d1SPatrick Williams                + rule_id
178*a93243d1SPatrick Williams                + "\n"
179*a93243d1SPatrick Williams            )
18023dd60baSBob King            handle_validation_error()
18123dd60baSBob King
182*a93243d1SPatrick Williams
183d114cd94SBob Kingdef check_infinite_loops_in_rule(config_json, rule_json, call_stack=[]):
184d114cd94SBob King    r"""
185d114cd94SBob King    Check if a 'run_rule' action in the specified rule causes an
186d114cd94SBob King    infinite loop.
187d114cd94SBob King    config_json: Configuration file JSON.
188d114cd94SBob King    rule_json: A rule in the JSON config file.
189d114cd94SBob King    call_stack: Current call stack of rules.
190d114cd94SBob King    """
191d114cd94SBob King
192*a93243d1SPatrick Williams    call_stack.append(rule_json["id"])
193*a93243d1SPatrick Williams    for action in rule_json.get("actions", {}):
194*a93243d1SPatrick Williams        if "run_rule" in action:
195*a93243d1SPatrick Williams            run_rule_id = action["run_rule"]
196d114cd94SBob King            if run_rule_id in call_stack:
197d114cd94SBob King                call_stack.append(run_rule_id)
198*a93243d1SPatrick Williams                sys.stderr.write(
199*a93243d1SPatrick Williams                    "Infinite loop caused by run_rule actions.\n"
200*a93243d1SPatrick Williams                    + str(call_stack)
201*a93243d1SPatrick Williams                    + "\n"
202*a93243d1SPatrick Williams                )
203d114cd94SBob King                handle_validation_error()
204d114cd94SBob King            else:
205*a93243d1SPatrick Williams                for rule in config_json.get("rules", {}):
206*a93243d1SPatrick Williams                    if rule["id"] == run_rule_id:
207*a93243d1SPatrick Williams                        check_infinite_loops_in_rule(
208*a93243d1SPatrick Williams                            config_json, rule, call_stack
209*a93243d1SPatrick Williams                        )
210d114cd94SBob King    call_stack.pop()
211d114cd94SBob King
212*a93243d1SPatrick Williams
213d114cd94SBob Kingdef check_infinite_loops(config_json):
214d114cd94SBob King    r"""
215d114cd94SBob King    Check if rule in config file is called recursively, causing an
216d114cd94SBob King    infinite loop.
217d114cd94SBob King    config_json: Configuration file JSON
218d114cd94SBob King    """
219d114cd94SBob King
220*a93243d1SPatrick Williams    for rule in config_json.get("rules", {}):
221d114cd94SBob King        check_infinite_loops_in_rule(config_json, rule)
222d114cd94SBob King
223*a93243d1SPatrick Williams
2245b27a95bSBob Kingdef check_duplicate_object_id(config_json):
2255b27a95bSBob King    r"""
2265b27a95bSBob King    Check that there aren't any JSON objects with the same 'id' property value.
2275b27a95bSBob King    config_json: Configuration file JSON
2285b27a95bSBob King    """
2295b27a95bSBob King
230*a93243d1SPatrick Williams    json_ids = get_values(config_json, "id")
2315b27a95bSBob King    unique_ids = set()
2325b27a95bSBob King    for id in json_ids:
2335b27a95bSBob King        if id in unique_ids:
234*a93243d1SPatrick Williams            sys.stderr.write(
235*a93243d1SPatrick Williams                "Error: Duplicate ID.\n"
236*a93243d1SPatrick Williams                + "Found multiple objects with the ID "
237*a93243d1SPatrick Williams                + id
238*a93243d1SPatrick Williams                + "\n"
239*a93243d1SPatrick Williams            )
2405b27a95bSBob King            handle_validation_error()
2415b27a95bSBob King        else:
2425b27a95bSBob King            unique_ids.add(id)
2435b27a95bSBob King
244*a93243d1SPatrick Williams
24595b796a4SBob Kingdef check_duplicate_rule_id(config_json):
24695b796a4SBob King    r"""
24795b796a4SBob King    Check that there aren't any "rule" elements with the same 'id' field.
24895b796a4SBob King    config_json: Configuration file JSON
24995b796a4SBob King    """
25095b796a4SBob King    rule_ids = []
251*a93243d1SPatrick Williams    for rule in config_json.get("rules", {}):
252*a93243d1SPatrick Williams        rule_id = rule["id"]
25395b796a4SBob King        if rule_id in rule_ids:
254*a93243d1SPatrick Williams            sys.stderr.write(
255*a93243d1SPatrick Williams                "Error: Duplicate rule ID.\n"
256*a93243d1SPatrick Williams                + "Found multiple rules with the ID "
257*a93243d1SPatrick Williams                + rule_id
258*a93243d1SPatrick Williams                + "\n"
259*a93243d1SPatrick Williams            )
26095b796a4SBob King            handle_validation_error()
26195b796a4SBob King        else:
26295b796a4SBob King            rule_ids.append(rule_id)
26395b796a4SBob King
264*a93243d1SPatrick Williams
26595b796a4SBob Kingdef check_duplicate_chassis_number(config_json):
26695b796a4SBob King    r"""
267*a93243d1SPatrick Williams    Check that there aren't any "chassis" elements with the same 'number'
268*a93243d1SPatrick Williams    field.
26995b796a4SBob King    config_json: Configuration file JSON
27095b796a4SBob King    """
27195b796a4SBob King    numbers = []
272*a93243d1SPatrick Williams    for chassis in config_json.get("chassis", {}):
273*a93243d1SPatrick Williams        number = chassis["number"]
27495b796a4SBob King        if number in numbers:
275*a93243d1SPatrick Williams            sys.stderr.write(
276*a93243d1SPatrick Williams                "Error: Duplicate chassis number.\n"
277*a93243d1SPatrick Williams                + "Found multiple chassis with the number "
278*a93243d1SPatrick Williams                + str(number)
279*a93243d1SPatrick Williams                + "\n"
280*a93243d1SPatrick Williams            )
28195b796a4SBob King            handle_validation_error()
28295b796a4SBob King        else:
28395b796a4SBob King            numbers.append(number)
28495b796a4SBob King
285*a93243d1SPatrick Williams
28695b796a4SBob Kingdef check_duplicate_device_id(config_json):
28795b796a4SBob King    r"""
28895b796a4SBob King    Check that there aren't any "devices" with the same 'id' field.
28995b796a4SBob King    config_json: Configuration file JSON
29095b796a4SBob King    """
29195b796a4SBob King    device_ids = []
292*a93243d1SPatrick Williams    for chassis in config_json.get("chassis", {}):
293*a93243d1SPatrick Williams        for device in chassis.get("devices", {}):
294*a93243d1SPatrick Williams            device_id = device["id"]
29595b796a4SBob King            if device_id in device_ids:
296*a93243d1SPatrick Williams                sys.stderr.write(
297*a93243d1SPatrick Williams                    "Error: Duplicate device ID.\n"
298*a93243d1SPatrick Williams                    + "Found multiple devices with the ID "
299*a93243d1SPatrick Williams                    + device_id
300*a93243d1SPatrick Williams                    + "\n"
301*a93243d1SPatrick Williams                )
30295b796a4SBob King                handle_validation_error()
30395b796a4SBob King            else:
30495b796a4SBob King                device_ids.append(device_id)
30595b796a4SBob King
306*a93243d1SPatrick Williams
30795b796a4SBob Kingdef check_duplicate_rail_id(config_json):
30895b796a4SBob King    r"""
30995b796a4SBob King    Check that there aren't any "rails" with the same 'id' field.
31095b796a4SBob King    config_json: Configuration file JSON
31195b796a4SBob King    """
31295b796a4SBob King    rail_ids = []
313*a93243d1SPatrick Williams    for chassis in config_json.get("chassis", {}):
314*a93243d1SPatrick Williams        for device in chassis.get("devices", {}):
315*a93243d1SPatrick Williams            for rail in device.get("rails", {}):
316*a93243d1SPatrick Williams                rail_id = rail["id"]
31795b796a4SBob King                if rail_id in rail_ids:
318*a93243d1SPatrick Williams                    sys.stderr.write(
319*a93243d1SPatrick Williams                        "Error: Duplicate rail ID.\n"
320*a93243d1SPatrick Williams                        + "Found multiple rails with the ID "
321*a93243d1SPatrick Williams                        + rail_id
322*a93243d1SPatrick Williams                        + "\n"
323*a93243d1SPatrick Williams                    )
32495b796a4SBob King                    handle_validation_error()
32595b796a4SBob King                else:
32695b796a4SBob King                    rail_ids.append(rail_id)
32795b796a4SBob King
328*a93243d1SPatrick Williams
32995b796a4SBob Kingdef check_for_duplicates(config_json):
33095b796a4SBob King    r"""
33195b796a4SBob King    Check for duplicate ID.
33295b796a4SBob King    """
33395b796a4SBob King    check_duplicate_rule_id(config_json)
33495b796a4SBob King
33595b796a4SBob King    check_duplicate_chassis_number(config_json)
33695b796a4SBob King
33795b796a4SBob King    check_duplicate_device_id(config_json)
33895b796a4SBob King
33995b796a4SBob King    check_duplicate_rail_id(config_json)
34095b796a4SBob King
3415b27a95bSBob King    check_duplicate_object_id(config_json)
3425b27a95bSBob King
343*a93243d1SPatrick Williams
3445cc01280SBob Kingdef validate_schema(config, schema):
3455cc01280SBob King    r"""
3465cc01280SBob King    Validates the specified config file using the specified
3475cc01280SBob King    schema file.
3485cc01280SBob King
3495cc01280SBob King    config:   Path of the file containing the config JSON
3505cc01280SBob King    schema:   Path of the file containing the schema JSON
3515cc01280SBob King    """
3525cc01280SBob King
3535cc01280SBob King    with open(config) as config_handle:
3545cc01280SBob King        config_json = json.load(config_handle)
3555cc01280SBob King
3565cc01280SBob King        with open(schema) as schema_handle:
3575cc01280SBob King            schema_json = json.load(schema_handle)
3585cc01280SBob King
3595cc01280SBob King            try:
3605cc01280SBob King                jsonschema.validate(config_json, schema_json)
3615cc01280SBob King            except jsonschema.ValidationError as e:
3625cc01280SBob King                print(e)
36395b796a4SBob King                handle_validation_error()
36495b796a4SBob King
36595b796a4SBob King    return config_json
3665cc01280SBob King
367*a93243d1SPatrick Williams
368b7552f0cSBob Kingdef validate_JSON_format(file):
369b7552f0cSBob King    with open(file) as json_data:
370b7552f0cSBob King        try:
371b7552f0cSBob King            return json.load(json_data)
372*a93243d1SPatrick Williams        except ValueError:
373b7552f0cSBob King            return False
374b7552f0cSBob King        return True
375b7552f0cSBob King
3765cc01280SBob King
377*a93243d1SPatrick Williamsif __name__ == "__main__":
3785cc01280SBob King    parser = argparse.ArgumentParser(
379*a93243d1SPatrick Williams        description="phosphor-regulators configuration file validator"
380*a93243d1SPatrick Williams    )
3815cc01280SBob King
382*a93243d1SPatrick Williams    parser.add_argument(
383*a93243d1SPatrick Williams        "-s",
384*a93243d1SPatrick Williams        "--schema-file",
385*a93243d1SPatrick Williams        dest="schema_file",
386*a93243d1SPatrick Williams        help="The phosphor-regulators schema file",
387*a93243d1SPatrick Williams    )
3885cc01280SBob King
389*a93243d1SPatrick Williams    parser.add_argument(
390*a93243d1SPatrick Williams        "-c",
391*a93243d1SPatrick Williams        "--configuration-file",
392*a93243d1SPatrick Williams        dest="configuration_file",
393*a93243d1SPatrick Williams        help="The phosphor-regulators configuration file",
394*a93243d1SPatrick Williams    )
3955cc01280SBob King
3965cc01280SBob King    args = parser.parse_args()
3975cc01280SBob King
3985cc01280SBob King    if not args.schema_file:
3995cc01280SBob King        parser.print_help()
4005cc01280SBob King        sys.exit("Error: Schema file is required.")
401b7552f0cSBob King    if not os.path.exists(args.schema_file):
402b7552f0cSBob King        parser.print_help()
403b7552f0cSBob King        sys.exit("Error: Schema file does not exist.")
404b7552f0cSBob King    if not os.access(args.schema_file, os.R_OK):
405b7552f0cSBob King        parser.print_help()
406b7552f0cSBob King        sys.exit("Error: Schema file is not readable.")
407b7552f0cSBob King    if not validate_JSON_format(args.schema_file):
408b7552f0cSBob King        parser.print_help()
409b7552f0cSBob King        sys.exit("Error: Schema file is not in the JSON format.")
4105cc01280SBob King    if not args.configuration_file:
4115cc01280SBob King        parser.print_help()
4125cc01280SBob King        sys.exit("Error: Configuration file is required.")
413b7552f0cSBob King    if not os.path.exists(args.configuration_file):
414b7552f0cSBob King        parser.print_help()
415b7552f0cSBob King        sys.exit("Error: Configuration file does not exist.")
416b7552f0cSBob King    if not os.access(args.configuration_file, os.R_OK):
417b7552f0cSBob King        parser.print_help()
418b7552f0cSBob King        sys.exit("Error: Configuration file is not readable.")
419b7552f0cSBob King    if not validate_JSON_format(args.configuration_file):
420b7552f0cSBob King        parser.print_help()
421b7552f0cSBob King        sys.exit("Error: Configuration file is not in the JSON format.")
4225cc01280SBob King
42395b796a4SBob King    config_json = validate_schema(args.configuration_file, args.schema_file)
424d114cd94SBob King
42595b796a4SBob King    check_for_duplicates(config_json)
426d114cd94SBob King
427d114cd94SBob King    check_infinite_loops(config_json)
428d114cd94SBob King
42923dd60baSBob King    check_run_rule_value_exists(config_json)
430e959754bSBob King
431e959754bSBob King    check_set_device_value_exists(config_json)
4329146df2bSBob King
4339146df2bSBob King    check_rule_id_exists(config_json)
434a533d700SBob King
4355d4a9c78SShawn McCarney    check_device_id_exists(config_json)
4365d4a9c78SShawn McCarney
437a533d700SBob King    check_number_of_elements_in_masks(config_json)
438