Merge lp:~mpontillo/maas/migrate-dns-settings-1.7 into lp:maas/1.7
- migrate-dns-settings-1.7
- Merge into 1.7
Proposed by
Mike Pontillo
Status: | Merged |
---|---|
Approved by: | Mike Pontillo |
Approved revision: | no longer in the source branch. |
Merged at revision: | 3375 |
Proposed branch: | lp:~mpontillo/maas/migrate-dns-settings-1.7 |
Merge into: | lp:maas/1.7 |
Diff against target: |
639 lines (+372/-31) 5 files modified
docs/changelog.rst (+11/-0) src/maasserver/management/commands/edit_named_options.py (+96/-12) src/maasserver/tests/test_commands_edit_named_options.py (+141/-9) src/maasserver/utils/isc.py (+25/-4) src/maasserver/utils/tests/test_isc.py (+99/-6) |
To merge this branch: | bzr merge lp:~mpontillo/maas/migrate-dns-settings-1.7 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Andres Rodriguez (community) | Approve | ||
Review via email: mp+264089@code.launchpad.net |
This proposal supersedes a proposal from 2015-07-08.
Commit message
Backport DNS migration fixes (Merge revision 4077 from trunk)
Description of the change
To post a comment you must log in.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'docs/changelog.rst' |
2 | --- docs/changelog.rst 2015-05-26 16:36:50 +0000 |
3 | +++ docs/changelog.rst 2015-07-08 00:32:06 +0000 |
4 | @@ -2,6 +2,17 @@ |
5 | Changelog |
6 | ========= |
7 | |
8 | +1.7.6 |
9 | +===== |
10 | + |
11 | +Bug Fix Update |
12 | +-------------- |
13 | + |
14 | +#1470585 Accept list of forwarders for upstream_dns rather than just one. |
15 | + |
16 | +#1413388 Fix upgrade issue where it would remove custom DNS config, |
17 | + potentially breaking DNS. |
18 | + |
19 | 1.7.5 |
20 | ===== |
21 | |
22 | |
23 | === modified file 'src/maasserver/management/commands/edit_named_options.py' |
24 | --- src/maasserver/management/commands/edit_named_options.py 2015-07-01 01:38:49 +0000 |
25 | +++ src/maasserver/management/commands/edit_named_options.py 2015-07-08 00:32:06 +0000 |
26 | @@ -11,6 +11,8 @@ |
27 | print_function, |
28 | unicode_literals, |
29 | ) |
30 | +from collections import OrderedDict |
31 | + |
32 | |
33 | str = None |
34 | |
35 | @@ -19,16 +21,20 @@ |
36 | 'Command', |
37 | ] |
38 | |
39 | +from copy import deepcopy |
40 | from datetime import datetime |
41 | from optparse import make_option |
42 | import os |
43 | import shutil |
44 | +import sys |
45 | |
46 | from django.core.management.base import ( |
47 | BaseCommand, |
48 | CommandError, |
49 | ) |
50 | +from maasserver.models import Config |
51 | from maasserver.utils.isc import ( |
52 | + ISCParseException, |
53 | make_isc_string, |
54 | parse_isc_string, |
55 | ) |
56 | @@ -42,6 +48,24 @@ |
57 | '--config-path', dest='config_path', |
58 | default="/etc/bind/named.conf.options", |
59 | help="Specify the configuration file to edit."), |
60 | + make_option( |
61 | + '--dry-run', dest='dry_run', |
62 | + default=False, action='store_true', |
63 | + help="Do not edit any configuration; instead, print to stdout the " |
64 | + "actions that would be performed, and/or the new " |
65 | + "configuration that would be written."), |
66 | + make_option( |
67 | + '--force', dest='force', |
68 | + default=False, action='store_true', |
69 | + help="Force the BIND configuration to be written, even if it " |
70 | + "appears as though nothing has changed."), |
71 | + make_option( |
72 | + '--migrate-conflicting-options', default=False, |
73 | + dest='migrate_conflicting_options', action='store_true', |
74 | + help="Causes any options that conflict with MAAS-managed options " |
75 | + "to be deleted from the BIND configuration and moved to the " |
76 | + "MAAS-managed configuration. Requires the MAAS database to " |
77 | + "be configured and running."), |
78 | ) |
79 | help = ( |
80 | "Edit the named.conf.options file so that it includes the " |
81 | @@ -66,13 +90,12 @@ |
82 | """ |
83 | try: |
84 | config_dict = parse_isc_string(options_file) |
85 | - except Exception as e: |
86 | - # Yes, it throws bare exceptions :( |
87 | + except ISCParseException as e: |
88 | raise CommandError("Failed to parse %s: %s" % ( |
89 | config_path, e.message)) |
90 | options_block = config_dict.get("options", None) |
91 | if options_block is None: |
92 | - # Something is horribly wrong with the file, bail out rather |
93 | + # Something is horribly wrong with the file; bail out rather |
94 | # than doing anything drastic. |
95 | raise CommandError( |
96 | "Can't find options {} block in %s, bailing out without " |
97 | @@ -85,14 +108,37 @@ |
98 | options_block['include'] = '"%s%s%s"' % ( |
99 | dir, os.path.sep, MAAS_NAMED_CONF_OPTIONS_INSIDE_NAME) |
100 | |
101 | - def remove_forwarders(self, options_block): |
102 | + def migrate_forwarders(self, options_block, dry_run, stdout): |
103 | """Remove existing forwarders from the options block. |
104 | |
105 | It's a syntax error to have more than one in the combined |
106 | - configuration for named so we just remove whatever was there. |
107 | - There is no data loss due to the backup file made later. |
108 | + configuration for named, so we just remove whatever was there. |
109 | + |
110 | + Migrate any forwarders in the configuration file to the MAAS config. |
111 | """ |
112 | if 'forwarders' in options_block: |
113 | + bind_forwarders = options_block['forwarders'] |
114 | + |
115 | + if not dry_run: |
116 | + config, created = Config.objects.get_or_create( |
117 | + name='upstream_dns', |
118 | + defaults={'value': ' '.join(bind_forwarders)}) |
119 | + if not created: |
120 | + # A configuration value already exists, so add the |
121 | + # additional values we found in the configuration file to |
122 | + # MAAS. |
123 | + if config.value is None: |
124 | + config.value = '' |
125 | + maas_forwarders = OrderedDict.fromkeys( |
126 | + config.value.split()) |
127 | + maas_forwarders.update(bind_forwarders) |
128 | + config.value = ' '.join(maas_forwarders) |
129 | + config.save() |
130 | + else: |
131 | + stdout.write( |
132 | + "// Append to MAAS forwarders: %s\n" |
133 | + % ' '.join(bind_forwarders)) |
134 | + |
135 | del options_block['forwarders'] |
136 | |
137 | def back_up_existing_file(self, config_path): |
138 | @@ -104,21 +150,59 @@ |
139 | raise CommandError( |
140 | "Failed to make a backup of %s, exiting: %s" % ( |
141 | config_path, e.message)) |
142 | + return backup_destination |
143 | + |
144 | + def write_new_named_conf_options(self, fd, backup_filename, new_content): |
145 | + fd.write("""\ |
146 | +// |
147 | +// This file is managed by MAAS. Although MAAS attempts to preserve changes |
148 | +// made here, it is possible to create conflicts that MAAS can not resolve. |
149 | +// |
150 | +// DNS settings available in MAAS (for example, forwarders and |
151 | +// dnssec-validation) should be managed only in MAAS. |
152 | +// |
153 | +// The previous configuration file was backed up at: |
154 | +// %s |
155 | +// |
156 | +""" % backup_filename) |
157 | + fd.write(new_content) |
158 | + fd.write("\n") |
159 | |
160 | def handle(self, *args, **options): |
161 | """Entry point for BaseCommand.""" |
162 | # Read stuff in, validate. |
163 | config_path = options.get('config_path') |
164 | + dry_run = options.get('dry_run') |
165 | + force = options.get('force') |
166 | + stdout = options.get('stdout') |
167 | + if stdout is None: |
168 | + stdout = sys.stdout |
169 | + migrate_conflicting_options = options.get( |
170 | + 'migrate_conflicting_options') |
171 | + |
172 | options_file = self.read_file(config_path) |
173 | config_dict = self.parse_file(config_path, options_file) |
174 | + original_config = deepcopy(config_dict) |
175 | + |
176 | options_block = config_dict['options'] |
177 | |
178 | - # Modify the config. |
179 | + # Modify the configuration (if necessary). |
180 | self.set_up_include_statement(options_block, config_path) |
181 | - self.remove_forwarders(options_block) |
182 | + |
183 | + if migrate_conflicting_options: |
184 | + self.migrate_forwarders(options_block, dry_run, stdout) |
185 | + |
186 | + # Re-parse the new configuration, so we can detect any changes. |
187 | new_content = make_isc_string(config_dict) |
188 | + new_config = parse_isc_string(new_content) |
189 | |
190 | - # Back up and write new file. |
191 | - self.back_up_existing_file(config_path) |
192 | - with open(config_path, "wb") as fd: |
193 | - fd.write(new_content) |
194 | + if original_config != new_config or force: |
195 | + # The configuration has changed. Back up and write new file. |
196 | + if dry_run: |
197 | + self.write_new_named_conf_options( |
198 | + stdout, config_path, new_content) |
199 | + else: |
200 | + backup_filename = self.back_up_existing_file(config_path) |
201 | + with open(config_path, "wb") as fd: |
202 | + self.write_new_named_conf_options( |
203 | + fd, backup_filename, new_content) |
204 | |
205 | === modified file 'src/maasserver/tests/test_commands_edit_named_options.py' |
206 | --- src/maasserver/tests/test_commands_edit_named_options.py 2014-07-18 17:05:57 +0000 |
207 | +++ src/maasserver/tests/test_commands_edit_named_options.py 2015-07-08 00:32:06 +0000 |
208 | @@ -15,6 +15,7 @@ |
209 | __all__ = [] |
210 | |
211 | from codecs import getwriter |
212 | +from collections import OrderedDict |
213 | from io import BytesIO |
214 | import os |
215 | import shutil |
216 | @@ -22,11 +23,22 @@ |
217 | |
218 | from django.core.management import call_command |
219 | from django.core.management.base import CommandError |
220 | +from maasserver.management.commands.edit_named_options import ( |
221 | + Command as command_module, |
222 | + ) |
223 | +from maasserver.models import Config |
224 | from maasserver.testing.factory import factory |
225 | from maasserver.testing.testcase import MAASServerTestCase |
226 | +from maasserver.utils import get_one |
227 | +from maasserver.utils.isc import ( |
228 | + make_isc_string, |
229 | + parse_isc_string, |
230 | + read_isc_file, |
231 | + ) |
232 | from provisioningserver.dns.config import MAAS_NAMED_CONF_OPTIONS_INSIDE_NAME |
233 | from testtools.matchers import ( |
234 | Contains, |
235 | + Equals, |
236 | FileContains, |
237 | Not, |
238 | ) |
239 | @@ -43,7 +55,27 @@ |
240 | OPTIONS_FILE_WITH_FORWARDERS = textwrap.dedent("""\ |
241 | options { |
242 | directory "/var/cache/bind"; |
243 | - forwarders { 192.168.1.1; }; |
244 | + forwarders { 192.168.1.1; 192.168.1.2; }; |
245 | + auth-nxdomain no; # conform to RFC1035 |
246 | + listen-on-v6 { any; }; |
247 | + }; |
248 | +""") |
249 | + |
250 | +OPTIONS_FILE_WITH_FORWARDERS_AND_DNSSEC = textwrap.dedent("""\ |
251 | + options { |
252 | + directory "/var/cache/bind"; |
253 | + forwarders { 192.168.1.1; 192.168.1.2; }; |
254 | + dnssec-validation no; |
255 | + auth-nxdomain no; # conform to RFC1035 |
256 | + listen-on-v6 { any; }; |
257 | + }; |
258 | +""") |
259 | + |
260 | +OPTIONS_FILE_WITH_EXTRA_AND_DUP_FORWARDER = textwrap.dedent("""\ |
261 | + options { |
262 | + directory "/var/cache/bind"; |
263 | + forwarders { 192.168.1.2; 192.168.1.3; }; |
264 | + dnssec-validation no; |
265 | auth-nxdomain no; # conform to RFC1035 |
266 | listen-on-v6 { any; }; |
267 | }; |
268 | @@ -90,18 +122,27 @@ |
269 | self.assertContentFailsWithMessage( |
270 | OPTIONS_FILE, "Failed to make a backup") |
271 | |
272 | - def test_removes_existing_forwarders_config(self): |
273 | + def test_does_not_remove_existing_forwarders_config(self): |
274 | options_file = self.make_file(contents=OPTIONS_FILE_WITH_FORWARDERS) |
275 | call_command( |
276 | "edit_named_options", config_path=options_file, |
277 | stdout=self.stdout) |
278 | |
279 | + options = read_isc_file(options_file) |
280 | + self.assertThat(make_isc_string(options), Contains('forwarders')) |
281 | + |
282 | + def test_removes_existing_forwarders_config_if_migrate_set(self): |
283 | + options_file = self.make_file(contents=OPTIONS_FILE_WITH_FORWARDERS) |
284 | + call_command( |
285 | + "edit_named_options", config_path=options_file, |
286 | + migrate_conflicting_options=True, stdout=self.stdout) |
287 | + |
288 | # Check that the file was re-written without forwarders (since |
289 | # that's now in the included file). |
290 | + options = read_isc_file(options_file) |
291 | self.assertThat( |
292 | - options_file, |
293 | - Not(FileContains( |
294 | - matcher=Contains('forwarders')))) |
295 | + make_isc_string(options), |
296 | + Not(Contains('forwarders'))) |
297 | |
298 | def test_normal_operation(self): |
299 | options_file = self.make_file(contents=OPTIONS_FILE) |
300 | @@ -114,11 +155,10 @@ |
301 | MAAS_NAMED_CONF_OPTIONS_INSIDE_NAME) |
302 | |
303 | # Check that the file was re-written with the include statement. |
304 | + options = read_isc_file(options_file) |
305 | self.assertThat( |
306 | - options_file, |
307 | - FileContains( |
308 | - matcher=Contains( |
309 | - 'include "%s";' % expected_path))) |
310 | + make_isc_string(options), |
311 | + Contains('include "%s";' % expected_path)) |
312 | |
313 | # Check that the backup was made. |
314 | options_file_base = os.path.dirname(options_file) |
315 | @@ -128,3 +168,95 @@ |
316 | [backup_file] = files |
317 | backup_file = os.path.join(options_file_base, backup_file) |
318 | self.assertThat(backup_file, FileContains(OPTIONS_FILE)) |
319 | + |
320 | + def test_migrates_bind_config_to_database(self): |
321 | + options_file = self.make_file( |
322 | + contents=OPTIONS_FILE_WITH_FORWARDERS_AND_DNSSEC) |
323 | + call_command( |
324 | + "edit_named_options", config_path=options_file, |
325 | + migrate_conflicting_options=True, stdout=self.stdout) |
326 | + |
327 | + upstream_dns = get_one(Config.objects.filter(name="upstream_dns")) |
328 | + self.assertThat({'192.168.1.1', '192.168.1.2'}, |
329 | + Equals(set(upstream_dns.value.split()))) |
330 | + |
331 | + def test_migrate_combines_with_existing_forwarders(self): |
332 | + options_file = self.make_file( |
333 | + contents=OPTIONS_FILE_WITH_FORWARDERS_AND_DNSSEC) |
334 | + call_command( |
335 | + "edit_named_options", config_path=options_file, |
336 | + migrate_conflicting_options=True, stdout=self.stdout) |
337 | + |
338 | + upstream_dns = get_one(Config.objects.filter(name="upstream_dns")) |
339 | + self.assertThat(OrderedDict.fromkeys(['192.168.1.1', '192.168.1.2']), |
340 | + Equals(OrderedDict.fromkeys( |
341 | + upstream_dns.value.split()))) |
342 | + |
343 | + options_file = self.make_file( |
344 | + contents=OPTIONS_FILE_WITH_EXTRA_AND_DUP_FORWARDER) |
345 | + |
346 | + call_command( |
347 | + "edit_named_options", config_path=options_file, |
348 | + migrate_conflicting_options=True, stdout=self.stdout) |
349 | + |
350 | + upstream_dns = get_one(Config.objects.filter(name="upstream_dns")) |
351 | + self.assertThat( |
352 | + OrderedDict.fromkeys( |
353 | + ['192.168.1.1', '192.168.1.2', '192.168.1.3']), |
354 | + Equals(OrderedDict.fromkeys(upstream_dns.value.split()))) |
355 | + |
356 | + def test_dry_run_migrates_nothing_and_prints_config(self): |
357 | + options_file = self.make_file( |
358 | + contents=OPTIONS_FILE_WITH_FORWARDERS_AND_DNSSEC) |
359 | + call_command( |
360 | + "edit_named_options", config_path=options_file, |
361 | + migrate_conflicting_options=True, dry_run=True, stdout=self.stdout) |
362 | + |
363 | + upstream_dns = get_one(Config.objects.filter(name="upstream_dns")) |
364 | + self.assertIsNone(upstream_dns) |
365 | + |
366 | + # Check that a proper configuration was written to stdout. |
367 | + config = parse_isc_string(self.stdout.getvalue()) |
368 | + self.assertIsNotNone(config) |
369 | + |
370 | + def test_repeat_migrations_migrate_nothing(self): |
371 | + options_file = self.make_file( |
372 | + contents=OPTIONS_FILE_WITH_FORWARDERS_AND_DNSSEC) |
373 | + backup_mock = self.patch(command_module, "back_up_existing_file") |
374 | + |
375 | + call_command( |
376 | + "edit_named_options", config_path=options_file, |
377 | + migrate_conflicting_options=True, stdout=self.stdout) |
378 | + |
379 | + self.assertTrue(backup_mock.called) |
380 | + backup_mock.reset_mock() |
381 | + |
382 | + write_mock = self.patch(command_module, "write_new_named_conf_options") |
383 | + |
384 | + call_command( |
385 | + "edit_named_options", config_path=options_file, |
386 | + migrate_conflicting_options=True, stdout=self.stdout) |
387 | + |
388 | + self.assertFalse(backup_mock.called) |
389 | + self.assertFalse(write_mock.called) |
390 | + |
391 | + def test_repeat_forced_migrations_write_file_anyway(self): |
392 | + options_file = self.make_file( |
393 | + contents=OPTIONS_FILE_WITH_FORWARDERS_AND_DNSSEC) |
394 | + backup_mock = self.patch(command_module, "back_up_existing_file") |
395 | + |
396 | + call_command( |
397 | + "edit_named_options", config_path=options_file, |
398 | + migrate_conflicting_options=True, stdout=self.stdout) |
399 | + |
400 | + self.assertTrue(backup_mock.called) |
401 | + backup_mock.reset_mock() |
402 | + |
403 | + write_mock = self.patch(command_module, "write_new_named_conf_options") |
404 | + |
405 | + call_command( |
406 | + "edit_named_options", config_path=options_file, |
407 | + migrate_conflicting_options=True, force=True, stdout=self.stdout) |
408 | + |
409 | + self.assertTrue(backup_mock.called) |
410 | + self.assertTrue(write_mock.called) |
411 | |
412 | === modified file 'src/maasserver/utils/isc.py' |
413 | --- src/maasserver/utils/isc.py 2015-07-01 01:42:07 +0000 |
414 | +++ src/maasserver/utils/isc.py 2015-07-08 00:32:06 +0000 |
415 | @@ -33,18 +33,26 @@ |
416 | print_function, |
417 | unicode_literals, |
418 | ) |
419 | +from collections import OrderedDict |
420 | + |
421 | |
422 | str = None |
423 | |
424 | __metaclass__ = type |
425 | __all__ = [ |
426 | + 'ISCParseException', |
427 | 'make_isc_string', |
428 | 'parse_isc_string', |
429 | + 'read_isc_file', |
430 | ] |
431 | |
432 | import copy |
433 | |
434 | |
435 | +class ISCParseException(Exception): |
436 | + """Thrown when an ISC string cannot be parsed.""" |
437 | + |
438 | + |
439 | def _clip(char_list): |
440 | """Clips char_list to individual stanza. |
441 | |
442 | @@ -64,7 +72,7 @@ |
443 | return index, char_list[:index] |
444 | elif item == '}': |
445 | skip -= 1 |
446 | - raise Exception("Invalid brackets.") |
447 | + raise ISCParseException("Invalid brackets.") |
448 | |
449 | |
450 | def _parse_tokens(char_list): |
451 | @@ -86,11 +94,11 @@ |
452 | {'10.1.0/32': True, '10.1.1/32': True}}} |
453 | """ |
454 | index = 0 |
455 | - dictionary_fragment = {} |
456 | + dictionary_fragment = OrderedDict() |
457 | new_char_list = copy.deepcopy(char_list) |
458 | if type(new_char_list) == str: |
459 | return new_char_list |
460 | - if type(new_char_list) == dict: |
461 | + if type(new_char_list) == OrderedDict: |
462 | return new_char_list |
463 | last_open = None |
464 | continuous_line = False |
465 | @@ -146,6 +154,8 @@ |
466 | # fine) |
467 | elif new_char_list[index] not in ['{', ';', '}']: |
468 | key = new_char_list[index] |
469 | + if type(dictionary_fragment) == list: |
470 | + raise ISCParseException("Dictionary expected; got a list") |
471 | dictionary_fragment[key] = '' |
472 | index += 1 |
473 | index += 1 |
474 | @@ -277,7 +287,18 @@ |
475 | new_list[-1] = '%s%s' % (new_list[-1], terminator) |
476 | isc_list.append( |
477 | '%s { %s }%s' % (option, ' '.join(new_list), terminator)) |
478 | - elif type(isc_dict[option]) == dict: |
479 | + elif (type(isc_dict[option]) == OrderedDict or |
480 | + type(isc_dict[option]) == dict): |
481 | isc_list.append('%s { %s }%s' % ( |
482 | option, make_isc_string(isc_dict[option]), terminator)) |
483 | return '\n'.join(isc_list) |
484 | + |
485 | + |
486 | +def read_isc_file(isc_file): |
487 | + """Given the specified filename, parses it to create a dictionary. |
488 | + |
489 | + :param:isc_file: the filename to read |
490 | + :return:dict: dictionary of ISC file representation |
491 | + """ |
492 | + with open(isc_file, "r") as f: |
493 | + return parse_isc_string(f.read()) |
494 | |
495 | === modified file 'src/maasserver/utils/tests/test_isc.py' |
496 | --- src/maasserver/utils/tests/test_isc.py 2015-07-01 01:43:35 +0000 |
497 | +++ src/maasserver/utils/tests/test_isc.py 2015-07-08 00:32:06 +0000 |
498 | @@ -9,19 +9,23 @@ |
499 | unicode_literals, |
500 | ) |
501 | |
502 | + |
503 | str = None |
504 | |
505 | __metaclass__ = type |
506 | __all__ = [] |
507 | |
508 | - |
509 | +from collections import OrderedDict |
510 | from textwrap import dedent |
511 | |
512 | from maasserver.utils.isc import ( |
513 | + ISCParseException, |
514 | make_isc_string, |
515 | parse_isc_string, |
516 | + read_isc_file, |
517 | ) |
518 | from maastesting.testcase import MAASTestCase |
519 | +from testtools import ExpectedException |
520 | |
521 | |
522 | class TestParseISCString(MAASTestCase): |
523 | @@ -39,10 +43,11 @@ |
524 | """) |
525 | options = parse_isc_string(testdata) |
526 | self.assertEqual( |
527 | - {u'options': {u'auth-nxdomain': u'no', |
528 | - u'directory': u'"/var/cache/bind"', |
529 | - u'dnssec-validation': u'auto', |
530 | - u'listen-on-v6': {u'any': True}}}, options) |
531 | + OrderedDict({u'options': OrderedDict({u'auth-nxdomain': u'no', |
532 | + u'directory': u'"/var/cache/bind"', |
533 | + u'dnssec-validation': u'auto', |
534 | + u'listen-on-v6': OrderedDict({u'any': True})})}), |
535 | + options) |
536 | |
537 | def test_parses_bind_acl(self): |
538 | testdata = dedent("""\ |
539 | @@ -159,6 +164,94 @@ |
540 | config_string = make_isc_string(config) |
541 | config = parse_isc_string(config_string) |
542 | self.assertEqual( |
543 | + OrderedDict( |
544 | + [(u'acl canonical-int-ns', |
545 | + OrderedDict( |
546 | + [(u'91.189.90.151', True), (u'91.189.89.192', True)])), |
547 | + (u'options', OrderedDict( |
548 | + [(u'directory', u'"/var/cache/bind"'), |
549 | + (u'forwarders', OrderedDict( |
550 | + [(u'91.189.94.2', True)])), |
551 | + (u'dnssec-validation', u'auto'), |
552 | + (u'auth-nxdomain', u'no'), |
553 | + (u'listen-on-v6', OrderedDict([(u'any', True)])), |
554 | + (u'allow-query', OrderedDict([(u'any', True)])), |
555 | + (u'allow-transfer', OrderedDict( |
556 | + [(u'10.222.64.1', True), |
557 | + (u'canonical-int-ns', True)])), |
558 | + (u'notify', u'explicit'), |
559 | + (u'also-notify', OrderedDict( |
560 | + [(u'91.189.90.151', True), |
561 | + (u'91.189.89.192', True)])), |
562 | + (u'allow-query-cache', OrderedDict( |
563 | + [(u'10.222.64.0/18', True)])), |
564 | + (u'recursion', u'yes')])), |
565 | + (u'zone "."', OrderedDict( |
566 | + [(u'type', u'master'), |
567 | + (u'file', u'"/etc/bind/db.special"')]))]), |
568 | + config) |
569 | + |
570 | + def test_parser_preserves_order(self): |
571 | + testdata = dedent("""\ |
572 | + forwarders { |
573 | + 9.9.9.9; |
574 | + 8.8.8.8; |
575 | + 7.7.7.7; |
576 | + 6.6.6.6; |
577 | + 5.5.5.5; |
578 | + 4.4.4.4; |
579 | + 3.3.3.3; |
580 | + 2.2.2.2; |
581 | + 1.1.1.1; |
582 | + }; |
583 | + """) |
584 | + forwarders = parse_isc_string(testdata) |
585 | + self.assertEqual(OrderedDict([(u'forwarders', OrderedDict( |
586 | + [(u'9.9.9.9', True), (u'8.8.8.8', True), (u'7.7.7.7', True), |
587 | + (u'6.6.6.6', True), (u'5.5.5.5', True), (u'4.4.4.4', True), |
588 | + (u'3.3.3.3', True), (u'2.2.2.2', True), (u'1.1.1.1', True)]))]), |
589 | + forwarders) |
590 | + |
591 | + def test_parse_unmatched_brackets_throws_iscparseexception(self): |
592 | + with ExpectedException(ISCParseException): |
593 | + parse_isc_string("forwarders {") |
594 | + |
595 | + def test_parse_malformed_list_throws_iscparseexception(self): |
596 | + with ExpectedException(ISCParseException): |
597 | + parse_isc_string("forwarders {{}a;;b}") |
598 | + |
599 | + def test_read_isc_file(self): |
600 | + testdata = dedent("""\ |
601 | + acl canonical-int-ns { 91.189.90.151; 91.189.89.192; }; |
602 | + |
603 | + options { |
604 | + directory "/var/cache/bind"; |
605 | + |
606 | + forwarders { |
607 | + 91.189.94.2; |
608 | + 91.189.94.2; |
609 | + }; |
610 | + |
611 | + dnssec-validation auto; |
612 | + |
613 | + auth-nxdomain no; # conform to RFC1035 |
614 | + listen-on-v6 { any; }; |
615 | + |
616 | + allow-query { any; }; |
617 | + allow-transfer { 10.222.64.1; canonical-int-ns; }; |
618 | + |
619 | + notify explicit; |
620 | + also-notify { 91.189.90.151; 91.189.89.192; }; |
621 | + |
622 | + allow-query-cache { 10.222.64.0/18; }; |
623 | + recursion yes; |
624 | + }; |
625 | + |
626 | + zone "." { type master; file "/etc/bind/db.special"; }; |
627 | + """) |
628 | + testfile = self.make_file(contents=testdata) |
629 | + parsed = read_isc_file(testfile) |
630 | + self.assertEqual( |
631 | {u'acl canonical-int-ns': |
632 | {u'91.189.89.192': True, u'91.189.90.151': True}, |
633 | u'options': {u'allow-query': {u'any': True}, |
634 | @@ -176,4 +269,4 @@ |
635 | u'recursion': u'yes'}, |
636 | u'zone "."': |
637 | {u'file': u'"/etc/bind/db.special"', u'type': u'master'}}, |
638 | - config) |
639 | + parsed) |
lgtm!