Merge lp:~ankatare/juju-ci-tools/juju-aws-add-credential into lp:juju-ci-tools
- juju-aws-add-credential
- Merge into trunk
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 |
Related bugs: |
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.
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_
Please see inline comments for further review comments.
Christopher Lee (veebers) wrote : | # |
Added further comment from discussion in our meeting.
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.
Christopher Lee (veebers) wrote : | # |
Running the tests I get a fail: http://
This is related to test_get_
I've included notes on resolving this inline with the test.
I see that tests for changes to jujupy/client.py are still incoming.
viswesuwara nathan (viswesn) wrote : | # |
It's good to have unit testing for
1) assess_
2) add_credential
3) list_credential.
I also specified couple of comments (see below)
ABHAY (ankatare) wrote : | # |
> It's good to have unit testing for
> 1) assess_
> 2) add_credential ( comment incorporated)
> 3) list_credential. ( comment incorporated)
>
> I also specified couple of comments (see below)
viswesuwara nathan (viswesn) wrote : | # |
Find the review comments specified. Thanks
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.
ABHAY (ankatare) wrote : | # |
All Review comments Incorporated Now
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_
If you had concerns about it that's ok but please state why etc.
Otherwise, the test test_assess_
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.
Christopher Lee (veebers) wrote : | # |
Getting there, just some small fixes. Also waiting on the test that you have in progress.
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_
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.)
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 ^_^).
Christopher Lee (veebers) wrote : | # |
LGTM, thanks for that.
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
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) |
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.