Merge lp:~makyo/juju-quickstart/ssh-3-watch-keys into lp:juju-quickstart

Proposed by Madison Scott-Clary
Status: Merged
Merged at revision: 37
Proposed branch: lp:~makyo/juju-quickstart/ssh-3-watch-keys
Merge into: lp:juju-quickstart
Diff against target: 424 lines (+269/-46)
4 files modified
quickstart/app.py (+80/-17)
quickstart/tests/test_app.py (+139/-29)
quickstart/tests/test_utils.py (+31/-0)
quickstart/utils.py (+19/-0)
To merge this branch: bzr merge lp:~makyo/juju-quickstart/ssh-3-watch-keys
Reviewer Review Type Date Requested Status
Juju GUI Hackers Pending
Review via email: mp+198851@code.launchpad.net

Description of the change

Watch for SSH key creation

Allow the user to create SSH keys in another term/window, watch for creation and continue when available.

To test: make check
To QA:
1. Move ~/.ssh to a back up
2. Run quickstart - select w when prompted to watch
3. Create ssh keys as instructed in another term/window
4. Quickstart should continue
5. Move ~/.ssh back; other options should work as expected.

https://codereview.appspot.com/39610049/

To post a comment you must log in.
Revision history for this message
Madison Scott-Clary (makyo) wrote :
Download full text (6.4 KiB)

Reviewers: mp+198851_code.launchpad.net,

Message:
Please take a look.

Description:
Watch for SSH key creation

Allow the user to create SSH keys in another term/window, watch for
creation and continue when available.

To test: make check
To QA:
1. Move ~/.ssh to a back up
2. Run quickstart - select w when prompted to watch
3. Create ssh keys as instructed in another term/window
4. Quickstart should continue
5. Move ~/.ssh back; other options should work as expected.

https://code.launchpad.net/~makyo/juju-quickstart/ssh-3-watch-keys/+merge/198851

(do not edit description out of merge proposal)

Please review this at https://codereview.appspot.com/39610049/

Affected files (+64, -10 lines):
   A [revision details]
   M quickstart/app.py
   M quickstart/tests/test_app.py

Index: [revision details]
=== added file '[revision details]'
--- [revision details] 2012-01-01 00:00:00 +0000
+++ [revision details] 2012-01-01 00:00:00 +0000
@@ -0,0 +1,2 @@
+Old revision:
<email address hidden>
+New revision: <email address hidden>

Index: quickstart/app.py
=== modified file 'quickstart/app.py'
--- quickstart/app.py 2013-12-09 18:37:40 +0000
+++ quickstart/app.py 2013-12-13 00:18:27 +0000
@@ -94,25 +94,42 @@
      Allow the user to let quickstart create SSH keys, or quit by raising a
      ProgramExit if they would like to create the key themselves.
      """
- ssh_dir = os.path.join(os.path.expanduser('~'), '.ssh')
- retcode = utils.call('/usr/bin/find', ssh_dir, '-name', 'id_*.pub')[0]
- if retcode > 0:
+ if check_ssh_keys():
          print('No SSH keys were found in ~/.ssh\n\n'
                'Would you like quickstart to create them for you? Note
that '
                'this will create a new SSH key with the passphrase you
provide '
                'available to your SSH agent. ssh-keygen will ask for a '
                'passphrase, but juju quickstart will not have access to
it.\n'
- 'Alternatively, you may create the keys on your own.\n')
- create_keys = raw_input('Continue [y/N]? ')
+ 'Alternatively, you may create the keys on your own. '
+ 'Additionally, quickstart can watch for the creation of SSH '
+ 'keys that you create in another terminal or window.\n')
+ create_keys = raw_input('Continue [y/N] or watch for new keys
[w]? ')
          if create_keys.lower() == 'y':
              create_ssh_keys()
+ elif create_keys.lower() == 'w':
+ watch_for_ssh_keys()
          else:
- raise ProgramExit('Please run these commands and follow the '
- 'instructions they produce before running '
+ raise ProgramExit('Please run this command and follow the '
+ 'instructions it produces before running '
                                'quickstart again:\n\n'
                                'ssh-keygen -b 4096 -t rsa')

+def check_ssh_keys():
+ ssh_dir = os.path.join(os.path.expanduser('~'), '.ssh')
+ return utils.call('/usr/bin/find', ssh_dir, '-name', 'id_*.pub')[0]
+
+
+def watch_for_ssh_ke...

Read more...

Revision history for this message
Gary Poster (gary) wrote :

Wording suggestions. Didn't look other than that for now.

https://codereview.appspot.com/39610049/diff/1/quickstart/app.py
File quickstart/app.py (right):

https://codereview.appspot.com/39610049/diff/1/quickstart/app.py#newcode99
quickstart/app.py:99: 'Would you like quickstart to create them for you?
Note that '
What do you think of something like this? Please feel free to polish or
counter-propose.

----

No SSH keys were found in ~/.ssh

To proceed, quickstart can automatically create keys for you [a], or it
can give you a command to run yourself [s] in another terminal or window
to create the keys.

Note that, if you want quickstart to create your key, the underlying
program (ssh-keygen) will ask for a passphrase, but quickstart will not
have access to it.

Automatically create keys [a], create the keys yourself [s], or cancel
[C]?

https://codereview.appspot.com/39610049/diff/1/quickstart/app.py#newcode112
quickstart/app.py:112: raise ProgramExit('Please run this command and
follow the '
If you would like to create the keys yourself, please run this command
and then re-run quickstart:

ssh-keygen -b 4096 -t rsa

https://codereview.appspot.com/39610049/diff/1/quickstart/app.py#newcode120
quickstart/app.py:120: return utils.call('/usr/bin/find', ssh_dir,
'-name', 'id_*.pub')[0]
We can wait for Windows compatibility, but it would be nice if
quickstart used built in Python tools rather than find. <shrug> for
now.

https://codereview.appspot.com/39610049/

Revision history for this message
Gary Poster (gary) wrote :

What you've added looks good to me, with the changes I've suggested, but
QA bad.

------------------------------------------------
Continue [y/N] or watch for new keys [w]? w
Please run this command in another terminal or window and follow the
instructions it produces; quickstart will continue when keys are
generated. ^C to quit.

ssh-keygen -b 4096 -t rsa

.
.
.
.
.
.
.
.
.
.
.
bootstrapping the ec2 environment (type: ec2)
juju-quickstart: error: ERROR error parsing environment "ec2": no public
ssh keys found
------------------------------------------------

Do we need to start (restart) an agent?

https://codereview.appspot.com/39610049/diff/1/quickstart/app.py
File quickstart/app.py (right):

https://codereview.appspot.com/39610049/diff/1/quickstart/app.py#newcode112
quickstart/app.py:112: raise ProgramExit('Please run this command and
follow the '
On 2013/12/13 01:49:45, gary.poster wrote:
> If you would like to create the keys yourself, please run this command

, follow its instructions,

> and then
> re-run quickstart:

> ssh-keygen -b 4096 -t rsa

https://codereview.appspot.com/39610049/diff/1/quickstart/app.py#newcode120
quickstart/app.py:120: return utils.call('/usr/bin/find', ssh_dir,
'-name', 'id_*.pub')[0]
On 2013/12/13 01:49:45, gary.poster wrote:
> We can wait for Windows compatibility, but it would be nice if
quickstart used
> built in Python tools rather than find. <shrug> for now.
That is: please leave as is for now unless you find yourself really
motivated and it looks really fast and easy.

https://codereview.appspot.com/39610049/diff/1/quickstart/app.py#newcode130
quickstart/app.py:130: print('.')
I think this would be much better. OK with you?

print('.', end="")

https://codereview.appspot.com/39610049/diff/1/quickstart/tests/test_app.py
File quickstart/tests/test_app.py (right):

https://codereview.appspot.com/39610049/diff/1/quickstart/tests/test_app.py#newcode245
quickstart/tests/test_app.py:245: def test_watch(self, mock_print,
mock_sleep):
Nice test.

https://codereview.appspot.com/39610049/

Revision history for this message
Madison Scott-Clary (makyo) wrote :

Thanks for the comments. Will re-propose after fixing QA issue.

https://codereview.appspot.com/39610049/diff/1/quickstart/app.py
File quickstart/app.py (right):

https://codereview.appspot.com/39610049/diff/1/quickstart/app.py#newcode99
quickstart/app.py:99: 'Would you like quickstart to create them for you?
Note that '
On 2013/12/13 01:49:45, gary.poster wrote:
> What do you think of something like this? Please feel free to polish
or
> counter-propose.

> ----

> No SSH keys were found in ~/.ssh

> To proceed, quickstart can automatically create keys for you [a], or
it can give
> you a command to run yourself [s] in another terminal or window to
create the
> keys.

> Note that, if you want quickstart to create your key, the underlying
program
> (ssh-keygen) will ask for a passphrase, but quickstart will not have
access to
> it.

> Automatically create keys [a], create the keys yourself [s], or cancel
[C]?

Done.

https://codereview.appspot.com/39610049/diff/1/quickstart/app.py#newcode112
quickstart/app.py:112: raise ProgramExit('Please run this command and
follow the '
On 2013/12/13 01:49:45, gary.poster wrote:
> If you would like to create the keys yourself, please run this command
and then
> re-run quickstart:

> ssh-keygen -b 4096 -t rsa

Done.

https://codereview.appspot.com/39610049/diff/1/quickstart/app.py#newcode120
quickstart/app.py:120: return utils.call('/usr/bin/find', ssh_dir,
'-name', 'id_*.pub')[0]
On 2013/12/13 13:51:45, gary.poster wrote:
> On 2013/12/13 01:49:45, gary.poster wrote:
> > We can wait for Windows compatibility, but it would be nice if
quickstart used
> > built in Python tools rather than find. <shrug> for now.
> That is: please leave as is for now unless you find yourself really
motivated
> and it looks really fast and easy.

This is encompassed by ssh keys 2.5 - this find is fragile and does not
a) verify that the keys are valid for ssh, or b) take into account that
keys can be stored elsewhere with an ssh config. Talked with Francesco,
leaving it as an increment.

https://codereview.appspot.com/39610049/diff/1/quickstart/app.py#newcode130
quickstart/app.py:130: print('.')
On 2013/12/13 13:51:45, gary.poster wrote:
> I think this would be much better. OK with you?

> print('.', end="")

Done.

https://codereview.appspot.com/39610049/

35. By Madison Scott-Clary

Wording changes per review.

36. By Madison Scott-Clary

tests agree with changes.

37. By Madison Scott-Clary

Tests pass, using agent now.

Revision history for this message
Madison Scott-Clary (makyo) wrote :
Revision history for this message
Gary Poster (gary) wrote :

Other than needing to add some docstrings, this looks good, but the QA
is still bad.

Note I am using a passphrase when I QA.

I was able to get it to work when I did the following.

(1) In the other terminal, also *add* the new key

ssh-keygen -b 4096 -t rsa && ssh-agent && ssh-add

(2) In the code, I did a time.sleep(10) after it saw the key in order to
wait for me to type the password in. This is obviously not ideal.

Note that I am pretty sure that create_ssh_keys should run ssh-add also.

Ideally, I think the script would run ssh-agent and ssh-add. In quick
hacks, I couldn't get this to work. I started debugging and it might be
that it needed some time in between the call to ssh-agent and ssh-add.
I'm not sure. Note that ssh-add needs to be interactive, so utils.call
can't work as is. We need stdin and stdout to be used.

Sorry it is still not working for me.

https://codereview.appspot.com/39610049/diff/20001/quickstart/app.py
File quickstart/app.py (right):

https://codereview.appspot.com/39610049/diff/20001/quickstart/app.py#newcode119
quickstart/app.py:119: def check_ssh_keys():
docstring please

https://codereview.appspot.com/39610049/diff/20001/quickstart/app.py#newcode124
quickstart/app.py:124: def watch_for_ssh_keys():
docstring please

https://codereview.appspot.com/39610049/

Revision history for this message
Madison Scott-Clary (makyo) wrote :

Putting this branch on hold, then. Thanks for the QA, Gary. We need to
find a check_ssh_keys method that actually checks if keys are available
to the agent, which will be the case once the user enters the password
(no matter how it works: automatic creation or manual). I'll add the
docstrings and push, then start researching tonight/this weekend on SSH
solutions.

Thanks again

https://codereview.appspot.com/39610049/

Revision history for this message
Gary Poster (gary) wrote :

Cool.

I could very well be wrong, but I think that the script should start the agent and add the key. Any thing else seems like it could easily go wrong and hang forever.

That's what I was trying to get to work, briefly. It's what the other code path needs, and it seems like it is not working yet either.

Gary

> On Dec 13, 2013, at 6:31 PM, <email address hidden> wrote:
>
> Putting this branch on hold, then. Thanks for the QA, Gary. We need to
> find a check_ssh_keys method that actually checks if keys are available
> to the agent, which will be the case once the user enters the password
> (no matter how it works: automatic creation or manual). I'll add the
> docstrings and push, then start researching tonight/this weekend on SSH
> solutions.
>
> Thanks again
>
> https://codereview.appspot.com/39610049/

38. By Madison Scott-Clary

Better check for keys, ssh-agent management.

39. By Madison Scott-Clary

tests pass

40. By Madison Scott-Clary

Was not able to communicate with existing ssh agents, starting one by default now.

Revision history for this message
Madison Scott-Clary (makyo) wrote :
Revision history for this message
Francesco Banconi (frankban) wrote :
Download full text (3.3 KiB)

This branch looks very good Matthew, thank you for this work and for
investigating this ssh stuff!
I wrote a lot of comments, but many of them are just nice to
have/minors/do-whit-them-what-you-want.
LGTM with (possible) changes.
QA ok with some comments (about the initial agent check and the SIGINT
handling).

https://codereview.appspot.com/39610049/diff/40001/quickstart/app.py
File quickstart/app.py (right):

https://codereview.appspot.com/39610049/diff/40001/quickstart/app.py#newcode98
quickstart/app.py:98: utils.start_ssh_agent()
It seems an OSError raised from this call would crash the application.
Also, this always starts the agent, even if the user has one already
running. Do we have a way to avoid that? This could also be tackled in
another card.

https://codereview.appspot.com/39610049/diff/40001/quickstart/app.py#newcode108
quickstart/app.py:108: 'keys yourself [s], or cancel [C]? ')
Maybe ^C here too? <shrug>

https://codereview.appspot.com/39610049/diff/40001/quickstart/app.py#newcode114
quickstart/app.py:114: raise ProgramExit('If you would like to create
the keys yourself, '
This is displayed as an error, but I am not sure this is an error.
sys.exit(msg) could be used instead.

https://codereview.appspot.com/39610049/diff/40001/quickstart/app.py#newcode120
quickstart/app.py:120: def check_ssh_keys():
Very nice commented function, thank you.

https://codereview.appspot.com/39610049/diff/40001/quickstart/app.py#newcode124
quickstart/app.py:124: in some way. Raise OSError (via
utils.start_ssh_agent) if we couldn't
I don't see the start_ssh_agent call here. Is this still true?

https://codereview.appspot.com/39610049/diff/40001/quickstart/app.py#newcode131
quickstart/app.py:131: retcode, output, error =
utils.call('/usr/bin/ssh-add', '-l')
error does not seem to be used here and can be safely replaced with _

https://codereview.appspot.com/39610049/diff/40001/quickstart/app.py#newcode150
quickstart/app.py:150: raise
ProgramExit(msg.format(error).encode('utf-8'))
You can avoid encoding ProgramExit messages <shrug>.

https://codereview.appspot.com/39610049/diff/40001/quickstart/app.py#newcode164
quickstart/app.py:164: while not check_ssh_keys():
^C produces a traceback. It's not that important, but we can easily
avoid it with something like:

     try:
         while not check_ssh_keys():
             print('.', end='')
             sys.stdout.flush()
             time.sleep(3)
     except KeyboardInterrupt:
         sys.exit('\nquitting')

https://codereview.appspot.com/39610049/diff/40001/quickstart/app.py#newcode166
quickstart/app.py:166: sys.stdout.flush()
Interesting, why is this required? A comment would be nice.

https://codereview.appspot.com/39610049/diff/40001/quickstart/app.py#newcode185
quickstart/app.py:185: if retcode > 0:
Is it ok if retcode is < 0? Can this happen?
If not, we can just check "if retcode:".

https://codereview.appspot.com/39610049/diff/40001/quickstart/app.py#newcode188
quickstart/app.py:188: if retcode > 0:
Same as above.

https://codereview.appspot.com/39610049/diff/40001/quickstart/tests/test_app.py
File quickstart/tests/test_app.py (right):

https://codereview.appspot.com/39610049/diff/40001/quick...

Read more...

41. By Madison Scott-Clary

responding to frankban's review comments.

Revision history for this message
Madison Scott-Clary (makyo) wrote :

Please take a look.

https://codereview.appspot.com/39610049/diff/40001/quickstart/app.py
File quickstart/app.py (right):

https://codereview.appspot.com/39610049/diff/40001/quickstart/app.py#newcode98
quickstart/app.py:98: utils.start_ssh_agent()
On 2013/12/17 17:22:39, frankban wrote:
> It seems an OSError raised from this call would crash the application.
Also,
> this always starts the agent, even if the user has one already
running. Do we
> have a way to avoid that? This could also be tackled in another card.

Oops, will catch the error.

Through testing, even if another agent is running, quickstart can only
use that agent for authentication, not for adding keys (and so ssh-add
will always fail with rc 2, "Cannot contact agent"). We can investigate
this part further in another card, though, if there's a way around it.

https://codereview.appspot.com/39610049/diff/40001/quickstart/app.py#newcode108
quickstart/app.py:108: 'keys yourself [s], or cancel [C]? ')
On 2013/12/17 17:22:39, frankban wrote:
> Maybe ^C here too? <shrug>

Done.

https://codereview.appspot.com/39610049/diff/40001/quickstart/app.py#newcode114
quickstart/app.py:114: raise ProgramExit('If you would like to create
the keys yourself, '
On 2013/12/17 17:22:39, frankban wrote:
> This is displayed as an error, but I am not sure this is an error.
> sys.exit(msg) could be used instead.

Done.

https://codereview.appspot.com/39610049/diff/40001/quickstart/app.py#newcode124
quickstart/app.py:124: in some way. Raise OSError (via
utils.start_ssh_agent) if we couldn't
On 2013/12/17 17:22:39, frankban wrote:
> I don't see the start_ssh_agent call here. Is this still true?

Nope, good catch.

https://codereview.appspot.com/39610049/diff/40001/quickstart/app.py#newcode131
quickstart/app.py:131: retcode, output, error =
utils.call('/usr/bin/ssh-add', '-l')
On 2013/12/17 17:22:39, frankban wrote:
> error does not seem to be used here and can be safely replaced with _

Done.

https://codereview.appspot.com/39610049/diff/40001/quickstart/app.py#newcode164
quickstart/app.py:164: while not check_ssh_keys():
On 2013/12/17 17:22:39, frankban wrote:
> ^C produces a traceback. It's not that important, but we can easily
avoid it
> with something like:

> try:
> while not check_ssh_keys():
> print('.', end='')
> sys.stdout.flush()
> time.sleep(3)
> except KeyboardInterrupt:
> sys.exit('\nquitting')

Done.

https://codereview.appspot.com/39610049/diff/40001/quickstart/app.py#newcode166
quickstart/app.py:166: sys.stdout.flush()
On 2013/12/17 17:22:39, frankban wrote:
> Interesting, why is this required? A comment would be nice.

Done.

With end being empty, stdout's buffer won't flush until after it reaches
a certain number of bytes.

https://codereview.appspot.com/39610049/

Revision history for this message
Richard Harding (rharding) wrote :

QA:

The text is too long and should wrap. I had the text wrapping mid-word
because of the length. I think a standard 78ish for terminal work would
be really nice.

http://uploads.mitechie.com/lp/quickstart.png

A list of options would read cleaner.

"Quickstart can

[a] Automatically create keys
[s] Provide you commands to create keys yourself
[C] Cancel Quickstart
"

There was a big lack of warning that it was going on into bootstrap and
such after the key creation. Maybe a pause/continue makes sense there?

Trying manual instructions:

What's the .? Is it doing something? I thought it was waiting for me to
enter a command?

Again, the instructions but up. It's not clear what's part of this step
and the last bit of info.

Once I did enter the command, the ...... wasn't cleared before the
'bootstraping' log line so I got.

........................bootstrapping the azure environment (type:
azure)

Trying to cancel:

I get a traceback during a ctrl-c to kill. It should be caught/exit
cleanly.

If I start to automatically create, but cancel when it asks me for a
passphrase it also dumps a traceback at me.

I'd propose something that looks to the user more like:

http://paste.ubuntu.com/6595673/

Note the use of more usual opposites of automatic/manual, indentation to
help distinguish commands to run manually as a callout, etc.

https://codereview.appspot.com/39610049/

Revision history for this message
Richard Harding (rharding) wrote :

The QA was LGTM with the comments and discussion points in my previous
message.

https://codereview.appspot.com/39610049/

Revision history for this message
Madison Scott-Clary (makyo) wrote :

*** Submitted:

Watch for SSH key creation

Allow the user to create SSH keys in another term/window, watch for
creation and continue when available.

To test: make check
To QA:
1. Move ~/.ssh to a back up
2. Run quickstart - select w when prompted to watch
3. Create ssh keys as instructed in another term/window
4. Quickstart should continue
5. Move ~/.ssh back; other options should work as expected.

R=gary.poster, frankban, rharding
CC=
https://codereview.appspot.com/39610049

https://codereview.appspot.com/39610049/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'quickstart/app.py'
2--- quickstart/app.py 2013-12-09 18:37:40 +0000
3+++ quickstart/app.py 2013-12-17 21:07:10 +0000
4@@ -25,6 +25,7 @@
5 import json
6 import logging
7 import os
8+import sys
9 import time
10
11 import jujuclient
12@@ -94,23 +95,82 @@
13 Allow the user to let quickstart create SSH keys, or quit by raising a
14 ProgramExit if they would like to create the key themselves.
15 """
16- ssh_dir = os.path.join(os.path.expanduser('~'), '.ssh')
17- retcode = utils.call('/usr/bin/find', ssh_dir, '-name', 'id_*.pub')[0]
18- if retcode > 0:
19+ utils.start_ssh_agent()
20+ if not check_ssh_keys():
21 print('No SSH keys were found in ~/.ssh\n\n'
22- 'Would you like quickstart to create them for you? Note that '
23- 'this will create a new SSH key with the passphrase you provide '
24- 'available to your SSH agent. ssh-keygen will ask for a '
25- 'passphrase, but juju quickstart will not have access to it.\n'
26- 'Alternatively, you may create the keys on your own.\n')
27- create_keys = raw_input('Continue [y/N]? ')
28- if create_keys.lower() == 'y':
29+ 'To proceed, quickstart can automatically create keys for you '
30+ '[a], or it can give you a command to run yourself [s] in '
31+ 'another terminal or window to create the keys.\n\n'
32+ 'Note that, if you want quickstart to create your key, the '
33+ 'underlying program (ssh-keygen) will ask for a passphrase, '
34+ 'but quickstart will not have access to it.\n\n')
35+ create_keys = raw_input('Automatically create keys [a], create the '
36+ 'keys yourself [s], or cancel [C or ^C]? ')
37+ if create_keys.lower() == 'a':
38 create_ssh_keys()
39- else:
40- raise ProgramExit('Please run these commands and follow the '
41- 'instructions they produce before running '
42- 'quickstart again:\n\n'
43- 'ssh-keygen -b 4096 -t rsa')
44+ elif create_keys.lower() == 's':
45+ watch_for_ssh_keys()
46+ else:
47+ msg = 'If you would like to create the keys yourself, ' \
48+ 'please run this command, follow its ' \
49+ 'instructions, and then re-run quickstart:\n\n' \
50+ 'ssh-keygen -b 4096 -t rsa && ssh-agent'
51+ sys.exit(msg)
52+
53+
54+def check_ssh_keys():
55+ """Check whether or not ssh keys exist and are loaded by the agent.
56+
57+ Raise a program exit if we can't load the keys because they're broken
58+ in some way.
59+
60+ Return true if there are ssh identities loaded and available in an agent,
61+ false otherwise.
62+ """
63+ no_keys_msg = 'The agent has no identities.\n'
64+ retcode, output, _ = utils.call('/usr/bin/ssh-add', '-l')
65+ if retcode == 0:
66+ # We have keys and an agent.
67+ return True
68+ elif retcode == 1 and output == no_keys_msg:
69+ # We have an agent, but no keys currently loaded.
70+ retcode, output, error = utils.call('/usr/bin/ssh-add')
71+ if retcode == 0:
72+ # We were able to load an identity
73+ return True
74+ elif retcode == 1:
75+ # If ssh-add is called without -l and there are no identities
76+ # available, there will be no output or error, but retcode will
77+ # still be 1.
78+ return False
79+ else:
80+ # We weren't able to load keys for some other reason, such as being
81+ # readable by group or world, or malformed.
82+ msg = 'error attempting to add ssh keys! {}'
83+ raise ProgramExit(msg.format(error).encode('utf-8'))
84+ return False
85+
86+
87+def watch_for_ssh_keys():
88+ """Watch for generation of ssh keys from another terminal or window.
89+
90+ This will run until keys become visible to quickstart or killed by
91+ the user.
92+ """
93+ print('Please run this command in another terminal or window and '
94+ 'follow the instructions it produces; quickstart will '
95+ 'continue when keys are generated. ^C to quit.\n\n'
96+ 'ssh-keygen -b 4096 -t rsa && ssh-agent\n')
97+ try:
98+ while not check_ssh_keys():
99+ # Print and flush the buffer immediately; an empty end kwarg will
100+ # not cause the buffer to flush until after a certain number of
101+ # bytes.
102+ print('.', end='')
103+ sys.stdout.flush()
104+ time.sleep(3)
105+ except KeyboardInterrupt:
106+ sys.exit('\nquitting')
107
108
109 def create_ssh_keys():
110@@ -121,15 +181,18 @@
111 NB: this involves user interaction for entering the passphrase; this may
112 have to change if creating SSH keys takes place in the urwid interface.
113 """
114- key_file = os.path.join(os.path.expanduser('~'), '.ssh', 'id_juju_rsa')
115+ key_file = os.path.join(os.path.expanduser('~'), '.ssh', 'id_rsa')
116 retcode, _, error = utils.call('/usr/bin/ssh-keygen',
117 '-q', # silent
118 '-b', '4096', # 4096 bytes
119 '-t', 'rsa', # RSA type
120 '-C', 'Generated with Juju Quickstart',
121 '-f', key_file)
122- if retcode > 0:
123+ if retcode:
124 raise ProgramExit('error generating ssh key! {}'.format(error))
125+ retcode, _, error = utils.call('/usr/bin/ssh-agent')
126+ if retcode:
127+ raise ProgramExit('error starting ssh-agent! {}'.format(error))
128 print('a new ssh key was generated in {}'.format(key_file))
129
130
131
132=== modified file 'quickstart/tests/test_app.py'
133--- quickstart/tests/test_app.py 2013-12-09 18:37:40 +0000
134+++ quickstart/tests/test_app.py 2013-12-17 21:07:10 +0000
135@@ -181,63 +181,165 @@
136 class TestEnsureSSHKeys(
137 helpers.CallTestsMixin, ProgramExitTestsMixin, unittest.TestCase):
138
139- print_msg = 'No SSH keys were found in ~/.ssh\n\n' \
140- 'Would you like quickstart to create them for you? Note ' \
141- 'that this will create a new SSH key with the passphrase ' \
142- 'you provide available to your SSH agent. ssh-keygen will ' \
143- 'ask for a passphrase, but juju quickstart will not have ' \
144- 'access to it.\nAlternatively, you may create the keys on ' \
145- 'your own.\n'
146- ssh_key_find = ('/usr/bin/find',
147- os.path.join(os.path.expanduser('~'), '.ssh'),
148- '-name', 'id_*.pub')
149-
150- def patch_raw_input(self, return_value='N'):
151+ print_msg = 'No SSH keys were found in ~/.ssh\n\nTo proceed, quickstart ' \
152+ 'can automatically create keys for you [a], or it can give ' \
153+ 'you a command to run yourself [s] in another terminal or ' \
154+ 'window to create the keys.\n\nNote that, if you want ' \
155+ 'quickstart to create your key, the underlying program ' \
156+ '(ssh-keygen) will ask for a passphrase, but quickstart ' \
157+ 'will not have access to it.\n\n'
158+
159+ def patch_check_ssh_keys(self, return_value=False):
160+ mock_check_ssh_keys = mock.Mock(return_value=return_value)
161+ return mock.patch('quickstart.app.check_ssh_keys', mock_check_ssh_keys)
162+
163+ def patch_raw_input(self, return_value='C'):
164 mock_raw_input = mock.Mock(return_value=return_value)
165 return mock.patch('__builtin__.raw_input', mock_raw_input)
166
167 def test_success(self, mock_print):
168- with self.patch_call(0) as mock_call:
169+ with self.patch_check_ssh_keys(return_value=True) as mock_check:
170 app.ensure_ssh_keys()
171- mock_call.assert_called_with(*self.ssh_key_find)
172+ self.assertTrue(mock_check.called)
173
174 def test_failure_no_keygen(self, mock_print):
175- msg = 'Please run these commands and follow the instructions they ' \
176- 'produce before running quickstart again:\n\n' \
177- 'ssh-keygen -b 4096 -t rsa'
178- with self.assert_program_exit(msg):
179- with self.patch_call(130) as mock_call:
180+ msg = 'If you would like to create the keys yourself, please run ' \
181+ 'this command, follow its instructions, and then re-run ' \
182+ 'quickstart:\n\nssh-keygen -b 4096 -t rsa && ssh-agent'
183+ with mock.patch('sys.exit') as mock_exit:
184+ with self.patch_check_ssh_keys() as mock_check:
185 with self.patch_raw_input() as mock_raw_input:
186 app.ensure_ssh_keys()
187- mock_call.assert_called_with(*self.ssh_key_find)
188+ self.assertTrue(mock_check.called)
189 mock_print.assert_has_calls([mock.call(self.print_msg)])
190 self.assertTrue(mock_raw_input.called)
191+ mock_exit.assert_called_once_with(msg)
192
193 def test_failure_with_keygen(self, mock_print):
194- with self.patch_call(130) as mock_call:
195- with self.patch_raw_input(return_value='Y') as mock_raw_input:
196+ with self.patch_check_ssh_keys() as mock_check:
197+ with self.patch_raw_input(return_value='A') as mock_raw_input:
198 with mock.patch('quickstart.app.create_ssh_keys') \
199 as mock_create_ssh_keys:
200 app.ensure_ssh_keys()
201- mock_call.assert_called_with(*self.ssh_key_find)
202+ self.assertTrue(mock_check.called)
203 mock_print.assert_has_calls([mock.call(self.print_msg)])
204 self.assertTrue(mock_raw_input.called)
205 self.assertTrue(mock_create_ssh_keys.called)
206
207+ def test_failure_with_watch(self, mock_print):
208+ with self.patch_check_ssh_keys() as mock_check:
209+ with self.patch_raw_input(return_value='S') as mock_raw_input:
210+ with mock.patch('quickstart.app.watch_for_ssh_keys') \
211+ as mock_watch_for_ssh_keys:
212+ app.ensure_ssh_keys()
213+ self.assertTrue(mock_check.called)
214+ mock_print.assert_has_calls([mock.call(self.print_msg)])
215+ self.assertTrue(mock_raw_input.called)
216+ self.assertTrue(mock_watch_for_ssh_keys.called)
217+
218+
219+class TestCheckSSHKeys(
220+ helpers.CallTestsMixin, ProgramExitTestsMixin, unittest.TestCase):
221+
222+ def test_keys_and_agent(self):
223+ with self.patch_call(0) as mock_call:
224+ have_keys = app.check_ssh_keys()
225+ mock_call.assert_called_once_with('/usr/bin/ssh-add', '-l')
226+ self.assertTrue(have_keys)
227+
228+ def test_agent_no_keys_success(self):
229+ side_effects = (
230+ (1, 'The agent has no identities.\n', ''),
231+ (0, '', ''),
232+ )
233+ with self.patch_multiple_calls(side_effects) as mock_call:
234+ have_keys = app.check_ssh_keys()
235+ mock_call.assert_has_calls([
236+ mock.call('/usr/bin/ssh-add', '-l'),
237+ mock.call('/usr/bin/ssh-add'),
238+ ])
239+ self.assertTrue(have_keys)
240+
241+ def test_agent_no_keys_failure(self):
242+ side_effects = (
243+ (1, 'The agent has no identities.\n', ''),
244+ (1, 'Still no identities...', ''),
245+ )
246+ with self.patch_multiple_calls(side_effects) as mock_call:
247+ have_keys = app.check_ssh_keys()
248+ mock_call.assert_has_calls([
249+ mock.call('/usr/bin/ssh-add', '-l'),
250+ mock.call('/usr/bin/ssh-add'),
251+ ])
252+ self.assertFalse(have_keys)
253+
254+ def test_agent_bad_keys(self):
255+ side_effects = (
256+ (1, 'The agent has no identities.\n', ''),
257+ (2, '', 'Oh no!'),
258+ )
259+ msg = 'error attempting to add ssh keys! Oh no!'
260+ with self.assert_program_exit(msg):
261+ with self.patch_multiple_calls(side_effects) as mock_call:
262+ app.check_ssh_keys()
263+ mock_call.assert_has_calls([
264+ mock.call('/usr/bin/ssh-add', '-l'),
265+ mock.call('/usr/bin/ssh-add'),
266+ ])
267+
268+ def test_no_agent(self):
269+ with self.patch_call(2) as mock_call:
270+ have_keys = app.check_ssh_keys()
271+ mock_call.assert_called_once_with('/usr/bin/ssh-add', '-l')
272+ self.assertFalse(have_keys)
273+
274+
275+@mock.patch('time.sleep')
276+@helpers.mock_print
277+class TestWatchForSSHKeys(helpers.CallTestsMixin, unittest.TestCase):
278+
279+ def test_watch(self, mock_print, mock_sleep):
280+ with mock.patch('quickstart.app.check_ssh_keys',
281+ mock.Mock(side_effect=(False, True))):
282+ app.watch_for_ssh_keys()
283+ mock_print.assert_has_calls([
284+ mock.call('Please run this command in another terminal or window '
285+ 'and follow the instructions it produces; quickstart '
286+ 'will continue when keys are generated. ^C to quit.\n\n'
287+ 'ssh-keygen -b 4096 -t rsa && ssh-agent\n'),
288+ mock.call('.', end='')
289+ ])
290+ mock_sleep.assert_called_once_with(3)
291+
292+ def test_cancel(self, mock_print, mock_sleep):
293+ with mock.patch('quickstart.app.check_ssh_keys',
294+ mock.Mock(side_effect=KeyboardInterrupt)):
295+ with mock.patch('sys.exit') as mock_exit:
296+ app.watch_for_ssh_keys()
297+ mock_print.assert_called_once_with(
298+ 'Please run this command in another terminal or window '
299+ 'and follow the instructions it produces; quickstart '
300+ 'will continue when keys are generated. ^C to quit.\n\n'
301+ 'ssh-keygen -b 4096 -t rsa && ssh-agent\n')
302+ mock_exit.assert_called_once_with('\nquitting')
303+
304
305 @helpers.mock_print
306 class TestCreateSSHKeys(
307 helpers.CallTestsMixin, ProgramExitTestsMixin, unittest.TestCase):
308
309 def test_success(self, mock_print):
310- key_file = os.path.join(os.path.expanduser('~'), '.ssh', 'id_juju_rsa')
311+ key_file = os.path.join(os.path.expanduser('~'), '.ssh', 'id_rsa')
312 with self.patch_call(0) as mock_call:
313 app.create_ssh_keys()
314- mock_call.assert_called_once_with('/usr/bin/ssh-keygen',
315- '-q', '-b', '4096', '-t', 'rsa',
316- '-C',
317- 'Generated with Juju Quickstart',
318- '-f', key_file)
319+ mock_call.assert_has_calls([
320+ mock.call('/usr/bin/ssh-keygen',
321+ '-q', '-b', '4096', '-t', 'rsa',
322+ '-C',
323+ 'Generated with Juju Quickstart',
324+ '-f', key_file),
325+ mock.call('/usr/bin/ssh-agent')
326+ ])
327 mock_print.assert_called_with('a new ssh key was generated in {}'
328 .format(key_file))
329
330@@ -246,6 +348,14 @@
331 with self.patch_call(1, error='Oh no!') as mock_call:
332 app.create_ssh_keys()
333 self.assertTrue(mock_call.called)
334+ with self.assert_program_exit('error starting ssh-agent! Oh no!'):
335+ side_effects = (
336+ (0, '', ''),
337+ (1, '', 'Oh no!'),
338+ )
339+ with self.patch_multiple_calls(side_effects) as mock_call:
340+ app.create_ssh_keys()
341+ self.assertTrue(mock_call.called)
342
343
344 @helpers.mock_print
345
346=== modified file 'quickstart/tests/test_utils.py'
347--- quickstart/tests/test_utils.py 2013-12-16 23:52:37 +0000
348+++ quickstart/tests/test_utils.py 2013-12-17 21:07:10 +0000
349@@ -154,6 +154,37 @@
350 utils.call('echo', 'we are the borg!')
351
352
353+@helpers.mock_print
354+class TestStartSSHAgent(helpers.CallTestsMixin, unittest.TestCase):
355+
356+ def test_success(self, mock_print):
357+ out = 'SSH_AUTH_SOCK=/tmp/ssh-authsock/agent.21000; ' \
358+ 'export SSH_AUTH_SOCK;\n' \
359+ 'SSH_AGENT_PID=21001; export SSH_AGENT_PID;\n' \
360+ 'echo Agent pid 21001;'
361+ with self.patch_call(0, out, '') as mock_call:
362+ with mock.patch('os.putenv') as mock_putenv:
363+ utils.start_ssh_agent()
364+ mock_call.assert_called_once_with('/usr/bin/ssh-agent')
365+ mock_putenv.assert_has_calls([
366+ mock.call('SSH_AUTH_SOCK', '/tmp/ssh-authsock/agent.21000'),
367+ mock.call('SSH_AGENT_PID', '21001'),
368+ ])
369+ mock_print.assert_called_once_with(
370+ 'ssh-agent has been started; it will run for the duration of '
371+ 'quickstart, but will be unavailable once quickstart finishes. '
372+ 'To interact with Juju or quickstart again after quickstart '
373+ 'finishes, please run the following in a terminal to start '
374+ 'ssh-agent:\n\n'
375+ 'eval `ssh-agent`')
376+
377+ def test_failure(self, mock_print):
378+ with self.patch_call(1, 'Cannot start agent!', '') as mock_call:
379+ with self.assertRaises(OSError):
380+ utils.start_ssh_agent()
381+ self.assertTrue(mock_call.called)
382+
383+
384 @mock.patch('__builtin__.print', mock.Mock())
385 class TestCheckGuiCharmUrl(unittest.TestCase):
386
387
388=== modified file 'quickstart/utils.py'
389--- quickstart/utils.py 2013-12-17 15:23:28 +0000
390+++ quickstart/utils.py 2013-12-17 21:07:10 +0000
391@@ -42,7 +42,9 @@
392 import httplib
393 import json
394 import logging
395+import os
396 import pipes
397+import re
398 import socket
399 import subprocess
400 import urllib2
401@@ -105,6 +107,23 @@
402 return retcode, output.decode('utf-8'), error.decode('utf-8')
403
404
405+def start_ssh_agent():
406+ """Start an ssh-agent and propagate its environment variables"""
407+ retcode, output, error = call('/usr/bin/ssh-agent')
408+ if retcode != 0:
409+ raise OSError(error.encode('utf-8'))
410+ os.putenv('SSH_AUTH_SOCK',
411+ re.search('SSH_AUTH_SOCK=([^;]+);', output).group(1))
412+ os.putenv('SSH_AGENT_PID',
413+ re.search('SSH_AGENT_PID=([^;]+);', output).group(1))
414+ print('ssh-agent has been started; it will run for the duration of '
415+ 'quickstart, but will be unavailable once quickstart finishes. '
416+ 'To interact with Juju or quickstart again after quickstart '
417+ 'finishes, please run the following in a terminal to start '
418+ 'ssh-agent:\n\n'
419+ 'eval `ssh-agent`')
420+
421+
422 def check_gui_charm_url(charm_url):
423 """Print (to stdout or to logs) info and warnings about the charm URL."""
424 print('charm URL: {}'.format(charm_url))

Subscribers

People subscribed via source and target branches