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 check_run_rule_value_exists(config_json): 51 r""" 52 Check if any run_rule actions specify a rule ID that does not exist. 53 config_json: Configuration file JSON 54 """ 55 56 rule_ids = get_values(config_json, 'run_rule') 57 valid_rule_ids = get_rule_ids(config_json) 58 for rule_id in rule_ids: 59 if rule_id not in valid_rule_ids: 60 sys.stderr.write("Error: Rule ID does not exist.\n"+\ 61 "Found run_rule action that specifies invalid rule ID "+\ 62 rule_id+'\n') 63 handle_validation_error() 64 65def check_infinite_loops_in_rule(config_json, rule_json, call_stack=[]): 66 r""" 67 Check if a 'run_rule' action in the specified rule causes an 68 infinite loop. 69 config_json: Configuration file JSON. 70 rule_json: A rule in the JSON config file. 71 call_stack: Current call stack of rules. 72 """ 73 74 call_stack.append(rule_json['id']) 75 for action in rule_json.get('actions', {}): 76 if 'run_rule' in action: 77 run_rule_id = action['run_rule'] 78 if run_rule_id in call_stack: 79 call_stack.append(run_rule_id) 80 sys.stderr.write(\ 81 "Infinite loop caused by run_rule actions.\n"+\ 82 str(call_stack)+'\n') 83 handle_validation_error() 84 else: 85 for rule in config_json.get('rules', {}): 86 if rule['id'] == run_rule_id: 87 check_infinite_loops_in_rule(\ 88 config_json, rule, call_stack) 89 call_stack.pop() 90 91def check_infinite_loops(config_json): 92 r""" 93 Check if rule in config file is called recursively, causing an 94 infinite loop. 95 config_json: Configuration file JSON 96 """ 97 98 for rule in config_json.get('rules', {}): 99 check_infinite_loops_in_rule(config_json, rule) 100 101def check_duplicate_object_id(config_json): 102 r""" 103 Check that there aren't any JSON objects with the same 'id' property value. 104 config_json: Configuration file JSON 105 """ 106 107 json_ids = get_values(config_json, 'id') 108 unique_ids = set() 109 for id in json_ids: 110 if id in unique_ids: 111 sys.stderr.write("Error: Duplicate ID.\n"+\ 112 "Found multiple objects with the ID "+id+'\n') 113 handle_validation_error() 114 else: 115 unique_ids.add(id) 116 117def check_duplicate_rule_id(config_json): 118 r""" 119 Check that there aren't any "rule" elements with the same 'id' field. 120 config_json: Configuration file JSON 121 """ 122 rule_ids = [] 123 for rule in config_json.get('rules', {}): 124 rule_id = rule['id'] 125 if rule_id in rule_ids: 126 sys.stderr.write("Error: Duplicate rule ID.\n"+\ 127 "Found multiple rules with the ID "+rule_id+'\n') 128 handle_validation_error() 129 else: 130 rule_ids.append(rule_id) 131 132def check_duplicate_chassis_number(config_json): 133 r""" 134 Check that there aren't any "chassis" elements with the same 'number' field. 135 config_json: Configuration file JSON 136 """ 137 numbers = [] 138 for chassis in config_json.get('chassis', {}): 139 number = chassis['number'] 140 if number in numbers: 141 sys.stderr.write("Error: Duplicate chassis number.\n"+\ 142 "Found multiple chassis with the number "+str(number)+'\n') 143 handle_validation_error() 144 else: 145 numbers.append(number) 146 147def check_duplicate_device_id(config_json): 148 r""" 149 Check that there aren't any "devices" with the same 'id' field. 150 config_json: Configuration file JSON 151 """ 152 device_ids = [] 153 for chassis in config_json.get('chassis', {}): 154 for device in chassis.get('devices', {}): 155 device_id = device['id'] 156 if device_id in device_ids: 157 sys.stderr.write("Error: Duplicate device ID.\n"+\ 158 "Found multiple devices with the ID "+device_id+'\n') 159 handle_validation_error() 160 else: 161 device_ids.append(device_id) 162 163def check_duplicate_rail_id(config_json): 164 r""" 165 Check that there aren't any "rails" with the same 'id' field. 166 config_json: Configuration file JSON 167 """ 168 rail_ids = [] 169 for chassis in config_json.get('chassis', {}): 170 for device in chassis.get('devices', {}): 171 for rail in device.get('rails', {}): 172 rail_id = rail['id'] 173 if rail_id in rail_ids: 174 sys.stderr.write("Error: Duplicate rail ID.\n"+\ 175 "Found multiple rails with the ID "+rail_id+'\n') 176 handle_validation_error() 177 else: 178 rail_ids.append(rail_id) 179 180def check_for_duplicates(config_json): 181 r""" 182 Check for duplicate ID. 183 """ 184 check_duplicate_rule_id(config_json) 185 186 check_duplicate_chassis_number(config_json) 187 188 check_duplicate_device_id(config_json) 189 190 check_duplicate_rail_id(config_json) 191 192 check_duplicate_object_id(config_json) 193 194def validate_schema(config, schema): 195 r""" 196 Validates the specified config file using the specified 197 schema file. 198 199 config: Path of the file containing the config JSON 200 schema: Path of the file containing the schema JSON 201 """ 202 203 with open(config) as config_handle: 204 config_json = json.load(config_handle) 205 206 with open(schema) as schema_handle: 207 schema_json = json.load(schema_handle) 208 209 try: 210 jsonschema.validate(config_json, schema_json) 211 except jsonschema.ValidationError as e: 212 print(e) 213 handle_validation_error() 214 215 return config_json 216 217if __name__ == '__main__': 218 219 parser = argparse.ArgumentParser( 220 description='phosphor-regulators configuration file validator') 221 222 parser.add_argument('-s', '--schema-file', dest='schema_file', 223 help='The phosphor-regulators schema file') 224 225 parser.add_argument('-c', '--configuration-file', dest='configuration_file', 226 help='The phosphor-regulators configuration file') 227 228 args = parser.parse_args() 229 230 if not args.schema_file: 231 parser.print_help() 232 sys.exit("Error: Schema file is required.") 233 if not args.configuration_file: 234 parser.print_help() 235 sys.exit("Error: Configuration file is required.") 236 237 config_json = validate_schema(args.configuration_file, args.schema_file) 238 239 check_for_duplicates(config_json) 240 241 check_infinite_loops(config_json) 242 243 check_run_rule_value_exists(config_json) 244