Merge lp:~jtv/maas/rpc-curtin-network-preseed 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: 3146
Proposed branch: lp:~jtv/maas/rpc-curtin-network-preseed
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 213 lines (+158/-0)
5 files modified
src/provisioningserver/rpc/cluster.py (+29/-0)
src/provisioningserver/rpc/clusterservice.py (+18/-0)
src/provisioningserver/rpc/osystems.py (+24/-0)
src/provisioningserver/rpc/tests/test_clusterservice.py (+49/-0)
src/provisioningserver/rpc/tests/test_osystems.py (+38/-0)
To merge this branch: bzr merge lp:~jtv/maas/rpc-curtin-network-preseed
Reviewer Review Type Date Requested Status
Andres Rodriguez (community) Approve
Review via email: mp+236645@code.launchpad.net

Commit message

Add (but do not call yet) an RPC call for composing a node's curtin network preseed.

Description of the change

This is tedious stuff. I've lost count of the number of pass-through functions I've had to define just to get my arguments to the right place. Some of my parameters did not fit the AMP mould, so I wrapped those into a JSON argument rather than come up with yet another data structure.

In my osystems test I still have a self.patch instead of self.patch_autospec. The reason is that self.patch_autospec broke for reasons that looked completely spurious. It told me that the call was receiving multiple values for the same keyword argument. How can that happen? I was passing **kwargs, where kwargs was a normal dict. But the errors were always for the arguments that were lists, never for the ones that were dicts. So I suspect the autospec does something clever along the lines of “if it's a list, expand it as multiple arguments.” The other test would have failed if this happened in real life, so I'm convinced this is a Mock bug.

Jeroen

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

lgtm! (but it is late and I might be missing any code correctness)

review: Approve
Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

Thanks for the review! I wasn't expecting that, but it reduces my dependency on the review backlog, and that is very nice.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/provisioningserver/rpc/cluster.py'
2--- src/provisioningserver/rpc/cluster.py 2014-09-27 04:40:39 +0000
3+++ src/provisioningserver/rpc/cluster.py 2014-10-01 01:47:10 +0000
4@@ -185,6 +185,35 @@
5 }
6
7
8+class ComposeCurtinNetworkPreseed(amp.Command):
9+ """Generate Curtin network preseed for a node.
10+
11+ :param osystem: Operating system identifier, e.g. `ubuntu`.
12+ :param config: A dict detailing the network configuration:
13+ `interfaces` maps to a list of pairs of interface name and MAC address.
14+ `ips_mapping` maps to a dict which maps MAC addresses to lists of
15+ IP addresses (at most one IPv4 and one IPv6 each) to be assigned to the
16+ corresponding network interfaces.
17+ `gateways_mapping` maps to a dict which maps MAC addresses to lists of
18+ gateway IP addresses (at most one IPv4 and one IPv6) to be used by the
19+ corresponding network interfaces.
20+ :param disable_ipv4: Should IPv4 networking be disabled on the node?
21+ :since: 1.7
22+ """
23+
24+ arguments = [
25+ (b"osystem", amp.Unicode()),
26+ (b"config", StructureAsJSON()),
27+ (b"disable_ipv4", amp.Boolean()),
28+ ]
29+ response = [
30+ (b"data", StructureAsJSON()),
31+ ]
32+ errors = {
33+ exceptions.NoSuchOperatingSystem: b"NoSuchOperatingSystem",
34+ }
35+
36+
37 class _Power(amp.Command):
38 """Base class for power control commands.
39
40
41=== modified file 'src/provisioningserver/rpc/clusterservice.py'
42--- src/provisioningserver/rpc/clusterservice.py 2014-09-30 10:23:13 +0000
43+++ src/provisioningserver/rpc/clusterservice.py 2014-10-01 01:47:10 +0000
44@@ -64,6 +64,7 @@
45 start_monitors,
46 )
47 from provisioningserver.rpc.osystems import (
48+ compose_curtin_network_preseed,
49 gen_operating_systems,
50 get_os_release_title,
51 get_preseed_data,
52@@ -200,6 +201,23 @@
53 consumer_key, token_key, token_secret, metadata_url),
54 }
55
56+ @cluster.ComposeCurtinNetworkPreseed.responder
57+ def compose_curtin_network_preseed(self, osystem, config, disable_ipv4):
58+ """compose_curtin_network_preseed()
59+
60+ Implementation of
61+ :py:class:`~provisioningserver.rpc.cluster.ComposeCurtinNetworkPreseed`
62+ """
63+ interfaces = config.get('interfaces', [])
64+ interfaces = [tuple(interface) for interface in interfaces]
65+ ips_mapping = config.get('ips_mapping', {})
66+ gateways_mapping = config.get('gateways_mapping', {})
67+ return {
68+ 'data': compose_curtin_network_preseed(
69+ osystem, interfaces, ips_mapping, gateways_mapping,
70+ disable_ipv4=disable_ipv4),
71+ }
72+
73 @log_call(level=logging.DEBUG)
74 @cluster.PowerOn.responder
75 def power_on(self, system_id, hostname, power_type, context):
76
77=== modified file 'src/provisioningserver/rpc/osystems.py'
78--- src/provisioningserver/rpc/osystems.py 2014-09-27 04:40:39 +0000
79+++ src/provisioningserver/rpc/osystems.py 2014-10-01 01:47:10 +0000
80@@ -115,3 +115,27 @@
81 preseed_type, Node(node_system_id, node_hostname),
82 Token(consumer_key, token_key, token_secret),
83 metadata_url.geturl())
84+
85+
86+def compose_curtin_network_preseed(os_name, interfaces, ips_mapping,
87+ gateways_mapping, disable_ipv4):
88+ """Compose Curtin network preseed for a node.
89+
90+ :param os_name: Identifying name of the operating system for which a
91+ preseed should be generated.
92+ :param interfaces: A list of interface/MAC pairs for the node.
93+ :param ips_mapping: A dict mapping MAC addresses to containers of the
94+ corresponding network interfaces' IP addresses.
95+ :param gateways_mapping: A `defaultdict` mapping MAC addresses to
96+ containers of the corresponding network interfaces' default gateways.
97+ :param disable_ipv4: Should this node be installed without IPv4 networking?
98+ :return: Preseed data, as JSON.
99+ """
100+ try:
101+ osystem = OperatingSystemRegistry[os_name]
102+ except KeyError:
103+ raise exceptions.NoSuchOperatingSystem(os_name)
104+ else:
105+ return osystem.compose_curtin_network_preseed(
106+ interfaces, ips_mapping, gateways_mapping,
107+ disable_ipv4=disable_ipv4)
108
109=== modified file 'src/provisioningserver/rpc/tests/test_clusterservice.py'
110--- src/provisioningserver/rpc/tests/test_clusterservice.py 2014-09-30 10:23:13 +0000
111+++ src/provisioningserver/rpc/tests/test_clusterservice.py 2014-10-01 01:47:10 +0000
112@@ -1043,6 +1043,55 @@
113 Cluster(), cluster.GetPreseedData, arguments)
114
115
116+class TestClusterProtocol_ComposeCurtinNetworkPreseed(MAASTestCase):
117+
118+ run_tests_with = MAASTwistedRunTest.make_factory(timeout=5)
119+
120+ def make_args(self, osystem=None):
121+ if osystem is None:
122+ osystem = factory.make_name('os')
123+ mac = factory.make_mac_address()
124+ return {
125+ 'osystem': osystem,
126+ 'config': {
127+ 'interfaces': [(factory.make_name('eth'), mac)],
128+ 'ips_mapping': {mac: [factory.make_ipv4_address()]},
129+ 'gateways_mapping': {mac: [factory.make_ipv4_address()]},
130+ },
131+ 'disable_ipv4': factory.pick_bool(),
132+ }
133+
134+ def test__is_registered(self):
135+ protocol = Cluster()
136+ responder = protocol.locateResponder(
137+ cluster.ComposeCurtinNetworkPreseed.commandName)
138+ self.assertIsNotNone(responder)
139+
140+ @inlineCallbacks
141+ def test__calls_compose_curtin_network_preseed(self):
142+ preseed = [factory.make_name('preseed')]
143+ fake = self.patch_autospec(
144+ clusterservice, 'compose_curtin_network_preseed')
145+ fake.return_value = preseed
146+ args = self.make_args()
147+
148+ response = yield call_responder(
149+ Cluster(), cluster.ComposeCurtinNetworkPreseed, args)
150+
151+ self.expectThat(response, Equals({'data': preseed}))
152+ self.expectThat(
153+ fake,
154+ MockCalledOnceWith(
155+ args['osystem'], args['config'], args['disable_ipv4']))
156+
157+ @inlineCallbacks
158+ def test__fails_for_unknown_OS(self):
159+ args = self.make_args(osystem=factory.make_name('nonexistent-os'))
160+ with ExpectedException(exceptions.NoSuchOperatingSystem):
161+ yield call_responder(
162+ Cluster(), cluster.ComposeCurtinNetworkPreseed, args)
163+
164+
165 class TestClusterProtocol_PowerOn_PowerOff(MAASTestCase):
166
167 run_tests_with = MAASTwistedRunTest.make_factory(timeout=5)
168
169=== modified file 'src/provisioningserver/rpc/tests/test_osystems.py'
170--- src/provisioningserver/rpc/tests/test_osystems.py 2014-09-27 04:40:39 +0000
171+++ src/provisioningserver/rpc/tests/test_osystems.py 2014-10-01 01:47:10 +0000
172@@ -199,3 +199,41 @@
173 (sentinel.consumer_key, sentinel.token_key,
174 sentinel.token_secret),
175 metadata_url.geturl()))
176+
177+
178+class TestComposeCurtinNetworkPreseed(MAASTestCase):
179+
180+ def make_args(self):
181+ mac = factory.make_mac_address()
182+ return {
183+ 'interfaces': [(factory.make_name('eth'), mac)],
184+ 'ips_mapping': {
185+ mac: [
186+ factory.make_ipv4_address(),
187+ factory.make_ipv6_address(),
188+ ],
189+ },
190+ 'gateways_mapping': {
191+ mac: [
192+ factory.make_ipv4_address(),
193+ factory.make_ipv6_address(),
194+ ],
195+ },
196+ 'disable_ipv4': factory.pick_bool(),
197+ }
198+
199+ def test__forwards_to_OS_implementation(self):
200+ args = self.make_args()
201+ for os_name, osystem in OperatingSystemRegistry:
202+ fake = self.patch(osystem, 'compose_curtin_network_preseed')
203+ fake.return_value = factory.make_name('preseed-%s' % os_name)
204+ self.assertEqual(
205+ fake.return_value,
206+ osystems.compose_curtin_network_preseed(os_name, **args))
207+
208+ def test__works_with_real_implementation(self):
209+ ubuntu = OperatingSystemRegistry['ubuntu']
210+ args = self.make_args()
211+ self.assertEqual(
212+ ubuntu.compose_curtin_network_preseed(**args),
213+ osystems.compose_curtin_network_preseed('ubuntu', **args))