Merge ~ack/maas:backport-1817687 into maas:2.5
- Git
- lp:~ack/maas
- backport-1817687
- Merge into 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) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Alberto Donato (community) | Approve | ||
Review via email: mp+365328@code.launchpad.net |
Commit message
backport 591ea5acfcdeed9
Description of the change
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
1 | diff --git a/src/maascli/init.py b/src/maascli/init.py |
2 | index 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 |
56 | diff --git a/src/maascli/snappy.py b/src/maascli/snappy.py |
57 | index 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() |
130 | diff --git a/src/maasserver/management/commands/configauth.py b/src/maasserver/management/commands/configauth.py |
131 | index 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 | |
185 | diff --git a/src/maasserver/management/commands/createadmin.py b/src/maasserver/management/commands/createadmin.py |
186 | index 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: |
222 | diff --git a/src/maasserver/rbac.py b/src/maasserver/rbac.py |
223 | index 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 | |
286 | diff --git a/src/maasserver/tests/test_commands.py b/src/maasserver/tests/test_commands.py |
287 | index 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): |
335 | diff --git a/src/maasserver/tests/test_commands_configauth.py b/src/maasserver/tests/test_commands_configauth.py |
336 | index 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 = [ |
398 | diff --git a/src/maasserver/tests/test_rbac.py b/src/maasserver/tests/test_rbac.py |
399 | index 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))) |
441 | diff --git a/utilities/check-imports b/utilities/check-imports |
442 | index 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.**"), |