1*8aaca124Schristos############################################################################ 2*8aaca124Schristos# Copyright (C) Internet Systems Consortium, Inc. ("ISC") 3*8aaca124Schristos# 4*8aaca124Schristos# SPDX-License-Identifier: MPL-2.0 5*8aaca124Schristos# 6*8aaca124Schristos# This Source Code Form is subject to the terms of the Mozilla Public 7*8aaca124Schristos# License, v. 2.0. If a copy of the MPL was not distributed with this 8*8aaca124Schristos# file, you can obtain one at https://mozilla.org/MPL/2.0/. 9*8aaca124Schristos# 10*8aaca124Schristos# See the COPYRIGHT file distributed with this work for additional 11*8aaca124Schristos# information regarding copyright ownership. 12*8aaca124Schristos############################################################################ 13*8aaca124Schristos 14*8aaca124Schristos""" 15*8aaca124SchristosUtility to check ISC config grammar consistency. It detects statement names 16*8aaca124Schristoswhich use different grammar depending on position in the configuration file. 17*8aaca124SchristosE.g. "max-zone-ttl" in dnssec-policy uses '<duration>' 18*8aaca124Schristosvs. '( unlimited | <duration> ) used in options. 19*8aaca124Schristos""" 20*8aaca124Schristos 21*8aaca124Schristosfrom collections import namedtuple 22*8aaca124Schristosfrom itertools import groupby 23*8aaca124Schristosimport fileinput 24*8aaca124Schristos 25*8aaca124Schristosimport parsegrammar 26*8aaca124Schristos 27*8aaca124Schristos 28*8aaca124Schristosdef statement2block(grammar, path): 29*8aaca124Schristos """Return mapping statement name to "path" where it is allowed. 30*8aaca124Schristos _top is placeholder name for the namesless topmost context. 31*8aaca124Schristos 32*8aaca124Schristos E.g. { 33*8aaca124Schristos 'options: [('_top',)], 34*8aaca124Schristos 'server': [('_top', 'view'), ('_top',)], 35*8aaca124Schristos 'rate-limit': [('_top', 'options'), ('_top', 'view')], 36*8aaca124Schristos 'slip': [('_top', 'options', 'rate-limit'), ('_top', 'view', 'rate-limit')] 37*8aaca124Schristos } 38*8aaca124Schristos """ 39*8aaca124Schristos key2place = {} 40*8aaca124Schristos 41*8aaca124Schristos for key in grammar: 42*8aaca124Schristos assert not key.startswith("_") 43*8aaca124Schristos key2place.setdefault(key, []).append(tuple(path)) 44*8aaca124Schristos if "_mapbody" in grammar[key]: 45*8aaca124Schristos nested2block = statement2block(grammar[key]["_mapbody"], path + [key]) 46*8aaca124Schristos # merge to uppermost output dictionary 47*8aaca124Schristos for nested_key, nested_path in nested2block.items(): 48*8aaca124Schristos key2place.setdefault(nested_key, []).extend(nested_path) 49*8aaca124Schristos return key2place 50*8aaca124Schristos 51*8aaca124Schristos 52*8aaca124Schristosdef get_statement_grammar(grammar, path, name): 53*8aaca124Schristos """Descend into grammar dict using provided path 54*8aaca124Schristos and return final dict found there. 55*8aaca124Schristos 56*8aaca124Schristos Intermediate steps into "_mapbody" subkeys are done automatically. 57*8aaca124Schristos """ 58*8aaca124Schristos assert path[0] == "_top" 59*8aaca124Schristos path = list(path) + [name] 60*8aaca124Schristos for step in path[1:]: 61*8aaca124Schristos if "_mapbody" in grammar: 62*8aaca124Schristos grammar = grammar["_mapbody"] 63*8aaca124Schristos grammar = grammar[step] 64*8aaca124Schristos return grammar 65*8aaca124Schristos 66*8aaca124Schristos 67*8aaca124SchristosStatement = namedtuple("Statement", ["path", "name", "subgrammar"]) 68*8aaca124Schristos 69*8aaca124Schristos 70*8aaca124Schristosdef groupby_grammar(statements): 71*8aaca124Schristos """ 72*8aaca124Schristos Return groups of Statement tuples with identical grammars and flags. 73*8aaca124Schristos See itertools.groupby. 74*8aaca124Schristos """ 75*8aaca124Schristos 76*8aaca124Schristos def keyfunc(statement): 77*8aaca124Schristos return sorted(statement.subgrammar.items()) 78*8aaca124Schristos 79*8aaca124Schristos groups = [] 80*8aaca124Schristos statements = sorted(statements, key=keyfunc) 81*8aaca124Schristos for _key, group in groupby(statements, keyfunc): 82*8aaca124Schristos groups.append(list(group)) # Store group iterator as a list 83*8aaca124Schristos return groups 84*8aaca124Schristos 85*8aaca124Schristos 86*8aaca124Schristosdef diff_statements(whole_grammar, places): 87*8aaca124Schristos """ 88*8aaca124Schristos Return map {statement name: [groups of [Statement]s with identical grammar]. 89*8aaca124Schristos """ 90*8aaca124Schristos out = {} 91*8aaca124Schristos for statement_name, paths in places.items(): 92*8aaca124Schristos grammars = [] 93*8aaca124Schristos for path in paths: 94*8aaca124Schristos statement_grammar = get_statement_grammar( 95*8aaca124Schristos whole_grammar, path, statement_name 96*8aaca124Schristos ) 97*8aaca124Schristos grammars.append(Statement(path, statement_name, statement_grammar)) 98*8aaca124Schristos groups = groupby_grammar(grammars) 99*8aaca124Schristos out[statement_name] = groups 100*8aaca124Schristos return out 101*8aaca124Schristos 102*8aaca124Schristos 103*8aaca124Schristosdef pformat_grammar(node, level=1): 104*8aaca124Schristos """Pretty print a given grammar node in the same way as cfg_test would""" 105*8aaca124Schristos 106*8aaca124Schristos def sortkey(item): 107*8aaca124Schristos """Treat 'type' specially and always put it first, for zone types""" 108*8aaca124Schristos key, _ = item 109*8aaca124Schristos if key == "type": 110*8aaca124Schristos return "" 111*8aaca124Schristos return key 112*8aaca124Schristos 113*8aaca124Schristos if "_grammar" in node: # no nesting 114*8aaca124Schristos assert "_id" not in node 115*8aaca124Schristos assert "_mapbody" not in node 116*8aaca124Schristos out = node["_grammar"] + ";" 117*8aaca124Schristos if "_flags" in node: 118*8aaca124Schristos out += " // " + ", ".join(node["_flags"]) 119*8aaca124Schristos return out + "\n" 120*8aaca124Schristos 121*8aaca124Schristos # a nested map 122*8aaca124Schristos out = "" 123*8aaca124Schristos indent = level * "\t" 124*8aaca124Schristos if not node.get("_ignore_this_level"): 125*8aaca124Schristos if "_id" in node: 126*8aaca124Schristos out += node["_id"] + " " 127*8aaca124Schristos out += "{\n" 128*8aaca124Schristos 129*8aaca124Schristos for key, subnode in sorted(node["_mapbody"].items(), key=sortkey): 130*8aaca124Schristos if not subnode.get("_ignore_this_level"): 131*8aaca124Schristos out += f"{indent}{subnode.get('_pprint_name', key)}" 132*8aaca124Schristos inner_grammar = pformat_grammar(node["_mapbody"][key], level=level + 1) 133*8aaca124Schristos else: # act as if we were not in a map 134*8aaca124Schristos inner_grammar = pformat_grammar(node["_mapbody"][key], level=level) 135*8aaca124Schristos if inner_grammar[0] != ";": # we _did_ find some arguments 136*8aaca124Schristos out += " " 137*8aaca124Schristos out += inner_grammar 138*8aaca124Schristos 139*8aaca124Schristos if not node.get("_ignore_this_level"): 140*8aaca124Schristos out += indent[:-1] + "};" # unindent the closing bracket 141*8aaca124Schristos if "_flags" in node: 142*8aaca124Schristos out += " // " + ", ".join(node["_flags"]) 143*8aaca124Schristos return out + "\n" 144*8aaca124Schristos 145*8aaca124Schristos 146*8aaca124Schristosdef main(): 147*8aaca124Schristos """ 148*8aaca124Schristos Ingest output from cfg_test --grammar and print out statements which use 149*8aaca124Schristos different grammar in different contexts. 150*8aaca124Schristos """ 151*8aaca124Schristos with fileinput.input() as filein: 152*8aaca124Schristos grammar = parsegrammar.parse_mapbody(filein) 153*8aaca124Schristos places = statement2block(grammar, ["_top"]) 154*8aaca124Schristos 155*8aaca124Schristos for statementname, groups in diff_statements(grammar, places).items(): 156*8aaca124Schristos if len(groups) > 1: 157*8aaca124Schristos print(f'statement "{statementname}" is inconsistent across blocks') 158*8aaca124Schristos for group in groups: 159*8aaca124Schristos print( 160*8aaca124Schristos "- path:", ", ".join(" -> ".join(variant.path) for variant in group) 161*8aaca124Schristos ) 162*8aaca124Schristos print(" ", pformat_grammar(group[0].subgrammar, level=1)) 163*8aaca124Schristos print() 164*8aaca124Schristos 165*8aaca124Schristos 166*8aaca124Schristosif __name__ == "__main__": 167*8aaca124Schristos main() 168