Merge lp:~mskalka/juju-ci-tools/assess_add_credentials into lp:juju-ci-tools

Proposed by Michael Skalka
Status: Merged
Merged at revision: 2002
Proposed branch: lp:~mskalka/juju-ci-tools/assess_add_credentials
Merge into: lp:juju-ci-tools
Diff against target: 626 lines (+617/-0)
2 files modified
assess_add_credentials.py (+307/-0)
tests/test_assess_add_credentials.py (+310/-0)
To merge this branch: bzr merge lp:~mskalka/juju-ci-tools/assess_add_credentials
Reviewer Review Type Date Requested Status
Curtis Hovey (community) code Approve
Review via email: mp+322596@code.launchpad.net

Description of the change

Adds assess_add_credentials.

This test pulls in credential information from cloud-city then manually adds those credentials to a new temporary JUJU_HOME, verifies they were written properly, and attempts to bootstrap using those those credentials from the temp directory.

The test will fail if the credentials inputted do not match the supplied credentials, or if the client cannot bootstrap with the credentials.

Currently this test will always fail on GCE as reported by bug #1682562.

To post a comment you must log in.
Revision history for this message
Curtis Hovey (sinzui) wrote :

Thank you. I have some suggestions inline to make the script run on all hosts.

review: Approve (code)
2000. By Michael Skalka

changes as per MP comments

Revision history for this message
Michael Skalka (mskalka) wrote :

Made the suggested changes.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'assess_add_credentials.py'
2--- assess_add_credentials.py 1970-01-01 00:00:00 +0000
3+++ assess_add_credentials.py 2017-04-17 14:06:57 +0000
4@@ -0,0 +1,307 @@
5+#!/usr/bin/env python
6+"""Assess proper functionality of juju add-credential."""
7+
8+from __future__ import print_function
9+
10+import argparse
11+import logging
12+import sys
13+import os
14+import yaml
15+import pexpect
16+import shutil
17+
18+from deploy_stack import (
19+ BootstrapManager,
20+ )
21+from utility import (
22+ configure_logging,
23+ add_basic_testing_arguments,
24+ JujuAssertionError,
25+ temp_dir,
26+ )
27+
28+__metaclass__ = type
29+
30+
31+log = logging.getLogger("assess_add_credentials")
32+
33+script_dir = os.path.dirname(__file__)
34+cloud_city = os.path.join(script_dir, 'cloud-city')
35+
36+
37+def assess_add_credentials(args):
38+ """Tests if juju's add-credentials command works as expected.
39+
40+ Adds credentials from our real source to our juju client and tests if
41+ that client can bootstrap.
42+
43+ :param client: Client object used in bootstrap check
44+ :param args: Test arguments
45+ """
46+
47+ if 'vmaas' in args.env:
48+ env = 'maas'
49+ elif 'gce' in args.env:
50+ env = 'google'
51+ else:
52+ env = args.env.split('parallel-')[1]
53+
54+ # Grab the credentials from cloud-city
55+ cred = get_credentials(env)
56+
57+ # Fix the keypath to reflect local username
58+ key_path = cred['credentials'].get('private-key-path')
59+ if key_path:
60+ cred['credentials']['private-key-path'] = os.path.join(
61+ cloud_city, '{}-key'.format(env))
62+
63+ verify_add_credentials(args, env, cred)
64+ verify_credentials_match(env, cred)
65+ verify_bootstrap(args)
66+
67+ log.info('SUCCESS')
68+
69+
70+def verify_add_credentials(args, env, cred):
71+ """Adds the supplied credential to juju with 'juju add-credential'.
72+
73+ :param args: Testing arguments
74+ :param env: String environment name
75+ :param cred: Dict of credential information
76+ """
77+ testing_variations = {
78+ 'aws': add_aws,
79+ 'google': add_gce,
80+ 'rackspace': add_rackspace,
81+ 'maas': add_maas,
82+ 'joyent': add_joyent,
83+ 'azure': add_azure
84+ }
85+
86+ log.info("Adding {} credential from /cloud-city/credentials.yaml "
87+ "into testing instance".format(args.env))
88+ with pexpect.spawn('juju add-credential {}'.format(env)) as child:
89+ try:
90+ testing_variations[env](child, env, cred)
91+ except pexpect.TIMEOUT:
92+ log.error('Buffer: {}'.format(child.buffer))
93+ log.error('Before: {}'.format(child.before))
94+ raise Exception(
95+ 'Registering user failed: pexpect session timed out')
96+
97+
98+def get_credentials(env):
99+ """Gets the stored test credentials.
100+
101+ :return: Dict of credential information
102+ """
103+ with open(os.path.join(cloud_city, 'credentials.yaml')) as f:
104+ creds_dict = yaml.load(f)
105+ cred = creds_dict['credentials'][env]
106+ return cred
107+
108+
109+def verify_bootstrap(args):
110+ """Verify the client can bootstrap with the newly added credentials
111+
112+ :param args: Testing arguments
113+ """
114+ env_file = os.path.join(
115+ os.environ['HOME'], 'cloud-city', 'environments.yaml')
116+ shutil.copy(env_file, os.environ['JUJU_DATA'])
117+ bs_manager = BootstrapManager.from_args(args)
118+ with bs_manager.booted_context(args.upload_tools):
119+ log.info('Bootstrap successfull, tearing down client')
120+
121+
122+def verify_credentials_match(env, cred):
123+ """Verify the credentials entered match the stored credentials.
124+
125+ :param env: String environment name
126+ :param cred: Dict of credential information
127+ """
128+ with open(os.path.join(os.environ['JUJU_DATA'], 'credentials.yaml')) as f:
129+ test_creds = yaml.load(f)
130+ test_creds = test_creds['credentials'][env][env]
131+ if not test_creds == cred['credentials']:
132+ error = 'Credential miss-match after manual add'
133+ raise JujuAssertionError(error)
134+
135+
136+def end_session(session):
137+ """Convenience function to check a pexpect session has properly closed.
138+ """
139+ session.expect(pexpect.EOF)
140+ session.close()
141+ if session.exitstatus != 0:
142+ log.error('Buffer: {}'.format(session.buffer))
143+ log.error('Before: {}'.format(session.before))
144+ raise Exception('pexpect process exited with {}'.format(
145+ session.exitstatus))
146+
147+
148+def add_aws(child, env, cred):
149+ """Adds credentials for AWS to test client using real credentials.
150+
151+ :param child: pexpect.spawn object of the juju add-credential command
152+ :param env: String environment name
153+ :param cred: Dict of credential information
154+ """
155+ access_key = cred['credentials']['access-key']
156+ secret_key = cred['credentials']['secret-key']
157+
158+ child.expect('Enter credential name:')
159+ child.sendline(env)
160+ child.expect('Enter access-key:')
161+ child.sendline(access_key)
162+ child.expect('Enter secret-key:')
163+ child.sendline(secret_key)
164+ end_session(child)
165+ log.info('Added AWS credential')
166+
167+
168+def add_gce(child, env, cred):
169+ """Adds credentials for GCE to test client using real credentials.
170+
171+ :param child: pexpect.spawn object of the juju add-credential command
172+ :param env: String environment name
173+ :param cred: Dict of credential information
174+ """
175+ auth_type = cred['credentials']['auth-type']
176+ project_id = cred['credentials']['project-id']
177+ private_key = cred['credentials']['private-key']
178+ client_email = cred['credentials']['client-email']
179+ client_id = cred['credentials']['client-id']
180+
181+ child.expect('Enter credential name:')
182+ child.sendline(env)
183+ child.expect('Select auth-type:')
184+ child.sendline(auth_type)
185+ child.expect('Enter client-id:')
186+ child.sendline(client_id)
187+ child.expect('Enter client-email:')
188+ child.sendline(client_email)
189+ child.expect('Enter private-key:')
190+ child.send(private_key)
191+ child.sendline('')
192+ child.expect('Enter project-id:')
193+ child.sendline(project_id)
194+ end_session(child)
195+ log.info('Added GCE credential')
196+
197+
198+def add_rackspace(child, env, cred):
199+ """Adds credentials for Rackspace to test client using real credentials.
200+
201+ :param child: pexpect.spawn object of the juju add-credential command
202+ :param env: String environment name
203+ :param cred: Dict of credential information
204+ """
205+ username = cred['credentials']['username']
206+ password = cred['credentials']['password']
207+ tenant_name = cred['credentials']['tenant-name']
208+
209+ child.expect('Enter credential name:')
210+ child.sendline(env)
211+ child.expect('Enter username:')
212+ child.sendline(username)
213+ child.expect('Enter password:')
214+ child.sendline(password)
215+ child.expect('Enter tenant-name:')
216+ child.sendline(tenant_name)
217+ end_session(child)
218+ log.info('Added Rackspace credential')
219+
220+
221+def add_maas(child, env, cred):
222+ """Adds credentials for MaaS to test client using real credentials.
223+
224+ :param child: pexpect.spawn object of the juju add-credential command
225+ :param env: String environment name
226+ :param cred: Dict of credential information
227+ """
228+ maas_oauth = cred['credentials']['maas-oauth']
229+
230+ child.expect('Enter credential name:')
231+ child.sendline(env)
232+ child.expect('Enter maas-oauth:')
233+ child.sendline(maas_oauth)
234+ end_session(child)
235+ log.info('Added MaaS credential')
236+
237+
238+def add_joyent(child, env, cred):
239+ """Adds credentials for Joyent to test client using real credentials.
240+
241+ :param child: pexpect.spawn object of the juju add-credential command
242+ :param env: String environment name
243+ :param cred: Dict of credential information
244+ """
245+ algorithm = cred['credentials']['algorithm']
246+ sdc_user = cred['credentials']['sdc-user']
247+ sdc_key_id = cred['credentials']['sdc-key-id']
248+ private_key_path = os.path.join(
249+ os.environ['HOME'], 'cloud-city', 'joyent-key')
250+
251+ child.expect('Enter credential name:')
252+ child.sendline(env)
253+ child.expect('Enter sdc-user:')
254+ child.sendline(sdc_user)
255+ child.expect('Enter sdc-key-id:')
256+ child.sendline(sdc_key_id)
257+ child.expect('Enter private-key-path:')
258+ child.sendline(private_key_path)
259+ child.expect(',rsa-sha512]:')
260+ child.sendline(algorithm)
261+ end_session(child)
262+ log.info('Added Joyent credential')
263+
264+
265+def add_azure(child, env, cred):
266+ """Adds credentials for Azure to test client using real credentials.
267+
268+ :param child: pexpect.spawn object of the juju add-credential command
269+ :param env: String environment name
270+ :param cred: Dict of credential information
271+ """
272+ auth_type = cred['credentials']['auth-type']
273+ application_id = cred['credentials']['application-id']
274+ subscription_id = cred['credentials']['subscription-id']
275+ application_password = cred['credentials']['application-password']
276+
277+ child.expect('Enter credential name:')
278+ child.sendline(env)
279+ child.expect('Select auth-type:')
280+ child.sendline(auth_type)
281+ child.expect('Enter application-id:')
282+ child.sendline(application_id)
283+ child.expect('Enter subscription-id:')
284+ child.sendline(subscription_id)
285+ child.expect('Enter application-password:')
286+ child.sendline(application_password)
287+ end_session(child)
288+ log.info('Added Azure credential')
289+
290+
291+def parse_args(argv):
292+ """Parse all arguments."""
293+ parser = argparse.ArgumentParser(
294+ description='Test if juju properly adds credentials with the '
295+ 'add-credential command.')
296+ add_basic_testing_arguments(parser)
297+ return parser.parse_args(argv)
298+
299+
300+def main(argv=None):
301+ args = parse_args(argv)
302+ configure_logging(args.verbose)
303+ with temp_dir() as temp:
304+ os.environ['JUJU_HOME'] = temp
305+ os.environ['JUJU_DATA'] = temp
306+ assess_add_credentials(args)
307+ return 0
308+
309+
310+if __name__ == '__main__':
311+ sys.exit(main())
312
313=== added file 'tests/test_assess_add_credentials.py'
314--- tests/test_assess_add_credentials.py 1970-01-01 00:00:00 +0000
315+++ tests/test_assess_add_credentials.py 2017-04-17 14:06:57 +0000
316@@ -0,0 +1,310 @@
317+"""Tests for assess_destroy_model module."""
318+
319+import logging
320+import StringIO
321+import yaml
322+import os
323+import io
324+
325+from mock import (
326+ Mock,
327+ call,
328+ patch,
329+ mock_open
330+ )
331+from assess_add_credentials import (
332+ verify_add_credentials,
333+ get_credentials,
334+ verify_bootstrap,
335+ verify_credentials_match,
336+ add_aws,
337+ add_gce,
338+ add_maas,
339+ add_azure,
340+ add_joyent,
341+ add_rackspace,
342+ parse_args,
343+ main,
344+ )
345+from tests import (
346+ parse_error,
347+ TestCase,
348+ )
349+from utility import (
350+ JujuAssertionError,
351+ )
352+
353+
354+dummy_test_pass = u"""
355+ credentials:
356+ aws:
357+ aws:
358+ auth-type: access-key
359+ access-key: foo
360+ secret-key: verysecret-key"""
361+
362+dummy_test_fail = u"""
363+ credentials:
364+ aws:
365+ aws:
366+ auth-type: access-key
367+ access-key: bar
368+ secret-key: verysecret-key"""
369+
370+dummy_creds = u"""
371+ credentials:
372+ aws:
373+ credentials:
374+ auth-type: access-key
375+ access-key: foo
376+ secret-key: verysecret-key
377+ azure:
378+ credentials:
379+ auth-type: service-principal-secret
380+ application-id: foo-bar-baz
381+ application-password: somepass
382+ subscription-id: someid
383+ google:
384+ credentials:
385+ auth-type: oauth2
386+ client-email: foo@developer.gserviceaccount.com
387+ client-id: foo.apps.googleusercontent.com
388+ private-key: |
389+ -----BEGIN PRIVATE KEY-----
390+ somekeyfoo
391+ -----END PRIVATE KEY-----
392+ project-id: gothic-list-89514
393+ joyent:
394+ credentials:
395+ auth-type: userpass
396+ algorithm: rsa-sha256
397+ private-key: |
398+ -----BEGIN RSA PRIVATE KEY-----
399+ somekeybar
400+ -----END RSA PRIVATE KEY-----
401+ sdc-key-id: AA:AA
402+ sdc-user: dummyuser
403+ maas:
404+ credentials:
405+ auth-type: oauth1
406+ maas-oauth: EXAMPLE:DUMMY:OAUTH
407+ rackspace:
408+ credentials:
409+ auth-type: userpass
410+ password: somepass
411+ tenant-name: "123456789"
412+ username: userfoo
413+ """
414+
415+dummy_loaded = yaml.load(dummy_creds)
416+
417+
418+class TestParseArgs(TestCase):
419+
420+ def test_common_args(self):
421+ args = parse_args(["an-env", "/bin/juju", "/tmp/logs", "an-env-mod"])
422+ self.assertEqual("an-env", args.env)
423+ self.assertEqual("/bin/juju", args.juju_bin)
424+ self.assertEqual("/tmp/logs", args.logs)
425+ self.assertEqual("an-env-mod", args.temp_env_name)
426+ self.assertEqual(False, args.debug)
427+
428+ def test_help(self):
429+ fake_stdout = StringIO.StringIO()
430+ with parse_error(self) as fake_stderr:
431+ with patch("sys.stdout", fake_stdout):
432+ parse_args(["--help"])
433+ self.assertEqual("", fake_stderr.getvalue())
434+ self.assertNotIn("TODO", fake_stdout.getvalue())
435+
436+
437+class TestMain(TestCase):
438+
439+ def test_main(self):
440+ argv = ["an-env", "/bin/juju", "/tmp/logs", "an-env-mod", "--verbose"]
441+ args = parse_args(argv)
442+ with patch("assess_add_credentials.configure_logging",
443+ autospec=True) as mock_cl:
444+ with patch("assess_add_credentials.assess_add_credentials",
445+ autospec=True) as mock_assess:
446+ main(argv)
447+ mock_cl.assert_called_once_with(logging.DEBUG)
448+ mock_assess.assert_called_once_with(args)
449+
450+
451+class TestAssess(TestCase):
452+
453+ def test_verify_add_credentials(self):
454+ argv = ["an-env", "/bin/juju", "/tmp/logs", "an-env-mod", "--verbose"]
455+ args = parse_args(argv)
456+ cred = dummy_loaded['credentials']['aws']
457+ with patch('pexpect.spawn', autospec=True) as mock_assess:
458+ with patch('assess_add_credentials.end_session',
459+ return_value=None):
460+ verify_add_credentials(args, 'aws', cred)
461+ self.assertEqual(
462+ [call('juju add-credential aws'),
463+ call().__enter__(),
464+ call().__enter__().expect('Enter credential name:'),
465+ call().__enter__().sendline('aws'),
466+ call().__enter__().expect('Enter access-key:'),
467+ call().__enter__().sendline('foo'),
468+ call().__enter__().expect('Enter secret-key:'),
469+ call().__enter__().sendline('verysecret-key'),
470+ call().__exit__(None, None, None)],
471+ mock_assess.mock_calls)
472+
473+ def test_get_credentials(self):
474+ m = mock_open()
475+ with patch('assess_add_credentials.open', m, create=True) as o:
476+ o.return_value = io.StringIO(dummy_creds)
477+ with patch.dict(os.environ, {'HOME': '/'}):
478+ out = get_credentials('aws')
479+ self.assertEqual(out, dummy_loaded['credentials']['aws'])
480+
481+ def test_verify_bootstrap(self):
482+ argv = ["an-env", "/bin/juju", "/tmp/logs", "an-env-mod", "--verbose"]
483+ client = Mock(spec=["is_jes_enabled"])
484+ args = parse_args(argv)
485+ with patch.dict(os.environ, {'JUJU_DATA': '/', 'HOME': '/'}):
486+ with patch('shutil.copy', autospec=True, return_value=None):
487+ with patch(
488+ "assess_destroy_model.BootstrapManager.booted_context",
489+ autospec=True) as mock_bc:
490+ with patch('deploy_stack.client_from_config',
491+ return_value=client) as mock_cfc:
492+ verify_bootstrap(args)
493+ mock_cfc.assert_called_once_with('an-env', "/bin/juju", debug=False,
494+ soft_deadline=None)
495+ self.assertEqual(mock_bc.call_count, 1)
496+
497+ def test_verify_credentials_match_pass(self):
498+ m = mock_open()
499+ with patch('assess_add_credentials.open', m, create=True) as o:
500+ o.return_value = io.StringIO(dummy_test_pass)
501+ with patch.dict(os.environ, {'JUJU_DATA': '/'}):
502+ verify_credentials_match(
503+ 'aws', dummy_loaded['credentials']['aws'])
504+
505+ def test_verify_credentials_match_fail(self):
506+ m = mock_open()
507+ creds = dummy_loaded['credentials']['aws']
508+ with patch('assess_add_credentials.open', m, create=True) as o:
509+ o.return_value = io.StringIO(dummy_test_fail)
510+ with patch.dict(os.environ, {'JUJU_DATA': '/'}):
511+ pattern = 'Credential miss-match after manual add'
512+ with self.assertRaisesRegexp(JujuAssertionError, pattern):
513+ verify_credentials_match('aws', creds)
514+
515+ def test_add_aws(self):
516+ env = 'aws'
517+ creds = dummy_loaded['credentials'][env]
518+ with patch('pexpect.spawn', autospec=True) as mock_assess:
519+ with patch('assess_add_credentials.end_session',
520+ return_value=None):
521+ add_aws(mock_assess, env, creds)
522+ self.assertEqual(
523+ [call.expect('Enter credential name:'),
524+ call.sendline('aws'),
525+ call.expect('Enter access-key:'),
526+ call.sendline('foo'),
527+ call.expect('Enter secret-key:'),
528+ call.sendline('verysecret-key')],
529+ mock_assess.mock_calls)
530+
531+ def test_add_gce(self):
532+ env = 'google'
533+ creds = dummy_loaded['credentials'][env]
534+ with patch('pexpect.spawn', autospec=True) as mock_assess:
535+ with patch('assess_add_credentials.end_session',
536+ return_value=None):
537+ add_gce(mock_assess, env, creds)
538+ self.assertEqual(
539+ [call.expect('Enter credential name:'),
540+ call.sendline('google'),
541+ call.expect('Select auth-type:'),
542+ call.sendline('oauth2'),
543+ call.expect('Enter client-id:'),
544+ call.sendline('foo.apps.googleusercontent.com'),
545+ call.expect('Enter client-email:'),
546+ call.sendline('foo@developer.gserviceaccount.com'),
547+ call.expect('Enter private-key:'),
548+ call.send('-----BEGIN PRIVATE KEY-----\n'
549+ 'somekeyfoo\n-----END PRIVATE KEY-----\n'),
550+ call.sendline(''),
551+ call.expect('Enter project-id:'),
552+ call.sendline('gothic-list-89514')],
553+ mock_assess.mock_calls)
554+
555+ def test_add_maas(self):
556+ env = 'maas'
557+ creds = dummy_loaded['credentials'][env]
558+ with patch('pexpect.spawn', autospec=True) as mock_assess:
559+ with patch('assess_add_credentials.end_session',
560+ return_value=None):
561+ add_maas(mock_assess, env, creds)
562+ self.assertEqual(
563+ [call.expect('Enter credential name:'),
564+ call.sendline('maas'),
565+ call.expect('Enter maas-oauth:'),
566+ call.sendline('EXAMPLE:DUMMY:OAUTH')],
567+ mock_assess.mock_calls)
568+
569+ def test_add_azure(self):
570+ env = 'azure'
571+ creds = dummy_loaded['credentials'][env]
572+ with patch('pexpect.spawn', autospec=True) as mock_assess:
573+ with patch('assess_add_credentials.end_session',
574+ return_value=None):
575+ add_azure(mock_assess, env, creds)
576+ self.assertEqual(
577+ [call.expect('Enter credential name:'),
578+ call.sendline('azure'),
579+ call.expect('Select auth-type:'),
580+ call.sendline('service-principal-secret'),
581+ call.expect('Enter application-id:'),
582+ call.sendline('foo-bar-baz'),
583+ call.expect('Enter subscription-id:'),
584+ call.sendline('someid'),
585+ call.expect('Enter application-password:'),
586+ call.sendline('somepass')],
587+ mock_assess.mock_calls)
588+
589+ def test_add_joyent(self):
590+ env = 'joyent'
591+ creds = dummy_loaded['credentials'][env]
592+ with patch('pexpect.spawn', autospec=True) as mock_assess:
593+ with patch('assess_add_credentials.end_session',
594+ return_value=None):
595+ with patch.dict(os.environ, {'HOME': '/'}):
596+ add_joyent(mock_assess, env, creds)
597+ self.assertEqual(
598+ [call.expect('Enter credential name:'),
599+ call.sendline('joyent'),
600+ call.expect('Enter sdc-user:'),
601+ call.sendline('dummyuser'),
602+ call.expect('Enter sdc-key-id:'),
603+ call.sendline('AA:AA'),
604+ call.expect('Enter private-key-path:'),
605+ call.sendline('/cloud-city/joyent-key'),
606+ call.expect(',rsa-sha512]:'),
607+ call.sendline('rsa-sha256')],
608+ mock_assess.mock_calls)
609+
610+ def test_add_rackspace(self):
611+ env = 'rackspace'
612+ creds = dummy_loaded['credentials'][env]
613+ with patch('pexpect.spawn', autospec=True) as mock_assess:
614+ with patch('assess_add_credentials.end_session',
615+ return_value=None):
616+ add_rackspace(mock_assess, env, creds)
617+ self.assertEqual(
618+ [call.expect('Enter credential name:'),
619+ call.sendline('rackspace'),
620+ call.expect('Enter username:'),
621+ call.sendline('userfoo'),
622+ call.expect('Enter password:'),
623+ call.sendline('somepass'),
624+ call.expect('Enter tenant-name:'),
625+ call.sendline('123456789')],
626+ mock_assess.mock_calls)

Subscribers

People subscribed via source and target branches