|
1 #!/usr/bin/env python |
|
2 # vim: set ft=python : |
|
3 |
|
4 """ |
|
5 Process zonefiles with template expansions. |
|
6 """ |
|
7 |
|
8 __version__ = '0.0.1-dev' |
|
9 |
|
10 import optparse |
|
11 import codecs |
|
12 import os.path |
|
13 from datetime import datetime |
|
14 import logging |
|
15 |
|
16 log = logging.getLogger() |
|
17 |
|
18 # command-line options, global state |
|
19 options = None |
|
20 |
|
21 def parse_options (argv) : |
|
22 """ |
|
23 Parse command-line arguments. |
|
24 """ |
|
25 |
|
26 parser = optparse.OptionParser( |
|
27 prog = argv[0], |
|
28 usage = '%prog: [options]', |
|
29 version = __version__, |
|
30 |
|
31 # module docstring |
|
32 description = __doc__, |
|
33 ) |
|
34 |
|
35 # logging |
|
36 general = optparse.OptionGroup(parser, "General Options") |
|
37 |
|
38 general.add_option('-q', '--quiet', dest='loglevel', action='store_const', const=logging.ERROR, help="Less output") |
|
39 general.add_option('-v', '--verbose', dest='loglevel', action='store_const', const=logging.INFO, help="More output") |
|
40 general.add_option('-D', '--debug', dest='loglevel', action='store_const', const=logging.DEBUG, help="Even more output") |
|
41 |
|
42 parser.add_option_group(general) |
|
43 |
|
44 parser.add_option('-c', '--input-charset', metavar='CHARSET', default='utf-8', |
|
45 help="Encoding used for input files") |
|
46 |
|
47 parser.add_option('-o', '--output', metavar='FILE', default='-', |
|
48 help="Write to output file; default stdout") |
|
49 |
|
50 parser.add_option('--output-charset', metavar='CHARSET', default='utf-8', |
|
51 help="Encoding used for output files") |
|
52 |
|
53 parser.add_option('--expand', metavar='NAME=VALUE', action='append', |
|
54 help="Expand given template variable in zone") |
|
55 |
|
56 parser.add_option('--serial', metavar='FILE', |
|
57 help="Read/expand serial from given .serial file") |
|
58 |
|
59 parser.add_option('--update-serial', action='store_true', |
|
60 help="Update serial in given .serial file") |
|
61 |
|
62 # defaults |
|
63 parser.set_defaults( |
|
64 loglevel = logging.WARN, |
|
65 expand = [], |
|
66 ) |
|
67 |
|
68 # parse |
|
69 options, args = parser.parse_args(argv[1:]) |
|
70 |
|
71 # configure |
|
72 logging.basicConfig( |
|
73 format = '%(processName)s: %(name)s: %(levelname)s %(funcName)s : %(message)s', |
|
74 level = options.loglevel, |
|
75 ) |
|
76 |
|
77 return options, args |
|
78 |
|
79 def process_file (file, expansions) : |
|
80 """ |
|
81 Process file, expanding lines. |
|
82 """ |
|
83 |
|
84 for line in file : |
|
85 line = line.format(**expansions) |
|
86 |
|
87 yield line |
|
88 |
|
89 def write_lines (file, lines, suffix='\n') : |
|
90 for line in lines : |
|
91 file.write(line + suffix) |
|
92 |
|
93 def open_file (path, mode, charset) : |
|
94 """ |
|
95 Open unicode-enabled file from path, with - using stdio. |
|
96 """ |
|
97 |
|
98 if path == '-' : |
|
99 # use stdin/out based on mode |
|
100 stream, func = { |
|
101 'r': (sys.stdin, codecs.getreader), |
|
102 'w': (sys.stdout, codecs.getwriter), |
|
103 }[mode[0]] |
|
104 |
|
105 # wrap |
|
106 return func(charset)(stream) |
|
107 |
|
108 else : |
|
109 # open |
|
110 return codecs.open(path, mode, charset) |
|
111 |
|
112 def process_serial (path, update=False) : |
|
113 """ |
|
114 Update/process new serial number from given file, based on date. |
|
115 |
|
116 Returns the new serial as a string. |
|
117 """ |
|
118 |
|
119 DATE_FMT = '%Y%m%d' |
|
120 DATE_LEN = 8 |
|
121 |
|
122 SERIAL_FMT = "{date:8}{count:02}" |
|
123 SERIAL_LEN = 10 |
|
124 |
|
125 if os.path.exists(path) : |
|
126 # read current |
|
127 serial = open(path).read().strip() |
|
128 |
|
129 assert len(serial) == SERIAL_LEN |
|
130 |
|
131 old_serial = int(serial) |
|
132 |
|
133 old_date = datetime.strptime(serial[:DATE_LEN], DATE_FMT).date() |
|
134 old_count = int(serial[DATE_LEN:]) |
|
135 |
|
136 else : |
|
137 log.warn("given .serial does not exist, assuming from today: %s", path) |
|
138 old_serial = old_date = old_count = None |
|
139 |
|
140 if update : |
|
141 # update |
|
142 today = datetime.now().date() |
|
143 |
|
144 if not old_serial : |
|
145 # fresh start |
|
146 date = today |
|
147 count = 1 |
|
148 |
|
149 log.info("Starting with fresh serial: %s:%s", date, count) |
|
150 |
|
151 elif old_date < today : |
|
152 # update date |
|
153 date = today |
|
154 count = 1 |
|
155 |
|
156 log.info("Updating to today: %s -> %s", old_date, date) |
|
157 |
|
158 elif old_date == today : |
|
159 # keep date, update count |
|
160 date = old_date |
|
161 count = old_count + 1 |
|
162 |
|
163 if count > 99 : |
|
164 raise Exception("Serial update rollover: %s, %s", date, count) |
|
165 |
|
166 log.info("Updating today's count: %s, %s", date, count) |
|
167 |
|
168 else : |
|
169 raise Exception("Invalid serial: %s:%s", old_date, old_count) |
|
170 |
|
171 else : |
|
172 date = old_date |
|
173 count = old_count |
|
174 |
|
175 serial = SERIAL_FMT.format(date=date.strftime(DATE_FMT), count=count) |
|
176 |
|
177 open(path, 'w').write(serial) |
|
178 |
|
179 return serial |
|
180 |
|
181 def parse_expand (expand) : |
|
182 """ |
|
183 Parse an --expand foo=bar to (key, value) |
|
184 """ |
|
185 |
|
186 key, value = expand.split('=', 1) |
|
187 |
|
188 return key, value |
|
189 |
|
190 def main (argv) : |
|
191 global options |
|
192 |
|
193 options, args = parse_options(argv) |
|
194 |
|
195 # expands |
|
196 expand = dict(parse_expand(expand) for expand in options.expand) |
|
197 |
|
198 # serial? |
|
199 if options.serial : |
|
200 serial = process_serial(options.serial, update=options.update_serial) |
|
201 |
|
202 expand['serial'] = serial |
|
203 |
|
204 # input |
|
205 if args : |
|
206 # open files |
|
207 input_files = [open_file(path, 'r', options.input_charset) for path in args] |
|
208 |
|
209 else : |
|
210 # default to stdout |
|
211 input_files = [open_file('-', 'r', options.input_charset)] |
|
212 |
|
213 # process |
|
214 lines = [] |
|
215 |
|
216 for file in input_files : |
|
217 log.info("Reading zone: %s", file) |
|
218 |
|
219 lines += list(process_file(file, expand)) |
|
220 |
|
221 # output |
|
222 output = open_file(options.output, 'w', options.output_charset) |
|
223 write_lines(output, lines, suffix='') |
|
224 |
|
225 return 0 |
|
226 |
|
227 if __name__ == '__main__': |
|
228 import sys |
|
229 |
|
230 sys.exit(main(sys.argv)) |