Merge lp:~allenap/maas/rpc-get-preseed-data into lp:~maas-committers/maas/trunk

Proposed by Gavin Panella
Status: Merged
Approved by: Gavin Panella
Approved revision: no longer in the source branch.
Merged at revision: 2553
Proposed branch: lp:~allenap/maas/rpc-get-preseed-data
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 438 lines (+236/-22)
10 files modified
src/maasserver/compose_preseed.py (+11/-2)
src/maasserver/tests/test_compose_preseed.py (+5/-2)
src/provisioningserver/drivers/osystem/__init__.py (+19/-4)
src/provisioningserver/drivers/osystem/tests/test_windows.py (+13/-9)
src/provisioningserver/drivers/osystem/windows.py (+3/-3)
src/provisioningserver/rpc/cluster.py (+31/-1)
src/provisioningserver/rpc/clusterservice.py (+16/-0)
src/provisioningserver/rpc/osystems.py (+28/-1)
src/provisioningserver/rpc/tests/test_clusterservice.py (+70/-0)
src/provisioningserver/rpc/tests/test_osystems.py (+40/-0)
To merge this branch: bzr merge lp:~allenap/maas/rpc-get-preseed-data
Reviewer Review Type Date Requested Status
Blake Rouse (community) Approve
Review via email: mp+226746@code.launchpad.net

Commit message

New RPC call GetPreseedData

To post a comment you must log in.
Revision history for this message
Blake Rouse (blake-rouse) wrote :

Looks good. One comment inline.

You did not update the docstring for OperatingSystem for compose_preseed. Would be good to update it for Node and Token objects are now passed to that method.

review: Approve
Revision history for this message
Gavin Panella (allenap) wrote :

Thanks for the review! Good catches on the docstrings.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/maasserver/compose_preseed.py'
2--- src/maasserver/compose_preseed.py 2014-06-20 13:38:39 +0000
3+++ src/maasserver/compose_preseed.py 2014-07-14 21:39:26 +0000
4@@ -20,7 +20,11 @@
5
6 from maasserver.enum import PRESEED_TYPE
7 from maasserver.utils import absolute_reverse
8-from provisioningserver.drivers.osystem import OperatingSystemRegistry
9+from provisioningserver.drivers.osystem import (
10+ Node as OSystemNode,
11+ OperatingSystemRegistry,
12+ Token as OSystemToken,
13+ )
14 import yaml
15
16
17@@ -108,9 +112,14 @@
18 if preseed_type == PRESEED_TYPE.CURTIN:
19 metadata_url = absolute_reverse(
20 'curtin-metadata', base_url=base_url)
21+ node_for_osystem = OSystemNode(
22+ node.system_id, node.hostname)
23+ token_for_osystem = OSystemToken(
24+ token.consumer.key, token.key, token.secret)
25 try:
26 return osystem.compose_preseed(
27- preseed_type, node, token, metadata_url)
28+ preseed_type, node_for_osystem, token_for_osystem,
29+ metadata_url)
30 except NotImplementedError:
31 pass
32
33
34=== modified file 'src/maasserver/tests/test_compose_preseed.py'
35--- src/maasserver/tests/test_compose_preseed.py 2014-06-11 15:26:45 +0000
36+++ src/maasserver/tests/test_compose_preseed.py 2014-07-14 21:39:26 +0000
37@@ -126,10 +126,13 @@
38 mock_compose = self.patch(osystem, 'compose_preseed')
39 node = factory.make_node(
40 osystem=osystem.name, status=NODE_STATUS.READY)
41-
42 token = NodeKey.objects.get_token_for_node(node)
43 url = absolute_reverse('curtin-metadata')
44 compose_preseed(PRESEED_TYPE.CURTIN, node)
45 self.assertThat(
46 mock_compose,
47- MockCalledOnceWith(PRESEED_TYPE.CURTIN, node, token, url))
48+ MockCalledOnceWith(
49+ PRESEED_TYPE.CURTIN,
50+ (node.system_id, node.hostname),
51+ (token.consumer.key, token.key, token.secret),
52+ url))
53
54=== modified file 'src/provisioningserver/drivers/osystem/__init__.py'
55--- src/provisioningserver/drivers/osystem/__init__.py 2014-07-11 14:07:08 +0000
56+++ src/provisioningserver/drivers/osystem/__init__.py 2014-07-14 21:39:26 +0000
57@@ -13,8 +13,10 @@
58
59 __metaclass__ = type
60 __all__ = [
61+ "Node",
62 "OperatingSystem",
63 "OperatingSystemRegistry",
64+ "Token",
65 ]
66
67 from abc import (
68@@ -22,6 +24,7 @@
69 abstractmethod,
70 abstractproperty,
71 )
72+from collections import namedtuple
73
74 from provisioningserver.utils.registry import Registry
75
76@@ -36,6 +39,16 @@
77 XINSTALL = 'xinstall'
78
79
80+# A cluster-side representation of a Node, relevant to the osystem code,
81+# with only minimal fields.
82+Node = namedtuple("Node", ("system_id", "hostname"))
83+
84+
85+# A cluster-side representation of a Token, relevant to the osystem code,
86+# with only minimal fields.
87+Token = namedtuple("Token", ("consumer_key", "token_key", "token_secret"))
88+
89+
90 class OperatingSystem:
91 """Skeleton for an operating system."""
92
93@@ -139,10 +152,12 @@
94 """Composes the preseed for the given node.
95
96 :param preseed_type: Preseed type to compose.
97- :param node: Node preseed needs generating.
98- :param token: OAuth token for url.
99- :param metadata_url: Metdata url for node.
100- :returns: Preseed for node.
101+ :param node: Node preseed needs generating for.
102+ :type node: :py:class:`Node`
103+ :param token: OAuth token for URL.
104+ :type token: :py:class:`Token`
105+ :param metadata_url: Metdata URL for node.
106+ :returns: Preseed data for node.
107 :raise:
108 NotImplementedError: doesn't implement a custom preseed
109 """
110
111=== modified file 'src/provisioningserver/drivers/osystem/tests/test_windows.py'
112--- src/provisioningserver/drivers/osystem/tests/test_windows.py 2014-07-10 17:08:58 +0000
113+++ src/provisioningserver/drivers/osystem/tests/test_windows.py 2014-07-14 21:39:26 +0000
114@@ -18,7 +18,10 @@
115
116 from maastesting.factory import factory
117 from maastesting.testcase import MAASTestCase
118-from mock import MagicMock
119+from provisioningserver.drivers.osystem import (
120+ Node,
121+ Token,
122+ )
123 from provisioningserver.drivers.osystem.windows import (
124 BOOT_IMAGE_PURPOSE,
125 Config,
126@@ -135,9 +138,10 @@
127 machine = factory.make_name('hostname')
128 dns = factory.make_name('dns')
129 hostname = '%s.%s' % (machine, dns)
130- node = MagicMock()
131- node.hostname = hostname
132- return node
133+ return Node(
134+ system_id=factory.make_name("system_id"),
135+ hostname=hostname,
136+ )
137
138 def make_token(self, consumer_key=None, token_key=None, token_secret=None):
139 if consumer_key is None:
140@@ -146,11 +150,11 @@
141 token_key = factory.make_name('token_key')
142 if token_secret is None:
143 token_secret = factory.make_name('secret_key')
144- token = MagicMock()
145- token.consumer.key = consumer_key
146- token.key = token_key
147- token.secret = token_secret
148- return token
149+ return Token(
150+ consumer_key=consumer_key,
151+ token_key=token_key,
152+ token_secret=token_secret,
153+ )
154
155 def test_compose_pressed_not_implemented_for_curtin(self):
156 osystem = WindowsOS()
157
158=== modified file 'src/provisioningserver/drivers/osystem/windows.py'
159--- src/provisioningserver/drivers/osystem/windows.py 2014-07-10 17:08:58 +0000
160+++ src/provisioningserver/drivers/osystem/windows.py 2014-07-14 21:39:26 +0000
161@@ -98,9 +98,9 @@
162 credentials = {
163 'maas_metadata_url': metadata_url,
164 'maas_oauth_consumer_secret': '',
165- 'maas_oauth_consumer_key': token.consumer.key,
166- 'maas_oauth_token_key': token.key,
167- 'maas_oauth_token_secret': token.secret,
168+ 'maas_oauth_consumer_key': token.consumer_key,
169+ 'maas_oauth_token_key': token.token_key,
170+ 'maas_oauth_token_secret': token.token_secret,
171 'hostname': hostname,
172 }
173 return credentials
174
175=== modified file 'src/provisioningserver/rpc/cluster.py'
176--- src/provisioningserver/rpc/cluster.py 2014-07-09 21:45:26 +0000
177+++ src/provisioningserver/rpc/cluster.py 2014-07-14 21:39:26 +0000
178@@ -24,7 +24,10 @@
179 ]
180
181 from provisioningserver.rpc import exceptions
182-from provisioningserver.rpc.arguments import StructureAsJSON
183+from provisioningserver.rpc.arguments import (
184+ ParsedURL,
185+ StructureAsJSON,
186+ )
187 from provisioningserver.rpc.common import Identify
188 from twisted.protocols import amp
189
190@@ -119,3 +122,30 @@
191 exceptions.NoSuchOperatingSystem: (
192 b"NoSuchOperatingSystem"),
193 }
194+
195+
196+class GetPreseedData(amp.Command):
197+ """Get OS-specific preseed data.
198+
199+ :since: 1.7
200+ """
201+
202+ arguments = [
203+ (b"osystem", amp.Unicode()),
204+ (b"preseed_type", amp.Unicode()),
205+ (b"node_system_id", amp.Unicode()),
206+ (b"node_hostname", amp.Unicode()),
207+ (b"consumer_key", amp.Unicode()),
208+ (b"token_key", amp.Unicode()),
209+ (b"token_secret", amp.Unicode()),
210+ (b"metadata_url", ParsedURL()),
211+ ]
212+ response = [
213+ (b"data", StructureAsJSON()),
214+ ]
215+ errors = {
216+ exceptions.NoSuchOperatingSystem: (
217+ b"NoSuchOperatingSystem"),
218+ NotImplementedError: (
219+ b"NotImplementedError"),
220+ }
221
222=== modified file 'src/provisioningserver/rpc/clusterservice.py'
223--- src/provisioningserver/rpc/clusterservice.py 2014-07-09 20:59:05 +0000
224+++ src/provisioningserver/rpc/clusterservice.py 2014-07-14 21:39:26 +0000
225@@ -41,6 +41,7 @@
226 from provisioningserver.rpc.interfaces import IConnection
227 from provisioningserver.rpc.osystems import (
228 gen_operating_systems,
229+ get_preseed_data,
230 validate_license_key,
231 )
232 from twisted.application.internet import (
233@@ -130,6 +131,21 @@
234 """
235 return {"is_valid": validate_license_key(osystem, release, key)}
236
237+ @cluster.GetPreseedData.responder
238+ def get_preseed_data(
239+ self, osystem, preseed_type, node_system_id, node_hostname,
240+ consumer_key, token_key, token_secret, metadata_url):
241+ """get_preseed_data()
242+
243+ Implementation of
244+ :py:class:`~provisioningserver.rpc.cluster.GetPreseedData`.
245+ """
246+ return {
247+ "data": get_preseed_data(
248+ osystem, preseed_type, node_system_id, node_hostname,
249+ consumer_key, token_key, token_secret, metadata_url),
250+ }
251+
252 @amp.StartTLS.responder
253 def get_tls_parameters(self):
254 """get_tls_parameters()
255
256=== modified file 'src/provisioningserver/rpc/osystems.py'
257--- src/provisioningserver/rpc/osystems.py 2014-07-11 16:57:48 +0000
258+++ src/provisioningserver/rpc/osystems.py 2014-07-14 21:39:26 +0000
259@@ -17,7 +17,11 @@
260 "validate_license_key",
261 ]
262
263-from provisioningserver.drivers.osystem import OperatingSystemRegistry
264+from provisioningserver.drivers.osystem import (
265+ Node,
266+ OperatingSystemRegistry,
267+ Token,
268+ )
269 from provisioningserver.rpc import exceptions
270
271
272@@ -70,3 +74,26 @@
273 raise exceptions.NoSuchOperatingSystem(osystem)
274 else:
275 return osystem.validate_license_key(release, key)
276+
277+
278+def get_preseed_data(
279+ osystem, preseed_type, node_system_id, node_hostname,
280+ consumer_key, token_key, token_secret, metadata_url):
281+ """Composes preseed data for the given node.
282+
283+ :param preseed_type: The preseed type being composed.
284+ :param node: The node for which a preseed is being composed.
285+ :param token: OAuth token for the metadata URL.
286+ :param metadata_url: The metdata URL for the node.
287+ :returns: Preseed data for the given node.
288+ :raise NotImplementedError: when the specified operating system does
289+ not require custom preseed data.
290+ """
291+ try:
292+ osystem = OperatingSystemRegistry[osystem]
293+ except KeyError:
294+ raise exceptions.NoSuchOperatingSystem(osystem)
295+ else:
296+ return osystem.compose_preseed(
297+ preseed_type, Node(node_system_id, node_hostname),
298+ Token(consumer_key, token_key, token_secret), metadata_url)
299
300=== modified file 'src/provisioningserver/rpc/tests/test_clusterservice.py'
301--- src/provisioningserver/rpc/tests/test_clusterservice.py 2014-07-11 16:57:48 +0000
302+++ src/provisioningserver/rpc/tests/test_clusterservice.py 2014-07-14 21:39:26 +0000
303@@ -18,6 +18,7 @@
304 import json
305 import os.path
306 from random import randint
307+from urlparse import urlparse
308
309 from fixtures import EnvironmentVariable
310 from maastesting.factory import factory
311@@ -36,6 +37,10 @@
312 )
313 from provisioningserver.boot import tftppath
314 from provisioningserver.boot.tests.test_tftppath import make_osystem
315+from provisioningserver.drivers.osystem import (
316+ OperatingSystem,
317+ OperatingSystemRegistry,
318+ )
319 from provisioningserver.power_schema import JSON_POWER_TYPE_PARAMETERS
320 from provisioningserver.rpc import (
321 cluster,
322@@ -761,3 +766,68 @@
323 with ExpectedException(exceptions.NoSuchOperatingSystem):
324 yield call_responder(
325 Cluster(), cluster.ValidateLicenseKey, arguments)
326+
327+
328+class TestClusterProtocol_GetPreseedData(MAASTestCase):
329+
330+ run_tests_with = AsynchronousDeferredRunTest.make_factory(timeout=5)
331+
332+ def make_arguments(self):
333+ return {
334+ "osystem": factory.make_name("osystem"),
335+ "preseed_type": factory.make_name("preseed_type"),
336+ "node_system_id": factory.make_name("system_id"),
337+ "node_hostname": factory.make_name("hostname"),
338+ "consumer_key": factory.make_name("consumer_key"),
339+ "token_key": factory.make_name("token_key"),
340+ "token_secret": factory.make_name("token_secret"),
341+ "metadata_url": urlparse(
342+ "https://%s/path/to/metadata" % factory.make_hostname()),
343+ }
344+
345+ def test_is_registered(self):
346+ protocol = Cluster()
347+ responder = protocol.locateResponder(
348+ cluster.GetPreseedData.commandName)
349+ self.assertIsNot(responder, None)
350+
351+ @inlineCallbacks
352+ def test_calls_get_preseed_data(self):
353+ get_preseed_data = self.patch(clusterservice, "get_preseed_data")
354+ get_preseed_data.return_value = factory.make_name("data")
355+ arguments = self.make_arguments()
356+ observed = yield call_responder(
357+ Cluster(), cluster.GetPreseedData, arguments)
358+ expected = {"data": get_preseed_data.return_value}
359+ self.assertEqual(expected, observed)
360+ # The arguments are passed to the responder positionally.
361+ self.assertThat(get_preseed_data, MockCalledOnceWith(
362+ arguments["osystem"], arguments["preseed_type"],
363+ arguments["node_system_id"], arguments["node_hostname"],
364+ arguments["consumer_key"], arguments["token_key"],
365+ arguments["token_secret"], arguments["metadata_url"]))
366+
367+ @inlineCallbacks
368+ def test_exception_when_os_does_not_exist(self):
369+ # A remote NoSuchOperatingSystem exception is re-raised locally.
370+ get_preseed_data = self.patch(
371+ clusterservice, "get_preseed_data")
372+ get_preseed_data.side_effect = exceptions.NoSuchOperatingSystem()
373+ arguments = self.make_arguments()
374+ with ExpectedException(exceptions.NoSuchOperatingSystem):
375+ yield call_responder(
376+ Cluster(), cluster.GetPreseedData, arguments)
377+
378+ @inlineCallbacks
379+ def test_exception_when_preseed_not_implemented(self):
380+ # A remote NotImplementedError exception is re-raised locally.
381+ # Choose an operating system which has not overridden the
382+ # default compose_preseed.
383+ osystem_name = next(
384+ osystem_name for osystem_name, osystem in OperatingSystemRegistry
385+ if osystem.compose_preseed == OperatingSystem.compose_preseed)
386+ arguments = self.make_arguments()
387+ arguments["osystem"] = osystem_name
388+ with ExpectedException(exceptions.NoSuchOperatingSystem):
389+ yield call_responder(
390+ Cluster(), cluster.GetPreseedData, arguments)
391
392=== modified file 'src/provisioningserver/rpc/tests/test_osystems.py'
393--- src/provisioningserver/rpc/tests/test_osystems.py 2014-07-11 20:16:44 +0000
394+++ src/provisioningserver/rpc/tests/test_osystems.py 2014-07-14 21:39:26 +0000
395@@ -128,3 +128,43 @@
396 self.assertThat(
397 os_specific_validate_license_key,
398 MockCalledOnceWith(self.release, sentinel.key))
399+
400+
401+class TestGetPreseedDataErrors(MAASTestCase):
402+
403+ def test_throws_exception_when_os_does_not_exist(self):
404+ self.assertRaises(
405+ exceptions.NoSuchOperatingSystem,
406+ osystems.get_preseed_data, factory.make_name("no-such-os"),
407+ sentinel.preseed_type, sentinel.node_system_id,
408+ sentinel.node_hostname, sentinel.consumer_key,
409+ sentinel.token_key, sentinel.token_secret,
410+ sentinel.metadata_url)
411+
412+
413+class TestGetPreseedData(MAASTestCase):
414+
415+ # Check for every OS.
416+ scenarios = [
417+ (osystem.name, {"osystem": osystem})
418+ for _, osystem in OperatingSystemRegistry
419+ ]
420+
421+ def test_get_preseed_data_calls_compose_preseed(self):
422+ # get_preseed_data() calls compose_preseed() on the
423+ # OperatingSystem instances.
424+ os_specific_compose_preseed = self.patch(
425+ self.osystem, "compose_preseed")
426+ osystems.get_preseed_data(
427+ self.osystem.name, sentinel.preseed_type,
428+ sentinel.node_system_id, sentinel.node_hostname,
429+ sentinel.consumer_key, sentinel.token_key,
430+ sentinel.token_secret, sentinel.metadata_url)
431+ self.assertThat(
432+ os_specific_compose_preseed,
433+ MockCalledOnceWith(
434+ sentinel.preseed_type,
435+ (sentinel.node_system_id, sentinel.node_hostname),
436+ (sentinel.consumer_key, sentinel.token_key,
437+ sentinel.token_secret),
438+ sentinel.metadata_url))