Merge lp:~ankatare/juju-ci-tools/juju-aws-add-credential into lp:juju-ci-tools

Proposed by ABHAY
Status: Needs review
Proposed branch: lp:~ankatare/juju-ci-tools/juju-aws-add-credential
Merge into: lp:juju-ci-tools
Diff against target: 412 lines (+381/-0)
4 files modified
assess_add_credentials.py (+136/-0)
jujupy/client.py (+11/-0)
jujupy/tests/test_client.py (+23/-0)
tests/test_assess_add_credentials.py (+211/-0)
To merge this branch: bzr merge lp:~ankatare/juju-ci-tools/juju-aws-add-credential
Reviewer Review Type Date Requested Status
Christopher Lee (community) Approve
ABHAY (community) Approve
Review via email: mp+314534@code.launchpad.net

Commit message

code changes for aws add-credential --in progess

Description of the change

First stage coding is done for review; modified most of the code.

To post a comment you must log in.
Revision history for this message
Christopher Lee (veebers) wrote :

Some overall comments:
  - run "make lint" often, it will catch any of the smaller errors that might creep in (not there is a couple issues with this branch it will catch).

  - Please don't delete your branch and re-propose unless it's really needed (in this case we lose the previous comments from the original PR).

  - Tests are needed for the methods added. In most cases you'll have more joy if you write the tests first, among other things it helps you layout the structure of the code before you start implementing things.

I've had to repeat a couple of comments here, please if you disagree with a comment or have a question about it please comment why that is so we can have a discussion etc. (another reason not to delete the merge proposal).

Please see inline comments regarding code specifics.

review: Needs Fixing
Revision history for this message
Christopher Lee (veebers) wrote :

As requested in the previous review, please write tests for the methods added to jujupy and to cover the methods added to assess_add_credentials.py.

Please see inline comments for further review comments.

review: Needs Fixing
Revision history for this message
Christopher Lee (veebers) wrote :

Added missing comment.

Revision history for this message
Christopher Lee (veebers) wrote :

Added further comment from discussion in our meeting.

Revision history for this message
Christopher Lee (veebers) wrote :

This is looking good. Just a couple of minor comments.

Don't forget that there needs to be tests.
Also, don't forget to run 'make lint' before pushing. There is currently 1 lint error in this branch.

review: Needs Fixing
Revision history for this message
Christopher Lee (veebers) wrote :

Running the tests I get a fail: http://pastebin.ubuntu.com/23855616/
This is related to test_get_aws_credentials_from_file's call to get_aws_credentials_from_file actually trying to read that file but it doesn't actually exist (it might for you on you machine?).
I've included notes on resolving this inline with the test.

I see that tests for changes to jujupy/client.py are still incoming.

review: Needs Fixing
Revision history for this message
viswesuwara nathan (viswesn) wrote :

It's good to have unit testing for
1) assess_aws_credentials,
2) add_credential
3) list_credential.

I also specified couple of comments (see below)

Revision history for this message
ABHAY (ankatare) wrote :

> It's good to have unit testing for
> 1) assess_aws_credentials, ( started working on )
> 2) add_credential ( comment incorporated)
> 3) list_credential. ( comment incorporated)
>
> I also specified couple of comments (see below)

review: Approve
Revision history for this message
viswesuwara nathan (viswesn) wrote :

Find the review comments specified. Thanks

Revision history for this message
ABHAY (ankatare) wrote :

All comments incorporated.

Revision history for this message
viswesuwara nathan (viswesn) wrote :

Please find my review comments. And most of my earlier review comments were not addressed. Please address them now and I specified them now too.

Revision history for this message
ABHAY (ankatare) wrote :

All Review comments Incorporated Now

Revision history for this message
viswesuwara nathan (viswesn) wrote :

looks good.

Revision history for this message
Christopher Lee (veebers) wrote :

Your docstrings are using single quotes (') when they should be (") please remedy this. (it might seem pedantic but code keeping within the same coding standard is much easier to read and modify).

Docstrings descriptions and the param statements are intended to state what the intention of the function is and the param statements are there to state what the param is expected to look like (e.g. a list, dict, object of some type) as well as what it's used for (e.g. file object from which to read credential details from.)

Is there a reason why you didn't take my suggestion in the email I sent re: fixing the test_assess_aws_credential (separate out internals into functions to make it easier to test)?
If you had concerns about it that's ok but please state why etc.
Otherwise, the test test_assess_aws_credential is doing _way_ to much to be a useful unit test as it's testing almost everything else within that function (that could be tested interdependently)

Some considerations for some upcoming branches for this code && tests, please think on how you might be able to update this code to handle checking credentials for multiple substrates (GCE etc.) and how those credentials might be passed into the test (i.e. is it a single file with multiple entries in it, or should the test script get run once per substrate, or query the client object that gets created as to what credentials it knows, grab them and wipe the clients memory and proceed from there).
We can discuss a bit further in depth these ideas in one of our upcoming meetings.

review: Needs Fixing
Revision history for this message
ABHAY (ankatare) wrote :

Review Comments Incorporated

1852. By ABHAY

code changes to incorporate review comments

1853. By ABHAY

small code change as discussed in today's call

1854. By ABHAY

code change to add phrase

Revision history for this message
Christopher Lee (veebers) wrote :

Getting there, just some small fixes. Also waiting on the test that you have in progress.

review: Needs Fixing
1855. By ABHAY

incorporated review comments

Revision history for this message
Christopher Lee (veebers) wrote :

Looking good, just a couple of minor cleanup things left.

Regarding unit test function names, they need to be improved. They need to be more than test_<function name>, the name should describe what the test is intended to confirm.

i.e. test_this_function_raises_when_something_doesnt_match().

This is almost approved. Once it is well sit it here until the boostrap branch of this is done too as that will remove the need for passing in a AWS file and will instead use the credential details the client knows about (as per CI testing.)

review: Approve
Revision history for this message
Christopher Lee (veebers) wrote :

Sorry that last comment should have been 'needs fixing' I had approved on the mind. (It's so very close to being approved though ^_^).

review: Needs Fixing
1856. By ABHAY

incorporated review comments

Revision history for this message
ABHAY (ankatare) wrote :

Waiting for your Review

Revision history for this message
Christopher Lee (veebers) wrote :

LGTM, thanks for that.

review: Approve

Unmerged revisions

1856. By ABHAY

incorporated review comments

1855. By ABHAY

incorporated review comments

1854. By ABHAY

code change to add phrase

1853. By ABHAY

small code change as discussed in today's call

1852. By ABHAY

code changes to incorporate review comments

1851. By ABHAY

code changes for test case assess_aws_credential

1850. By ABHAY

minor code change to incorporate review comments

1849. By ABHAY

Review comments incorporated

1848. By ABHAY

make lint error removed

1847. By ABHAY

review comments incorporated

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-02-14 05:01:16 +0000
4@@ -0,0 +1,136 @@
5+#!/usr/bin/env python
6+"""
7+ Juju add-credential validation using aws credential yaml file and to
8+ verify credentials specified in the yaml file.
9+
10+ Usage: python assess_add_credentials.py --credential-file=/path/to/yaml_file
11+"""
12+import yaml
13+import csv
14+import sys
15+import logging
16+from argparse import ArgumentParser
17+from utility import (
18+ add_basic_testing_arguments,
19+ JujuAssertionError,
20+ temp_yaml_file,
21+ )
22+from assess_autoload_credentials import (
23+ client_from_config,
24+ begin_autoload_test,
25+ )
26+
27+log = logging.getLogger("assess_add_credentials")
28+
29+
30+def make_aws_credential_dict(cloud_name, access_key, secret_key):
31+ """Returns aws credentials dictionary data structure
32+
33+ :param cloud_name: cloud name for aws
34+ :param access_key: access-key specified in credential file
35+ :param secret_key: secret-key specefied in credential file
36+ :return: aws credential dictionary
37+
38+ """
39+ return {
40+ 'credentials': {
41+ 'aws': {
42+ cloud_name: {
43+ 'auth-type': 'access-key',
44+ 'access-key': access_key,
45+ 'secret-key': secret_key,
46+ }
47+ }
48+ }
49+ }
50+
51+
52+def verify_aws_add_credentials(
53+ client, cloud_name, expected_access_key, expected_secret_key):
54+ """Verify that 'cloud_name', credentials are the expected provided values.
55+
56+ :param client: juju client
57+ :param cloud_name: Name of the cloud specified in credential file
58+ :param expected_access_key: Expected access-key
59+ :param expected_secret_key: Expected secret key
60+
61+ """
62+ actual_credentials_details = yaml.safe_load(
63+ client.list_credentials('aws', show_secret=True))
64+ actual_credentials = actual_credentials_details[
65+ 'credentials']['aws']['cloud-credentials'][cloud_name]['details']
66+ if expected_access_key != actual_credentials['access-key']:
67+ raise JujuAssertionError('AWS access-key mismatch!.')
68+ if expected_secret_key != actual_credentials['secret-key']:
69+ raise JujuAssertionError('AWS secret-key mismatch!.')
70+ log.info("AWS credential validated successfully")
71+
72+
73+def make_aws_cloud_data(credential_file, cloud_name):
74+ """Generate aws credentials dict from credential file
75+
76+ :param credential_file : Expected to be a filepath string.
77+
78+ """
79+ with open(credential_file, 'rt') as acf:
80+ access_key, secret_key = get_aws_credentials_from_file(acf)
81+ aws_cloud_data = make_aws_credential_dict(
82+ cloud_name, access_key, secret_key)
83+ return aws_cloud_data, access_key, secret_key
84+
85+
86+def assess_aws_credentials(client_base, credential_file):
87+ """Perform juju add-credential using yaml file and verify it.
88+
89+ :param client_base: juju client
90+ :param credential_file: credential_file is expected to be a filepath string
91+
92+ """
93+ cloud_name = 'juju'
94+ cloud_details = make_aws_cloud_data(credential_file, cloud_name)
95+ cloud_data, access_key, secret_key = cloud_details
96+ with temp_yaml_file(cloud_data) as aws_cloud:
97+ with begin_autoload_test(client_base) as (client, tmp_scratch_dir):
98+ client.add_credential('aws', aws_cloud)
99+ verify_aws_add_credentials(
100+ client, cloud_name, access_key, secret_key)
101+
102+
103+def get_aws_credentials_from_file(aws_csv_file):
104+ """Read credential values from aws credential csv file.
105+
106+ :param: aws_csv_file: aws_csv_file is expected to be a file-like object.
107+
108+ """
109+ credential_details = {}
110+ reader = csv.reader(aws_csv_file, delimiter='=', quotechar='|')
111+ for row in reader:
112+ credential_details[row[0]] = row[1]
113+ return (credential_details['AWSAccessKeyId'],
114+ credential_details['AWSSecretKey'])
115+
116+
117+def parse_args(argv):
118+ """Parse all arguments."""
119+
120+ parser = ArgumentParser(
121+ description="Testing add-credential command.")
122+
123+ add_basic_testing_arguments(parser)
124+
125+ parser.add_argument(
126+ '--credential-file', required=True, action='store',
127+ help='AWS credential file to be used during bootstrap'
128+ )
129+ return parser.parse_args(argv)
130+
131+
132+def main(argv=None):
133+ args = parse_args(argv)
134+ client = client_from_config(args.env, args.juju_bin, False)
135+ assess_aws_credentials(client, args.credential_file)
136+ return 0
137+
138+
139+if __name__ == '__main__':
140+ sys.exit(main())
141
142=== modified file 'jujupy/client.py'
143--- jujupy/client.py 2017-02-13 15:14:15 +0000
144+++ jujupy/client.py 2017-02-14 05:01:16 +0000
145@@ -2674,6 +2674,17 @@
146 return self.get_juju_output('list-clouds', '--format',
147 format, include_e=False)
148
149+ def add_credential(self, cloud_name, cloud_file):
150+ return self.juju('add-credential',
151+ ("--replace", cloud_name, "-f", cloud_file),
152+ include_e=False)
153+
154+ def list_credentials(self, cloud_name, show_secret=False, format='json'):
155+ args = ('list-credentials', cloud_name, '--format', format)
156+ if show_secret:
157+ args += ('--show-secrets',)
158+ return self.get_juju_output(*args, include_e=False)
159+
160 def generate_tool(self, source_dir, stream=None):
161 args = ('generate-tools', '-d', source_dir)
162 if stream is not None:
163
164=== modified file 'jujupy/tests/test_client.py'
165--- jujupy/tests/test_client.py 2017-02-13 15:06:00 +0000
166+++ jujupy/tests/test_client.py 2017-02-14 05:01:16 +0000
167@@ -3291,6 +3291,29 @@
168 self.assertEqual(cloned.env.controller.name, controller_name)
169 self.assertEqual(fake_client.env.controller.name, 'name')
170
171+ def test_add_credential(self):
172+ client = ModelClient(JujuData('foo'), None, None)
173+ with patch.object(client, 'juju', autospec=True) as mock:
174+ client.add_credential('aws', 'credential-file')
175+ mock.assert_called_once_with('add-credential', ('--replace', 'aws',
176+ '-f', 'credential-file'), include_e=False)
177+
178+ def test_list_credentials(self):
179+ env = JujuData('foo')
180+ client = ModelClient(env, None, None)
181+ with patch.object(client, 'get_juju_output') as mock:
182+ client.list_credentials('aws')
183+ mock.assert_called_with(
184+ 'list-credentials', 'aws', '--format', 'json', include_e=False)
185+
186+ def test_list_credentials_with_show_secret(self):
187+ env = JujuData('foo')
188+ client = ModelClient(env, None, None)
189+ with patch.object(client, 'get_juju_output') as mock:
190+ client.list_credentials('aws', 'show_secret')
191+ mock.assert_called_with('list-credentials', 'aws', '--format', 'json',
192+ '--show-secrets', include_e=False)
193+
194 def test_list_clouds(self):
195 env = JujuData('foo')
196 client = ModelClient(env, None, None)
197
198=== added file 'tests/test_assess_add_credentials.py'
199--- tests/test_assess_add_credentials.py 1970-01-01 00:00:00 +0000
200+++ tests/test_assess_add_credentials.py 2017-02-14 05:01:16 +0000
201@@ -0,0 +1,211 @@
202+"""Tests for assess_add_credentials module."""
203+
204+import assess_add_credentials as aac
205+from assess_add_credentials import (
206+ make_aws_credential_dict,
207+ parse_args,
208+ get_aws_credentials_from_file,
209+ verify_aws_add_credentials,
210+ make_aws_cloud_data,
211+ assess_aws_credentials,
212+ )
213+
214+from tests import (
215+ TestCase,
216+ )
217+
218+from mock import (
219+ Mock,
220+ patch,
221+ )
222+from utility import (
223+ JujuAssertionError,
224+ )
225+import StringIO
226+import tempfile
227+from textwrap import dedent
228+import yaml
229+
230+
231+class TestParseArgs(TestCase):
232+
233+ def test_common_args(self):
234+ credential_file = './rootkey.csv'
235+ args = parse_args(["an-env", "/bin/juju", "/tmp/logs",
236+ "an-env-mod", '--credential-file', credential_file])
237+ self.assertEqual("an-env", args.env)
238+ self.assertEqual("/bin/juju", args.juju_bin)
239+ self.assertEqual("/tmp/logs", args.logs)
240+ self.assertEqual("an-env-mod", args.temp_env_name)
241+ self.assertEqual(False, args.debug)
242+ self.assertEqual(credential_file, args.credential_file)
243+
244+
245+class TestawsCredentials(TestCase):
246+
247+ def test_make_aws_cloud_data(self):
248+ exp_access_key = 'KeyId123'
249+ exp_secret_key = 'SecretKey321'
250+ cloud_name = 'juju'
251+ exp_cloud_data = {
252+ 'credentials': {
253+ 'aws': {
254+ cloud_name: {
255+ 'auth-type': 'access-key',
256+ 'access-key': exp_access_key,
257+ 'secret-key': exp_secret_key,
258+ }
259+ }
260+ }
261+ }
262+ with tempfile.NamedTemporaryFile() as fp:
263+ fp.write(dedent("""\
264+ AWSAccessKeyId=KeyId123
265+ AWSSecretKey=SecretKey321
266+ """))
267+ fp.flush()
268+
269+ actual_output = make_aws_cloud_data(fp.name, cloud_name)
270+ cloud_data, access_key, secret_key = actual_output
271+ actual_cloud_data = cloud_data
272+ actual_access_key = access_key
273+ actual_secret_key = secret_key
274+ self.assertEqual(actual_cloud_data, exp_cloud_data)
275+ self.assertEqual(actual_access_key, exp_access_key)
276+ self.assertEqual(actual_secret_key, exp_secret_key)
277+
278+ def test_assess_aws_credential(self):
279+ mock_begin_client = Mock()
280+ mock_client = Mock()
281+ with tempfile.NamedTemporaryFile() as fp:
282+ fp.write(dedent("""\
283+ AWSAccessKeyId=KeyId123
284+ AWSSecretKey=SecretKey321
285+ """))
286+ fp.flush()
287+ with patch.object(
288+ aac, "begin_autoload_test", autospec=True) as m_bat:
289+ m_bat.return_value.__enter__.return_value = (
290+ mock_begin_client, 'tmp_dir_path')
291+ with patch.object(
292+ aac, 'verify_aws_add_credentials',
293+ autospec=True) as vaac:
294+ assess_aws_credentials(mock_client, fp.name)
295+ vaac.assert_called_once_with(
296+ mock_begin_client, 'juju', 'KeyId123', 'SecretKey321')
297+
298+ def test_make_aws_credential_dict_returns_dict_with_details(self):
299+ cloud_name = 'cloud_name'
300+ access_key = 'access_key'
301+ secret_key = 'secret_key'
302+ actual_output = make_aws_credential_dict(
303+ cloud_name, access_key, secret_key)
304+ expected_output = {
305+ 'credentials': {
306+ 'aws': {
307+ cloud_name: {
308+ 'auth-type': 'access-key',
309+ 'access-key': access_key,
310+ 'secret-key': secret_key,
311+ }
312+ }
313+ }
314+ }
315+ self.assertDictEqual(actual_output, expected_output)
316+
317+ def test_get_aws_credentials_from_file(self):
318+ expected_aws_access_key = 'TestAccessKey'
319+ expected_aws_secret_key = 'TestSecretKey'
320+ fake_csv_file_obj = StringIO.StringIO(
321+ dedent("""\
322+ AWSAccessKeyId=TestAccessKey
323+ AWSSecretKey=TestSecretKey
324+ """))
325+ actual_aws_access_key, actual_aws_secret_key = \
326+ get_aws_credentials_from_file(fake_csv_file_obj)
327+ self.assertEqual(actual_aws_access_key, expected_aws_access_key)
328+ self.assertEqual(actual_aws_secret_key, expected_aws_secret_key)
329+
330+ def test_verify_aws_add_credentials_with_correct_values(self):
331+ cloud_name = 'cloud_name'
332+ access_key = 'test_access_key'
333+ secret_key = 'test_secret_key'
334+ actual_output = yaml.safe_dump({
335+ "credentials": {
336+ "aws": {
337+ "cloud-credentials": {
338+ cloud_name: {
339+ "auth-type": "access-key", "details": {
340+ "access-key": "{}".format(access_key),
341+ "secret-key": "{}".format(secret_key)
342+ }
343+ }
344+ }
345+ }
346+ }
347+ })
348+ mock_client = Mock()
349+ with patch.object(mock_client, "list_credentials",
350+ return_value=actual_output, autospec=True):
351+ verify_aws_add_credentials(
352+ mock_client, cloud_name, access_key, secret_key)
353+ self.assertIn("AWS credential validated successfully",
354+ self.log_stream.getvalue())
355+
356+ def test_verify_aws_add_credentials_with_access_key_mismatch(self):
357+ cloud_name = 'cloud_name'
358+ actual_access_key = 'test_access_key'
359+ actual_secret_key = 'test_secret_key'
360+ expected_access_key = 'access_key'
361+ expected_secret_key = 'test_secret_key'
362+ actual_output = yaml.safe_dump({
363+ "credentials": {
364+ "aws": {
365+ "cloud-credentials": {
366+ cloud_name: {
367+ "auth-type": "access-key", "details": {
368+ "access-key": "{}".format(actual_access_key),
369+ "secret-key": "{}".format(actual_secret_key)
370+ }
371+ }
372+ }
373+ }
374+ }
375+ })
376+ mock_client = Mock()
377+ with patch.object(mock_client, "list_credentials",
378+ return_value=actual_output, autospec=True):
379+ with self.assertRaisesRegexp(JujuAssertionError,
380+ 'AWS access-key mismatch!.'):
381+ verify_aws_add_credentials(
382+ mock_client, cloud_name, expected_access_key,
383+ expected_secret_key)
384+
385+ def test_verify_aws_add_credentials_with_secret_key_mismatch(self):
386+ cloud_name = 'cloud_name'
387+ actual_access_key = 'test_access_key'
388+ actual_secret_key = 'test_secret_key'
389+ expected_access_key = 'test_access_key'
390+ expected_secret_key = 'secret_key'
391+ actual_output = yaml.safe_dump({
392+ "credentials": {
393+ "aws": {
394+ "cloud-credentials": {
395+ cloud_name: {
396+ "auth-type": "access-key", "details": {
397+ "access-key": "{}".format(actual_access_key),
398+ "secret-key": "{}".format(actual_secret_key)
399+ }
400+ }
401+ }
402+ }
403+ }
404+ })
405+ mock_client = Mock()
406+ with patch.object(mock_client, "list_credentials",
407+ return_value=actual_output, autospec=True):
408+ with self.assertRaisesRegexp(JujuAssertionError,
409+ 'AWS secret-key mismatch!.'):
410+ verify_aws_add_credentials(
411+ mock_client, cloud_name, expected_access_key,
412+ expected_secret_key)

Subscribers

People subscribed via source and target branches