Merge lp:~mpontillo/maas/migrate-dns-settings-1.7 into lp:maas/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
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)

To post a comment you must log in.
Revision history for this message
Andres Rodriguez (andreserl) wrote :

lgtm!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'docs/changelog.rst'
--- docs/changelog.rst 2015-05-26 16:36:50 +0000
+++ docs/changelog.rst 2015-07-08 00:32:06 +0000
@@ -2,6 +2,17 @@
2Changelog2Changelog
3=========3=========
44
51.7.6
6=====
7
8Bug Fix Update
9--------------
10
11#1470585 Accept list of forwarders for upstream_dns rather than just one.
12
13#1413388 Fix upgrade issue where it would remove custom DNS config,
14 potentially breaking DNS.
15
51.7.5161.7.5
6=====17=====
718
819
=== modified file 'src/maasserver/management/commands/edit_named_options.py'
--- src/maasserver/management/commands/edit_named_options.py 2015-07-01 01:38:49 +0000
+++ src/maasserver/management/commands/edit_named_options.py 2015-07-08 00:32:06 +0000
@@ -11,6 +11,8 @@
11 print_function,11 print_function,
12 unicode_literals,12 unicode_literals,
13 )13 )
14from collections import OrderedDict
15
1416
15str = None17str = None
1618
@@ -19,16 +21,20 @@
19 'Command',21 'Command',
20 ]22 ]
2123
24from copy import deepcopy
22from datetime import datetime25from datetime import datetime
23from optparse import make_option26from optparse import make_option
24import os27import os
25import shutil28import shutil
29import sys
2630
27from django.core.management.base import (31from django.core.management.base import (
28 BaseCommand,32 BaseCommand,
29 CommandError,33 CommandError,
30 )34 )
35from maasserver.models import Config
31from maasserver.utils.isc import (36from maasserver.utils.isc import (
37 ISCParseException,
32 make_isc_string,38 make_isc_string,
33 parse_isc_string,39 parse_isc_string,
34 )40 )
@@ -42,6 +48,24 @@
42 '--config-path', dest='config_path',48 '--config-path', dest='config_path',
43 default="/etc/bind/named.conf.options",49 default="/etc/bind/named.conf.options",
44 help="Specify the configuration file to edit."),50 help="Specify the configuration file to edit."),
51 make_option(
52 '--dry-run', dest='dry_run',
53 default=False, action='store_true',
54 help="Do not edit any configuration; instead, print to stdout the "
55 "actions that would be performed, and/or the new "
56 "configuration that would be written."),
57 make_option(
58 '--force', dest='force',
59 default=False, action='store_true',
60 help="Force the BIND configuration to be written, even if it "
61 "appears as though nothing has changed."),
62 make_option(
63 '--migrate-conflicting-options', default=False,
64 dest='migrate_conflicting_options', action='store_true',
65 help="Causes any options that conflict with MAAS-managed options "
66 "to be deleted from the BIND configuration and moved to the "
67 "MAAS-managed configuration. Requires the MAAS database to "
68 "be configured and running."),
45 )69 )
46 help = (70 help = (
47 "Edit the named.conf.options file so that it includes the "71 "Edit the named.conf.options file so that it includes the "
@@ -66,13 +90,12 @@
66 """90 """
67 try:91 try:
68 config_dict = parse_isc_string(options_file)92 config_dict = parse_isc_string(options_file)
69 except Exception as e:93 except ISCParseException as e:
70 # Yes, it throws bare exceptions :(
71 raise CommandError("Failed to parse %s: %s" % (94 raise CommandError("Failed to parse %s: %s" % (
72 config_path, e.message))95 config_path, e.message))
73 options_block = config_dict.get("options", None)96 options_block = config_dict.get("options", None)
74 if options_block is None:97 if options_block is None:
75 # Something is horribly wrong with the file, bail out rather98 # Something is horribly wrong with the file; bail out rather
76 # than doing anything drastic.99 # than doing anything drastic.
77 raise CommandError(100 raise CommandError(
78 "Can't find options {} block in %s, bailing out without "101 "Can't find options {} block in %s, bailing out without "
@@ -85,14 +108,37 @@
85 options_block['include'] = '"%s%s%s"' % (108 options_block['include'] = '"%s%s%s"' % (
86 dir, os.path.sep, MAAS_NAMED_CONF_OPTIONS_INSIDE_NAME)109 dir, os.path.sep, MAAS_NAMED_CONF_OPTIONS_INSIDE_NAME)
87110
88 def remove_forwarders(self, options_block):111 def migrate_forwarders(self, options_block, dry_run, stdout):
89 """Remove existing forwarders from the options block.112 """Remove existing forwarders from the options block.
90113
91 It's a syntax error to have more than one in the combined114 It's a syntax error to have more than one in the combined
92 configuration for named so we just remove whatever was there.115 configuration for named, so we just remove whatever was there.
93 There is no data loss due to the backup file made later.116
117 Migrate any forwarders in the configuration file to the MAAS config.
94 """118 """
95 if 'forwarders' in options_block:119 if 'forwarders' in options_block:
120 bind_forwarders = options_block['forwarders']
121
122 if not dry_run:
123 config, created = Config.objects.get_or_create(
124 name='upstream_dns',
125 defaults={'value': ' '.join(bind_forwarders)})
126 if not created:
127 # A configuration value already exists, so add the
128 # additional values we found in the configuration file to
129 # MAAS.
130 if config.value is None:
131 config.value = ''
132 maas_forwarders = OrderedDict.fromkeys(
133 config.value.split())
134 maas_forwarders.update(bind_forwarders)
135 config.value = ' '.join(maas_forwarders)
136 config.save()
137 else:
138 stdout.write(
139 "// Append to MAAS forwarders: %s\n"
140 % ' '.join(bind_forwarders))
141
96 del options_block['forwarders']142 del options_block['forwarders']
97143
98 def back_up_existing_file(self, config_path):144 def back_up_existing_file(self, config_path):
@@ -104,21 +150,59 @@
104 raise CommandError(150 raise CommandError(
105 "Failed to make a backup of %s, exiting: %s" % (151 "Failed to make a backup of %s, exiting: %s" % (
106 config_path, e.message))152 config_path, e.message))
153 return backup_destination
154
155 def write_new_named_conf_options(self, fd, backup_filename, new_content):
156 fd.write("""\
157//
158// This file is managed by MAAS. Although MAAS attempts to preserve changes
159// made here, it is possible to create conflicts that MAAS can not resolve.
160//
161// DNS settings available in MAAS (for example, forwarders and
162// dnssec-validation) should be managed only in MAAS.
163//
164// The previous configuration file was backed up at:
165// %s
166//
167""" % backup_filename)
168 fd.write(new_content)
169 fd.write("\n")
107170
108 def handle(self, *args, **options):171 def handle(self, *args, **options):
109 """Entry point for BaseCommand."""172 """Entry point for BaseCommand."""
110 # Read stuff in, validate.173 # Read stuff in, validate.
111 config_path = options.get('config_path')174 config_path = options.get('config_path')
175 dry_run = options.get('dry_run')
176 force = options.get('force')
177 stdout = options.get('stdout')
178 if stdout is None:
179 stdout = sys.stdout
180 migrate_conflicting_options = options.get(
181 'migrate_conflicting_options')
182
112 options_file = self.read_file(config_path)183 options_file = self.read_file(config_path)
113 config_dict = self.parse_file(config_path, options_file)184 config_dict = self.parse_file(config_path, options_file)
185 original_config = deepcopy(config_dict)
186
114 options_block = config_dict['options']187 options_block = config_dict['options']
115188
116 # Modify the config.189 # Modify the configuration (if necessary).
117 self.set_up_include_statement(options_block, config_path)190 self.set_up_include_statement(options_block, config_path)
118 self.remove_forwarders(options_block)191
192 if migrate_conflicting_options:
193 self.migrate_forwarders(options_block, dry_run, stdout)
194
195 # Re-parse the new configuration, so we can detect any changes.
119 new_content = make_isc_string(config_dict)196 new_content = make_isc_string(config_dict)
197 new_config = parse_isc_string(new_content)
120198
121 # Back up and write new file.199 if original_config != new_config or force:
122 self.back_up_existing_file(config_path)200 # The configuration has changed. Back up and write new file.
123 with open(config_path, "wb") as fd:201 if dry_run:
124 fd.write(new_content)202 self.write_new_named_conf_options(
203 stdout, config_path, new_content)
204 else:
205 backup_filename = self.back_up_existing_file(config_path)
206 with open(config_path, "wb") as fd:
207 self.write_new_named_conf_options(
208 fd, backup_filename, new_content)
125209
=== modified file 'src/maasserver/tests/test_commands_edit_named_options.py'
--- src/maasserver/tests/test_commands_edit_named_options.py 2014-07-18 17:05:57 +0000
+++ src/maasserver/tests/test_commands_edit_named_options.py 2015-07-08 00:32:06 +0000
@@ -15,6 +15,7 @@
15__all__ = []15__all__ = []
1616
17from codecs import getwriter17from codecs import getwriter
18from collections import OrderedDict
18from io import BytesIO19from io import BytesIO
19import os20import os
20import shutil21import shutil
@@ -22,11 +23,22 @@
2223
23from django.core.management import call_command24from django.core.management import call_command
24from django.core.management.base import CommandError25from django.core.management.base import CommandError
26from maasserver.management.commands.edit_named_options import (
27 Command as command_module,
28 )
29from maasserver.models import Config
25from maasserver.testing.factory import factory30from maasserver.testing.factory import factory
26from maasserver.testing.testcase import MAASServerTestCase31from maasserver.testing.testcase import MAASServerTestCase
32from maasserver.utils import get_one
33from maasserver.utils.isc import (
34 make_isc_string,
35 parse_isc_string,
36 read_isc_file,
37 )
27from provisioningserver.dns.config import MAAS_NAMED_CONF_OPTIONS_INSIDE_NAME38from provisioningserver.dns.config import MAAS_NAMED_CONF_OPTIONS_INSIDE_NAME
28from testtools.matchers import (39from testtools.matchers import (
29 Contains,40 Contains,
41 Equals,
30 FileContains,42 FileContains,
31 Not,43 Not,
32 )44 )
@@ -43,7 +55,27 @@
43OPTIONS_FILE_WITH_FORWARDERS = textwrap.dedent("""\55OPTIONS_FILE_WITH_FORWARDERS = textwrap.dedent("""\
44 options {56 options {
45 directory "/var/cache/bind";57 directory "/var/cache/bind";
46 forwarders { 192.168.1.1; };58 forwarders { 192.168.1.1; 192.168.1.2; };
59 auth-nxdomain no; # conform to RFC1035
60 listen-on-v6 { any; };
61 };
62""")
63
64OPTIONS_FILE_WITH_FORWARDERS_AND_DNSSEC = textwrap.dedent("""\
65 options {
66 directory "/var/cache/bind";
67 forwarders { 192.168.1.1; 192.168.1.2; };
68 dnssec-validation no;
69 auth-nxdomain no; # conform to RFC1035
70 listen-on-v6 { any; };
71 };
72""")
73
74OPTIONS_FILE_WITH_EXTRA_AND_DUP_FORWARDER = textwrap.dedent("""\
75 options {
76 directory "/var/cache/bind";
77 forwarders { 192.168.1.2; 192.168.1.3; };
78 dnssec-validation no;
47 auth-nxdomain no; # conform to RFC103579 auth-nxdomain no; # conform to RFC1035
48 listen-on-v6 { any; };80 listen-on-v6 { any; };
49 };81 };
@@ -90,18 +122,27 @@
90 self.assertContentFailsWithMessage(122 self.assertContentFailsWithMessage(
91 OPTIONS_FILE, "Failed to make a backup")123 OPTIONS_FILE, "Failed to make a backup")
92124
93 def test_removes_existing_forwarders_config(self):125 def test_does_not_remove_existing_forwarders_config(self):
94 options_file = self.make_file(contents=OPTIONS_FILE_WITH_FORWARDERS)126 options_file = self.make_file(contents=OPTIONS_FILE_WITH_FORWARDERS)
95 call_command(127 call_command(
96 "edit_named_options", config_path=options_file,128 "edit_named_options", config_path=options_file,
97 stdout=self.stdout)129 stdout=self.stdout)
98130
131 options = read_isc_file(options_file)
132 self.assertThat(make_isc_string(options), Contains('forwarders'))
133
134 def test_removes_existing_forwarders_config_if_migrate_set(self):
135 options_file = self.make_file(contents=OPTIONS_FILE_WITH_FORWARDERS)
136 call_command(
137 "edit_named_options", config_path=options_file,
138 migrate_conflicting_options=True, stdout=self.stdout)
139
99 # Check that the file was re-written without forwarders (since140 # Check that the file was re-written without forwarders (since
100 # that's now in the included file).141 # that's now in the included file).
142 options = read_isc_file(options_file)
101 self.assertThat(143 self.assertThat(
102 options_file,144 make_isc_string(options),
103 Not(FileContains(145 Not(Contains('forwarders')))
104 matcher=Contains('forwarders'))))
105146
106 def test_normal_operation(self):147 def test_normal_operation(self):
107 options_file = self.make_file(contents=OPTIONS_FILE)148 options_file = self.make_file(contents=OPTIONS_FILE)
@@ -114,11 +155,10 @@
114 MAAS_NAMED_CONF_OPTIONS_INSIDE_NAME)155 MAAS_NAMED_CONF_OPTIONS_INSIDE_NAME)
115156
116 # Check that the file was re-written with the include statement.157 # Check that the file was re-written with the include statement.
158 options = read_isc_file(options_file)
117 self.assertThat(159 self.assertThat(
118 options_file,160 make_isc_string(options),
119 FileContains(161 Contains('include "%s";' % expected_path))
120 matcher=Contains(
121 'include "%s";' % expected_path)))
122162
123 # Check that the backup was made.163 # Check that the backup was made.
124 options_file_base = os.path.dirname(options_file)164 options_file_base = os.path.dirname(options_file)
@@ -128,3 +168,95 @@
128 [backup_file] = files168 [backup_file] = files
129 backup_file = os.path.join(options_file_base, backup_file)169 backup_file = os.path.join(options_file_base, backup_file)
130 self.assertThat(backup_file, FileContains(OPTIONS_FILE))170 self.assertThat(backup_file, FileContains(OPTIONS_FILE))
171
172 def test_migrates_bind_config_to_database(self):
173 options_file = self.make_file(
174 contents=OPTIONS_FILE_WITH_FORWARDERS_AND_DNSSEC)
175 call_command(
176 "edit_named_options", config_path=options_file,
177 migrate_conflicting_options=True, stdout=self.stdout)
178
179 upstream_dns = get_one(Config.objects.filter(name="upstream_dns"))
180 self.assertThat({'192.168.1.1', '192.168.1.2'},
181 Equals(set(upstream_dns.value.split())))
182
183 def test_migrate_combines_with_existing_forwarders(self):
184 options_file = self.make_file(
185 contents=OPTIONS_FILE_WITH_FORWARDERS_AND_DNSSEC)
186 call_command(
187 "edit_named_options", config_path=options_file,
188 migrate_conflicting_options=True, stdout=self.stdout)
189
190 upstream_dns = get_one(Config.objects.filter(name="upstream_dns"))
191 self.assertThat(OrderedDict.fromkeys(['192.168.1.1', '192.168.1.2']),
192 Equals(OrderedDict.fromkeys(
193 upstream_dns.value.split())))
194
195 options_file = self.make_file(
196 contents=OPTIONS_FILE_WITH_EXTRA_AND_DUP_FORWARDER)
197
198 call_command(
199 "edit_named_options", config_path=options_file,
200 migrate_conflicting_options=True, stdout=self.stdout)
201
202 upstream_dns = get_one(Config.objects.filter(name="upstream_dns"))
203 self.assertThat(
204 OrderedDict.fromkeys(
205 ['192.168.1.1', '192.168.1.2', '192.168.1.3']),
206 Equals(OrderedDict.fromkeys(upstream_dns.value.split())))
207
208 def test_dry_run_migrates_nothing_and_prints_config(self):
209 options_file = self.make_file(
210 contents=OPTIONS_FILE_WITH_FORWARDERS_AND_DNSSEC)
211 call_command(
212 "edit_named_options", config_path=options_file,
213 migrate_conflicting_options=True, dry_run=True, stdout=self.stdout)
214
215 upstream_dns = get_one(Config.objects.filter(name="upstream_dns"))
216 self.assertIsNone(upstream_dns)
217
218 # Check that a proper configuration was written to stdout.
219 config = parse_isc_string(self.stdout.getvalue())
220 self.assertIsNotNone(config)
221
222 def test_repeat_migrations_migrate_nothing(self):
223 options_file = self.make_file(
224 contents=OPTIONS_FILE_WITH_FORWARDERS_AND_DNSSEC)
225 backup_mock = self.patch(command_module, "back_up_existing_file")
226
227 call_command(
228 "edit_named_options", config_path=options_file,
229 migrate_conflicting_options=True, stdout=self.stdout)
230
231 self.assertTrue(backup_mock.called)
232 backup_mock.reset_mock()
233
234 write_mock = self.patch(command_module, "write_new_named_conf_options")
235
236 call_command(
237 "edit_named_options", config_path=options_file,
238 migrate_conflicting_options=True, stdout=self.stdout)
239
240 self.assertFalse(backup_mock.called)
241 self.assertFalse(write_mock.called)
242
243 def test_repeat_forced_migrations_write_file_anyway(self):
244 options_file = self.make_file(
245 contents=OPTIONS_FILE_WITH_FORWARDERS_AND_DNSSEC)
246 backup_mock = self.patch(command_module, "back_up_existing_file")
247
248 call_command(
249 "edit_named_options", config_path=options_file,
250 migrate_conflicting_options=True, stdout=self.stdout)
251
252 self.assertTrue(backup_mock.called)
253 backup_mock.reset_mock()
254
255 write_mock = self.patch(command_module, "write_new_named_conf_options")
256
257 call_command(
258 "edit_named_options", config_path=options_file,
259 migrate_conflicting_options=True, force=True, stdout=self.stdout)
260
261 self.assertTrue(backup_mock.called)
262 self.assertTrue(write_mock.called)
131263
=== modified file 'src/maasserver/utils/isc.py'
--- src/maasserver/utils/isc.py 2015-07-01 01:42:07 +0000
+++ src/maasserver/utils/isc.py 2015-07-08 00:32:06 +0000
@@ -33,18 +33,26 @@
33 print_function,33 print_function,
34 unicode_literals,34 unicode_literals,
35)35)
36from collections import OrderedDict
37
3638
37str = None39str = None
3840
39__metaclass__ = type41__metaclass__ = type
40__all__ = [42__all__ = [
43 'ISCParseException',
41 'make_isc_string',44 'make_isc_string',
42 'parse_isc_string',45 'parse_isc_string',
46 'read_isc_file',
43]47]
4448
45import copy49import copy
4650
4751
52class ISCParseException(Exception):
53 """Thrown when an ISC string cannot be parsed."""
54
55
48def _clip(char_list):56def _clip(char_list):
49 """Clips char_list to individual stanza.57 """Clips char_list to individual stanza.
5058
@@ -64,7 +72,7 @@
64 return index, char_list[:index]72 return index, char_list[:index]
65 elif item == '}':73 elif item == '}':
66 skip -= 174 skip -= 1
67 raise Exception("Invalid brackets.")75 raise ISCParseException("Invalid brackets.")
6876
6977
70def _parse_tokens(char_list):78def _parse_tokens(char_list):
@@ -86,11 +94,11 @@
86 {'10.1.0/32': True, '10.1.1/32': True}}}94 {'10.1.0/32': True, '10.1.1/32': True}}}
87 """95 """
88 index = 096 index = 0
89 dictionary_fragment = {}97 dictionary_fragment = OrderedDict()
90 new_char_list = copy.deepcopy(char_list)98 new_char_list = copy.deepcopy(char_list)
91 if type(new_char_list) == str:99 if type(new_char_list) == str:
92 return new_char_list100 return new_char_list
93 if type(new_char_list) == dict:101 if type(new_char_list) == OrderedDict:
94 return new_char_list102 return new_char_list
95 last_open = None103 last_open = None
96 continuous_line = False104 continuous_line = False
@@ -146,6 +154,8 @@
146 # fine)154 # fine)
147 elif new_char_list[index] not in ['{', ';', '}']:155 elif new_char_list[index] not in ['{', ';', '}']:
148 key = new_char_list[index]156 key = new_char_list[index]
157 if type(dictionary_fragment) == list:
158 raise ISCParseException("Dictionary expected; got a list")
149 dictionary_fragment[key] = ''159 dictionary_fragment[key] = ''
150 index += 1160 index += 1
151 index += 1161 index += 1
@@ -277,7 +287,18 @@
277 new_list[-1] = '%s%s' % (new_list[-1], terminator)287 new_list[-1] = '%s%s' % (new_list[-1], terminator)
278 isc_list.append(288 isc_list.append(
279 '%s { %s }%s' % (option, ' '.join(new_list), terminator))289 '%s { %s }%s' % (option, ' '.join(new_list), terminator))
280 elif type(isc_dict[option]) == dict:290 elif (type(isc_dict[option]) == OrderedDict or
291 type(isc_dict[option]) == dict):
281 isc_list.append('%s { %s }%s' % (292 isc_list.append('%s { %s }%s' % (
282 option, make_isc_string(isc_dict[option]), terminator))293 option, make_isc_string(isc_dict[option]), terminator))
283 return '\n'.join(isc_list)294 return '\n'.join(isc_list)
295
296
297def read_isc_file(isc_file):
298 """Given the specified filename, parses it to create a dictionary.
299
300 :param:isc_file: the filename to read
301 :return:dict: dictionary of ISC file representation
302 """
303 with open(isc_file, "r") as f:
304 return parse_isc_string(f.read())
284305
=== modified file 'src/maasserver/utils/tests/test_isc.py'
--- src/maasserver/utils/tests/test_isc.py 2015-07-01 01:43:35 +0000
+++ src/maasserver/utils/tests/test_isc.py 2015-07-08 00:32:06 +0000
@@ -9,19 +9,23 @@
9 unicode_literals,9 unicode_literals,
10)10)
1111
12
12str = None13str = None
1314
14__metaclass__ = type15__metaclass__ = type
15__all__ = []16__all__ = []
1617
1718from collections import OrderedDict
18from textwrap import dedent19from textwrap import dedent
1920
20from maasserver.utils.isc import (21from maasserver.utils.isc import (
22 ISCParseException,
21 make_isc_string,23 make_isc_string,
22 parse_isc_string,24 parse_isc_string,
25 read_isc_file,
23 )26 )
24from maastesting.testcase import MAASTestCase27from maastesting.testcase import MAASTestCase
28from testtools import ExpectedException
2529
2630
27class TestParseISCString(MAASTestCase):31class TestParseISCString(MAASTestCase):
@@ -39,10 +43,11 @@
39 """)43 """)
40 options = parse_isc_string(testdata)44 options = parse_isc_string(testdata)
41 self.assertEqual(45 self.assertEqual(
42 {u'options': {u'auth-nxdomain': u'no',46 OrderedDict({u'options': OrderedDict({u'auth-nxdomain': u'no',
43 u'directory': u'"/var/cache/bind"',47 u'directory': u'"/var/cache/bind"',
44 u'dnssec-validation': u'auto',48 u'dnssec-validation': u'auto',
45 u'listen-on-v6': {u'any': True}}}, options)49 u'listen-on-v6': OrderedDict({u'any': True})})}),
50 options)
4651
47 def test_parses_bind_acl(self):52 def test_parses_bind_acl(self):
48 testdata = dedent("""\53 testdata = dedent("""\
@@ -159,6 +164,94 @@
159 config_string = make_isc_string(config)164 config_string = make_isc_string(config)
160 config = parse_isc_string(config_string)165 config = parse_isc_string(config_string)
161 self.assertEqual(166 self.assertEqual(
167 OrderedDict(
168 [(u'acl canonical-int-ns',
169 OrderedDict(
170 [(u'91.189.90.151', True), (u'91.189.89.192', True)])),
171 (u'options', OrderedDict(
172 [(u'directory', u'"/var/cache/bind"'),
173 (u'forwarders', OrderedDict(
174 [(u'91.189.94.2', True)])),
175 (u'dnssec-validation', u'auto'),
176 (u'auth-nxdomain', u'no'),
177 (u'listen-on-v6', OrderedDict([(u'any', True)])),
178 (u'allow-query', OrderedDict([(u'any', True)])),
179 (u'allow-transfer', OrderedDict(
180 [(u'10.222.64.1', True),
181 (u'canonical-int-ns', True)])),
182 (u'notify', u'explicit'),
183 (u'also-notify', OrderedDict(
184 [(u'91.189.90.151', True),
185 (u'91.189.89.192', True)])),
186 (u'allow-query-cache', OrderedDict(
187 [(u'10.222.64.0/18', True)])),
188 (u'recursion', u'yes')])),
189 (u'zone "."', OrderedDict(
190 [(u'type', u'master'),
191 (u'file', u'"/etc/bind/db.special"')]))]),
192 config)
193
194 def test_parser_preserves_order(self):
195 testdata = dedent("""\
196 forwarders {
197 9.9.9.9;
198 8.8.8.8;
199 7.7.7.7;
200 6.6.6.6;
201 5.5.5.5;
202 4.4.4.4;
203 3.3.3.3;
204 2.2.2.2;
205 1.1.1.1;
206 };
207 """)
208 forwarders = parse_isc_string(testdata)
209 self.assertEqual(OrderedDict([(u'forwarders', OrderedDict(
210 [(u'9.9.9.9', True), (u'8.8.8.8', True), (u'7.7.7.7', True),
211 (u'6.6.6.6', True), (u'5.5.5.5', True), (u'4.4.4.4', True),
212 (u'3.3.3.3', True), (u'2.2.2.2', True), (u'1.1.1.1', True)]))]),
213 forwarders)
214
215 def test_parse_unmatched_brackets_throws_iscparseexception(self):
216 with ExpectedException(ISCParseException):
217 parse_isc_string("forwarders {")
218
219 def test_parse_malformed_list_throws_iscparseexception(self):
220 with ExpectedException(ISCParseException):
221 parse_isc_string("forwarders {{}a;;b}")
222
223 def test_read_isc_file(self):
224 testdata = dedent("""\
225 acl canonical-int-ns { 91.189.90.151; 91.189.89.192; };
226
227 options {
228 directory "/var/cache/bind";
229
230 forwarders {
231 91.189.94.2;
232 91.189.94.2;
233 };
234
235 dnssec-validation auto;
236
237 auth-nxdomain no; # conform to RFC1035
238 listen-on-v6 { any; };
239
240 allow-query { any; };
241 allow-transfer { 10.222.64.1; canonical-int-ns; };
242
243 notify explicit;
244 also-notify { 91.189.90.151; 91.189.89.192; };
245
246 allow-query-cache { 10.222.64.0/18; };
247 recursion yes;
248 };
249
250 zone "." { type master; file "/etc/bind/db.special"; };
251 """)
252 testfile = self.make_file(contents=testdata)
253 parsed = read_isc_file(testfile)
254 self.assertEqual(
162 {u'acl canonical-int-ns':255 {u'acl canonical-int-ns':
163 {u'91.189.89.192': True, u'91.189.90.151': True},256 {u'91.189.89.192': True, u'91.189.90.151': True},
164 u'options': {u'allow-query': {u'any': True},257 u'options': {u'allow-query': {u'any': True},
@@ -176,4 +269,4 @@
176 u'recursion': u'yes'},269 u'recursion': u'yes'},
177 u'zone "."':270 u'zone "."':
178 {u'file': u'"/etc/bind/db.special"', u'type': u'master'}},271 {u'file': u'"/etc/bind/db.special"', u'type': u'master'}},
179 config)272 parsed)

Subscribers

People subscribed via source and target branches

to all changes: