Bob King | 5cc0128 | 2019-12-17 18:11:57 +0800 | [diff] [blame] | 1 | #!/usr/bin/env python |
| 2 | |
| 3 | import argparse |
| 4 | import json |
| 5 | import sys |
| 6 | import jsonschema |
| 7 | |
| 8 | r""" |
| 9 | Validates the phosphor-regulators configuration file. Checks it against a JSON |
| 10 | schema as well as doing some extra checks that can't be encoded in the schema. |
| 11 | """ |
| 12 | |
Bob King | 95b796a | 2020-01-15 14:45:06 +0800 | [diff] [blame] | 13 | def handle_validation_error(): |
| 14 | sys.exit("Validation failed.") |
| 15 | |
Bob King | d114cd9 | 2020-02-10 13:56:02 +0800 | [diff] [blame] | 16 | def check_infinite_loops_in_rule(config_json, rule_json, call_stack=[]): |
| 17 | r""" |
| 18 | Check if a 'run_rule' action in the specified rule causes an |
| 19 | infinite loop. |
| 20 | config_json: Configuration file JSON. |
| 21 | rule_json: A rule in the JSON config file. |
| 22 | call_stack: Current call stack of rules. |
| 23 | """ |
| 24 | |
| 25 | call_stack.append(rule_json['id']) |
| 26 | for action in rule_json.get('actions', {}): |
| 27 | if 'run_rule' in action: |
| 28 | run_rule_id = action['run_rule'] |
| 29 | if run_rule_id in call_stack: |
| 30 | call_stack.append(run_rule_id) |
| 31 | sys.stderr.write(\ |
| 32 | "Infinite loop caused by run_rule actions.\n"+\ |
| 33 | str(call_stack)+'\n') |
| 34 | handle_validation_error() |
| 35 | else: |
| 36 | for rule in config_json.get('rules', {}): |
| 37 | if rule['id'] == run_rule_id: |
| 38 | check_infinite_loops_in_rule(\ |
| 39 | config_json, rule, call_stack) |
| 40 | call_stack.pop() |
| 41 | |
| 42 | def check_infinite_loops(config_json): |
| 43 | r""" |
| 44 | Check if rule in config file is called recursively, causing an |
| 45 | infinite loop. |
| 46 | config_json: Configuration file JSON |
| 47 | """ |
| 48 | |
| 49 | for rule in config_json.get('rules', {}): |
| 50 | check_infinite_loops_in_rule(config_json, rule) |
| 51 | |
Bob King | 5b27a95 | 2020-01-20 18:04:15 +0800 | [diff] [blame] | 52 | def check_duplicate_object_id(config_json): |
| 53 | r""" |
| 54 | Check that there aren't any JSON objects with the same 'id' property value. |
| 55 | config_json: Configuration file JSON |
| 56 | """ |
| 57 | |
| 58 | json_ids = check_duplicate_rule_id(config_json)+\ |
| 59 | check_duplicate_device_id(config_json)+\ |
| 60 | check_duplicate_rail_id(config_json) |
| 61 | unique_ids = set() |
| 62 | for id in json_ids: |
| 63 | if id in unique_ids: |
| 64 | sys.stderr.write("Error: Duplicate ID.\n"+\ |
| 65 | "Found multiple objects with the ID "+id+'\n') |
| 66 | handle_validation_error() |
| 67 | else: |
| 68 | unique_ids.add(id) |
| 69 | |
Bob King | 95b796a | 2020-01-15 14:45:06 +0800 | [diff] [blame] | 70 | def check_duplicate_rule_id(config_json): |
| 71 | r""" |
| 72 | Check that there aren't any "rule" elements with the same 'id' field. |
| 73 | config_json: Configuration file JSON |
| 74 | """ |
| 75 | rule_ids = [] |
| 76 | for rule in config_json.get('rules', {}): |
| 77 | rule_id = rule['id'] |
| 78 | if rule_id in rule_ids: |
| 79 | sys.stderr.write("Error: Duplicate rule ID.\n"+\ |
| 80 | "Found multiple rules with the ID "+rule_id+'\n') |
| 81 | handle_validation_error() |
| 82 | else: |
| 83 | rule_ids.append(rule_id) |
Bob King | 5b27a95 | 2020-01-20 18:04:15 +0800 | [diff] [blame] | 84 | return rule_ids |
Bob King | 95b796a | 2020-01-15 14:45:06 +0800 | [diff] [blame] | 85 | |
| 86 | def check_duplicate_chassis_number(config_json): |
| 87 | r""" |
| 88 | Check that there aren't any "chassis" elements with the same 'number' field. |
| 89 | config_json: Configuration file JSON |
| 90 | """ |
| 91 | numbers = [] |
| 92 | for chassis in config_json.get('chassis', {}): |
| 93 | number = chassis['number'] |
| 94 | if number in numbers: |
| 95 | sys.stderr.write("Error: Duplicate chassis number.\n"+\ |
| 96 | "Found multiple chassis with the number "+str(number)+'\n') |
| 97 | handle_validation_error() |
| 98 | else: |
| 99 | numbers.append(number) |
| 100 | |
| 101 | def check_duplicate_device_id(config_json): |
| 102 | r""" |
| 103 | Check that there aren't any "devices" with the same 'id' field. |
| 104 | config_json: Configuration file JSON |
| 105 | """ |
| 106 | device_ids = [] |
| 107 | for chassis in config_json.get('chassis', {}): |
| 108 | for device in chassis.get('devices', {}): |
| 109 | device_id = device['id'] |
| 110 | if device_id in device_ids: |
| 111 | sys.stderr.write("Error: Duplicate device ID.\n"+\ |
| 112 | "Found multiple devices with the ID "+device_id+'\n') |
| 113 | handle_validation_error() |
| 114 | else: |
| 115 | device_ids.append(device_id) |
Bob King | 5b27a95 | 2020-01-20 18:04:15 +0800 | [diff] [blame] | 116 | return device_ids |
Bob King | 95b796a | 2020-01-15 14:45:06 +0800 | [diff] [blame] | 117 | |
| 118 | def check_duplicate_rail_id(config_json): |
| 119 | r""" |
| 120 | Check that there aren't any "rails" with the same 'id' field. |
| 121 | config_json: Configuration file JSON |
| 122 | """ |
| 123 | rail_ids = [] |
| 124 | for chassis in config_json.get('chassis', {}): |
| 125 | for device in chassis.get('devices', {}): |
| 126 | for rail in device.get('rails', {}): |
| 127 | rail_id = rail['id'] |
| 128 | if rail_id in rail_ids: |
| 129 | sys.stderr.write("Error: Duplicate rail ID.\n"+\ |
| 130 | "Found multiple rails with the ID "+rail_id+'\n') |
| 131 | handle_validation_error() |
| 132 | else: |
| 133 | rail_ids.append(rail_id) |
Bob King | 5b27a95 | 2020-01-20 18:04:15 +0800 | [diff] [blame] | 134 | return rail_ids |
Bob King | 95b796a | 2020-01-15 14:45:06 +0800 | [diff] [blame] | 135 | |
| 136 | def check_for_duplicates(config_json): |
| 137 | r""" |
| 138 | Check for duplicate ID. |
| 139 | """ |
| 140 | check_duplicate_rule_id(config_json) |
| 141 | |
| 142 | check_duplicate_chassis_number(config_json) |
| 143 | |
| 144 | check_duplicate_device_id(config_json) |
| 145 | |
| 146 | check_duplicate_rail_id(config_json) |
| 147 | |
Bob King | 5b27a95 | 2020-01-20 18:04:15 +0800 | [diff] [blame] | 148 | check_duplicate_object_id(config_json) |
| 149 | |
Bob King | 5cc0128 | 2019-12-17 18:11:57 +0800 | [diff] [blame] | 150 | def validate_schema(config, schema): |
| 151 | r""" |
| 152 | Validates the specified config file using the specified |
| 153 | schema file. |
| 154 | |
| 155 | config: Path of the file containing the config JSON |
| 156 | schema: Path of the file containing the schema JSON |
| 157 | """ |
| 158 | |
| 159 | with open(config) as config_handle: |
| 160 | config_json = json.load(config_handle) |
| 161 | |
| 162 | with open(schema) as schema_handle: |
| 163 | schema_json = json.load(schema_handle) |
| 164 | |
| 165 | try: |
| 166 | jsonschema.validate(config_json, schema_json) |
| 167 | except jsonschema.ValidationError as e: |
| 168 | print(e) |
Bob King | 95b796a | 2020-01-15 14:45:06 +0800 | [diff] [blame] | 169 | handle_validation_error() |
| 170 | |
| 171 | return config_json |
Bob King | 5cc0128 | 2019-12-17 18:11:57 +0800 | [diff] [blame] | 172 | |
| 173 | if __name__ == '__main__': |
| 174 | |
| 175 | parser = argparse.ArgumentParser( |
| 176 | description='phosphor-regulators configuration file validator') |
| 177 | |
| 178 | parser.add_argument('-s', '--schema-file', dest='schema_file', |
| 179 | help='The phosphor-regulators schema file') |
| 180 | |
| 181 | parser.add_argument('-c', '--configuration-file', dest='configuration_file', |
| 182 | help='The phosphor-regulators configuration file') |
| 183 | |
| 184 | args = parser.parse_args() |
| 185 | |
| 186 | if not args.schema_file: |
| 187 | parser.print_help() |
| 188 | sys.exit("Error: Schema file is required.") |
| 189 | if not args.configuration_file: |
| 190 | parser.print_help() |
| 191 | sys.exit("Error: Configuration file is required.") |
| 192 | |
Bob King | 95b796a | 2020-01-15 14:45:06 +0800 | [diff] [blame] | 193 | config_json = validate_schema(args.configuration_file, args.schema_file) |
Bob King | d114cd9 | 2020-02-10 13:56:02 +0800 | [diff] [blame] | 194 | |
Bob King | 95b796a | 2020-01-15 14:45:06 +0800 | [diff] [blame] | 195 | check_for_duplicates(config_json) |
Bob King | d114cd9 | 2020-02-10 13:56:02 +0800 | [diff] [blame] | 196 | |
| 197 | check_infinite_loops(config_json) |
| 198 | |