Lei YU | 43082e7 | 2020-03-09 10:32:48 +0800 | [diff] [blame] | 1 | #!/usr/bin/env python3 |
Bob King | 5cc0128 | 2019-12-17 18:11:57 +0800 | [diff] [blame] | 2 | |
| 3 | import argparse |
| 4 | import json |
Bob King | 5cc0128 | 2019-12-17 18:11:57 +0800 | [diff] [blame] | 5 | import jsonschema |
Bob King | b7552f0 | 2020-10-15 14:34:17 +0800 | [diff] [blame] | 6 | import os |
| 7 | import sys |
Bob King | 5cc0128 | 2019-12-17 18:11:57 +0800 | [diff] [blame] | 8 | |
| 9 | r""" |
| 10 | Validates the phosphor-regulators configuration file. Checks it against a JSON |
| 11 | schema as well as doing some extra checks that can't be encoded in the schema. |
| 12 | """ |
| 13 | |
Bob King | 95b796a | 2020-01-15 14:45:06 +0800 | [diff] [blame] | 14 | def handle_validation_error(): |
| 15 | sys.exit("Validation failed.") |
| 16 | |
Bob King | 23dd60b | 2020-02-26 10:23:21 +0800 | [diff] [blame] | 17 | def get_values(json_element, key, result = None): |
| 18 | r""" |
| 19 | Finds all occurrences of a key within the specified JSON element and its |
| 20 | children. Returns the associated values. |
| 21 | To search the entire configuration file, pass the root JSON element |
| 22 | json_element: JSON element within the config file. |
| 23 | key: key name. |
| 24 | result: list of values found with the specified key. |
| 25 | """ |
| 26 | |
| 27 | if result is None: |
| 28 | result = [] |
| 29 | if type(json_element) is dict: |
| 30 | for json_key in json_element: |
| 31 | if json_key == key: |
| 32 | result.append(json_element[json_key]) |
| 33 | elif type(json_element[json_key]) in (list, dict): |
| 34 | get_values(json_element[json_key], key, result) |
| 35 | elif type(json_element) is list: |
| 36 | for item in json_element: |
| 37 | if type(item) in (list, dict): |
| 38 | get_values(item, key, result) |
| 39 | return result |
| 40 | |
| 41 | def get_rule_ids(config_json): |
| 42 | r""" |
| 43 | Get all rule IDs in the configuration file. |
| 44 | config_json: Configuration file JSON |
| 45 | """ |
| 46 | rule_ids = [] |
| 47 | for rule in config_json.get('rules', {}): |
| 48 | rule_ids.append(rule['id']) |
| 49 | return rule_ids |
| 50 | |
Bob King | e959754 | 2020-02-26 10:47:40 +0800 | [diff] [blame] | 51 | def get_device_ids(config_json): |
| 52 | r""" |
| 53 | Get all device IDs in the configuration file. |
| 54 | config_json: Configuration file JSON |
| 55 | """ |
| 56 | device_ids = [] |
| 57 | for chassis in config_json.get('chassis', {}): |
| 58 | for device in chassis.get('devices', {}): |
| 59 | device_ids.append(device['id']) |
| 60 | return device_ids |
| 61 | |
Bob King | a533d70 | 2020-02-26 10:51:24 +0800 | [diff] [blame] | 62 | def check_number_of_elements_in_masks(config_json): |
| 63 | r""" |
| 64 | Check if the number of bit masks in the 'masks' property matches the number |
| 65 | of byte values in the 'values' property. |
| 66 | config_json: Configuration file JSON |
| 67 | """ |
| 68 | |
| 69 | i2c_write_bytes = get_values(config_json, 'i2c_write_bytes') |
| 70 | i2c_compare_bytes = get_values(config_json, 'i2c_compare_bytes') |
| 71 | |
| 72 | for object in i2c_write_bytes: |
| 73 | if 'masks' in object: |
| 74 | if len(object.get('masks', [])) != len(object.get('values', [])): |
| 75 | sys.stderr.write("Error: Invalid i2c_write_bytes action.\n"+\ |
| 76 | "The masks array must have the same size as the values array. "+\ |
| 77 | "masks: "+str(object.get('masks', []))+\ |
| 78 | ", values: "+str(object.get('values', []))+'.\n') |
| 79 | handle_validation_error() |
| 80 | |
| 81 | for object in i2c_compare_bytes: |
| 82 | if 'masks' in object: |
| 83 | if len(object.get('masks', [])) != len(object.get('values', [])): |
| 84 | sys.stderr.write("Error: Invalid i2c_compare_bytes action.\n"+\ |
| 85 | "The masks array must have the same size as the values array. "+\ |
| 86 | "masks: "+str(object.get('masks', []))+\ |
| 87 | ", values: "+str(object.get('values', []))+'.\n') |
| 88 | handle_validation_error() |
| 89 | |
Bob King | 9146df2 | 2020-02-26 10:49:33 +0800 | [diff] [blame] | 90 | def check_rule_id_exists(config_json): |
| 91 | r""" |
| 92 | Check if a rule_id property specifies a rule ID that does not exist. |
| 93 | config_json: Configuration file JSON |
| 94 | """ |
| 95 | |
| 96 | rule_ids = get_values(config_json, 'rule_id') |
| 97 | valid_rule_ids = get_rule_ids(config_json) |
| 98 | for rule_id in rule_ids: |
| 99 | if rule_id not in valid_rule_ids: |
| 100 | sys.stderr.write("Error: Rule ID does not exist.\n"+\ |
| 101 | "Found rule_id value that specifies invalid rule ID "+\ |
| 102 | rule_id+'\n') |
| 103 | handle_validation_error() |
| 104 | |
Bob King | e959754 | 2020-02-26 10:47:40 +0800 | [diff] [blame] | 105 | def check_set_device_value_exists(config_json): |
| 106 | r""" |
| 107 | Check if a set_device action specifies a device ID that does not exist. |
| 108 | config_json: Configuration file JSON |
| 109 | """ |
| 110 | |
| 111 | device_ids = get_values(config_json, 'set_device') |
| 112 | valid_device_ids = get_device_ids(config_json) |
| 113 | for device_id in device_ids: |
| 114 | if device_id not in valid_device_ids: |
| 115 | sys.stderr.write("Error: Device ID does not exist.\n"+\ |
| 116 | "Found set_device action that specifies invalid device ID "+\ |
| 117 | device_id+'\n') |
| 118 | handle_validation_error() |
| 119 | |
Bob King | 23dd60b | 2020-02-26 10:23:21 +0800 | [diff] [blame] | 120 | def check_run_rule_value_exists(config_json): |
| 121 | r""" |
| 122 | Check if any run_rule actions specify a rule ID that does not exist. |
| 123 | config_json: Configuration file JSON |
| 124 | """ |
| 125 | |
| 126 | rule_ids = get_values(config_json, 'run_rule') |
| 127 | valid_rule_ids = get_rule_ids(config_json) |
| 128 | for rule_id in rule_ids: |
| 129 | if rule_id not in valid_rule_ids: |
| 130 | sys.stderr.write("Error: Rule ID does not exist.\n"+\ |
| 131 | "Found run_rule action that specifies invalid rule ID "+\ |
| 132 | rule_id+'\n') |
| 133 | handle_validation_error() |
| 134 | |
Bob King | d114cd9 | 2020-02-10 13:56:02 +0800 | [diff] [blame] | 135 | def check_infinite_loops_in_rule(config_json, rule_json, call_stack=[]): |
| 136 | r""" |
| 137 | Check if a 'run_rule' action in the specified rule causes an |
| 138 | infinite loop. |
| 139 | config_json: Configuration file JSON. |
| 140 | rule_json: A rule in the JSON config file. |
| 141 | call_stack: Current call stack of rules. |
| 142 | """ |
| 143 | |
| 144 | call_stack.append(rule_json['id']) |
| 145 | for action in rule_json.get('actions', {}): |
| 146 | if 'run_rule' in action: |
| 147 | run_rule_id = action['run_rule'] |
| 148 | if run_rule_id in call_stack: |
| 149 | call_stack.append(run_rule_id) |
| 150 | sys.stderr.write(\ |
| 151 | "Infinite loop caused by run_rule actions.\n"+\ |
| 152 | str(call_stack)+'\n') |
| 153 | handle_validation_error() |
| 154 | else: |
| 155 | for rule in config_json.get('rules', {}): |
| 156 | if rule['id'] == run_rule_id: |
| 157 | check_infinite_loops_in_rule(\ |
| 158 | config_json, rule, call_stack) |
| 159 | call_stack.pop() |
| 160 | |
| 161 | def check_infinite_loops(config_json): |
| 162 | r""" |
| 163 | Check if rule in config file is called recursively, causing an |
| 164 | infinite loop. |
| 165 | config_json: Configuration file JSON |
| 166 | """ |
| 167 | |
| 168 | for rule in config_json.get('rules', {}): |
| 169 | check_infinite_loops_in_rule(config_json, rule) |
| 170 | |
Bob King | 5b27a95 | 2020-01-20 18:04:15 +0800 | [diff] [blame] | 171 | def check_duplicate_object_id(config_json): |
| 172 | r""" |
| 173 | Check that there aren't any JSON objects with the same 'id' property value. |
| 174 | config_json: Configuration file JSON |
| 175 | """ |
| 176 | |
Bob King | 23dd60b | 2020-02-26 10:23:21 +0800 | [diff] [blame] | 177 | json_ids = get_values(config_json, 'id') |
Bob King | 5b27a95 | 2020-01-20 18:04:15 +0800 | [diff] [blame] | 178 | unique_ids = set() |
| 179 | for id in json_ids: |
| 180 | if id in unique_ids: |
| 181 | sys.stderr.write("Error: Duplicate ID.\n"+\ |
| 182 | "Found multiple objects with the ID "+id+'\n') |
| 183 | handle_validation_error() |
| 184 | else: |
| 185 | unique_ids.add(id) |
| 186 | |
Bob King | 95b796a | 2020-01-15 14:45:06 +0800 | [diff] [blame] | 187 | def check_duplicate_rule_id(config_json): |
| 188 | r""" |
| 189 | Check that there aren't any "rule" elements with the same 'id' field. |
| 190 | config_json: Configuration file JSON |
| 191 | """ |
| 192 | rule_ids = [] |
| 193 | for rule in config_json.get('rules', {}): |
| 194 | rule_id = rule['id'] |
| 195 | if rule_id in rule_ids: |
| 196 | sys.stderr.write("Error: Duplicate rule ID.\n"+\ |
| 197 | "Found multiple rules with the ID "+rule_id+'\n') |
| 198 | handle_validation_error() |
| 199 | else: |
| 200 | rule_ids.append(rule_id) |
| 201 | |
| 202 | def check_duplicate_chassis_number(config_json): |
| 203 | r""" |
| 204 | Check that there aren't any "chassis" elements with the same 'number' field. |
| 205 | config_json: Configuration file JSON |
| 206 | """ |
| 207 | numbers = [] |
| 208 | for chassis in config_json.get('chassis', {}): |
| 209 | number = chassis['number'] |
| 210 | if number in numbers: |
| 211 | sys.stderr.write("Error: Duplicate chassis number.\n"+\ |
| 212 | "Found multiple chassis with the number "+str(number)+'\n') |
| 213 | handle_validation_error() |
| 214 | else: |
| 215 | numbers.append(number) |
| 216 | |
| 217 | def check_duplicate_device_id(config_json): |
| 218 | r""" |
| 219 | Check that there aren't any "devices" with the same 'id' field. |
| 220 | config_json: Configuration file JSON |
| 221 | """ |
| 222 | device_ids = [] |
| 223 | for chassis in config_json.get('chassis', {}): |
| 224 | for device in chassis.get('devices', {}): |
| 225 | device_id = device['id'] |
| 226 | if device_id in device_ids: |
| 227 | sys.stderr.write("Error: Duplicate device ID.\n"+\ |
| 228 | "Found multiple devices with the ID "+device_id+'\n') |
| 229 | handle_validation_error() |
| 230 | else: |
| 231 | device_ids.append(device_id) |
| 232 | |
| 233 | def check_duplicate_rail_id(config_json): |
| 234 | r""" |
| 235 | Check that there aren't any "rails" with the same 'id' field. |
| 236 | config_json: Configuration file JSON |
| 237 | """ |
| 238 | rail_ids = [] |
| 239 | for chassis in config_json.get('chassis', {}): |
| 240 | for device in chassis.get('devices', {}): |
| 241 | for rail in device.get('rails', {}): |
| 242 | rail_id = rail['id'] |
| 243 | if rail_id in rail_ids: |
| 244 | sys.stderr.write("Error: Duplicate rail ID.\n"+\ |
| 245 | "Found multiple rails with the ID "+rail_id+'\n') |
| 246 | handle_validation_error() |
| 247 | else: |
| 248 | rail_ids.append(rail_id) |
| 249 | |
| 250 | def check_for_duplicates(config_json): |
| 251 | r""" |
| 252 | Check for duplicate ID. |
| 253 | """ |
| 254 | check_duplicate_rule_id(config_json) |
| 255 | |
| 256 | check_duplicate_chassis_number(config_json) |
| 257 | |
| 258 | check_duplicate_device_id(config_json) |
| 259 | |
| 260 | check_duplicate_rail_id(config_json) |
| 261 | |
Bob King | 5b27a95 | 2020-01-20 18:04:15 +0800 | [diff] [blame] | 262 | check_duplicate_object_id(config_json) |
| 263 | |
Bob King | 5cc0128 | 2019-12-17 18:11:57 +0800 | [diff] [blame] | 264 | def validate_schema(config, schema): |
| 265 | r""" |
| 266 | Validates the specified config file using the specified |
| 267 | schema file. |
| 268 | |
| 269 | config: Path of the file containing the config JSON |
| 270 | schema: Path of the file containing the schema JSON |
| 271 | """ |
| 272 | |
| 273 | with open(config) as config_handle: |
| 274 | config_json = json.load(config_handle) |
| 275 | |
| 276 | with open(schema) as schema_handle: |
| 277 | schema_json = json.load(schema_handle) |
| 278 | |
| 279 | try: |
| 280 | jsonschema.validate(config_json, schema_json) |
| 281 | except jsonschema.ValidationError as e: |
| 282 | print(e) |
Bob King | 95b796a | 2020-01-15 14:45:06 +0800 | [diff] [blame] | 283 | handle_validation_error() |
| 284 | |
| 285 | return config_json |
Bob King | 5cc0128 | 2019-12-17 18:11:57 +0800 | [diff] [blame] | 286 | |
Bob King | b7552f0 | 2020-10-15 14:34:17 +0800 | [diff] [blame] | 287 | def validate_JSON_format(file): |
| 288 | with open(file) as json_data: |
| 289 | try: |
| 290 | return json.load(json_data) |
| 291 | except ValueError as err: |
| 292 | return False |
| 293 | return True |
| 294 | |
Bob King | 5cc0128 | 2019-12-17 18:11:57 +0800 | [diff] [blame] | 295 | if __name__ == '__main__': |
| 296 | |
| 297 | parser = argparse.ArgumentParser( |
| 298 | description='phosphor-regulators configuration file validator') |
| 299 | |
| 300 | parser.add_argument('-s', '--schema-file', dest='schema_file', |
| 301 | help='The phosphor-regulators schema file') |
| 302 | |
| 303 | parser.add_argument('-c', '--configuration-file', dest='configuration_file', |
| 304 | help='The phosphor-regulators configuration file') |
| 305 | |
| 306 | args = parser.parse_args() |
| 307 | |
| 308 | if not args.schema_file: |
| 309 | parser.print_help() |
| 310 | sys.exit("Error: Schema file is required.") |
Bob King | b7552f0 | 2020-10-15 14:34:17 +0800 | [diff] [blame] | 311 | if not os.path.exists(args.schema_file): |
| 312 | parser.print_help() |
| 313 | sys.exit("Error: Schema file does not exist.") |
| 314 | if not os.access(args.schema_file, os.R_OK): |
| 315 | parser.print_help() |
| 316 | sys.exit("Error: Schema file is not readable.") |
| 317 | if not validate_JSON_format(args.schema_file): |
| 318 | parser.print_help() |
| 319 | sys.exit("Error: Schema file is not in the JSON format.") |
Bob King | 5cc0128 | 2019-12-17 18:11:57 +0800 | [diff] [blame] | 320 | if not args.configuration_file: |
| 321 | parser.print_help() |
| 322 | sys.exit("Error: Configuration file is required.") |
Bob King | b7552f0 | 2020-10-15 14:34:17 +0800 | [diff] [blame] | 323 | if not os.path.exists(args.configuration_file): |
| 324 | parser.print_help() |
| 325 | sys.exit("Error: Configuration file does not exist.") |
| 326 | if not os.access(args.configuration_file, os.R_OK): |
| 327 | parser.print_help() |
| 328 | sys.exit("Error: Configuration file is not readable.") |
| 329 | if not validate_JSON_format(args.configuration_file): |
| 330 | parser.print_help() |
| 331 | sys.exit("Error: Configuration file is not in the JSON format.") |
Bob King | 5cc0128 | 2019-12-17 18:11:57 +0800 | [diff] [blame] | 332 | |
Bob King | 95b796a | 2020-01-15 14:45:06 +0800 | [diff] [blame] | 333 | config_json = validate_schema(args.configuration_file, args.schema_file) |
Bob King | d114cd9 | 2020-02-10 13:56:02 +0800 | [diff] [blame] | 334 | |
Bob King | 95b796a | 2020-01-15 14:45:06 +0800 | [diff] [blame] | 335 | check_for_duplicates(config_json) |
Bob King | d114cd9 | 2020-02-10 13:56:02 +0800 | [diff] [blame] | 336 | |
| 337 | check_infinite_loops(config_json) |
| 338 | |
Bob King | 23dd60b | 2020-02-26 10:23:21 +0800 | [diff] [blame] | 339 | check_run_rule_value_exists(config_json) |
Bob King | e959754 | 2020-02-26 10:47:40 +0800 | [diff] [blame] | 340 | |
| 341 | check_set_device_value_exists(config_json) |
Bob King | 9146df2 | 2020-02-26 10:49:33 +0800 | [diff] [blame] | 342 | |
| 343 | check_rule_id_exists(config_json) |
Bob King | a533d70 | 2020-02-26 10:51:24 +0800 | [diff] [blame] | 344 | |
| 345 | check_number_of_elements_in_masks(config_json) |