Merge lp:~blake-rouse/curtin/net-interfaces-parser into lp:~curtin-dev/curtin/trunk
- net-interfaces-parser
- Merge into trunk
Proposed by
Blake Rouse
Status: | Merged |
---|---|
Merged at revision: | 139 |
Proposed branch: | lp:~blake-rouse/curtin/net-interfaces-parser |
Merge into: | lp:~curtin-dev/curtin/trunk |
Diff against target: |
385 lines (+358/-1) 2 files modified
curtin/net/__init__.py (+114/-1) tests/unittests/test_net.py (+244/-0) |
To merge this branch: | bzr merge lp:~blake-rouse/curtin/net-interfaces-parser |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
curtin developers | Pending | ||
Review via email: mp+223097@code.launchpad.net |
Commit message
Add /etc/network/
Description of the change
Provides a nice method to parse an /etc/network/
To post a comment you must log in.
- 136. By Blake Rouse
-
Support source-directory and source for /etc/network/
interfaces. - 137. By Blake Rouse
-
Add tests for net interface parser.
- 138. By Blake Rouse
-
Merge trunk.
Revision history for this message
Blake Rouse (blake-rouse) wrote : | # |
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'curtin/net/__init__.py' |
2 | --- curtin/net/__init__.py 2014-03-25 23:13:46 +0000 |
3 | +++ curtin/net/__init__.py 2014-06-24 17:55:55 +0000 |
4 | @@ -1,6 +1,7 @@ |
5 | -# Copyright (C) 2013 Canonical Ltd. |
6 | +# Copyright (C) 2013-2014 Canonical Ltd. |
7 | # |
8 | # Author: Scott Moser <scott.moser@canonical.com> |
9 | +# Author: Blake Rouse <blake.rouse@canonical.com> |
10 | # |
11 | # Curtin is free software: you can redistribute it and/or modify it under |
12 | # the terms of the GNU Affero General Public License as published by the |
13 | @@ -22,6 +23,22 @@ |
14 | |
15 | SYS_CLASS_NET = "/sys/class/net/" |
16 | |
17 | +NET_CONFIG_OPTIONS = [ |
18 | + "address", "netmask", "broadcast", "network", "metric", "gateway", |
19 | + "pointtopoint", "media", "mtu", "hostname", "leasehours", "leasetime", |
20 | + "vendor", "client", "bootfile", "server", "hwaddr", "provider", "frame", |
21 | + "netnum", "endpoint", "local", "ttl", |
22 | + ] |
23 | + |
24 | +NET_CONFIG_COMMANDS = [ |
25 | + "pre-up", "up", "post-up", "down", "pre-down", "post-down", |
26 | + ] |
27 | + |
28 | +NET_CONFIG_BRIDGE_OPTIONS = [ |
29 | + "bridge_ageing", "bridge_bridgeprio", "bridge_fd", "bridge_gcinit", |
30 | + "bridge_hello", "bridge_maxage", "bridge_maxwait", "bridge_stp", |
31 | + ] |
32 | + |
33 | |
34 | def sys_dev_path(devname, path=""): |
35 | return SYS_CLASS_NET + devname + "/" + path |
36 | @@ -95,4 +112,100 @@ |
37 | return os.listdir(SYS_CLASS_NET) |
38 | |
39 | |
40 | +class ParserError(Exception): |
41 | + """Raised when parser has issue parsing the interfaces file.""" |
42 | + |
43 | + |
44 | +def parse_deb_config_data(ifaces, contents, path): |
45 | + """Parses the file contents, placing result into ifaces. |
46 | + |
47 | + :param ifaces: interface dictionary |
48 | + :param contents: contents of interfaces file |
49 | + :param path: directory interfaces file was located |
50 | + """ |
51 | + currif = None |
52 | + src_dir = path |
53 | + for line in contents.splitlines(): |
54 | + line = line.strip() |
55 | + if line.startswith('#'): |
56 | + continue |
57 | + split = line.split(' ') |
58 | + option = split[0] |
59 | + if option == "source-directory": |
60 | + src_dir = os.path.join(path, split[1]) |
61 | + elif option == "source": |
62 | + src_path = os.path.join(src_dir, split[1]) |
63 | + with open(src_path, "r") as fp: |
64 | + src_data = fp.read().strip() |
65 | + parse_deb_config_data( |
66 | + ifaces, src_data, |
67 | + os.path.dirname(os.path.abspath(src_path))) |
68 | + elif option == "auto": |
69 | + for iface in split[1:]: |
70 | + if iface not in ifaces: |
71 | + ifaces[iface] = {} |
72 | + ifaces[iface]['auto'] = True |
73 | + elif option == "iface": |
74 | + iface, family, method = split[1:4] |
75 | + if iface not in ifaces: |
76 | + ifaces[iface] = {} |
77 | + elif 'family' in ifaces[iface]: |
78 | + raise ParserError("Cannot define %s interface again.") |
79 | + ifaces[iface]['family'] = family |
80 | + ifaces[iface]['method'] = method |
81 | + currif = iface |
82 | + elif option == "hwaddress": |
83 | + ifaces[currif]['hwaddress'] = split[1] |
84 | + elif option in NET_CONFIG_OPTIONS: |
85 | + ifaces[currif][option] = split[1] |
86 | + elif option in NET_CONFIG_COMMANDS: |
87 | + if option not in ifaces[currif]: |
88 | + ifaces[currif][option] = [] |
89 | + ifaces[currif][option].append(' '.join(split[1:])) |
90 | + elif option.startswith('dns-'): |
91 | + if 'dns' not in ifaces[currif]: |
92 | + ifaces[currif]['dns'] = {} |
93 | + if option == 'dns-search': |
94 | + ifaces[currif]['dns']['search'] = [] |
95 | + for domain in split[1:]: |
96 | + ifaces[currif]['dns']['search'].append(domain) |
97 | + elif option == 'dns-nameservers': |
98 | + ifaces[currif]['dns']['nameservers'] = [] |
99 | + for server in split[1:]: |
100 | + ifaces[currif]['dns']['nameservers'].append(server) |
101 | + elif option.startswith('bridge_'): |
102 | + if 'bridge' not in ifaces[currif]: |
103 | + ifaces[currif]['bridge'] = {} |
104 | + if option in NET_CONFIG_BRIDGE_OPTIONS: |
105 | + bridge_option = option.replace('bridge_', '') |
106 | + ifaces[currif]['bridge'][bridge_option] = split[1] |
107 | + elif option == "bridge_ports": |
108 | + ifaces[currif]['bridge']['ports'] = [] |
109 | + for iface in split[1:]: |
110 | + ifaces[currif]['bridge']['ports'].append(iface) |
111 | + elif option == "bridge_hw" and split[1].lower() == "mac": |
112 | + ifaces[currif]['bridge']['mac'] = split[2] |
113 | + elif option == "bridge_pathcost": |
114 | + if 'pathcost' not in ifaces[currif]['bridge']: |
115 | + ifaces[currif]['bridge']['pathcost'] = {} |
116 | + ifaces[currif]['bridge']['pathcost'][split[1]] = split[2] |
117 | + elif option == "bridge_portprio": |
118 | + if 'portprio' not in ifaces[currif]['bridge']: |
119 | + ifaces[currif]['bridge']['portprio'] = {} |
120 | + ifaces[currif]['bridge']['portprio'][split[1]] = split[2] |
121 | + for iface in ifaces.keys(): |
122 | + if 'auto' not in ifaces[iface]: |
123 | + ifaces[iface]['auto'] = False |
124 | + |
125 | + |
126 | +def parse_deb_config(path): |
127 | + """Parses a debian network configuration file.""" |
128 | + ifaces = {} |
129 | + with open(path, "r") as fp: |
130 | + contents = fp.read().strip() |
131 | + parse_deb_config_data( |
132 | + ifaces, contents, |
133 | + os.path.dirname(os.path.abspath(path))) |
134 | + return ifaces |
135 | + |
136 | # vi: ts=4 expandtab syntax=python |
137 | |
138 | === added file 'tests/unittests/test_net.py' |
139 | --- tests/unittests/test_net.py 1970-01-01 00:00:00 +0000 |
140 | +++ tests/unittests/test_net.py 2014-06-24 17:55:55 +0000 |
141 | @@ -0,0 +1,244 @@ |
142 | +from unittest import TestCase |
143 | +import os |
144 | +import shutil |
145 | +import tempfile |
146 | + |
147 | +from curtin import net |
148 | +from textwrap import dedent |
149 | + |
150 | + |
151 | +class TestNetParserData(TestCase): |
152 | + |
153 | + def test_parse_deb_config_data_ignores_comments(self): |
154 | + contents = dedent("""\ |
155 | + # ignore |
156 | + # iface eth0 inet static |
157 | + # address 192.168.1.1 |
158 | + """) |
159 | + ifaces = {} |
160 | + net.parse_deb_config_data(ifaces, contents, '') |
161 | + self.assertEqual({}, ifaces) |
162 | + |
163 | + def test_parse_deb_config_data_basic(self): |
164 | + contents = dedent("""\ |
165 | + iface eth0 inet static |
166 | + address 192.168.1.2 |
167 | + netmask 255.255.255.0 |
168 | + hwaddress aa:bb:cc:dd:ee:ff |
169 | + """) |
170 | + ifaces = {} |
171 | + net.parse_deb_config_data(ifaces, contents, '') |
172 | + self.assertEqual({ |
173 | + 'eth0': { |
174 | + 'auto': False, |
175 | + 'family': 'inet', |
176 | + 'method': 'static', |
177 | + 'address': '192.168.1.2', |
178 | + 'netmask': '255.255.255.0', |
179 | + 'hwaddress': 'aa:bb:cc:dd:ee:ff', |
180 | + }, |
181 | + }, ifaces) |
182 | + |
183 | + def test_parse_deb_config_data_auto(self): |
184 | + contents = dedent("""\ |
185 | + auto eth0 eth1 |
186 | + iface eth0 inet manual |
187 | + iface eth1 inet manual |
188 | + """) |
189 | + ifaces = {} |
190 | + net.parse_deb_config_data(ifaces, contents, '') |
191 | + self.assertEqual({ |
192 | + 'eth0': { |
193 | + 'auto': True, |
194 | + 'family': 'inet', |
195 | + 'method': 'manual', |
196 | + }, |
197 | + 'eth1': { |
198 | + 'auto': True, |
199 | + 'family': 'inet', |
200 | + 'method': 'manual', |
201 | + }, |
202 | + }, ifaces) |
203 | + |
204 | + def test_parse_deb_config_data_error_on_redefine(self): |
205 | + contents = dedent("""\ |
206 | + iface eth0 inet static |
207 | + address 192.168.1.2 |
208 | + iface eth0 inet static |
209 | + address 192.168.1.3 |
210 | + """) |
211 | + ifaces = {} |
212 | + self.assertRaises( |
213 | + net.ParserError, |
214 | + net.parse_deb_config_data, ifaces, contents, '') |
215 | + |
216 | + def test_parse_deb_config_data_commands(self): |
217 | + contents = dedent("""\ |
218 | + iface eth0 inet manual |
219 | + pre-up preup1 |
220 | + pre-up preup2 |
221 | + up up1 |
222 | + post-up postup1 |
223 | + pre-down predown1 |
224 | + down down1 |
225 | + down down2 |
226 | + post-down postdown1 |
227 | + """) |
228 | + ifaces = {} |
229 | + net.parse_deb_config_data(ifaces, contents, '') |
230 | + self.assertEqual({ |
231 | + 'eth0': { |
232 | + 'auto': False, |
233 | + 'family': 'inet', |
234 | + 'method': 'manual', |
235 | + 'pre-up': ['preup1', 'preup2'], |
236 | + 'up': ['up1'], |
237 | + 'post-up': ['postup1'], |
238 | + 'pre-down': ['predown1'], |
239 | + 'down': ['down1', 'down2'], |
240 | + 'post-down': ['postdown1'], |
241 | + }, |
242 | + }, ifaces) |
243 | + |
244 | + def test_parse_deb_config_data_dns(self): |
245 | + contents = dedent("""\ |
246 | + iface eth0 inet static |
247 | + dns-nameservers 192.168.1.1 192.168.1.2 |
248 | + dns-search curtin local |
249 | + """) |
250 | + ifaces = {} |
251 | + net.parse_deb_config_data(ifaces, contents, '') |
252 | + self.assertEqual({ |
253 | + 'eth0': { |
254 | + 'auto': False, |
255 | + 'family': 'inet', |
256 | + 'method': 'static', |
257 | + 'dns': { |
258 | + 'nameservers': ['192.168.1.1', '192.168.1.2'], |
259 | + 'search': ['curtin', 'local'], |
260 | + }, |
261 | + }, |
262 | + }, ifaces) |
263 | + |
264 | + def test_parse_deb_config_data_bridge(self): |
265 | + contents = dedent("""\ |
266 | + iface eth0 inet manual |
267 | + iface eth1 inet manual |
268 | + iface br0 inet static |
269 | + address 192.168.1.1 |
270 | + netmask 255.255.255.0 |
271 | + bridge_maxwait 30 |
272 | + bridge_ports eth0 eth1 |
273 | + bridge_pathcost eth0 1 |
274 | + bridge_pathcost eth1 2 |
275 | + bridge_portprio eth0 0 |
276 | + bridge_portprio eth1 1 |
277 | + """) |
278 | + ifaces = {} |
279 | + net.parse_deb_config_data(ifaces, contents, '') |
280 | + self.assertEqual({ |
281 | + 'eth0': { |
282 | + 'auto': False, |
283 | + 'family': 'inet', |
284 | + 'method': 'manual', |
285 | + }, |
286 | + 'eth1': { |
287 | + 'auto': False, |
288 | + 'family': 'inet', |
289 | + 'method': 'manual', |
290 | + }, |
291 | + 'br0': { |
292 | + 'auto': False, |
293 | + 'family': 'inet', |
294 | + 'method': 'static', |
295 | + 'address': '192.168.1.1', |
296 | + 'netmask': '255.255.255.0', |
297 | + 'bridge': { |
298 | + 'maxwait': '30', |
299 | + 'ports': ['eth0', 'eth1'], |
300 | + 'pathcost': { |
301 | + 'eth0': '1', |
302 | + 'eth1': '2', |
303 | + }, |
304 | + 'portprio': { |
305 | + 'eth0': '0', |
306 | + 'eth1': '1' |
307 | + }, |
308 | + }, |
309 | + }, |
310 | + }, ifaces) |
311 | + |
312 | + |
313 | +class TestNetParser(TestCase): |
314 | + |
315 | + def setUp(self): |
316 | + self.target = tempfile.mkdtemp() |
317 | + |
318 | + def tearDown(self): |
319 | + shutil.rmtree(self.target) |
320 | + |
321 | + def make_config(self, path=None, name=None, contents=None, |
322 | + parse=True): |
323 | + if path is None: |
324 | + path = self.target |
325 | + if name is None: |
326 | + name = 'interfaces' |
327 | + path = os.path.join(path, name) |
328 | + if contents is None: |
329 | + contents = dedent("""\ |
330 | + auto eth0 |
331 | + iface eth0 inet static |
332 | + address 192.168.1.2 |
333 | + netmask 255.255.255.0 |
334 | + hwaddress aa:bb:cc:dd:ee:ff |
335 | + """) |
336 | + with open(path, 'w') as stream: |
337 | + stream.write(contents) |
338 | + ifaces = None |
339 | + if parse: |
340 | + ifaces = {} |
341 | + net.parse_deb_config_data(ifaces, contents, '') |
342 | + return path, ifaces |
343 | + |
344 | + def test_parse_deb_config(self): |
345 | + path, data = self.make_config() |
346 | + expected = net.parse_deb_config(path) |
347 | + self.assertEqual(data, expected) |
348 | + |
349 | + def test_parse_deb_config_source(self): |
350 | + path, data = self.make_config(name='interfaces2') |
351 | + contents = dedent("""\ |
352 | + source interfaces2 |
353 | + iface eth1 inet manual |
354 | + """) |
355 | + i_path, _ = self.make_config( |
356 | + contents=contents, parse=False) |
357 | + data['eth1'] = { |
358 | + 'auto': False, |
359 | + 'family': 'inet', |
360 | + 'method': 'manual', |
361 | + } |
362 | + expected = net.parse_deb_config(i_path) |
363 | + self.assertEqual(data, expected) |
364 | + |
365 | + def test_parse_deb_config_source_dir(self): |
366 | + subdir = os.path.join(self.target, 'interfaces.d') |
367 | + os.mkdir(subdir) |
368 | + path, data = self.make_config( |
369 | + path=subdir, name='interfaces2') |
370 | + contents = dedent("""\ |
371 | + source-directory interfaces.d |
372 | + source interfaces2 |
373 | + iface eth1 inet manual |
374 | + """) |
375 | + i_path, _ = self.make_config( |
376 | + contents=contents, parse=False) |
377 | + data['eth1'] = { |
378 | + 'auto': False, |
379 | + 'family': 'inet', |
380 | + 'method': 'manual', |
381 | + } |
382 | + expected = net.parse_deb_config(i_path) |
383 | + self.assertEqual(data, expected) |
384 | + |
385 | +# vi: ts=4 expandtab syntax=python |
Added the ability to handle source and source-directory. Added test coverage.