Merge lp:~blake-rouse/curtin/net-interfaces-parser into lp:~curtin-dev/curtin/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
Reviewer Review Type Date Requested Status
curtin developers Pending
Review via email: mp+223097@code.launchpad.net

Commit message

Add /etc/network/interfaces file parser utility.

Description of the change

Provides a nice method to parse an /etc/network/interfaces file. Since this is the file format that is passed to the hook commands, it makes it easy for any python hook command, to parse the file. This could easily be included in each curtin image, but adding it to curtin removes the need to have it per image.

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 :

Added the ability to handle source and source-directory. Added test coverage.

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

Subscribers

People subscribed via source and target branches