Merge lp:~jtv/maas/write-full-networking-config into lp:~maas-committers/maas/trunk

Proposed by Jeroen T. Vermeulen
Status: Merged
Approved by: Jeroen T. Vermeulen
Approved revision: no longer in the source branch.
Merged at revision: 3088
Proposed branch: lp:~jtv/maas/write-full-networking-config
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 618 lines (+566/-1)
2 files modified
src/maasserver/networking_preseed.py (+130/-0)
src/maasserver/tests/test_networking_preseed.py (+436/-1)
To merge this branch: bzr merge lp:~jtv/maas/write-full-networking-config
Reviewer Review Type Date Requested Status
Gavin Panella (community) Approve
Review via email: mp+235935@code.launchpad.net

Commit message

Code (but no actual call yet) for generating a node's full /etc/network/interfaces file during installation.

This is needed so that we can not only configure static IPv6 addresses, but also optionally disable IPv4.

Description of the change

Before we replace the existing network configuration script, we'll want to produce a udev rules file as well. The rules file assigns fixed names to the network interfaces.

Jeroen

To post a comment you must log in.
Revision history for this message
Gavin Panella (allenap) wrote :

A few small comments, but none of them are blockers. Nice new stuff :)

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/maasserver/networking_preseed.py'
2--- src/maasserver/networking_preseed.py 2014-09-24 03:02:54 +0000
3+++ src/maasserver/networking_preseed.py 2014-09-25 12:39:04 +0000
4@@ -28,10 +28,13 @@
5 'generate_networking_config',
6 ]
7
8+from collections import defaultdict
9+
10 from lxml import etree
11 from maasserver.dns.zonegenerator import get_dns_server_address
12 from maasserver.exceptions import UnresolvableHost
13 from maasserver.models.nodeprobeddetails import get_probed_details
14+from maasserver.models.staticipaddress import StaticIPAddress
15 from netaddr import (
16 IPAddress,
17 IPNetwork,
18@@ -239,3 +242,130 @@
19 ],
20 },
21 }
22+
23+
24+def compose_debian_network_interfaces_ipv4_stanza(interface):
25+ """Return a Debian `/etc/network/interfaces` stanza for DHCPv4."""
26+ return "iface %s inet dhcp" % interface
27+
28+
29+def compose_debian_network_interfaces_ipv6_stanza(interface, ip, gateway=None):
30+ """Return a Debian `/etc/network/interfaces` stanza for IPv6.
31+
32+ The stanza will configure a static address.
33+ """
34+ lines = [
35+ 'iface %s inet6 static' % interface,
36+ '\tnetmask 64',
37+ '\taddress %s' % ip,
38+ ]
39+ if gateway is not None:
40+ lines.append('\tgateway %s' % gateway)
41+ return '\n'.join(lines)
42+
43+
44+def extract_mac_string(mac):
45+ """Return normalised MAC address string from `MACAddress` model object."""
46+ return normalise_mac(unicode(mac))
47+
48+
49+def add_ip_to_mapping(mapping, macaddress, ip):
50+ """Add IP address to a `defaultdict` keyed by MAC string.
51+
52+ :param mapping: A `defaultdict` (defaulting to the empty set) mapping
53+ normalised MAC address strings to sets of `IPAddress`.
54+ :param macaddress: A `MACAddress`.
55+ :param ip: An IP address string. If it is empty or `None`, it will not
56+ be added to the mapping.
57+ """
58+ if ip in (None, ''):
59+ return
60+ assert isinstance(ip, (unicode, bytes, IPAddress))
61+ mac = extract_mac_string(macaddress)
62+ ip = IPAddress(ip)
63+ mapping[mac].add(ip)
64+
65+
66+def extract_ip(mapping, mac, ip_version):
67+ """Extract IP address for `mac` and given IP version from `mapping`.
68+
69+ :param mapping: A mapping as returned by `map_static_ips` or
70+ `map_gateways`.
71+ :param mac: A normalised MAC address string.
72+ :param ip_version: Either 4 or 6 (for IPv4 or IPv6 respectively). Only a
73+ static address for this version will be returned.
74+ :return: A matching IP address, or `None`.
75+ """
76+ for ip in mapping[mac]:
77+ if ip.version == ip_version:
78+ return ip
79+ return None
80+
81+
82+def map_static_ips(node):
83+ """Return a `defaultdict` mapping node's MAC addresses to their static IPs.
84+
85+ :param node: A `Node`.
86+ :return: A `defaultdict` (defaulting to the empty set) mapping normalised
87+ MAC address strings to sets of `IPAddress`.
88+ """
89+ mapping = defaultdict(set)
90+ for sip in StaticIPAddress.objects.filter(macaddress__node=node):
91+ for mac in sip.macaddress_set.all():
92+ add_ip_to_mapping(mapping, mac, sip.ip)
93+ return mapping
94+
95+
96+def map_gateways(node):
97+ """Return a `defaultdict` mapping node's MAC addresses to their gateways.
98+
99+ :param node: A `Node`.
100+ :return: A `defaultdict` (defaulting to the empty set) mapping normalised
101+ MAC address strings to sets of `IPAddress`.
102+ """
103+ mapping = defaultdict(set)
104+ for mac in node.macaddress_set.all():
105+ for cluster_interface in mac.get_cluster_interfaces():
106+ if cluster_interface.manages_static_range():
107+ add_ip_to_mapping(mapping, mac, cluster_interface.router_ip)
108+ return mapping
109+
110+
111+def has_static_ipv6_address(mapping):
112+ """Does `mapping` contain an IPv6 address?
113+
114+ :param mapping: A mapping as returned by `map_static_ips`.
115+ :return bool:
116+ """
117+ for ips in mapping.values():
118+ for ip in ips:
119+ if ip.version == 6:
120+ return True
121+ return False
122+
123+
124+def compose_debian_network_interfaces_file(node):
125+ """Return contents for a node's `/etc/network/interfaces` file."""
126+ static_ips = map_static_ips(node)
127+ # Should we disable IPv4 on this node? For safety's sake, we won't do this
128+ # if the node has no static IPv6 addresses. Otherwise it might become
129+ # accidentally unaddressable: it may have IPv6 addresses, but apart from
130+ # being able to guess autoconfigured addresses, we won't know what they
131+ # are.
132+ disable_ipv4 = (node.disable_ipv4 and has_static_ipv6_address(static_ips))
133+ gateways = map_gateways(node)
134+ stanzas = [
135+ 'auto lo',
136+ ]
137+ for interface, mac in extract_network_interfaces(node):
138+ stanzas.append('auto %s' % interface)
139+ if not disable_ipv4:
140+ stanzas.append(
141+ compose_debian_network_interfaces_ipv4_stanza(interface))
142+ static_ipv6 = extract_ip(static_ips, mac, 6)
143+ if static_ipv6 is not None:
144+ gateway = extract_ip(gateways, mac, 6)
145+ stanzas.append(
146+ compose_debian_network_interfaces_ipv6_stanza(
147+ interface, static_ipv6, gateway))
148+ return '%s\n' % '\n\n'.join(stanzas)
149
150=== modified file 'src/maasserver/tests/test_networking_preseed.py'
151--- src/maasserver/tests/test_networking_preseed.py 2014-09-24 03:07:01 +0000
152+++ src/maasserver/tests/test_networking_preseed.py 2014-09-25 12:39:04 +0000
153@@ -15,25 +15,41 @@
154 __all__ = [
155 ]
156
157+from collections import defaultdict
158 from random import randint
159+from textwrap import dedent
160
161 from maasserver import networking_preseed
162 from maasserver.dns import zonegenerator
163-from maasserver.enum import NODEGROUPINTERFACE_MANAGEMENT
164+from maasserver.enum import (
165+ NODEGROUP_STATUS,
166+ NODEGROUPINTERFACE_MANAGEMENT,
167+ )
168 from maasserver.exceptions import UnresolvableHost
169 from maasserver.networking_preseed import (
170+ add_ip_to_mapping,
171+ compose_debian_network_interfaces_file,
172+ compose_debian_network_interfaces_ipv4_stanza,
173+ compose_debian_network_interfaces_ipv6_stanza,
174+ extract_mac_string,
175 extract_network_interfaces,
176 generate_dns_server_entry,
177 generate_ethernet_link_entry,
178 generate_network_entry,
179 generate_networking_config,
180 generate_route_entries,
181+ has_static_ipv6_address,
182 list_dns_servers,
183+ map_gateways,
184+ map_static_ips,
185 normalise_mac,
186 )
187+import maasserver.networking_preseed as networking_preseed_module
188 from maasserver.testing.factory import factory
189 from maasserver.testing.testcase import MAASServerTestCase
190 from maastesting.matchers import MockCalledOnceWith
191+from mock import ANY
192+from netaddr import IPAddress
193 from testtools.matchers import HasLength
194
195
196@@ -481,3 +497,422 @@
197 },
198 ],
199 config['network_info']['services'])
200+
201+
202+class TestComposeDebianNetworkInterfaceIPv4Stanza(MAASServerTestCase):
203+
204+ def test__produces_dhcp_stanza(self):
205+ interface = factory.make_name('eth')
206+ expected = "iface %s inet dhcp" % interface
207+ self.assertEqual(
208+ expected.strip(),
209+ compose_debian_network_interfaces_ipv4_stanza(interface).strip())
210+
211+
212+class TestComposeDebianNetworkInterfacesIPv6Stanza(MAASServerTestCase):
213+
214+ def test__produces_static_stanza(self):
215+ ip = factory.make_ipv6_address()
216+ interface = factory.make_name('eth')
217+ expected = dedent("""\
218+ iface %s inet6 static
219+ \tnetmask 64
220+ \taddress %s
221+ """) % (interface, ip)
222+ self.assertEqual(
223+ expected.strip(),
224+ compose_debian_network_interfaces_ipv6_stanza(
225+ interface, ip).strip())
226+
227+ def test__includes_gateway_if_given(self):
228+ ip = factory.make_ipv6_address()
229+ interface = factory.make_name('eth')
230+ gateway = factory.make_ipv6_address()
231+ expected = dedent("""\
232+ iface %s inet6 static
233+ \tnetmask 64
234+ \taddress %s
235+ \tgateway %s
236+ """) % (interface, ip, gateway)
237+ self.assertEqual(
238+ expected.strip(),
239+ compose_debian_network_interfaces_ipv6_stanza(
240+ interface, ip, gateway).strip())
241+
242+
243+class TestExtractMACString(MAASServerTestCase):
244+
245+ def test__returns_string(self):
246+ self.assertIsInstance(
247+ extract_mac_string(factory.make_MACAddress()),
248+ unicode)
249+
250+ def test__returns_MAC_address(self):
251+ mac = factory.make_mac_address()
252+ self.assertEqual(
253+ normalise_mac(mac),
254+ extract_mac_string(factory.make_MACAddress(address=mac)))
255+
256+ def test__works_even_if_mac_address_is_already_string(self):
257+ # The ORM normally presents MACAddress.mac_address as a MAC object.
258+ # But a string will work too.
259+ mac_string = factory.make_mac_address()
260+ mac = factory.make_MACAddress()
261+ mac.mac_address = mac_string
262+ self.assertIsInstance(mac.mac_address, unicode)
263+ self.assertEqual(normalise_mac(mac_string), extract_mac_string(mac))
264+
265+
266+class TestAddIPToMapping(MAASServerTestCase):
267+
268+ def make_mapping(self):
269+ return defaultdict(set)
270+
271+ def test__adds_to_empty_entry(self):
272+ mapping = self.make_mapping()
273+ mac = factory.make_MACAddress()
274+ ip = factory.make_ipv4_address()
275+ add_ip_to_mapping(mapping, mac, ip)
276+ self.assertEqual(
277+ {mac.mac_address: {IPAddress(ip)}},
278+ mapping)
279+
280+ def test__adds_to_nonempty_entry(self):
281+ mapping = self.make_mapping()
282+ mac = factory.make_MACAddress()
283+ ip1 = factory.make_ipv4_address()
284+ add_ip_to_mapping(mapping, mac, ip1)
285+ ip2 = factory.make_ipv4_address()
286+ add_ip_to_mapping(mapping, mac, ip2)
287+ self.assertEqual(
288+ {mac.mac_address: {IPAddress(ip1), IPAddress(ip2)}},
289+ mapping)
290+
291+ def test__does_not_add_None(self):
292+ mapping = self.make_mapping()
293+ mac = factory.make_MACAddress()
294+ add_ip_to_mapping(mapping, mac, None)
295+ self.assertEqual({}, mapping)
296+
297+ def test__does_not_add_empty_string(self):
298+ mapping = self.make_mapping()
299+ mac = factory.make_MACAddress()
300+ add_ip_to_mapping(mapping, mac, '')
301+ self.assertEqual({}, mapping)
302+
303+
304+class TestMapStaticIPs(MAASServerTestCase):
305+
306+ def test__returns_empty_if_none_found(self):
307+ self.assertEqual({}, map_static_ips(factory.make_Node()))
308+
309+ def test__finds_IPv4_address(self):
310+ node = factory.make_Node()
311+ mac = factory.make_MACAddress(node=node)
312+ ip = factory.make_ipv4_address()
313+ factory.make_StaticIPAddress(ip=ip, mac=mac)
314+ self.assertEqual(
315+ {mac.mac_address: {IPAddress(ip)}},
316+ map_static_ips(node))
317+
318+ def test__finds_IPv6_address(self):
319+ node = factory.make_Node()
320+ mac = factory.make_MACAddress(node=node)
321+ ip = factory.make_ipv6_address()
322+ factory.make_StaticIPAddress(ip=ip, mac=mac)
323+ self.assertEqual(
324+ {mac.mac_address: {IPAddress(ip)}},
325+ map_static_ips(node))
326+
327+ def test__finds_addresses_on_multiple_MACs(self):
328+ node = factory.make_Node()
329+ mac1 = factory.make_MACAddress(node=node)
330+ mac2 = factory.make_MACAddress(node=node)
331+ ip1 = factory.make_ipv4_address()
332+ factory.make_StaticIPAddress(ip=ip1, mac=mac1)
333+ ip2 = factory.make_ipv4_address()
334+ factory.make_StaticIPAddress(ip=ip2, mac=mac2)
335+ self.assertEqual(
336+ {
337+ mac1.mac_address: {IPAddress(ip1)},
338+ mac2.mac_address: {IPAddress(ip2)},
339+ },
340+ map_static_ips(node))
341+
342+ def test__finds_multiple_addresses_on_MAC(self):
343+ node = factory.make_Node()
344+ mac = factory.make_MACAddress(node=node)
345+ ipv4 = factory.make_ipv4_address()
346+ ipv6 = factory.make_ipv6_address()
347+ factory.make_StaticIPAddress(ip=ipv4, mac=mac)
348+ factory.make_StaticIPAddress(ip=ipv6, mac=mac)
349+ self.assertEqual(
350+ {mac.mac_address: {IPAddress(ipv4), IPAddress(ipv6)}},
351+ map_static_ips(node))
352+
353+
354+class TestMapGateways(MAASServerTestCase):
355+
356+ def test__returns_empty_if_none_found(self):
357+ self.assertEqual({}, map_gateways(factory.make_Node()))
358+
359+ def test__finds_IPv4_gateway(self):
360+ network = factory.make_ipv4_network(slash=24)
361+ gateway = factory.pick_ip_in_network(network)
362+ cluster = factory.make_NodeGroup(status=NODEGROUP_STATUS.ACCEPTED)
363+ cluster_interface = factory.make_NodeGroupInterface(
364+ cluster, network=network, router_ip=gateway,
365+ management=NODEGROUPINTERFACE_MANAGEMENT.DHCP)
366+ node = factory.make_Node(nodegroup=cluster)
367+ mac = factory.make_MACAddress(
368+ node=node, cluster_interface=cluster_interface)
369+
370+ self.assertEqual(
371+ {mac.mac_address: {IPAddress(gateway)}},
372+ map_gateways(node))
373+
374+ def test__finds_IPv6_gateway(self):
375+ network = factory.make_ipv6_network()
376+ gateway = factory.pick_ip_in_network(network)
377+ cluster = factory.make_NodeGroup(status=NODEGROUP_STATUS.ACCEPTED)
378+ net_interface = factory.make_name('eth')
379+ ipv4_interface = factory.make_NodeGroupInterface(
380+ cluster, interface=net_interface,
381+ management=NODEGROUPINTERFACE_MANAGEMENT.UNMANAGED)
382+ factory.make_NodeGroupInterface(
383+ cluster, network=network, router_ip=gateway,
384+ interface=net_interface,
385+ management=NODEGROUPINTERFACE_MANAGEMENT.DHCP)
386+ node = factory.make_Node(nodegroup=cluster)
387+ mac = factory.make_MACAddress(
388+ node=node, cluster_interface=ipv4_interface)
389+
390+ self.assertEqual(
391+ {mac.mac_address: {IPAddress(gateway)}},
392+ map_gateways(node))
393+
394+ def test__finds_gateways_on_multiple_MACs(self):
395+ cluster = factory.make_NodeGroup(status=NODEGROUP_STATUS.ACCEPTED)
396+ node = factory.make_Node(nodegroup=cluster)
397+ network1 = factory.make_ipv4_network(slash=24)
398+ gateway1 = factory.pick_ip_in_network(network1)
399+ cluster_interface1 = factory.make_NodeGroupInterface(
400+ cluster, network=network1, router_ip=gateway1,
401+ management=NODEGROUPINTERFACE_MANAGEMENT.DHCP)
402+ mac1 = factory.make_MACAddress(
403+ node=node, cluster_interface=cluster_interface1)
404+ network2 = factory.make_ipv4_network(slash=24)
405+ gateway2 = factory.pick_ip_in_network(network2)
406+ cluster_interface2 = factory.make_NodeGroupInterface(
407+ cluster, network=network2, router_ip=gateway2,
408+ management=NODEGROUPINTERFACE_MANAGEMENT.DHCP)
409+ mac2 = factory.make_MACAddress(
410+ node=node, cluster_interface=cluster_interface2)
411+
412+ self.assertEqual(
413+ {
414+ mac1.mac_address: {IPAddress(gateway1)},
415+ mac2.mac_address: {IPAddress(gateway2)},
416+ },
417+ map_gateways(node))
418+
419+ def test__finds_multiple_gateways_on_MAC(self):
420+ cluster = factory.make_NodeGroup(status=NODEGROUP_STATUS.ACCEPTED)
421+ net_interface = factory.make_name('eth')
422+ ipv4_network = factory.make_ipv4_network(slash=24)
423+ ipv4_gateway = factory.pick_ip_in_network(ipv4_network)
424+ ipv4_interface = factory.make_NodeGroupInterface(
425+ cluster, network=ipv4_network, router_ip=ipv4_gateway,
426+ interface=net_interface,
427+ management=NODEGROUPINTERFACE_MANAGEMENT.DHCP)
428+ ipv6_network = factory.make_ipv6_network()
429+ ipv6_gateway = factory.pick_ip_in_network(ipv6_network)
430+ factory.make_NodeGroupInterface(
431+ cluster, network=ipv6_network, router_ip=ipv6_gateway,
432+ interface=net_interface,
433+ management=NODEGROUPINTERFACE_MANAGEMENT.DHCP)
434+ node = factory.make_Node(nodegroup=cluster)
435+ mac = factory.make_MACAddress(
436+ node=node, cluster_interface=ipv4_interface)
437+
438+ self.assertEqual(
439+ {
440+ mac.mac_address: {
441+ IPAddress(ipv4_gateway),
442+ IPAddress(ipv6_gateway),
443+ },
444+ },
445+ map_gateways(node))
446+
447+
448+class TestHasStaticIPv6Address(MAASServerTestCase):
449+
450+ def make_mapping(self):
451+ return defaultdict(set)
452+
453+ def test__returns_False_for_empty_mapping(self):
454+ self.assertFalse(has_static_ipv6_address(self.make_mapping()))
455+
456+ def test__finds_IPv6_address(self):
457+ mapping = self.make_mapping()
458+ add_ip_to_mapping(
459+ mapping, factory.make_MACAddress(), factory.make_ipv6_address())
460+ self.assertTrue(has_static_ipv6_address(mapping))
461+
462+ def test__ignores_IPv4_address(self):
463+ mapping = self.make_mapping()
464+ add_ip_to_mapping(
465+ mapping, factory.make_MACAddress(), factory.make_ipv4_address())
466+ self.assertFalse(has_static_ipv6_address(mapping))
467+
468+ def test__finds_IPv6_address_among_IPv4_addresses(self):
469+ mapping = self.make_mapping()
470+ add_ip_to_mapping(
471+ mapping, factory.make_MACAddress(), factory.make_ipv4_address())
472+ mac = factory.make_MACAddress()
473+ add_ip_to_mapping(mapping, mac, factory.make_ipv4_address())
474+ add_ip_to_mapping(mapping, mac, factory.make_ipv6_address())
475+ add_ip_to_mapping(mapping, mac, factory.make_ipv4_address())
476+ add_ip_to_mapping(
477+ mapping, factory.make_MACAddress(), factory.make_ipv4_address())
478+ self.assertTrue(has_static_ipv6_address(mapping))
479+
480+
481+class TestComposeDebianNetworkInterfacesFile(MAASServerTestCase):
482+
483+ def patch_node_interfaces(self, interfaces):
484+ """Inject given network interfaces data into `node`.
485+
486+ The network interfaces data should be an iterabl of tuples, each of
487+ an interface name and a MAC address.
488+ """
489+ fake = self.patch_autospec(
490+ networking_preseed_module, 'extract_network_interfaces')
491+ fake.return_value = [
492+ (interface, normalise_mac(mac))
493+ for interface, mac in interfaces
494+ ]
495+
496+ def test__always_generates_lo(self):
497+ self.assertIn(
498+ 'auto lo',
499+ compose_debian_network_interfaces_file(factory.make_Node()))
500+
501+ def test__generates_DHCPv4_config_if_IPv4_not_disabled(self):
502+ interface = factory.make_name('eth')
503+ node = factory.make_node_with_mac_attached_to_nodegroupinterface(
504+ disable_ipv4=False)
505+ mac = node.get_primary_mac()
506+ ipv6_network = factory.make_ipv6_network()
507+ factory.make_NodeGroupInterface(
508+ node.nodegroup, network=ipv6_network,
509+ management=NODEGROUPINTERFACE_MANAGEMENT.DHCP)
510+ factory.make_StaticIPAddress(
511+ ip=factory.pick_ip_in_network(ipv6_network), mac=mac)
512+ self.patch_node_interfaces([(interface, mac.mac_address.get_raw())])
513+
514+ self.assertIn(
515+ "\niface %s inet dhcp\n" % interface,
516+ compose_debian_network_interfaces_file(node))
517+
518+ def test__generates_DHCPv4_config_if_no_IPv6_configured(self):
519+ interface = factory.make_name('eth')
520+ node = factory.make_node_with_mac_attached_to_nodegroupinterface(
521+ disable_ipv4=True)
522+ mac = node.get_primary_mac()
523+ self.patch_node_interfaces([(interface, mac.mac_address.get_raw())])
524+ self.assertIn(
525+ "\niface %s inet dhcp\n" % interface,
526+ compose_debian_network_interfaces_file(node))
527+
528+ def test__generates_no_DHCPv4_config_if_node_should_use_IPv6_only(self):
529+ interface = factory.make_name('eth')
530+ ipv6_network = factory.make_ipv6_network()
531+ node = factory.make_node_with_mac_attached_to_nodegroupinterface(
532+ disable_ipv4=True)
533+ mac = node.get_primary_mac()
534+ factory.make_NodeGroupInterface(
535+ node.nodegroup, network=ipv6_network,
536+ management=NODEGROUPINTERFACE_MANAGEMENT.DHCP)
537+ factory.make_StaticIPAddress(
538+ ip=factory.pick_ip_in_network(ipv6_network), mac=mac)
539+ self.patch_node_interfaces([(interface, mac.mac_address.get_raw())])
540+
541+ # The space is significant: this should not match the inet6 line!
542+ self.assertNotIn(
543+ " inet ",
544+ compose_debian_network_interfaces_file(node))
545+
546+ def test__generates_static_IPv6_config(self):
547+ interface = factory.make_name('eth')
548+ ipv6_network = factory.make_ipv6_network()
549+ node = factory.make_node_with_mac_attached_to_nodegroupinterface()
550+ mac = node.get_primary_mac()
551+ factory.make_NodeGroupInterface(
552+ node.nodegroup, network=ipv6_network,
553+ management=NODEGROUPINTERFACE_MANAGEMENT.DHCP)
554+ factory.make_StaticIPAddress(
555+ ip=factory.pick_ip_in_network(ipv6_network), mac=mac)
556+ self.patch_node_interfaces([(interface, mac.mac_address.get_raw())])
557+ self.assertIn(
558+ "\niface %s inet6 static" % interface,
559+ compose_debian_network_interfaces_file(node))
560+
561+ def test__passes_ip_and_gateway_when_creating_IPv6_stanza(self):
562+ interface = factory.make_name('eth')
563+ ipv6_network = factory.make_ipv6_network()
564+ node = factory.make_node_with_mac_attached_to_nodegroupinterface()
565+ [ipv4_interface] = node.nodegroup.nodegroupinterface_set.all()
566+ mac = node.get_primary_mac()
567+ gateway = factory.pick_ip_in_network(ipv6_network)
568+ factory.make_NodeGroupInterface(
569+ node.nodegroup, network=ipv6_network, router_ip=gateway,
570+ interface=ipv4_interface.interface,
571+ management=NODEGROUPINTERFACE_MANAGEMENT.DHCP)
572+ static_ipv6 = factory.pick_ip_in_network(ipv6_network)
573+ factory.make_StaticIPAddress(ip=static_ipv6, mac=mac)
574+ self.patch_node_interfaces([(interface, mac.mac_address.get_raw())])
575+ fake = self.patch_autospec(
576+ networking_preseed_module,
577+ 'compose_debian_network_interfaces_ipv6_stanza')
578+ fake.return_value = factory.make_name('stanza')
579+
580+ compose_debian_network_interfaces_file(node)
581+
582+ self.assertThat(
583+ fake,
584+ MockCalledOnceWith(
585+ interface, IPAddress(static_ipv6), IPAddress(gateway)))
586+
587+ def test__omits_gateway_if_not_set(self):
588+ interface = factory.make_name('eth')
589+ ipv6_network = factory.make_ipv6_network()
590+ node = factory.make_node_with_mac_attached_to_nodegroupinterface()
591+ [ipv4_interface] = node.nodegroup.nodegroupinterface_set.all()
592+ mac = node.get_primary_mac()
593+ factory.make_NodeGroupInterface(
594+ node.nodegroup, network=ipv6_network, router_ip='',
595+ interface=ipv4_interface.interface,
596+ management=NODEGROUPINTERFACE_MANAGEMENT.UNMANAGED)
597+ static_ipv6 = factory.pick_ip_in_network(ipv6_network)
598+ factory.make_StaticIPAddress(ip=static_ipv6, mac=mac)
599+ self.patch_node_interfaces([(interface, mac.mac_address.get_raw())])
600+ fake = self.patch_autospec(
601+ networking_preseed_module,
602+ 'compose_debian_network_interfaces_ipv6_stanza')
603+ fake.return_value = factory.make_name('stanza')
604+
605+ compose_debian_network_interfaces_file(node)
606+
607+ self.assertThat(
608+ fake,
609+ MockCalledOnceWith(interface, ANY, None))
610+
611+ def test__writes_auto_lines(self):
612+ interface = factory.make_name('eth')
613+ node = factory.make_node_with_mac_attached_to_nodegroupinterface()
614+ self.patch_node_interfaces(
615+ [(interface, node.get_primary_mac().mac_address.get_raw())])
616+ interfaces_file = compose_debian_network_interfaces_file(node)
617+ self.assertIn('auto %s' % interface, interfaces_file)
618+ self.assertEqual(1, interfaces_file.count('auto %s' % interface))