Merge lp:~mpontillo/maas/remove-iscpy-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: 3373
Proposed branch: lp:~mpontillo/maas/remove-iscpy-1.7
Merge into: lp:maas/1.7
Diff against target: 552 lines (+471/-11)
5 files modified
buildout.cfg (+0/-1)
src/maasserver/management/commands/edit_named_options.py (+9/-9)
src/maasserver/utils/isc.py (+283/-0)
src/maasserver/utils/tests/test_isc.py (+179/-0)
versions.cfg (+0/-1)
To merge this branch: bzr merge lp:~mpontillo/maas/remove-iscpy-1.7
Reviewer Review Type Date Requested Status
Andres Rodriguez (community) Needs Information
Raphaël Badin (community) Approve
Review via email: mp+263450@code.launchpad.net

Commit message

Remove dependency on iscpy. Add MAAS utility (based on iscpy) for parsing named.options. Add tests for iscpy-derived code.

Description of the change

This is a portion of the fix for https://bugs.launchpad.net/maas/+bug/1413388.

Truthfully, it turned out to not be necessary to replace the parser. But the ISC configuration parser we were using hasn't been maintained for years, and with a MAAS-owned parser complete with unit tests, we can be more confident in the code.

To post a comment you must log in.
Revision history for this message
Mike Pontillo (mpontillo) wrote :

Note: the ported iscpy code has not been functionally changed. The code and function names have been reformatted to meet PEP8 guidelines, and the code now passes flake8.

Only the portions of the code that MAAS was using were ported over.

Revision history for this message
Raphaël Badin (rvb) wrote :

Looks good.
- This will have to be forward ported to 1.8 and trunk. I'd advise *always* doing work on trunk and *then* backport it to older versions. The main reason for this is that you can do the QA on trunk first and then backport it to older versions and QA the change there.
- This should go hand in hand with the packaging branch: https://code.launchpad.net/~rvb/maas/packaging.utopic-1413388/+merge/263477

review: Approve
Revision history for this message
Andres Rodriguez (andreserl) wrote :

I'd normally go for enhancing the parser library rather than "copying" code. The only concerns I have though, are licensing.

review: Needs Information
Revision history for this message
Mike Pontillo (mpontillo) wrote :

Understood; I have the same reservations. Upstream has not touched this code since 2012, and I could find an active community (such as a GitHub page) to contribute unit tests, etc. In addition, since dealing with ISC-style DHCP and DNS configuration files are part of MAAS core functionality, I think this code should be brought fully under our control.

The license is BSD, so (as I understand it as a non-lawyer) there should be no issue with including it in our code.

For more information, see:

https://pypi.python.org/pypi/iscpy

Revision history for this message
Raphaël Badin (rvb) wrote :

I agree with Mike on the licensing issue: this is BSD so as long as we preserve the copyright statement it should be okay.

Revision history for this message
Raphaël Badin (rvb) wrote :

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'buildout.cfg'
--- buildout.cfg 2014-10-01 15:22:21 +0000
+++ buildout.cfg 2015-07-01 01:43:38 +0000
@@ -72,7 +72,6 @@
72 djorm-ext-pgarray72 djorm-ext-pgarray
73 docutils73 docutils
74 crochet74 crochet
75 iscpy
76entry-points =75entry-points =
77 maas-region-admin=django.core.management:execute_from_command_line76 maas-region-admin=django.core.management:execute_from_command_line
78initialization =77initialization =
7978
=== modified file 'src/maasserver/management/commands/edit_named_options.py'
--- src/maasserver/management/commands/edit_named_options.py 2014-03-19 02:13:47 +0000
+++ src/maasserver/management/commands/edit_named_options.py 2015-07-01 01:43:38 +0000
@@ -1,4 +1,4 @@
1# Copyright 2014 Canonical Ltd. This software is licensed under the1# Copyright 2014-2015 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Django command: Edit the named.conf.options file so that it includes4"""Django command: Edit the named.conf.options file so that it includes
@@ -28,9 +28,9 @@
28 BaseCommand,28 BaseCommand,
29 CommandError,29 CommandError,
30 )30 )
31from iscpy import (31from maasserver.utils.isc import (
32 MakeISC,32 make_isc_string,
33 ParseISCString,33 parse_isc_string,
34 )34 )
35from provisioningserver.dns.config import MAAS_NAMED_CONF_OPTIONS_INSIDE_NAME35from provisioningserver.dns.config import MAAS_NAMED_CONF_OPTIONS_INSIDE_NAME
3636
@@ -60,12 +60,12 @@
60 return options_file60 return options_file
6161
62 def parse_file(self, config_path, options_file):62 def parse_file(self, config_path, options_file):
63 """Read the named.conf.options file and parse it with iscpy.63 """Read the named.conf.options file and parse it.
6464
65 We also use iscpy to insert the include statement that we need.65 Then insert the include statement that we need.
66 """66 """
67 try:67 try:
68 config_dict = ParseISCString(options_file)68 config_dict = parse_isc_string(options_file)
69 except Exception as e:69 except Exception as e:
70 # Yes, it throws bare exceptions :(70 # Yes, it throws bare exceptions :(
71 raise CommandError("Failed to parse %s: %s" % (71 raise CommandError("Failed to parse %s: %s" % (
@@ -80,7 +80,7 @@
80 return config_dict80 return config_dict
8181
82 def set_up_include_statement(self, options_block, config_path):82 def set_up_include_statement(self, options_block, config_path):
83 """Insert the 'include' directive into the iscpy-parsed options."""83 """Insert the 'include' directive into the parsed options."""
84 dir = os.path.join(os.path.dirname(config_path), "maas")84 dir = os.path.join(os.path.dirname(config_path), "maas")
85 options_block['include'] = '"%s%s%s"' % (85 options_block['include'] = '"%s%s%s"' % (
86 dir, os.path.sep, MAAS_NAMED_CONF_OPTIONS_INSIDE_NAME)86 dir, os.path.sep, MAAS_NAMED_CONF_OPTIONS_INSIDE_NAME)
@@ -116,7 +116,7 @@
116 # Modify the config.116 # Modify the config.
117 self.set_up_include_statement(options_block, config_path)117 self.set_up_include_statement(options_block, config_path)
118 self.remove_forwarders(options_block)118 self.remove_forwarders(options_block)
119 new_content = MakeISC(config_dict)119 new_content = make_isc_string(config_dict)
120120
121 # Back up and write new file.121 # Back up and write new file.
122 self.back_up_existing_file(config_path)122 self.back_up_existing_file(config_path)
123123
=== added file 'src/maasserver/utils/isc.py'
--- src/maasserver/utils/isc.py 1970-01-01 00:00:00 +0000
+++ src/maasserver/utils/isc.py 2015-07-01 01:43:38 +0000
@@ -0,0 +1,283 @@
1# Copyright (c) 2009, Purdue University
2# Copyright (c) 2015, Canonical Ltd.
3# All rights reserved.
4#
5# Redistribution and use in source and binary forms, with or without
6# modification, are permitted provided that the following conditions are met:
7#
8# Redistributions of source code must retain the above copyright notice, this
9# list of conditions and the following disclaimer.
10#
11# Redistributions in binary form must reproduce the above copyright notice,
12# this list of conditions and the following disclaimer in the documentation
13# and/or other materials provided with the distribution.
14#
15# Neither the name of the Purdue University nor the names of its contributors
16# may be used to endorse or promote products derived from this software without
17# specific prior written permission.
18#
19# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
22# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
23# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
24# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
25# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
26# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
27# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
28# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29# POSSIBILITY OF SUCH DAMAGE.
30
31from __future__ import (
32 absolute_import,
33 print_function,
34 unicode_literals,
35)
36
37str = None
38
39__metaclass__ = type
40__all__ = [
41 'make_isc_string',
42 'parse_isc_string',
43]
44
45import copy
46
47
48def _clip(char_list):
49 """Clips char_list to individual stanza.
50
51 Inputs:
52 char_list: partial of char_list from _parse_tokens
53
54 Outputs:
55 tuple: (int: skip to char list index, list: shortened char_list)
56 """
57 assert char_list[0] == '{'
58 char_list.pop(0)
59 skip = 0
60 for index, item in enumerate(char_list):
61 if item == '{':
62 skip += 1
63 elif item == '}' and skip == 0:
64 return index, char_list[:index]
65 elif item == '}':
66 skip -= 1
67 raise Exception("Invalid brackets.")
68
69
70def _parse_tokens(char_list):
71 """Parses exploded isc named.conf portions.
72
73 Inputs:
74 char_list: List of isc file parts
75
76 Outputs:
77 dict: fragment or full isc file dict
78 Recursive dictionary of isc file, dict values can be of 3 types,
79 dict, string and bool. Boolean values are always true. Booleans are false
80 if key is absent. Booleans represent situations in isc files such as:
81 acl "registered" { 10.1.0/32; 10.1.1:/32;}}
82
83 Example:
84
85 {'stanza1 "new"': 'test_info', 'stanza1 "embedded"': {'acl "registered"':
86 {'10.1.0/32': True, '10.1.1/32': True}}}
87 """
88 index = 0
89 dictionary_fragment = {}
90 new_char_list = copy.deepcopy(char_list)
91 if type(new_char_list) == str:
92 return new_char_list
93 if type(new_char_list) == dict:
94 return new_char_list
95 last_open = None
96 continuous_line = False
97 temp_list = []
98
99 # Prevent "may be referenced before assignment" error
100 key = None
101
102 while index < len(new_char_list):
103 if new_char_list[index] == '{':
104 last_open = index
105 if new_char_list[index] == ';' and continuous_line:
106 dictionary_fragment = temp_list
107 temp_list = []
108 continuous_line = False
109 if new_char_list[index] == ';':
110 continuous_line = False
111 if (len(new_char_list) > index + 1 and
112 new_char_list[index] == '}' and
113 new_char_list[index + 1] != ';'):
114 skip, value = _clip(new_char_list[last_open:])
115 temp_list.append({key: copy.deepcopy(_parse_tokens(value))})
116 continuous_line = True
117 if len(new_char_list) > index + 1 and new_char_list[index + 1] == '{':
118 # assert key is not None
119 key = new_char_list.pop(index)
120 skip, dict_value = _clip(new_char_list[index:])
121 if continuous_line:
122 temp_list.append(
123 {key: copy.deepcopy(_parse_tokens(dict_value))})
124 else:
125 dictionary_fragment[key] = copy.deepcopy(
126 _parse_tokens(dict_value))
127 index += skip
128 else:
129 if len(new_char_list[
130 index].split()) == 1 and '{' not in new_char_list:
131 for item in new_char_list:
132 if item in [';']:
133 continue
134 dictionary_fragment[item] = True
135
136 # If there are more than 1 'keywords' at new_char_list[index]
137 # ex - "recursion no;"
138 elif len(new_char_list[index].split()) >= 2:
139 dictionary_fragment[
140 new_char_list[index].split()[0]] = (
141 ' '.join(new_char_list[index].split()[1:]))
142 index += 1
143
144 # If there is just 1 'keyword' at new_char_list[index]
145 # ex "recursion;" (not a valid option, but for example's sake it's
146 # fine)
147 elif new_char_list[index] not in ['{', ';', '}']:
148 key = new_char_list[index]
149 dictionary_fragment[key] = ''
150 index += 1
151 index += 1
152
153 return dictionary_fragment
154
155
156def _scrub_comments(isc_string):
157 """Clears comments from an isc file
158
159 Inputs:
160 isc_string: string of isc file
161 Outputs:
162 string: string of scrubbed isc file
163 """
164 isc_list = []
165 if isc_string is None:
166 return ''
167 expanded_comment = False
168 for line in isc_string.split('\n'):
169 no_comment_line = ""
170 # Vet out any inline comments
171 if '/*' in line.strip():
172 try:
173 striped_line = line.strip()
174 chars = enumerate(striped_line)
175 while True:
176 i, c = chars.next()
177 try:
178 if c == '/' and striped_line[i + 1] == '*':
179 expanded_comment = True
180 chars.next() # Skip '*'
181 continue
182 elif c == '*' and striped_line[i + 1] == '/':
183 expanded_comment = False
184 chars.next() # Skip '/'
185 continue
186 except IndexError:
187 continue # We are at the end of the line
188 if expanded_comment:
189 continue
190 else:
191 no_comment_line += c
192 except StopIteration:
193 if no_comment_line:
194 isc_list.append(no_comment_line)
195 continue
196
197 if expanded_comment:
198 if '*/' in line.strip():
199 expanded_comment = False
200 isc_list.append(line.split('*/')[-1])
201 continue
202 else:
203 continue
204 if line.strip().startswith(('#', '//')):
205 continue
206 else:
207 isc_list.append(line.split('#')[0].split('//')[0].strip())
208 return '\n'.join(isc_list)
209
210
211def _explode(isc_string):
212 """Explodes isc file into relevant tokens.
213
214 Inputs:
215 isc_string: String of isc file
216
217 Outputs:
218 list: list of isc file tokens delimited by brackets and semicolons
219 ['stanza1 "new"', '{', 'test_info', ';', '}']
220 """
221 str_array = []
222 temp_string = []
223 for char in isc_string:
224 if char in ['\n']:
225 continue
226 if char in ['{', '}', ';']:
227 if ''.join(temp_string).strip() == '':
228 str_array.append(char)
229 else:
230 str_array.append(''.join(temp_string).strip())
231 str_array.append(char)
232 temp_string = []
233 else:
234 temp_string.append(char)
235 return str_array
236
237
238def parse_isc_string(isc_string):
239 """Makes a dictionary from an ISC file string
240
241 Inputs:
242 isc_string: string of isc file
243
244 Outputs:
245 dict: dictionary of ISC file representation
246 """
247 return _parse_tokens(_explode(_scrub_comments(isc_string)))
248
249
250def make_isc_string(isc_dict, terminate=True):
251 """Outputs an isc formatted file string from a dict
252
253 Inputs:
254 isc_dict: a recursive dictionary to be turned into an isc file
255 (from ParseTokens)
256
257 Outputs:
258 str: string of isc file without indentation
259 """
260 if terminate:
261 terminator = ';'
262 else:
263 terminator = ''
264 if type(isc_dict) == str:
265 return isc_dict
266 isc_list = []
267 for option in isc_dict:
268 if type(isc_dict[option]) == bool:
269 isc_list.append('%s%s' % (option, terminator))
270 elif (type(isc_dict[option]) == str or
271 type(isc_dict[option]) == unicode):
272 isc_list.append('%s %s%s' % (option, isc_dict[option], terminator))
273 elif type(isc_dict[option]) == list:
274 new_list = []
275 for item in isc_dict[option]:
276 new_list.append(make_isc_string(item, terminate=False))
277 new_list[-1] = '%s%s' % (new_list[-1], terminator)
278 isc_list.append(
279 '%s { %s }%s' % (option, ' '.join(new_list), terminator))
280 elif type(isc_dict[option]) == dict:
281 isc_list.append('%s { %s }%s' % (
282 option, make_isc_string(isc_dict[option]), terminator))
283 return '\n'.join(isc_list)
0284
=== added file 'src/maasserver/utils/tests/test_isc.py'
--- src/maasserver/utils/tests/test_isc.py 1970-01-01 00:00:00 +0000
+++ src/maasserver/utils/tests/test_isc.py 2015-07-01 01:43:38 +0000
@@ -0,0 +1,179 @@
1# Copyright 2015 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Test ISC configuration file parser/generator."""
5
6from __future__ import (
7 absolute_import,
8 print_function,
9 unicode_literals,
10)
11
12str = None
13
14__metaclass__ = type
15__all__ = []
16
17
18from textwrap import dedent
19
20from maasserver.utils.isc import (
21 make_isc_string,
22 parse_isc_string,
23 )
24from maastesting.testcase import MAASTestCase
25
26
27class TestParseISCString(MAASTestCase):
28
29 def test_parses_simple_bind_options(self):
30 testdata = dedent("""\
31 options {
32 directory "/var/cache/bind";
33
34 dnssec-validation auto;
35
36 auth-nxdomain no; # conform to RFC1035
37 listen-on-v6 { any; };
38 };
39 """)
40 options = parse_isc_string(testdata)
41 self.assertEqual(
42 {u'options': {u'auth-nxdomain': u'no',
43 u'directory': u'"/var/cache/bind"',
44 u'dnssec-validation': u'auto',
45 u'listen-on-v6': {u'any': True}}}, options)
46
47 def test_parses_bind_acl(self):
48 testdata = dedent("""\
49 acl goodclients {
50 192.0.2.0/24;
51 localhost;
52 localnets;
53 };
54 """)
55 acl = parse_isc_string(testdata)
56 self.assertEqual(
57 {u'acl goodclients': {u'192.0.2.0/24': True,
58 u'localhost': True,
59 u'localnets': True}}, acl)
60
61 def test_parses_multiple_forwarders(self):
62 testdata = dedent("""\
63 forwarders {
64 91.189.94.2;
65 91.189.94.3;
66 91.189.94.4;
67 91.189.94.5;
68 91.189.94.6;
69 };
70 """)
71 forwarders = parse_isc_string(testdata)
72 self.assertEqual(
73 {u'forwarders': {u'91.189.94.2': True,
74 u'91.189.94.3': True,
75 u'91.189.94.4': True,
76 u'91.189.94.5': True,
77 u'91.189.94.6': True}}, forwarders)
78
79 def test_parses_bug_1413388_config(self):
80 testdata = dedent("""\
81 acl canonical-int-ns { 91.189.90.151; 91.189.89.192; };
82
83 options {
84 directory "/var/cache/bind";
85
86 forwarders {
87 91.189.94.2;
88 91.189.94.2;
89 };
90
91 dnssec-validation auto;
92
93 auth-nxdomain no; # conform to RFC1035
94 listen-on-v6 { any; };
95
96 allow-query { any; };
97 allow-transfer { 10.222.64.1; canonical-int-ns; };
98
99 notify explicit;
100 also-notify { 91.189.90.151; 91.189.89.192; };
101
102 allow-query-cache { 10.222.64.0/18; };
103 recursion yes;
104 };
105
106 zone "." { type master; file "/etc/bind/db.special"; };
107 """)
108 config = parse_isc_string(testdata)
109 self.assertEqual(
110 {u'acl canonical-int-ns':
111 {u'91.189.89.192': True, u'91.189.90.151': True},
112 u'options': {u'allow-query': {u'any': True},
113 u'allow-query-cache': {u'10.222.64.0/18': True},
114 u'allow-transfer': {u'10.222.64.1': True,
115 u'canonical-int-ns': True},
116 u'also-notify': {u'91.189.89.192': True,
117 u'91.189.90.151': True},
118 u'auth-nxdomain': u'no',
119 u'directory': u'"/var/cache/bind"',
120 u'dnssec-validation': u'auto',
121 u'forwarders': {u'91.189.94.2': True},
122 u'listen-on-v6': {u'any': True},
123 u'notify': u'explicit',
124 u'recursion': u'yes'},
125 u'zone "."':
126 {u'file': u'"/etc/bind/db.special"', u'type': u'master'}},
127 config)
128
129 def test_parse_then_make_then_parse_generates_identical_config(self):
130 testdata = dedent("""\
131 acl canonical-int-ns { 91.189.90.151; 91.189.89.192; };
132
133 options {
134 directory "/var/cache/bind";
135
136 forwarders {
137 91.189.94.2;
138 91.189.94.2;
139 };
140
141 dnssec-validation auto;
142
143 auth-nxdomain no; # conform to RFC1035
144 listen-on-v6 { any; };
145
146 allow-query { any; };
147 allow-transfer { 10.222.64.1; canonical-int-ns; };
148
149 notify explicit;
150 also-notify { 91.189.90.151; 91.189.89.192; };
151
152 allow-query-cache { 10.222.64.0/18; };
153 recursion yes;
154 };
155
156 zone "." { type master; file "/etc/bind/db.special"; };
157 """)
158 config = parse_isc_string(testdata)
159 config_string = make_isc_string(config)
160 config = parse_isc_string(config_string)
161 self.assertEqual(
162 {u'acl canonical-int-ns':
163 {u'91.189.89.192': True, u'91.189.90.151': True},
164 u'options': {u'allow-query': {u'any': True},
165 u'allow-query-cache': {u'10.222.64.0/18': True},
166 u'allow-transfer': {u'10.222.64.1': True,
167 u'canonical-int-ns': True},
168 u'also-notify': {u'91.189.89.192': True,
169 u'91.189.90.151': True},
170 u'auth-nxdomain': u'no',
171 u'directory': u'"/var/cache/bind"',
172 u'dnssec-validation': u'auto',
173 u'forwarders': {u'91.189.94.2': True},
174 u'listen-on-v6': {u'any': True},
175 u'notify': u'explicit',
176 u'recursion': u'yes'},
177 u'zone "."':
178 {u'file': u'"/etc/bind/db.special"', u'type': u'master'}},
179 config)
0180
=== modified file 'versions.cfg'
--- versions.cfg 2014-10-02 15:28:09 +0000
+++ versions.cfg 2015-07-01 01:43:38 +0000
@@ -38,7 +38,6 @@
38extras = 0.0.338extras = 0.0.3
39fixtures = 0.3.1439fixtures = 0.3.14
40httplib2 = 0.840httplib2 = 0.8
41iscpy = 1.05
42iso8601 = 0.1.441iso8601 = 0.1.4
43junitxml = 0.642junitxml = 0.6
44keyring = 0.7.143keyring = 0.7.1

Subscribers

People subscribed via source and target branches

to all changes: