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

Proposed by Gavin Panella
Status: Rejected
Rejected by: Gavin Panella
Proposed branch: lp:~allenap/maas/rpc-get-secrets
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 293 lines (+122/-19)
5 files modified
src/maasserver/rpc/configuration.py (+17/-0)
src/maasserver/rpc/regionservice.py (+11/-0)
src/maasserver/rpc/tests/test_configuration.py (+25/-6)
src/maasserver/rpc/tests/test_regionservice.py (+52/-13)
src/provisioningserver/rpc/region.py (+17/-0)
To merge this branch: bzr merge lp:~allenap/maas/rpc-get-secrets
Reviewer Review Type Date Requested Status
Graham Binns (community) Approve
Review via email: mp+233823@code.launchpad.net

Commit message

GetSecrets RPC call to allow cluster access to the API.

Description of the change

This is based on Graham's GetSecrets work, which was vetoed by Julian. However, there is *too much* work to do to convert the code in provisioningserver.tags to using RPC right now. I am blocked on needing this, and removing Celery from the cluster is blocked on needing this. Please do not veto this branch in the same way that Graham's was.

To post a comment you must log in.
lp:~allenap/maas/rpc-get-secrets updated
2933. By Gavin Panella

Tests for get_api_credentials.

Revision history for this message
Julian Edwards (julian-edwards) wrote :

I'm not going to block it, but I'm not going to approve it either. Out of
interest, what constitutes too much work here?

The tags stuff is also blocked by the problem we talked about yesterday,
namely the GIL block. As I said in the other review I don't think the process
pipe will help as the reader blocks until it closes, so it needs twisted IO
instead (which is why ampoule was created I suspect).

I think that we need to think harder about the architecture of these jobs
rather than throwing RPC at everything. We might want to include API
parameters as part of the RPC call-out to do the job, for example, especially
in the case of the long running jobs where we don't want to wait around. We
can then reactor.spawnProcess() and leave it running and get notified when it
finishes.

Revision history for this message
Graham Binns (gmb) :
Revision history for this message
Graham Binns (gmb) :
review: Approve
Revision history for this message
Graham Binns (gmb) wrote :

(Not sure if Jool's comments still stand in the light of the pipe fork stuff not running in the reactor thread… I suspect they do to a point, though).

Revision history for this message
Julian Edwards (julian-edwards) wrote :

If the pipe stuff prevents the IO block, then we're golden. I still think it might be easier to send API creds to each job that needs them though.

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

> I'm not going to block it, but I'm not going to approve it either. Out
> of interest, what constitutes too much work here?

There are three functions in p.tags that correspond to API calls:

    def get_nodes_for_node_group(client, nodegroup_uuid):
        """Retrieve the UUIDs of nodes in a particular group.

    def get_details_for_nodes(client, nodegroup_uuid, system_ids):
        """Retrieve details for a set of nodes.

    def post_updated_nodes(client, tag_name, tag_definition, ...):
        """Update the nodes relevant for a particular tag.

The first and last of those could be rewritten as RPC calls as a
straight port. The middle one is the problem: it downloads many
megabytes (of XML) in a single call. Rewriting that to work over RPC
would mean inventing a mechanism for packing and unpacking the data over
the wire. That's square-nail-round-hammer territory, at least for now
when we'd have to rush a design and implementation.

This code works as it is right now. I can port the Celery tasks that
trigger it to RPC calls, but I think we should leave this code well
alone for now. It really isn't doing us any harm.

>
> The tags stuff is also blocked by the problem we talked about
> yesterday, namely the GIL block. As I said in the other review I don't
> think the process pipe will help as the reader blocks until it closes,
> so it needs twisted IO instead (which is why ampoule was created I
> suspect).

As discussed elsewhere, the process pipe (aka `pipefork` and
`objectfork`) needs to be run from a non-reactor thread. I'll add a
guard to prevent it from being used in the reactor thread.

>
> I think that we need to think harder about the architecture of these
> jobs rather than throwing RPC at everything. We might want to include
> API parameters as part of the RPC call-out to do the job, for example,
> especially in the case of the long running jobs where we don't want to
> wait around. We can then reactor.spawnProcess() and leave it running
> and get notified when it finishes.

I can live with passing the API parameters per-task rather than having a
GetSecrets call.

Revision history for this message
Julian Edwards (julian-edwards) wrote :

On Wednesday 10 Sep 2014 23:20:44 you wrote:
> The first and last of those could be rewritten as RPC calls as a
> straight port. The middle one is the problem: it downloads many
> megabytes (of XML) in a single call. Rewriting that to work over RPC
> would mean inventing a mechanism for packing and unpacking the data over
> the wire. That's square-nail-round-hammer territory, at least for now
> when we'd have to rush a design and implementation.
>
> This code works as it is right now. I can port the Celery tasks that
> trigger it to RPC calls, but I think we should leave this code well
> alone for now. It really isn't doing us any harm.

Fair enough, it sounds like we're better off with the API then.

> As discussed elsewhere, the process pipe (aka `pipefork` and
> `objectfork`) needs to be run from a non-reactor thread. I'll add a
> guard to prevent it from being used in the reactor thread.

Tiptop! Thanks.

> I can live with passing the API parameters per-task rather than having a
> GetSecrets call.

The more I think about it the more I like it. I indicates to the casual
viewer that this RPC will be using the API to return results, and that is a
special situation.

Cheers!

Unmerged revisions

2933. By Gavin Panella

Tests for get_api_credentials.

2932. By Gavin Panella

Rename an argument.

2931. By Gavin Panella

Move get_cluster_api_credentials() to the m.rpc.configuration module.

2930. By Gavin Panella

Merge lp:~gmb/maas/rpc-get-secrets, resolving several small conflicts.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/maasserver/rpc/configuration.py'
2--- src/maasserver/rpc/configuration.py 2014-09-04 13:19:09 +0000
3+++ src/maasserver/rpc/configuration.py 2014-09-08 22:06:15 +0000
4@@ -13,14 +13,19 @@
5
6 __metaclass__ = type
7 __all__ = [
8+ "get_api_credentials",
9 "get_archive_mirrors",
10 "get_proxies",
11 ]
12
13 from urlparse import urlparse
14
15+from apiclient.creds import convert_tuple_to_string
16 from maasserver.models.config import Config
17+from maasserver.models.nodegroup import NodeGroup
18+from maasserver.models.user import get_creds_tuple
19 from maasserver.utils.async import transactional
20+from provisioningserver.rpc import exceptions
21 from provisioningserver.utils.twisted import synchronous
22
23
24@@ -53,3 +58,15 @@
25 if http_proxy is not None:
26 http_proxy = urlparse(http_proxy)
27 return {"http": http_proxy, "https": http_proxy}
28+
29+
30+@synchronous
31+@transactional
32+def get_api_credentials(cluster_uuid):
33+ """Return the API creds for a cluster as a string."""
34+ try:
35+ the_cluster = NodeGroup.objects.get_by_natural_key(cluster_uuid)
36+ except NodeGroup.DoesNotExist:
37+ raise exceptions.NoSuchCluster.from_uuid(cluster_uuid)
38+ else:
39+ return convert_tuple_to_string(get_creds_tuple(the_cluster.api_token))
40
41=== modified file 'src/maasserver/rpc/regionservice.py'
42--- src/maasserver/rpc/regionservice.py 2014-09-05 08:10:06 +0000
43+++ src/maasserver/rpc/regionservice.py 2014-09-08 22:06:15 +0000
44@@ -268,6 +268,17 @@
45 d.addCallback(lambda node: {'system_id': node.system_id})
46 return d
47
48+ @region.GetSecrets.responder
49+ def get_secrets(self, cluster_uuid):
50+ """get_secrets()
51+
52+ Implementation of
53+ :py:class:`~provisioningserver.rpc.region.GetSecrets`.
54+ """
55+ d = deferToThread(configuration.get_api_credentials, cluster_uuid)
56+ d.addCallback(lambda creds: {b"credentials": creds})
57+ return d
58+
59
60 @implementer(IConnection)
61 class RegionServer(Region):
62
63=== modified file 'src/maasserver/rpc/tests/test_configuration.py'
64--- src/maasserver/rpc/tests/test_configuration.py 2014-09-04 13:19:09 +0000
65+++ src/maasserver/rpc/tests/test_configuration.py 2014-09-08 22:06:15 +0000
66@@ -16,16 +16,20 @@
67
68 from urlparse import urlparse
69
70+from apiclient.creds import convert_tuple_to_string
71 from maasserver.models.config import Config
72+from maasserver.models.user import get_creds_tuple
73 from maasserver.rpc.configuration import (
74+ get_api_credentials,
75 get_archive_mirrors,
76 get_proxies,
77 )
78-from maastesting.factory import factory
79-from maastesting.testcase import MAASTestCase
80-
81-
82-class TestGetArchiveMirrors(MAASTestCase):
83+from maasserver.testing.factory import factory
84+from maastesting.djangotestcase import TransactionTestCase
85+from provisioningserver.rpc import exceptions
86+
87+
88+class TestGetArchiveMirrors(TransactionTestCase):
89
90 def test_returns_populated_dict_when_main_and_port_is_set(self):
91 url = factory.make_parsed_url().geturl()
92@@ -36,7 +40,7 @@
93 get_archive_mirrors())
94
95
96-class TestGetProxies(MAASTestCase):
97+class TestGetProxies(TransactionTestCase):
98
99 def test_returns_populated_dict_when_http_proxy_is_not_set(self):
100 Config.objects.set_config("http_proxy", None)
101@@ -50,3 +54,18 @@
102 self.assertEqual(
103 {"http": urlparse(url), "https": urlparse(url)},
104 get_proxies())
105+
106+
107+class TestGetAPICredentials(TransactionTestCase):
108+
109+ def test__gets_credentials_for_cluster(self):
110+ nodegroup = factory.make_NodeGroup()
111+ token = nodegroup.api_token
112+ expected_credentials = convert_tuple_to_string(get_creds_tuple(token))
113+ observed_credentials = get_api_credentials(nodegroup.uuid)
114+ self.assertEqual(expected_credentials, observed_credentials)
115+
116+ def test__raises_exception_when_cluster_not_found(self):
117+ self.assertRaises(
118+ exceptions.NoSuchCluster,
119+ get_api_credentials, factory.make_UUID())
120
121=== modified file 'src/maasserver/rpc/tests/test_regionservice.py'
122--- src/maasserver/rpc/tests/test_regionservice.py 2014-09-05 16:38:32 +0000
123+++ src/maasserver/rpc/tests/test_regionservice.py 2014-09-08 22:06:15 +0000
124@@ -14,6 +14,7 @@
125 __metaclass__ = type
126 __all__ = []
127
128+
129 from collections import defaultdict
130 from contextlib import closing
131 from itertools import product
132@@ -23,6 +24,7 @@
133 import threading
134 from urlparse import urlparse
135
136+from apiclient.creds import convert_tuple_to_string
137 from crochet import wait_for_reactor
138 from django.db import connection
139 from django.db.utils import ProgrammingError
140@@ -39,6 +41,7 @@
141 from maasserver.models.event import Event
142 from maasserver.models.eventtype import EventType
143 from maasserver.models.node import Node
144+from maasserver.models.user import get_creds_tuple
145 from maasserver.rpc import regionservice
146 from maasserver.rpc.regionservice import (
147 Region,
148@@ -50,7 +53,6 @@
149 from maasserver.testing.architecture import make_usable_architecture
150 from maasserver.testing.factory import factory
151 from maasserver.testing.orm import reload_object
152-from maasserver.testing.testcase import MAASServerTestCase
153 from maasserver.utils.async import transactional
154 from maastesting.djangotestcase import TransactionTestCase
155 from maastesting.matchers import (
156@@ -84,6 +86,7 @@
157 GetBootSourcesV2,
158 GetClusterInterfaces,
159 GetProxies,
160+ GetSecrets,
161 Identify,
162 ListNodePowerParameters,
163 MarkNodeFailed,
164@@ -793,7 +796,7 @@
165 yield d.addErrback(check)
166
167
168-class TestRegionServer(MAASServerTestCase):
169+class TestRegionServer(TransactionTestCase):
170
171 def test_interfaces(self):
172 protocol = RegionServer()
173@@ -1574,7 +1577,7 @@
174 self.assertItemsEqual([], service._get_addresses())
175
176
177-class TestRegionProtocol_ReportForeignDHCPServer(MAASTestCase):
178+class TestRegionProtocol_ReportForeignDHCPServer(TransactionTestCase):
179
180 def test_create_node_is_registered(self):
181 protocol = Region()
182@@ -1629,7 +1632,7 @@
183 tasks.write_dhcp_config.apply_async, MockNotCalled())
184
185
186-class TestRegionProtocol_GetClusterInterfaces(MAASTestCase):
187+class TestRegionProtocol_GetClusterInterfaces(TransactionTestCase):
188
189 def test_create_node_is_registered(self):
190 protocol = Region()
191@@ -1666,9 +1669,9 @@
192 expected_interfaces, response["interfaces"])
193
194
195-class TestRegionProtocol_CreateNode(MAASTestCase):
196+class TestRegionProtocol_CreateNode(TransactionTestCase):
197
198- def test_create_node_is_registered(self):
199+ def test__is_registered(self):
200 protocol = Region()
201 responder = protocol.locateResponder(
202 CreateNode.commandName)
203@@ -1680,10 +1683,9 @@
204
205 @wait_for_reactor
206 @inlineCallbacks
207- def test_calls_create_node_function(self):
208- create_node_function = self.patch(regionservice, 'create_node')
209- create_node_function.return_value = yield deferToThread(
210- self.create_node)
211+ def test__calls_create_node_function(self):
212+ create_node = self.patch_autospec(regionservice, 'create_node')
213+ create_node.return_value = yield deferToThread(self.create_node)
214
215 params = {
216 'cluster_uuid': factory.make_name('uuid'),
217@@ -1698,11 +1700,48 @@
218 self.assertIsNotNone(response)
219
220 self.assertThat(
221- create_node_function,
222+ create_node,
223 MockCalledOnceWith(
224 params['cluster_uuid'], params['architecture'],
225 params['power_type'], params['power_parameters'],
226 params['mac_addresses']))
227 self.assertEqual(
228- create_node_function.return_value.system_id,
229- response['system_id'])
230+ {"system_id": create_node.return_value.system_id},
231+ response)
232+
233+
234+class TestRegionProtocol_GetSecrets(TransactionTestCase):
235+
236+ def test_identify_is_registered(self):
237+ protocol = Region()
238+ responder = protocol.locateResponder(GetSecrets.commandName)
239+ self.assertIsNot(responder, None)
240+
241+ @transactional
242+ def create_cluster(self):
243+ return factory.make_node_group()
244+
245+ @wait_for_reactor
246+ @inlineCallbacks
247+ def test_returns_api_secrets_for_cluster(self):
248+ cluster = yield deferToThread(self.create_cluster)
249+ expected_credentials = convert_tuple_to_string(
250+ get_creds_tuple(cluster.api_token))
251+
252+ response = yield call_responder(
253+ Region(), GetSecrets, {b'cluster_uuid': cluster.uuid})
254+ self.assertEqual(
255+ expected_credentials, response['credentials'])
256+
257+ @wait_for_reactor
258+ @inlineCallbacks
259+ def test_errors_if_cluster_not_found(self):
260+ cluster_uuid = factory.make_UUID()
261+ d = call_responder(
262+ Region(), GetSecrets, {b'cluster_uuid': cluster_uuid})
263+
264+ def check(error):
265+ self.assertIsInstance(error, Failure)
266+ self.assertIsInstance(error.value, NoSuchCluster)
267+
268+ yield d.addErrback(check)
269
270=== modified file 'src/provisioningserver/rpc/region.py'
271--- src/provisioningserver/rpc/region.py 2014-09-05 09:15:57 +0000
272+++ src/provisioningserver/rpc/region.py 2014-09-08 22:06:15 +0000
273@@ -329,3 +329,20 @@
274 ]
275 response = []
276 errors = []
277+
278+
279+class GetSecrets(amp.Command):
280+ """Get the API secrets for a cluster.
281+
282+ :since: 1.7
283+ """
284+
285+ arguments = [
286+ (b"cluster_uuid", amp.Unicode()),
287+ ]
288+ response = [
289+ (b"credentials", amp.Unicode()),
290+ ]
291+ errors = {
292+ NoSuchCluster: b"NoSuchCluster",
293+ }