Merge ~ajkavanagh/simplestreams:bug/1802407 into simplestreams:master

Proposed by Alex Kavanagh
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)
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/openstack.py

This change enables the openstack integration to connect to https
OpenStack endpoints by using the OS_CACERT environement variable.

LP: #1802407

To post a comment you must log in.
Revision history for this message
Scott Moser (smoser) wrote :

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.

review: Needs Fixing
Revision history for this message
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

Revision history for this message
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.

Revision history for this message
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.

Revision history for this message
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://bugs.launchpad.net/charm-glance-simplestreams-sync/+bug/1802407

Essentially, this change is needed by: https://review.openstack.org/#/c/623488/

Hope that's okay!?

2423d4d... by Alex Kavanagh

Update tests following review comments

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
Scott Moser (smoser) :
review: Approve
Revision history for this message
Alex Kavanagh (ajkavanagh) wrote :

Thanks for the merging the code!

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/.gitignore b/.gitignore
2index 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
12diff --git a/simplestreams/openstack.py b/simplestreams/openstack.py
13index 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
117diff --git a/tests/unittests/test_openstack.py b/tests/unittests/test_openstack.py
118new file mode 100644
119index 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)

Subscribers

People subscribed via source and target branches