Merge ~bjornt/maas:maas-init-configauth into maas:master
- Git
- lp:~bjornt/maas
- maas-init-configauth
- Merge into master
Status: | Merged |
---|---|
Approved by: | Björn Tillenius |
Approved revision: | 931edae410e3d1af3c29f003c000969bf6b15df5 |
Merge reported by: | MAAS Lander |
Merged at revision: | not available |
Proposed branch: | ~bjornt/maas:maas-init-configauth |
Merge into: | maas:master |
Diff against target: |
310 lines (+194/-9) 5 files modified
src/maascli/snappy.py (+49/-1) src/maascli/tests/test_snappy.py (+96/-1) src/maasserver/management/commands/configauth.py (+19/-6) src/maasserver/tests/test_commands_configauth.py (+29/-0) src/metadataserver/models/scriptresult.py (+1/-1) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Alberto Donato (community) | Approve | ||
MAAS Lander | Approve | ||
Review via email: mp+341390@code.launchpad.net |
Commit message
Configure external authentication in 'maas init'
Description of the change
Alberto Donato (ack) wrote : | # |
A question inline
Björn Tillenius (bjornt) : | # |
Alberto Donato (ack) wrote : | # |
an additional comment
Björn Tillenius (bjornt) wrote : | # |
Addressed your comments. Probably makes sense to re-review it, since I did some changes so that I could add some tests as well.
After discussion with Andres, I also added an --enable-idm option as well, since we don't want to advertise it too much now when IDM is not even released.
MAAS Lander (maas-lander) wrote : | # |
UNIT TESTS
-b maas-init-
STATUS: FAILED
LOG: http://
COMMIT: fd61026f4c1c33a
MAAS Lander (maas-lander) wrote : | # |
UNIT TESTS
-b maas-init-
STATUS: SUCCESS
COMMIT: aa861fb67ee08b9
MAAS Lander (maas-lander) wrote : | # |
LANDING
-b maas-init-
STATUS: FAILED BUILD
LOG: http://
MAAS Lander (maas-lander) wrote : | # |
LANDING
-b maas-init-
STATUS: FAILED BUILD
LOG: http://
- 931edae... by Björn Tillenius
-
Fix intermittent test failure.
Preview Diff
1 | diff --git a/src/maascli/snappy.py b/src/maascli/snappy.py | |||
2 | index 07bde83..08ff34b 100755 | |||
3 | --- a/src/maascli/snappy.py | |||
4 | +++ b/src/maascli/snappy.py | |||
5 | @@ -15,6 +15,7 @@ import argparse | |||
6 | 15 | from collections import OrderedDict | 15 | from collections import OrderedDict |
7 | 16 | from contextlib import contextmanager | 16 | from contextlib import contextmanager |
8 | 17 | import grp | 17 | import grp |
9 | 18 | import json | ||
10 | 18 | import os | 19 | import os |
11 | 19 | import pwd | 20 | import pwd |
12 | 20 | import random | 21 | import random |
13 | @@ -631,6 +632,25 @@ class cmd_init(SnappyCommand): | |||
14 | 631 | help=( | 632 | help=( |
15 | 632 | "Skip the admin creation when initializing in 'all' mode.")) | 633 | "Skip the admin creation when initializing in 'all' mode.")) |
16 | 633 | parser.add_argument( | 634 | parser.add_argument( |
17 | 635 | '--enable-idm', default=False, action="store_true", | ||
18 | 636 | help=("Enable configuring the use of an external IDM server. " | ||
19 | 637 | "This feature is currently experimental. " | ||
20 | 638 | "If this isn't enabled, all --idm-* arguments " | ||
21 | 639 | "will be ignored.")) | ||
22 | 640 | parser.add_argument( | ||
23 | 641 | '--idm-url', default=None, metavar='IDM_URL', | ||
24 | 642 | help=("The URL to the external IDM server to use for " | ||
25 | 643 | "authentication.")) | ||
26 | 644 | parser.add_argument( | ||
27 | 645 | '--idm-user', default=None, | ||
28 | 646 | help="The username to access the IDM server API.") | ||
29 | 647 | parser.add_argument( | ||
30 | 648 | '--idm-key', default=None, | ||
31 | 649 | help="The private key to access the IDM server API.") | ||
32 | 650 | parser.add_argument( | ||
33 | 651 | '--idm-agent-file', type=argparse.FileType('r'), | ||
34 | 652 | help="Agent file containing IDM authentication information") | ||
35 | 653 | parser.add_argument( | ||
36 | 634 | '--admin-username', default=None, metavar='USERNAME', | 654 | '--admin-username', default=None, metavar='USERNAME', |
37 | 635 | help="Username for the admin account.") | 655 | help="Username for the admin account.") |
38 | 636 | parser.add_argument( | 656 | parser.add_argument( |
39 | @@ -773,7 +793,13 @@ class cmd_init(SnappyCommand): | |||
40 | 773 | "Performing database migrations", | 793 | "Performing database migrations", |
41 | 774 | migrate_db, capture=sys.stdout.isatty()) | 794 | migrate_db, capture=sys.stdout.isatty()) |
42 | 775 | clear_line() | 795 | clear_line() |
44 | 776 | if not options.skip_admin: | 796 | if options.enable_idm: |
45 | 797 | print_msg('Configuring authentication') | ||
46 | 798 | configure_authentication(options) | ||
47 | 799 | auth_config = self._get_current_auth_config() | ||
48 | 800 | skip_create_admin = ( | ||
49 | 801 | options.skip_admin or auth_config['external_auth_url']) | ||
50 | 802 | if not skip_create_admin: | ||
51 | 777 | self._create_admin_account(options) | 803 | self._create_admin_account(options) |
52 | 778 | elif mode in ['region', 'region+rack']: | 804 | elif mode in ['region', 'region+rack']: |
53 | 779 | # When in 'region' or 'region+rack' the migrations for the database | 805 | # When in 'region' or 'region+rack' the migrations for the database |
54 | @@ -805,6 +831,28 @@ class cmd_init(SnappyCommand): | |||
55 | 805 | cmd.extend(['--ssh-import', options.admin_ssh_import]) | 831 | cmd.extend(['--ssh-import', options.admin_ssh_import]) |
56 | 806 | subprocess.call(cmd) | 832 | subprocess.call(cmd) |
57 | 807 | 833 | ||
58 | 834 | def _get_current_auth_config(self): | ||
59 | 835 | cmd = [ | ||
60 | 836 | os.path.join(os.environ['SNAP'], 'bin', 'maas-region'), | ||
61 | 837 | 'configauth', '--json'] | ||
62 | 838 | output = subprocess.check_output(cmd) | ||
63 | 839 | return json.loads(output) | ||
64 | 840 | |||
65 | 841 | |||
66 | 842 | def configure_authentication(options): | ||
67 | 843 | cmd = [ | ||
68 | 844 | os.path.join(os.environ['SNAP'], 'bin', 'maas-region'), | ||
69 | 845 | 'configauth'] | ||
70 | 846 | if options.idm_url is not None: | ||
71 | 847 | cmd.extend(['--idm-url', options.idm_url]) | ||
72 | 848 | if options.idm_user is not None: | ||
73 | 849 | cmd.extend(['--idm-user', options.idm_user]) | ||
74 | 850 | if options.idm_key is not None: | ||
75 | 851 | cmd.extend(['--idm-key', options.idm_key]) | ||
76 | 852 | if options.idm_agent_file is not None: | ||
77 | 853 | cmd.extend(['--idm-agent-file', options.idm_agent_file.name]) | ||
78 | 854 | subprocess.call(cmd) | ||
79 | 855 | |||
80 | 808 | 856 | ||
81 | 809 | class cmd_config(SnappyCommand): | 857 | class cmd_config(SnappyCommand): |
82 | 810 | """View or change controller configuration.""" | 858 | """View or change controller configuration.""" |
83 | diff --git a/src/maascli/tests/test_snappy.py b/src/maascli/tests/test_snappy.py | |||
84 | index f2adf70..2af352e 100644 | |||
85 | --- a/src/maascli/tests/test_snappy.py | |||
86 | +++ b/src/maascli/tests/test_snappy.py | |||
87 | @@ -9,11 +9,16 @@ import os | |||
88 | 9 | import random | 9 | import random |
89 | 10 | import signal | 10 | import signal |
90 | 11 | import subprocess | 11 | import subprocess |
91 | 12 | import tempfile | ||
92 | 12 | from textwrap import dedent | 13 | from textwrap import dedent |
93 | 13 | import time | 14 | import time |
95 | 14 | from unittest.mock import MagicMock | 15 | from unittest.mock import ( |
96 | 16 | MagicMock, | ||
97 | 17 | patch, | ||
98 | 18 | ) | ||
99 | 15 | 19 | ||
100 | 16 | from maascli import snappy | 20 | from maascli import snappy |
101 | 21 | from maascli.parser import ArgumentParser | ||
102 | 17 | from maastesting.factory import factory | 22 | from maastesting.factory import factory |
103 | 18 | from maastesting.matchers import MockCalledOnceWith | 23 | from maastesting.matchers import MockCalledOnceWith |
104 | 19 | from maastesting.testcase import MAASTestCase | 24 | from maastesting.testcase import MAASTestCase |
105 | @@ -138,6 +143,96 @@ class TestHelpers(MAASTestCase): | |||
106 | 138 | self.assertEqual('none', snappy.get_current_mode()) | 143 | self.assertEqual('none', snappy.get_current_mode()) |
107 | 139 | 144 | ||
108 | 140 | 145 | ||
109 | 146 | class TestConfigureAuthentication(MAASTestCase): | ||
110 | 147 | |||
111 | 148 | def setUp(self): | ||
112 | 149 | super().setUp() | ||
113 | 150 | self.maas_bin_path = 'snap-path/bin/maas-region' | ||
114 | 151 | self.mock_subprocess = self.patch(snappy, 'subprocess') | ||
115 | 152 | self.mock_environ = patch.dict( | ||
116 | 153 | snappy.os.environ, {'SNAP': 'snap-path'}, clear=True) | ||
117 | 154 | self.mock_environ.start() | ||
118 | 155 | self.parser = ArgumentParser() | ||
119 | 156 | snappy.cmd_init(self.parser) | ||
120 | 157 | |||
121 | 158 | def tearDown(self): | ||
122 | 159 | self.mock_subprocess.stop() | ||
123 | 160 | self.mock_environ.stop() | ||
124 | 161 | super().tearDown() | ||
125 | 162 | |||
126 | 163 | def test_no_options(self): | ||
127 | 164 | options = self.parser.parse_args([]) | ||
128 | 165 | snappy.configure_authentication(options) | ||
129 | 166 | [config_call] = self.mock_subprocess.mock_calls | ||
130 | 167 | method, args, kwargs = config_call | ||
131 | 168 | self.assertEqual('call', method) | ||
132 | 169 | self.assertEqual(([self.maas_bin_path, 'configauth'],), args) | ||
133 | 170 | self.assertEqual({}, kwargs) | ||
134 | 171 | |||
135 | 172 | def test_idm_url(self): | ||
136 | 173 | config_auth_args = ['--idm-url', 'http://idm.example.com/'] | ||
137 | 174 | options = self.parser.parse_args(config_auth_args) | ||
138 | 175 | snappy.configure_authentication(options) | ||
139 | 176 | [config_call] = self.mock_subprocess.mock_calls | ||
140 | 177 | method, args, kwargs = config_call | ||
141 | 178 | self.assertEqual('call', method) | ||
142 | 179 | self.assertEqual( | ||
143 | 180 | ([self.maas_bin_path, 'configauth'] + config_auth_args,), args) | ||
144 | 181 | self.assertEqual({}, kwargs) | ||
145 | 182 | |||
146 | 183 | def test_idm_user(self): | ||
147 | 184 | config_auth_args = ['--idm-user', 'some-user'] | ||
148 | 185 | options = self.parser.parse_args(config_auth_args) | ||
149 | 186 | snappy.configure_authentication(options) | ||
150 | 187 | [config_call] = self.mock_subprocess.mock_calls | ||
151 | 188 | method, args, kwargs = config_call | ||
152 | 189 | self.assertEqual('call', method) | ||
153 | 190 | self.assertEqual( | ||
154 | 191 | ([self.maas_bin_path, 'configauth'] + config_auth_args,), args) | ||
155 | 192 | self.assertEqual({}, kwargs) | ||
156 | 193 | |||
157 | 194 | def test_idm_key(self): | ||
158 | 195 | config_auth_args = ['--idm-key', 'some-key'] | ||
159 | 196 | options = self.parser.parse_args(config_auth_args) | ||
160 | 197 | snappy.configure_authentication(options) | ||
161 | 198 | [config_call] = self.mock_subprocess.mock_calls | ||
162 | 199 | method, args, kwargs = config_call | ||
163 | 200 | self.assertEqual('call', method) | ||
164 | 201 | self.assertEqual( | ||
165 | 202 | ([self.maas_bin_path, 'configauth'] + config_auth_args,), args) | ||
166 | 203 | self.assertEqual({}, kwargs) | ||
167 | 204 | |||
168 | 205 | def test_idm_agent_file(self): | ||
169 | 206 | _, agent_file_path = tempfile.mkstemp() | ||
170 | 207 | self.addCleanup(os.remove, agent_file_path) | ||
171 | 208 | config_auth_args = ['--idm-agent-file', agent_file_path] | ||
172 | 209 | options = self.parser.parse_args(config_auth_args) | ||
173 | 210 | snappy.configure_authentication(options) | ||
174 | 211 | [config_call] = self.mock_subprocess.mock_calls | ||
175 | 212 | method, args, kwargs = config_call | ||
176 | 213 | self.assertEqual('call', method) | ||
177 | 214 | self.assertEqual( | ||
178 | 215 | ([self.maas_bin_path, 'configauth'] + config_auth_args,), args) | ||
179 | 216 | self.assertEqual({}, kwargs) | ||
180 | 217 | |||
181 | 218 | def test_full(self): | ||
182 | 219 | _, agent_file = tempfile.mkstemp() | ||
183 | 220 | self.addCleanup(os.remove, agent_file) | ||
184 | 221 | config_auth_args = [ | ||
185 | 222 | '--idm-url', 'http://idm.example.com/', | ||
186 | 223 | '--idm-user', 'idm-user', | ||
187 | 224 | '--idm-key', 'idm-key', | ||
188 | 225 | '--idm-agent-file', agent_file] | ||
189 | 226 | options = self.parser.parse_args(config_auth_args) | ||
190 | 227 | snappy.configure_authentication(options) | ||
191 | 228 | [config_call] = self.mock_subprocess.mock_calls | ||
192 | 229 | method, args, kwargs = config_call | ||
193 | 230 | self.assertEqual('call', method) | ||
194 | 231 | self.assertEqual( | ||
195 | 232 | ([self.maas_bin_path, 'configauth'] + config_auth_args,), args) | ||
196 | 233 | self.assertEqual({}, kwargs) | ||
197 | 234 | |||
198 | 235 | |||
199 | 141 | class TestRenderSupervisord(MAASTestCase): | 236 | class TestRenderSupervisord(MAASTestCase): |
200 | 142 | 237 | ||
201 | 143 | TEST_TEMPLATE = dedent("""\ | 238 | TEST_TEMPLATE = dedent("""\ |
202 | diff --git a/src/maasserver/management/commands/configauth.py b/src/maasserver/management/commands/configauth.py | |||
203 | index 2e8df6c..5d7c7c4 100644 | |||
204 | --- a/src/maasserver/management/commands/configauth.py | |||
205 | +++ b/src/maasserver/management/commands/configauth.py | |||
206 | @@ -58,10 +58,16 @@ def is_valid_auth_url(auth_url): | |||
207 | 58 | return True | 58 | return True |
208 | 59 | 59 | ||
209 | 60 | 60 | ||
214 | 61 | def config_auth(config_manager, auth_url, auth_user, auth_key): | 61 | def get_auth_config(config_manager): |
215 | 62 | config_manager.set_config('external_auth_url', auth_url) | 62 | config_keys = [ |
216 | 63 | config_manager.set_config('external_auth_user', auth_user) | 63 | 'external_auth_url', 'external_auth_user', 'external_auth_key'] |
217 | 64 | config_manager.set_config('external_auth_key', auth_key) | 64 | return {key: config_manager.get_config(key) for key in config_keys} |
218 | 65 | |||
219 | 66 | |||
220 | 67 | def set_auth_config(config_manager, auth_url, auth_user, auth_key): | ||
221 | 68 | config_manager.set_config('external_auth_url', auth_url) | ||
222 | 69 | config_manager.set_config('external_auth_user', auth_user) | ||
223 | 70 | config_manager.set_config('external_auth_key', auth_key) | ||
224 | 65 | 71 | ||
225 | 66 | 72 | ||
226 | 67 | class Command(BaseCommand): | 73 | class Command(BaseCommand): |
227 | @@ -82,16 +88,23 @@ class Command(BaseCommand): | |||
228 | 82 | parser.add_argument( | 88 | parser.add_argument( |
229 | 83 | '--idm-agent-file', type=argparse.FileType('r'), | 89 | '--idm-agent-file', type=argparse.FileType('r'), |
230 | 84 | help="Agent file containing IDM authentication information") | 90 | help="Agent file containing IDM authentication information") |
231 | 91 | parser.add_argument( | ||
232 | 92 | '--json', action='store_true', default=False, | ||
233 | 93 | help="Return the current authentication configuration as JSON") | ||
234 | 85 | 94 | ||
235 | 86 | def handle(self, *args, **options): | 95 | def handle(self, *args, **options): |
236 | 87 | config_manager = Config.objects.db_manager(DEFAULT_DB_ALIAS) | 96 | config_manager = Config.objects.db_manager(DEFAULT_DB_ALIAS) |
237 | 88 | 97 | ||
238 | 98 | if options.get('json'): | ||
239 | 99 | print(json.dumps(get_auth_config(config_manager))) | ||
240 | 100 | return | ||
241 | 101 | |||
242 | 89 | auth_url, auth_user, auth_key = None, '', '' | 102 | auth_url, auth_user, auth_key = None, '', '' |
243 | 90 | 103 | ||
244 | 91 | agent_file = options.get('idm_agent_file') | 104 | agent_file = options.get('idm_agent_file') |
245 | 92 | if agent_file: | 105 | if agent_file: |
246 | 93 | auth_url, auth_user, auth_key = read_agent_file(agent_file) | 106 | auth_url, auth_user, auth_key = read_agent_file(agent_file) |
248 | 94 | config_auth(config_manager, auth_url, auth_user, auth_key) | 107 | set_auth_config(config_manager, auth_url, auth_user, auth_key) |
249 | 95 | return | 108 | return |
250 | 96 | 109 | ||
251 | 97 | auth_url = options.get('idm_url') | 110 | auth_url = options.get('idm_url') |
252 | @@ -111,4 +124,4 @@ class Command(BaseCommand): | |||
253 | 111 | if not auth_key: | 124 | if not auth_key: |
254 | 112 | auth_key = read_input("Private key for IDM API access: ") | 125 | auth_key = read_input("Private key for IDM API access: ") |
255 | 113 | 126 | ||
257 | 114 | config_auth(config_manager, auth_url, auth_user, auth_key) | 127 | set_auth_config(config_manager, auth_url, auth_user, auth_key) |
258 | diff --git a/src/maasserver/tests/test_commands_configauth.py b/src/maasserver/tests/test_commands_configauth.py | |||
259 | index e4ab929..f3fb896 100644 | |||
260 | --- a/src/maasserver/tests/test_commands_configauth.py | |||
261 | +++ b/src/maasserver/tests/test_commands_configauth.py | |||
262 | @@ -145,6 +145,35 @@ class TestChangeAuthCommand(MAASServerTestCase): | |||
263 | 145 | 'private-key', Config.objects.get_config('external_auth_key')) | 145 | 'private-key', Config.objects.get_config('external_auth_key')) |
264 | 146 | self.read_input.assert_not_called() | 146 | self.read_input.assert_not_called() |
265 | 147 | 147 | ||
266 | 148 | def test_configauth_json_empty(self): | ||
267 | 149 | mock_print = self.patch(configauth, 'print') | ||
268 | 150 | call_command('configauth', json=True) | ||
269 | 151 | self.read_input.assert_not_called() | ||
270 | 152 | [print_call] = mock_print.mock_calls | ||
271 | 153 | _, [output], kwargs = print_call | ||
272 | 154 | self.assertEqual({}, kwargs) | ||
273 | 155 | self.assertEqual( | ||
274 | 156 | {'external_auth_url': '', 'external_auth_user': '', | ||
275 | 157 | 'external_auth_key': ''}, | ||
276 | 158 | json.loads(output)) | ||
277 | 159 | |||
278 | 160 | def test_configauth_json_full(self): | ||
279 | 161 | Config.objects.set_config( | ||
280 | 162 | 'external_auth_url', 'http://idm.example.com/') | ||
281 | 163 | Config.objects.set_config('external_auth_user', 'maas') | ||
282 | 164 | Config.objects.set_config('external_auth_key', 'secret maas key') | ||
283 | 165 | mock_print = self.patch(configauth, 'print') | ||
284 | 166 | call_command('configauth', json=True) | ||
285 | 167 | self.read_input.assert_not_called() | ||
286 | 168 | [print_call] = mock_print.mock_calls | ||
287 | 169 | _, [output], kwargs = print_call | ||
288 | 170 | self.assertEqual({}, kwargs) | ||
289 | 171 | self.assertEqual( | ||
290 | 172 | {'external_auth_url': 'http://idm.example.com/', | ||
291 | 173 | 'external_auth_user': 'maas', | ||
292 | 174 | 'external_auth_key': 'secret maas key'}, | ||
293 | 175 | json.loads(output)) | ||
294 | 176 | |||
295 | 148 | 177 | ||
296 | 149 | class TestIsValidAuthSource(unittest.TestCase): | 178 | class TestIsValidAuthSource(unittest.TestCase): |
297 | 150 | 179 | ||
298 | diff --git a/src/metadataserver/models/scriptresult.py b/src/metadataserver/models/scriptresult.py | |||
299 | index d817a0e..9cd657f 100644 | |||
300 | --- a/src/metadataserver/models/scriptresult.py | |||
301 | +++ b/src/metadataserver/models/scriptresult.py | |||
302 | @@ -145,7 +145,7 @@ class ScriptResult(CleanSave, TimestampedModel): | |||
303 | 145 | # Get an estimated runtime from previous runs. | 145 | # Get an estimated runtime from previous runs. |
304 | 146 | for script_result in self.history.only( | 146 | for script_result in self.history.only( |
305 | 147 | 'status', 'started', 'ended', 'script_id', 'script_name', | 147 | 'status', 'started', 'ended', 'script_id', 'script_name', |
307 | 148 | 'script_set_id', 'physical_blockdevice_id'): | 148 | 'script_set_id', 'physical_blockdevice_id', 'created'): |
308 | 149 | # Only look at passed results when calculating an estimated | 149 | # Only look at passed results when calculating an estimated |
309 | 150 | # runtime. Failed results may take longer or shorter than | 150 | # runtime. Failed results may take longer or shorter than |
310 | 151 | # average. Don't use self.history.filter for this as the now | 151 | # average. Don't use self.history.filter for this as the now |
UNIT TESTS configauth lp:~bjornt/maas/+git/maas into -b master lp:~maas-committers/maas
-b maas-init-
STATUS: SUCCESS be7aa43ff03884c 26ec348d90
COMMIT: 771106b90988a23