1#!/usr/bin/env python3
2
3import argparse
4import json
5import jsonschema
6import os
7import sys
8
9r"""
10Validates the phosphor-regulators configuration file. Checks it against a JSON
11schema as well as doing some extra checks that can't be encoded in the schema.
12"""
13
14def handle_validation_error():
15    sys.exit("Validation failed.")
16
17def 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
41def 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
51def 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
62def 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
90def 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
105def 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
120def 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
135def 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
161def 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
171def 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
177    json_ids = get_values(config_json, 'id')
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
187def 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
202def 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
217def 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
233def 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
250def 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
262    check_duplicate_object_id(config_json)
263
264def 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)
283                handle_validation_error()
284
285    return config_json
286
287def 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
295if __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.")
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.")
320    if not args.configuration_file:
321        parser.print_help()
322        sys.exit("Error: Configuration file is required.")
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.")
332
333    config_json = validate_schema(args.configuration_file, args.schema_file)
334
335    check_for_duplicates(config_json)
336
337    check_infinite_loops(config_json)
338
339    check_run_rule_value_exists(config_json)
340
341    check_set_device_value_exists(config_json)
342
343    check_rule_id_exists(config_json)
344
345    check_number_of_elements_in_masks(config_json)
346