Merge ~ack/maas:backport-1817687 into maas:2.5

Proposed by Alberto Donato
Status: Merged
Approved by: Alberto Donato
Approved revision: 3c5587d999a59bef47539942d3b7eb5cb78d6f9f
Merge reported by: MAAS Lander
Merged at revision: not available
Proposed branch: ~ack/maas:backport-1817687
Merge into: maas:2.5
Diff against target: 454 lines (+166/-111)
9 files modified
src/maascli/init.py (+47/-0)
src/maascli/snappy.py (+2/-47)
src/maasserver/management/commands/configauth.py (+22/-12)
src/maasserver/management/commands/createadmin.py (+1/-18)
src/maasserver/rbac.py (+28/-7)
src/maasserver/tests/test_commands.py (+6/-6)
src/maasserver/tests/test_commands_configauth.py (+24/-19)
src/maasserver/tests/test_rbac.py (+35/-0)
utilities/check-imports (+1/-2)
Reviewer Review Type Date Requested Status
Alberto Donato (community) Approve
Review via email: mp+365328@code.launchpad.net

Commit message

backport 591ea5acfcdeed93763c396cc4b20741c9b6be8a - LP: #1817687 - prompt the user in maas configauth for creating the service in RBAC if not found

To post a comment you must log in.
Revision history for this message
Alberto Donato (ack) :
review: Approve

There was an error fetching revisions from git servers. Please try again in a few minutes. If the problem persists, contact Launchpad support.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/src/maascli/init.py b/src/maascli/init.py
2index 5f860c6..b9e6692 100644
3--- a/src/maascli/init.py
4+++ b/src/maascli/init.py
5@@ -223,3 +223,50 @@ def init_maas(options):
6 create_account_external_auth(auth_config, maas_config)
7 else:
8 create_admin_account(options)
9+
10+
11+def read_input(prompt):
12+ """Reads input from stdin."""
13+ while True:
14+ try:
15+ data = input(prompt)
16+ except EOFError:
17+ # Ctrl-d was pressed?
18+ print()
19+ continue
20+ except KeyboardInterrupt:
21+ print()
22+ raise SystemExit(1)
23+ else:
24+ # The assumption is that, since Python 3 return a Unicode string
25+ # from input(), it has Done The Right Thing with respect to
26+ # character encoding.
27+ return data
28+
29+
30+def prompt_for_choices(prompt, choices, default=None, help_text=None):
31+ """Prompt requires specific choice answeres.
32+
33+ If `help_text` is provided the 'help' is added as a choice.
34+ """
35+ invalid_msg = 'Invalid input, try again'
36+ if help_text:
37+ invalid_msg += " or type 'help'"
38+ invalid_msg += '.'
39+ value = None
40+ while True:
41+ value = read_input(prompt)
42+ if not value:
43+ if default:
44+ return default
45+ else:
46+ print_msg(invalid_msg)
47+ print_msg()
48+ elif value == 'help' and help_text:
49+ print_msg(help_text)
50+ print_msg()
51+ elif value not in choices:
52+ print_msg(invalid_msg)
53+ print_msg()
54+ else:
55+ return value
56diff --git a/src/maascli/snappy.py b/src/maascli/snappy.py
57index 4779f0d..a8e5322 100755
58--- a/src/maascli/snappy.py
59+++ b/src/maascli/snappy.py
60@@ -35,6 +35,8 @@ from maascli.init import (
61 deprecated_for,
62 init_maas,
63 print_msg,
64+ prompt_for_choices,
65+ read_input,
66 )
67 import netifaces
68 import tempita
69@@ -478,25 +480,6 @@ def perform_work(msg, cmd, *args, **kwargs):
70 return ret
71
72
73-def read_input(prompt):
74- """Reads input from stdin."""
75- while True:
76- try:
77- data = input(prompt)
78- except EOFError:
79- # Ctrl-d was pressed?
80- print()
81- continue
82- except KeyboardInterrupt:
83- print()
84- raise SystemExit(1)
85- else:
86- # The assumption is that, since Python 3 return a Unicode string
87- # from input(), it has Done The Right Thing with respect to
88- # character encoding.
89- return data
90-
91-
92 def required_prompt(prompt, help_text=None):
93 """Prompt for required input."""
94 value = None
95@@ -508,34 +491,6 @@ def required_prompt(prompt, help_text=None):
96 return value
97
98
99-def prompt_for_choices(prompt, choices, default=None, help_text=None):
100- """Prompt requires specific choice answeres.
101-
102- If `help_text` is provided the 'help' is added as a choice.
103- """
104- invalid_msg = 'Invalid input, try again'
105- if help_text:
106- invalid_msg += " or type 'help'"
107- invalid_msg += '.'
108- value = None
109- while True:
110- value = read_input(prompt)
111- if not value:
112- if default:
113- return default
114- else:
115- print_msg(invalid_msg)
116- print_msg()
117- elif value == 'help' and help_text:
118- print_msg(help_text)
119- print_msg()
120- elif value not in choices:
121- print_msg(invalid_msg)
122- print_msg()
123- else:
124- return value
125-
126-
127 def prompt_for_maas_url():
128 """Prompt for the MAAS URL."""
129 default_url = get_default_url()
130diff --git a/src/maasserver/management/commands/configauth.py b/src/maasserver/management/commands/configauth.py
131index d6bf7e8..2a8aa9a 100644
132--- a/src/maasserver/management/commands/configauth.py
133+++ b/src/maasserver/management/commands/configauth.py
134@@ -18,8 +18,10 @@ from django.db import DEFAULT_DB_ALIAS
135 from maascli.init import (
136 add_candid_options,
137 add_rbac_options,
138+ prompt_for_choices,
139+ read_input,
140 )
141-from maasserver.management.commands.createadmin import read_input
142+from maasserver.macaroon_auth import APIError
143 from maasserver.models import Config
144 from maasserver.models.rbacsync import (
145 RBAC_ACTION,
146@@ -79,19 +81,27 @@ def update_auth_details_from_rbac_registration(
147 services = {
148 service['name']: service
149 for service in client.get_registerable_services()}
150- if not services:
151- raise CommandError(
152- 'No registerable MAAS service on the specified RBAC server')
153- if service_name is not None:
154- service = services.get(service_name)
155- if service is None:
156+ if service_name is None:
157+ if not services:
158 raise CommandError(
159- 'Service "{}" is not known, available choices: {}'.
160- format(service_name, ', '.join(sorted(services))))
161-
162- else:
163+ 'No registerable MAAS service on the specified RBAC server')
164 service = _pick_service(services)
165-
166+ else:
167+ service = services.get(service_name)
168+ if service is None:
169+ create_service = prompt_for_choices(
170+ 'A service with the specified name was not found, '
171+ 'do you want to create one? (yes/no) [default=no]? ',
172+ ['yes', 'no'], default='no')
173+ if create_service == 'no':
174+ raise CommandError('Registration with RBAC service canceled')
175+ try:
176+ service = client.create_service(service_name)
177+ except APIError as error:
178+ if error.status_code == 409:
179+ raise CommandError(
180+ 'User not allowed to register this service')
181+ raise CommandError(str(error))
182 _register_service(client, service, auth_details)
183 print('Service "{}" registered'.format(service['name']))
184
185diff --git a/src/maasserver/management/commands/createadmin.py b/src/maasserver/management/commands/createadmin.py
186index ecb7e79..3296927 100644
187--- a/src/maasserver/management/commands/createadmin.py
188+++ b/src/maasserver/management/commands/createadmin.py
189@@ -14,6 +14,7 @@ from django.core.management.base import (
190 CommandError,
191 )
192 from django.db import DEFAULT_DB_ALIAS
193+from maascli.init import read_input
194 from maasserver.enum import KEYS_PROTOCOL_TYPE
195 from maasserver.models.config import Config
196 from maasserver.models.keysource import KeySource
197@@ -37,24 +38,6 @@ class SSHKeysError(CommandError):
198 """Error during SSH keys import."""
199
200
201-def read_input(prompt):
202- while True:
203- try:
204- data = input(prompt)
205- except EOFError:
206- # Ctrl-d was pressed?
207- print()
208- continue
209- except KeyboardInterrupt:
210- print()
211- raise SystemExit(1)
212- else:
213- # The assumption is that, since Python 3 return a Unicode string
214- # from input(), it has Done The Right Thing with respect to
215- # character encoding.
216- return data
217-
218-
219 def read_password(prompt):
220 while True:
221 try:
222diff --git a/src/maasserver/rbac.py b/src/maasserver/rbac.py
223index 7d63ea4..a1d9a43 100644
224--- a/src/maasserver/rbac.py
225+++ b/src/maasserver/rbac.py
226@@ -378,12 +378,18 @@ class RBACUserClient(MacaroonClient):
227 def __init__(self, url):
228 # no auth info is passed as this is meant for interactive use
229 super().__init__(url, None)
230+ self._maas_product = None
231+
232+ def create_service(self, name):
233+ """Create a MAAS service with the specified name."""
234+ maas = self._get_maas_product()
235+ data = {'name': name, 'product': {'$ref': maas['$uri']}}
236+ return self._api_request('POST', '/service', json=data)
237
238 def get_registerable_services(self):
239 """Return MAAS services that can be registered by the user."""
240 maas = self._get_maas_product()
241- services = self._request(
242- 'GET', self._url + self.API_BASE_URL + '/service/registerable')
243+ services = self._api_request('GET', '/service/registerable')
244 return [
245 service for service in services
246 if service['product']['$ref'] == maas['$uri']]
247@@ -396,11 +402,17 @@ class RBACUserClient(MacaroonClient):
248
249 def _get_maas_product(self):
250 """Return details for the maas product."""
251- products = self._request(
252- 'GET', self._url + self.API_BASE_URL + '/product')
253- [maas] = [
254- product for product in products if product['label'] == 'maas']
255- return maas
256+ if self._maas_product is None:
257+ products = self._api_request('GET', '/product')
258+ [maas] = [
259+ product for product in products if product['label'] == 'maas']
260+ self._maas_product = maas
261+ return self._maas_product
262+
263+ def _api_request(self, method, path, json=None, status_code=200):
264+ return self._request(
265+ method, self._url + self.API_BASE_URL + path,
266+ json=json, status_code=status_code)
267
268
269 class FakeRBACUserClient(RBACUserClient):
270@@ -410,6 +422,15 @@ class FakeRBACUserClient(RBACUserClient):
271 self.products = []
272 self.registered_services = []
273
274+ def create_service(self, name):
275+ maas = {
276+ 'name': 'maas',
277+ '$uri': '/api/rbac/v1/service/4',
278+ 'pending': True,
279+ 'product': {'$ref' '/api/rbac/v1/product/2'}}
280+ self.services.append(maas)
281+ return maas
282+
283 def get_products(self):
284 return self.products
285
286diff --git a/src/maasserver/tests/test_commands.py b/src/maasserver/tests/test_commands.py
287index 5a59dec..d16fb0d 100644
288--- a/src/maasserver/tests/test_commands.py
289+++ b/src/maasserver/tests/test_commands.py
290@@ -256,12 +256,12 @@ class TestCommands(MAASServerTestCase):
291
292 def test_prompt_for_username_returns_selected_username(self):
293 username = factory.make_name('user')
294- self.patch(createadmin, 'input').return_value = username
295+ self.patch(createadmin, 'read_input').return_value = username
296
297 self.assertEqual(username, createadmin.prompt_for_username())
298
299 def test_prompt_for_username_checks_for_empty_username(self):
300- self.patch(createadmin, 'input', lambda x: '')
301+ self.patch(createadmin, 'read_input', lambda x: '')
302
303 self.assertRaises(
304 createadmin.EmptyUsername,
305@@ -269,12 +269,12 @@ class TestCommands(MAASServerTestCase):
306
307 def test_prompt_for_email_returns_selected_email(self):
308 email = factory.make_email_address()
309- self.patch(createadmin, 'input').return_value = email
310+ self.patch(createadmin, 'read_input').return_value = email
311
312 self.assertEqual(email, createadmin.prompt_for_email())
313
314 def test_prompt_for_email_checks_for_empty_email(self):
315- self.patch(createadmin, 'input', lambda x: '')
316+ self.patch(createadmin, 'read_input', lambda x: '')
317
318 self.assertRaises(
319 createadmin.EmptyEmail,
320@@ -285,12 +285,12 @@ class TestCommands(MAASServerTestCase):
321 random.choice(
322 [KEYS_PROTOCOL_TYPE.LP, KEYS_PROTOCOL_TYPE.GH]),
323 factory.make_name('user-id'))
324- self.patch(createadmin, 'input').return_value = ssh_import
325+ self.patch(createadmin, 'read_input').return_value = ssh_import
326
327 self.assertEqual(ssh_import, createadmin.prompt_for_ssh_import())
328
329 def test_prompt_for_ssh_import_returns_None_for_no_user_id(self):
330- self.patch(createadmin, 'input').return_value = ''
331+ self.patch(createadmin, 'read_input').return_value = ''
332 self.assertEqual('', createadmin.prompt_for_ssh_import())
333
334 def test_validate_ssh_import_validates_protocol_and_user_id(self):
335diff --git a/src/maasserver/tests/test_commands_configauth.py b/src/maasserver/tests/test_commands_configauth.py
336index 74ec282..5e03928 100644
337--- a/src/maasserver/tests/test_commands_configauth.py
338+++ b/src/maasserver/tests/test_commands_configauth.py
339@@ -306,7 +306,7 @@ class TestConfigAuthCommand(MAASServerTestCase):
340 'rbac_url': 'http://rbac.example.com/'},
341 json.loads(output))
342
343- def test_configauth_rbac_with_name(self):
344+ def test_configauth_rbac_with_name_existing(self):
345 self.rbac_user_client.services = [
346 {'name': 'mymaas',
347 '$uri': '/api/rbac/v1/service/4',
348@@ -325,26 +325,31 @@ class TestConfigAuthCommand(MAASServerTestCase):
349 self.rbac_user_client.registered_services,
350 ['/api/rbac/v1/service/4'])
351
352- def test_configauth_rbac_unknown_name(self):
353- self.rbac_user_client.services = [
354- {'name': 'mymaas1',
355- '$uri': '/api/rbac/v1/service/4',
356- 'pending': True,
357- 'product': {'$ref' '/api/rbac/v1/product/2'}},
358- {'name': 'mymaas2',
359- '$uri': '/api/rbac/v1/service/4',
360- 'pending': True,
361- 'product': {'$ref' '/api/rbac/v1/product/2'}}]
362+ def test_configauth_rbac_with_name_create(self):
363+ patch_prompt = self.patch(configauth, 'prompt_for_choices')
364+ patch_prompt.return_value = 'yes'
365+ call_command(
366+ 'configauth', rbac_url='http://rbac.example.com',
367+ rbac_service_name='maas')
368+ patch_prompt.assert_called_once()
369+ self.assertEqual(
370+ 'http://rbac.example.com',
371+ Config.objects.get_config('rbac_url'))
372+ self.assertEqual(
373+ self.rbac_user_client.registered_services,
374+ ['/api/rbac/v1/service/4'])
375+
376+ def test_configauth_rbac_with_name_abort(self):
377+ patch_prompt = self.patch(configauth, 'prompt_for_choices')
378+ patch_prompt.return_value = 'no'
379 error = self.assertRaises(
380 CommandError, call_command,
381- 'configauth', candid_url='http://example.com:1234',
382- candid_user='user@admin', candid_key='private-key',
383- rbac_url='http://rbac.example.com',
384- rbac_service_name='unknown')
385- self.assertEqual(
386- str(error),
387- 'Service "unknown" is not known, available choices: '
388- 'mymaas1, mymaas2')
389+ 'configauth', rbac_url='http://rbac.example.com',
390+ rbac_service_name='maas')
391+ self.assertEqual(str(error), 'Registration with RBAC service canceled')
392+ patch_prompt.assert_called_once()
393+ self.assertEqual(Config.objects.get_config('rbac_url'), '')
394+ self.assertEqual(self.rbac_user_client.registered_services, [])
395
396 def test_configauth_rbac_registration_list(self):
397 self.rbac_user_client.services = [
398diff --git a/src/maasserver/tests/test_rbac.py b/src/maasserver/tests/test_rbac.py
399index 0314c7b..1e2a7dc 100644
400--- a/src/maasserver/tests/test_rbac.py
401+++ b/src/maasserver/tests/test_rbac.py
402@@ -549,3 +549,38 @@ class TestRBACUserClient(MAASServerTestCase):
403 'https://rbac.example.com/api/'
404 'rbac/v1/service/3/credentials',
405 auth=mock.ANY, cookies=mock.ANY, json=json))
406+
407+ def test_create_service(self):
408+ products = [
409+ {
410+ '$uri': '/api/rbac/v1/product/1',
411+ 'label': 'maas',
412+ 'name': 'MAAS',
413+ }
414+ ]
415+ maas = {
416+ '$uri': '/api/rbac/v1/service/2',
417+ 'name': 'maas',
418+ 'description': '',
419+ 'pending': True,
420+ 'product': {
421+ '$ref': '/api/rbac/v1/product/1'
422+ },
423+ }
424+ self.mock_responses(products, maas)
425+ self.assertEqual(self.client.create_service('maas'), maas)
426+ json = {
427+ 'name': 'maas',
428+ 'product': {'$ref': '/api/rbac/v1/product/1'}
429+ }
430+ self.assertThat(
431+ self.mock_request,
432+ MockCallsMatch(
433+ mock.call(
434+ 'GET',
435+ 'https://rbac.example.com/api/rbac/v1/product',
436+ auth=mock.ANY, cookies=mock.ANY, json=None),
437+ mock.call(
438+ 'POST',
439+ 'https://rbac.example.com/api/rbac/v1/service',
440+ auth=mock.ANY, cookies=mock.ANY, json=json)))
441diff --git a/utilities/check-imports b/utilities/check-imports
442index dd1b83e..5c825ec 100755
443--- a/utilities/check-imports
444+++ b/utilities/check-imports
445@@ -262,8 +262,7 @@ RegionControllerRule = Rule(
446 Allow("formencode|formencode.**"),
447 Allow("jsonschema|jsonschema.**"),
448 Allow("lxml|lxml.**"),
449- Allow("maascli.init.add_candid_options"),
450- Allow("maascli.init.add_rbac_options"),
451+ Allow("maascli.init.**"),
452 Allow("maascli.utils.parse_docstring"),
453 Allow("maasserver|maasserver.**"),
454 Allow("macaroonbakery|macaroonbakery.**"),

Subscribers

People subscribed via source and target branches