Merge ~ajkavanagh/simplestreams:bug/1802407 into simplestreams:master
- Git
- lp:~ajkavanagh/simplestreams
- bug/1802407
- Merge into master
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Scott Moser | ||||
Approved revision: | 2423d4d9b689d7f36a0325e06d388587775ef33b | ||||
Merge reported by: | Server Team CI bot | ||||
Merged at revision: | not available | ||||
Proposed branch: | ~ajkavanagh/simplestreams:bug/1802407 | ||||
Merge into: | simplestreams:master | ||||
Diff against target: |
387 lines (+303/-17) 3 files modified
.gitignore (+3/-0) simplestreams/openstack.py (+35/-17) tests/unittests/test_openstack.py (+265/-0) |
||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Scott Moser (community) | Approve | ||
Server Team CI bot | continuous-integration | Approve | |
Review via email: mp+360298@code.launchpad.net |
Commit message
Add SSL support to simplestreams/
This change enables the openstack integration to connect to https
OpenStack endpoints by using the OS_CACERT environement variable.
LP: #1802407
Description of the change
Alex Kavanagh (ajkavanagh) wrote : | # |
Sure, I'll see if I can rustle up some tests on it. I kind of avoided it as there weren't any for that part of the code!
- 5d1cda9... by Alex Kavanagh
-
Add .tox to .gitignore
- c9d83b3... by Alex Kavanagh
-
Add tests for the changes made to add SSL to the keystone auth
Alex Kavanagh (ajkavanagh) wrote : | # |
So I've now added tests to the branch to test the new code, and fixed a bug in the existing code (by writing the new tests!). I guess the slow addition of tests is a good idea.
Scott Moser (smoser) wrote : | # |
Some small comments inline.
Thank you for this though, it looks really good.
What bug was it that you found? Mostly just curious.
Alex Kavanagh (ajkavanagh) wrote : | # |
Thanks for the comments; I've replied and will update the merge proposal.
Re: the bug; it's a feature additional to add SSL support (and the ability to provide the cert), which was requested in the linked bug on the MP: https:/
Essentially, this change is needed by: https:/
Hope that's okay!?
- 2423d4d... by Alex Kavanagh
-
Update tests following review comments
Server Team CI bot (server-team-bot) wrote : | # |
PASSED: Continuous integration, rev:2423d4d
https:/
Executed test runs:
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
Click here to trigger a rebuild:
https:/
Scott Moser (smoser) : | # |
Alex Kavanagh (ajkavanagh) wrote : | # |
Thanks for the merging the code!
Preview Diff
1 | diff --git a/.gitignore b/.gitignore |
2 | index 14c2107..b1af602 100644 |
3 | --- a/.gitignore |
4 | +++ b/.gitignore |
5 | @@ -6,3 +6,6 @@ exdata |
6 | exdata-query |
7 | *.sjson |
8 | *.gpg |
9 | +MANIFEST |
10 | +.coverage |
11 | +.tox |
12 | diff --git a/simplestreams/openstack.py b/simplestreams/openstack.py |
13 | index 4899df7..ebf63c4 100644 |
14 | --- a/simplestreams/openstack.py |
15 | +++ b/simplestreams/openstack.py |
16 | @@ -40,20 +40,34 @@ OS_ENV_VARS = ( |
17 | ) |
18 | |
19 | |
20 | +# only used for legacy client connection |
21 | PT_V2 = ('username', 'password', 'tenant_id', 'tenant_name', 'auth_url', |
22 | 'cacert', 'insecure', ) |
23 | -PT_V3 = ('username', 'password', 'project_id', 'project_name', 'auth_url', |
24 | - 'cacert', 'insecure', 'user_domain_name', 'project_domain_name', |
25 | - 'user_domain_id', 'project_domain_id', ) |
26 | + |
27 | +# annoyingly the 'insecure' option in the old client constructor is now called |
28 | +# the 'verify' option in the session.Session() constructor |
29 | +PASSWORD_V2 = ('auth_url', 'username', 'password', 'user_id', 'trust_id', |
30 | + 'tenant_id', 'tenant_name', 'reauthenticate') |
31 | +PASSWORD_V3 = ('auth_url', 'password', 'username', |
32 | + 'user_id', 'user_domain_id', 'user_domain_name', |
33 | + 'trust_id', 'system_scope', |
34 | + 'domain_id', 'domain_name', |
35 | + 'project_id', 'project_name', |
36 | + 'project_domain_id', 'project_domain_name', |
37 | + 'reauthenticate') |
38 | +SESSION_ARGS = ('cert', 'timeout', 'verify', 'original_ip', 'redirect', |
39 | + 'addition_headers', 'app_name', 'app_version', |
40 | + 'additional_user_agent', |
41 | + 'discovery_cache', 'split_loggers', 'collect_timing') |
42 | |
43 | |
44 | Settings = collections.namedtuple('Settings', 'mod ident arg_set') |
45 | KS_VERSION_RESOLVER = {2: Settings(mod=ksclient_v2, |
46 | ident=v2, |
47 | - arg_set=PT_V2), |
48 | + arg_set=PASSWORD_V2), |
49 | 3: Settings(mod=ksclient_v3, |
50 | ident=v3, |
51 | - arg_set=PT_V3), } |
52 | + arg_set=PASSWORD_V3)} |
53 | |
54 | |
55 | def load_keystone_creds(**kwargs): |
56 | @@ -68,7 +82,7 @@ def load_keystone_creds(**kwargs): |
57 | # take off 'os_' |
58 | short = lc[3:] |
59 | if short in kwargs: |
60 | - ret[lc] = kwargs.get(lc) |
61 | + ret[short] = kwargs.get(short) |
62 | elif name in os.environ: |
63 | # take off 'os_' |
64 | ret[short] = os.environ[name] |
65 | @@ -76,13 +90,18 @@ def load_keystone_creds(**kwargs): |
66 | if 'insecure' in ret: |
67 | if isinstance(ret['insecure'], str): |
68 | ret['insecure'] = (ret['insecure'].lower() not in |
69 | - ("", "0", "no", "off")) |
70 | + ("", "0", "no", "off", 'false')) |
71 | else: |
72 | ret['insecure'] = bool(ret['insecure']) |
73 | |
74 | + # verify is the key that is used by requests, and thus the Session object. |
75 | + # i.e. verify is either False or a certificate path or file. |
76 | + if not ret.get('insecure', False) and 'cacert' in ret: |
77 | + ret['verify'] = ret['cacert'] |
78 | + |
79 | missing = [] |
80 | for req in ('username', 'auth_url'): |
81 | - if not ret.get(req): |
82 | + if not ret.get(req, None): |
83 | missing.append(req) |
84 | |
85 | if not (ret.get('auth_token') or ret.get('password')): |
86 | @@ -91,14 +110,11 @@ def load_keystone_creds(**kwargs): |
87 | api_version = get_ks_api_version(ret.get('auth_url', '')) or 2 |
88 | if (api_version == 2 and |
89 | not (ret.get('tenant_id') or ret.get('tenant_name'))): |
90 | - raise ValueError("(tenant_id or tenant_name)") |
91 | - |
92 | - if (api_version == 3 and |
93 | - not (ret.get('user_domain_name') and |
94 | - ret.get('project_domain_name') and |
95 | - ret.get('project_name'))): |
96 | - raise ValueError("(user_domain_name, project_domain_name " |
97 | - "or project_name)") |
98 | + missing.append("(tenant_id or tenant_name)") |
99 | + if api_version == 3: |
100 | + for k in ('user_domain_name', 'project_domain_name', 'project_name'): |
101 | + if not ret.get(k, None): |
102 | + missing.append(k) |
103 | |
104 | if missing: |
105 | raise ValueError("Need values for: %s" % missing) |
106 | @@ -167,7 +183,9 @@ def get_ksclient(**kwargs): |
107 | # Filter/select the args for the api version from the kwargs dictionary |
108 | kskw = {k: v for k, v in kwargs.items() if k in arg_set} |
109 | auth = KS_VERSION_RESOLVER[api_version].ident.Password(**kskw) |
110 | - sess = session.Session(auth=auth) |
111 | + authkw = {k: v for k, v in kwargs.items() if k in SESSION_ARGS} |
112 | + authkw['auth'] = auth |
113 | + sess = session.Session(**authkw) |
114 | client = KS_VERSION_RESOLVER[api_version].mod.Client(session=sess) |
115 | client.auth_ref = auth.get_access(sess) |
116 | return client |
117 | diff --git a/tests/unittests/test_openstack.py b/tests/unittests/test_openstack.py |
118 | new file mode 100644 |
119 | index 0000000..beec9a2 |
120 | --- /dev/null |
121 | +++ b/tests/unittests/test_openstack.py |
122 | @@ -0,0 +1,265 @@ |
123 | +import mock |
124 | +import unittest |
125 | + |
126 | +import simplestreams.openstack as s_openstack |
127 | + |
128 | + |
129 | +class TestOpenStack(unittest.TestCase): |
130 | + MOCK_OS_VARS = { |
131 | + 'OS_AUTH_TOKEN': 'a-token', |
132 | + 'OS_AUTH_URL': 'http://0.0.0.0/v2.0', |
133 | + 'OS_CACERT': 'some-cert', |
134 | + 'OS_IMAGE_API_VERSION': '2', |
135 | + 'OS_IMAGE_URL': 'http://1.2.3.4:9292/', |
136 | + 'OS_PASSWORD': 'some-password', |
137 | + 'OS_REGION_NAME': 'region1', |
138 | + 'OS_STORAGE_URL': 'http://1.2.3.4:8080/v1/AUTH_123456', |
139 | + 'OS_TENANT_ID': '123456789', |
140 | + 'OS_TENANT_NAME': 'a-project', |
141 | + 'OS_USERNAME': 'openstack', |
142 | + 'OS_INSECURE': 'true', |
143 | + 'OS_USER_DOMAIN_NAME': 'default', |
144 | + 'OS_PROJECT_DOMAIN_NAME': 'Default', |
145 | + 'OS_USER_DOMAIN_ID': 'default', |
146 | + 'OS_PROJECT_DOMAIN_ID': 'default', |
147 | + 'OS_PROJECT_NAME': 'some-project', |
148 | + 'OS_PROJECT_ID': 'project-id', |
149 | + } |
150 | + |
151 | + def test_load_keystone_creds_V2_from_osvars(self): |
152 | + |
153 | + with mock.patch('os.environ', new=self.MOCK_OS_VARS.copy()): |
154 | + creds = s_openstack.load_keystone_creds() |
155 | + |
156 | + self.assertEquals(creds, |
157 | + {'auth_token': 'a-token', |
158 | + 'auth_url': 'http://0.0.0.0/v2.0', |
159 | + 'cacert': 'some-cert', |
160 | + 'image_api_version': '2', |
161 | + 'image_url': 'http://1.2.3.4:9292/', |
162 | + 'insecure': True, |
163 | + 'password': 'some-password', |
164 | + 'project_domain_id': 'default', |
165 | + 'project_domain_name': 'Default', |
166 | + 'project_id': 'project-id', |
167 | + 'project_name': 'some-project', |
168 | + 'region_name': 'region1', |
169 | + 'storage_url': 'http://1.2.3.4:8080/v1/AUTH_123456', |
170 | + 'tenant_id': '123456789', |
171 | + 'tenant_name': 'a-project', |
172 | + 'user_domain_id': 'default', |
173 | + 'user_domain_name': 'default', |
174 | + 'username': 'openstack'}) |
175 | + |
176 | + def test_load_keystone_creds_V2_from_kwargs(self): |
177 | + |
178 | + with mock.patch('os.environ', new=self.MOCK_OS_VARS.copy()): |
179 | + creds = s_openstack.load_keystone_creds( |
180 | + password='the-password', |
181 | + username='myuser') |
182 | + |
183 | + self.assertEquals(creds, |
184 | + {'auth_token': 'a-token', |
185 | + 'auth_url': 'http://0.0.0.0/v2.0', |
186 | + 'cacert': 'some-cert', |
187 | + 'image_api_version': '2', |
188 | + 'image_url': 'http://1.2.3.4:9292/', |
189 | + 'insecure': True, |
190 | + 'password': 'the-password', |
191 | + 'project_domain_id': 'default', |
192 | + 'project_domain_name': 'Default', |
193 | + 'project_id': 'project-id', |
194 | + 'project_name': 'some-project', |
195 | + 'region_name': 'region1', |
196 | + 'storage_url': 'http://1.2.3.4:8080/v1/AUTH_123456', |
197 | + 'tenant_id': '123456789', |
198 | + 'tenant_name': 'a-project', |
199 | + 'user_domain_id': 'default', |
200 | + 'user_domain_name': 'default', |
201 | + 'username': 'myuser'}) |
202 | + |
203 | + def test_load_keystone_creds_V3_from_osvars(self): |
204 | + v3kwargs = self.MOCK_OS_VARS.copy() |
205 | + v3kwargs['OS_AUTH_URL'] = 'http://0.0.0.0/v3' |
206 | + |
207 | + with mock.patch('os.environ', new=v3kwargs): |
208 | + creds = s_openstack.load_keystone_creds() |
209 | + |
210 | + self.assertEquals(creds, |
211 | + {'auth_token': 'a-token', |
212 | + 'auth_url': 'http://0.0.0.0/v3', |
213 | + 'cacert': 'some-cert', |
214 | + 'image_api_version': '2', |
215 | + 'image_url': 'http://1.2.3.4:9292/', |
216 | + 'insecure': True, |
217 | + 'password': 'some-password', |
218 | + 'project_domain_id': 'default', |
219 | + 'project_domain_name': 'Default', |
220 | + 'project_id': 'project-id', |
221 | + 'project_name': 'some-project', |
222 | + 'region_name': 'region1', |
223 | + 'storage_url': 'http://1.2.3.4:8080/v1/AUTH_123456', |
224 | + 'tenant_id': '123456789', |
225 | + 'tenant_name': 'a-project', |
226 | + 'user_domain_id': 'default', |
227 | + 'user_domain_name': 'default', |
228 | + 'username': 'openstack'}) |
229 | + |
230 | + def test_load_keystone_creds_insecure(self): |
231 | + """test load_keystone_creds behaves correctly for OS_INSECURE values. |
232 | + """ |
233 | + kwargs = self.MOCK_OS_VARS.copy() |
234 | + test_pairs = (('off', False), |
235 | + ('no', False), |
236 | + ('false', False), |
237 | + ('', False), |
238 | + ('anything-else', True)) |
239 | + for val, expected in test_pairs: |
240 | + kwargs['OS_INSECURE'] = val |
241 | + with mock.patch('os.environ', new=kwargs): |
242 | + creds = s_openstack.load_keystone_creds() |
243 | + self.assertEqual(creds['insecure'], expected) |
244 | + |
245 | + def test_load_keystone_creds_verify(self): |
246 | + """Test that cacert comes across as verify.""" |
247 | + kwargs = self.MOCK_OS_VARS.copy() |
248 | + |
249 | + with mock.patch('os.environ', new=kwargs): |
250 | + creds = s_openstack.load_keystone_creds() |
251 | + self.assertNotIn('verify', creds) |
252 | + |
253 | + kwargs['OS_INSECURE'] = 'false' |
254 | + with mock.patch('os.environ', new=kwargs): |
255 | + creds = s_openstack.load_keystone_creds() |
256 | + self.assertEqual(creds['verify'], kwargs['OS_CACERT']) |
257 | + |
258 | + kwargs['OS_INSECURE'] = 'false' |
259 | + del kwargs['OS_CACERT'] |
260 | + with mock.patch('os.environ', new=kwargs): |
261 | + creds = s_openstack.load_keystone_creds() |
262 | + self.assertNotIn('verify', creds) |
263 | + |
264 | + def test_load_keystone_creds_missing(self): |
265 | + kwargs = self.MOCK_OS_VARS.copy() |
266 | + del kwargs['OS_USERNAME'] |
267 | + with mock.patch('os.environ', new=kwargs): |
268 | + with self.assertRaises(ValueError): |
269 | + s_openstack.load_keystone_creds() |
270 | + |
271 | + kwargs = self.MOCK_OS_VARS.copy() |
272 | + del kwargs['OS_AUTH_URL'] |
273 | + with mock.patch('os.environ', new=kwargs): |
274 | + with self.assertRaises(ValueError): |
275 | + s_openstack.load_keystone_creds() |
276 | + |
277 | + # either auth_token or password needs to be exist, but if both are |
278 | + # missing then raise an exception |
279 | + kwargs = self.MOCK_OS_VARS.copy() |
280 | + del kwargs['OS_AUTH_TOKEN'] |
281 | + with mock.patch('os.environ', new=kwargs): |
282 | + s_openstack.load_keystone_creds() |
283 | + kwargs = self.MOCK_OS_VARS.copy() |
284 | + del kwargs['OS_PASSWORD'] |
285 | + with mock.patch('os.environ', new=kwargs): |
286 | + s_openstack.load_keystone_creds() |
287 | + kwargs = self.MOCK_OS_VARS.copy() |
288 | + del kwargs['OS_AUTH_TOKEN'] |
289 | + del kwargs['OS_PASSWORD'] |
290 | + with mock.patch('os.environ', new=kwargs): |
291 | + with self.assertRaises(ValueError): |
292 | + s_openstack.load_keystone_creds() |
293 | + |
294 | + # API version 3 |
295 | + for k in ('OS_USER_DOMAIN_NAME', |
296 | + 'OS_PROJECT_DOMAIN_NAME', |
297 | + 'OS_PROJECT_NAME'): |
298 | + kwargs = self.MOCK_OS_VARS.copy() |
299 | + kwargs['OS_AUTH_URL'] = 'http://0.0.0.0/v3' |
300 | + del kwargs[k] |
301 | + with self.assertRaises(ValueError): |
302 | + s_openstack.load_keystone_creds() |
303 | + |
304 | + @mock.patch.object(s_openstack, '_LEGACY_CLIENTS', new=False) |
305 | + @mock.patch.object(s_openstack.session, 'Session') |
306 | + def test_get_ksclient_v2(self, m_session): |
307 | + kwargs = self.MOCK_OS_VARS.copy() |
308 | + mock_ksclient_v2 = mock.Mock() |
309 | + mock_ident_v2 = mock.Mock() |
310 | + with mock.patch.object( |
311 | + s_openstack, 'KS_VERSION_RESOLVER', new={}) as m: |
312 | + m[2] = s_openstack.Settings(mod=mock_ksclient_v2, |
313 | + ident=mock_ident_v2, |
314 | + arg_set=s_openstack.PASSWORD_V2) |
315 | + |
316 | + # test openstack ks 2 |
317 | + m_auth = mock.Mock() |
318 | + mock_ident_v2.Password.return_value = m_auth |
319 | + m_get_access = mock.Mock() |
320 | + m_auth.get_access.return_value = m_get_access |
321 | + m_session.return_value = mock.sentinel.session |
322 | + |
323 | + with mock.patch('os.environ', new=kwargs): |
324 | + creds = s_openstack.load_keystone_creds() |
325 | + |
326 | + c = s_openstack.get_ksclient(**creds) |
327 | + # verify that mock_ident_v2 is called with password |
328 | + mock_ident_v2.Password.assert_has_calls([ |
329 | + mock.call(auth_url='http://0.0.0.0/v2.0', |
330 | + password='some-password', |
331 | + tenant_id='123456789', |
332 | + tenant_name='a-project', |
333 | + username='openstack')]) |
334 | + # verify that the session was called with the v2 password |
335 | + m_session.assert_called_once_with(auth=m_auth) |
336 | + # verify that the client was called with the session |
337 | + mock_ksclient_v2.Client.assert_called_once_with( |
338 | + session=mock.sentinel.session) |
339 | + # finally check that the client as an auth_ref and that it contains |
340 | + # the get_access() call |
341 | + self.assertEqual(c.auth_ref, m_get_access) |
342 | + m_auth.get_access.assert_called_once_with(mock.sentinel.session) |
343 | + |
344 | + @mock.patch.object(s_openstack, '_LEGACY_CLIENTS', new=False) |
345 | + @mock.patch.object(s_openstack.session, 'Session') |
346 | + def test_get_ksclient_v3(self, m_session): |
347 | + kwargs = self.MOCK_OS_VARS.copy() |
348 | + kwargs['OS_AUTH_URL'] = 'http://0.0.0.0/v3' |
349 | + mock_ksclient_v3 = mock.Mock() |
350 | + mock_ident_v3 = mock.Mock() |
351 | + with mock.patch.object( |
352 | + s_openstack, 'KS_VERSION_RESOLVER', new={}) as m: |
353 | + m[3] = s_openstack.Settings(mod=mock_ksclient_v3, |
354 | + ident=mock_ident_v3, |
355 | + arg_set=s_openstack.PASSWORD_V3) |
356 | + |
357 | + # test openstack ks 3 |
358 | + m_auth = mock.Mock() |
359 | + mock_ident_v3.Password.return_value = m_auth |
360 | + m_get_access = mock.Mock() |
361 | + m_auth.get_access.return_value = m_get_access |
362 | + m_session.return_value = mock.sentinel.session |
363 | + |
364 | + with mock.patch('os.environ', new=kwargs): |
365 | + creds = s_openstack.load_keystone_creds() |
366 | + |
367 | + c = s_openstack.get_ksclient(**creds) |
368 | + # verify that mock_ident_v2 is called with password |
369 | + mock_ident_v3.Password.assert_has_calls([ |
370 | + mock.call(auth_url='http://0.0.0.0/v3', |
371 | + password='some-password', |
372 | + project_domain_id='default', |
373 | + project_domain_name='Default', |
374 | + project_id='project-id', |
375 | + project_name='some-project', |
376 | + user_domain_id='default', |
377 | + user_domain_name='default', |
378 | + username='openstack')]) |
379 | + # verify that the session was called with the v2 password |
380 | + m_session.assert_called_once_with(auth=m_auth) |
381 | + # verify that the client was called with the session |
382 | + mock_ksclient_v3.Client.assert_called_once_with( |
383 | + session=mock.sentinel.session) |
384 | + # finally check that the client as an auth_ref and that it contains |
385 | + # the get_access() call |
386 | + self.assertEqual(c.auth_ref, m_get_access) |
387 | + m_auth.get_access.assert_called_once_with(mock.sentinel.session) |
Hey, Thanks
This does look good.
I know there isnt much unit tests in place, but it would be good to add some here.
It seems you could reasonably easily add some tests to get_ksclient that the session it created was right. I'm not sure if you end up needing to mock session.Session or not for that, but either way it shoudlnt be too bad.
thanks.