-
Notifications
You must be signed in to change notification settings - Fork 7
/
dump-named-conf-json.py
executable file
·290 lines (247 loc) · 11 KB
/
dump-named-conf-json.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
#!/usr/bin/env python3
#
# parse_bind9.py
#
import sys
import errno
import os.path
from pathlib import Path
import argparse
import json
import pyparsing as pp
import bind9_parser
# from isc_clause import clause_statements
# from isc_utils import key_secret, g_expose_secrets
g_prog_name = os.path.basename(__file__)
g_root_dir = '.' # default to current working directory
g_include_depth = 0
g_include_directive = ''
g_semicolon = ';'
g_pe_clause_include = pp.Keyword("include")
g_verbosity = 0
def process_entire_file_content(a_content_data):
"""
process_entire_file_content takes the initial ISC-styled configuration file
and recursively incorporates all the content of
any uncommented 'include' clauses and returns in
form of a single continuous configuration file.
Each uncommented include clause will be
annotated by having its content surrounded by
additional '#' comments indicating its isc_file_name
of the include file along with its depth level
count of any nesting include clauses.
:param a_content_data: the entire content of ISC-styled configuration
file (i.e., named.conf)
:return: the entire content of configuration file after reading in all
uncommented include clauses and folding in the content of such
include files.
"""
global g_include_depth
global g_include_directive
# When encountering quoted string, strip the quotes, if any
quoted_string = pp.quotedString.addParseAction(pp.removeQuotes)
# lift isc_file_name and create an 'include_file' attribute
g_include_directive = g_pe_clause_include \
+ (
quoted_string
| pp.Word(pp.printables, excludeChars=';')
)("include_filepath") \
+ g_semicolon
# BIND provides a number of comment formats as follows:
#
# /* C style comment format needs opening and closing markers
# ** but allows multiple lines or */
# /* single lines */
# // C++ style comments single line format no closing required
# # PERL/SHELL style comments single lines no closing required
g_include_directive.ignore(pp.cppStyleComment)
g_include_directive.ignore(pp.pythonStyleComment)
# attach include processing method as parse action to
# g_include_directive expression
# Add a hook (read_include_contents) to do the following:
# * the parser to check for 'include' statement,
# * open any 'include' statement found, and
# * continue reading those 'include' files.
g_include_directive.addParseAction(read_include_contents)
# Now perform the parsing action against that large string
master_config_content = g_include_directive.transformString(a_content_data)
return master_config_content
def suppress_key_secrets(st, locn, toks):
"""
TODO: This parser hook routine (as noted by (st, locn, toks) arguments)
will stop masking the key secrets (thereby exposing its secret).
:param st: the original string being parsed
:param locn: the location of the matching substring
:param toks: a list of the matched tokens, packages as a ParseResults object
:return: Another long string containing content of include file
"""
def read_include_contents(st, locn, toks):
"""
This parser hook routine (as noted by (st, locn, toks) arguments)
will open another file as pointed to by the toks argument
:param st: the original string being parsed
:param locn: the location of the matching substring
:param toks: a list of the matched tokens, packages as a ParseResults object
:return: Another long string containing content of include file
"""
global g_include_depth
# If include file is an absolute path, make it relative to g_root_dir
if toks.include_filepath[0:1] == '/':
include_file_ref = g_root_dir + toks.include_filepath
else:
# If include file is a relative, then it is relative to g_root_dir
include_file_ref = g_root_dir + '/' + toks.include_filepath
if args.v:
print('Found:', include_file_ref)
g_include_depth = g_include_depth + 1
# Add a comment line into expanded master include file
# for later post-error analysis
# Do not wrap C-style comment ourselves of this same line because
# original line may too have this "/* ... */"
# Only way to avoid already-used but inlined C-style comment is to
# create a separate line
# TODO: Better cyclical detection of file include recursion
include_echo = "#{}# {}\n".format(g_prog_name, pp.line(locn, st).strip())
include_begin_echo = "# Begin of {} file.\n# Nested include-file depth: {}".format(
include_file_ref,
# pp.line(locn, st).strip(),
g_include_depth
)
include_end_echo = "# Nested include-file depth: {}\n# End of {} file.".format(
g_include_depth,
# pp.line(locn, st).strip(),
include_file_ref
)
# Check if file exist, gracefully raise exception
next_include_file = ''
try:
print('Opening', include_file_ref)
next_include_file = Path(include_file_ref).read_text()
except FileNotFoundError as _ric_err:
print('read_include_contents: err:', _ric_err)
raise FileNotFoundError
# guard against recursive includes (doesn't work for reuse of include files)
# Transform the detected 'include' statement into the following
# - Commented out include statement
# - Header Comment Line that is stating:
# - name of new include file
# - Depth of include nestings
# - Content of include file
# - Footer Comment Line is stating the same thing as above Header Comment Line
result_include_line = include_echo \
+ include_begin_echo \
+ '\n' \
+ g_include_directive.transformString(next_include_file) \
+ include_end_echo
g_include_depth = g_include_depth - 1
return result_include_line
def my_action_2(strg, loc, toks):
if g_verbosity:
print('my_action_2: index(strg,;)', strg.index(';'))
short_string = strg[loc:strg.index(';')]
print('my_action_2: len(strg):', len(short_string))
print('my_action_2: strg:', short_string)
print('my_action_2: loc:', loc)
print('my_action_2: toks:', toks)
if __name__ == '__main__':
parser = argparse.ArgumentParser(
'Build python list/dict from ISC Bind configuration file'
)
parser.add_argument('-d','--debug',
action='store_true',
default=False,
help='Enable debugging of PyParsing')
parser.add_argument('-x','--secrets',
action='store_true',
default=False,
help='Enable exposing key secrets. Default is to obfuscate the secret keys')
parser.add_argument('-v',
action='count',
default=0,
help='increase output g_verbosity')
parser.add_argument('-r', '--root',
type=str,
help='Set g_root_dir directory path')
parser.add_argument('config_filepath',
default='named.conf',
help='File path to named.conf file')
args = parser.parse_args()
if args.v:
print("g_verbosity turned on")
g_verbosity = g_verbosity + 1
if args.secrets:
print("WARNING: Exposing key secrets")
bind9_parser.g_expose_secrets = True
if args.config_filepath is None:
print("Must specify filepath/filespec to the named.conf file.")
exit(errno.ENOENT)
named_conf_filepath = args.config_filepath
# use g_include_directive.transformString to perform includes
if args.root is not None:
g_root_dir = args.root
# Need to check if there are overlaps, in case there is
# a named.conf in its original but relative ./etc/bind.
# Useful if a local but many named.conf files are being
# tested against a baseline set of include files.
# len = len(g_root_dir)
# if g_root_dir[0:len] == named_conf_filepath[0:len]:
# named_conf_filepath = named_conf_filepath[len:]
# Check if path ends with '/', if so, trim that '/' off.
if g_root_dir[-1] == '/':
g_root_dir = g_root_dir[0:-1]
if g_verbosity:
print("Current working directory: ", os.getcwd())
print('named_conf_filepath:', named_conf_filepath)
print('g_root_dir::', g_root_dir)
# Extract the basename of the named.conf filepath
conf_file_basename = os.path.basename(named_conf_filepath)
# read contents of starting named.conf file
# named_conf = 'split-horizon-2-bind9-servers/named-public_all.conf'
named_config = Path(named_conf_filepath).read_text()
toplevel_config = process_entire_file_content(named_config)
# print expanded configuration file
if g_verbosity:
# print('File: ', single_large_file)
print("About to parse...")
if args.debug:
print("Debugging PyParsing enabled.")
bind9_parser.clause_statements.setDebug()
if not bind9_parser.g_expose_secrets:
bind9_parser.key_secret.addParseAction(suppress_key_secrets)
my_clauses = bind9_parser.clause_statements \
.setDebug(g_verbosity) \
.setParseAction(my_action_2) \
.ignore(pp.cStyleComment) \
.ignore(pp.cppStyleComment) \
.ignore(pp.pythonStyleComment)
# my_clauses = my_clauses.enablePackrat()
# pp.__diag__.enable_debug_on_named_expressions = True
# pp.__diag__.warn_multiple_tokens_in_named_alternation = True
# pp.__diag__.warn_ungrouped_named_tokens_in_collection = True
# pp.__diag__.warn_name_set_on_empty_Forward = True
# pp.__diag__.warn_on_multiple_string_args_to_oneof = True
# pp.__diag__.enable("enable_debug_on_named_expressions")
# pp.__diag__.enable("warn_multiple_tokens_in_named_alternation")
# pp.__diag__.enable("warn_ungrouped_named_tokens_in_collection")
# pp.__diag__.enable("warn_name_set_on_empty_Forward")
# pp.__diag__.enable("warn_on_multiple_string_args_to_oneof")
if args.debug:
print("Start: Is the library quiet?")
result = my_clauses.parseString(toplevel_config, parseAll=True)
if args.debug:
print("End: Is the library quiet?")
if g_verbosity > 1:
print('len(result):', len(result))
print('\nPlain print(result):')
if g_verbosity > 0:
result.pprint()
print('result:', result.asDict())
import pprint
pp = pprint.PrettyPrinter(width=2, compact=False, indent=1)
print('\nprint(result.asDict()):')
pp.pprint(result.asDict())
print('\nJSON dump:')
y = json.dumps(result.asDict(), indent=4)
print('\njson-pretty: ', y)
print('end of result.')
sys.exit(1)