Merge lp:~makyo/juju-quickstart/ssh-3-watch-keys into lp:juju-quickstart
- ssh-3-watch-keys
- Merge into trunk
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Juju GUI Hackers | Pending | ||
Review via email:
|
Commit message
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.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Madison Scott-Clary (makyo) wrote : | # |
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Gary Poster (gary) wrote : | # |
Wording suggestions. Didn't look other than that for now.
https:/
File quickstart/app.py (right):
https:/
quickstart/
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:/
quickstart/
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:/
quickstart/
'-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.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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:/
File quickstart/app.py (right):
https:/
quickstart/
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:/
quickstart/
'-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:/
quickstart/
I think this would be much better. OK with you?
print('.', end="")
https:/
File quickstart/
https:/
quickstart/
mock_sleep):
Nice test.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Madison Scott-Clary (makyo) wrote : | # |
Thanks for the comments. Will re-propose after fixing QA issue.
https:/
File quickstart/app.py (right):
https:/
quickstart/
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:/
quickstart/
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:/
quickstart/
'-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:/
quickstart/
On 2013/12/13 13:51:45, gary.poster wrote:
> I think this would be much better. OK with you?
> print('.', end="")
Done.
- 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.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Madison Scott-Clary (makyo) wrote : | # |
Please take a look.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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:/
File quickstart/app.py (right):
https:/
quickstart/
docstring please
https:/
quickstart/
docstring please
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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:/
- 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.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Madison Scott-Clary (makyo) wrote : | # |
Please take a look.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Francesco Banconi (frankban) wrote : | # |
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/
LGTM with (possible) changes.
QA ok with some comments (about the initial agent check and the SIGINT
handling).
https:/
File quickstart/app.py (right):
https:/
quickstart/
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:/
quickstart/
Maybe ^C here too? <shrug>
https:/
quickstart/
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:/
quickstart/
Very nice commented function, thank you.
https:/
quickstart/
utils.start_
I don't see the start_ssh_agent call here. Is this still true?
https:/
quickstart/
utils.call(
error does not seem to be used here and can be safely replaced with _
https:/
quickstart/
ProgramExit(
You can avoid encoding ProgramExit messages <shrug>.
https:/
quickstart/
^C produces a traceback. It's not that important, but we can easily
avoid it with something like:
try:
while not check_ssh_keys():
except KeyboardInterrupt:
https:/
quickstart/
Interesting, why is this required? A comment would be nice.
https:/
quickstart/
Is it ok if retcode is < 0? Can this happen?
If not, we can just check "if retcode:".
https:/
quickstart/
Same as above.
https:/
File quickstart/
- 41. By Madison Scott-Clary
-
responding to frankban's review comments.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Madison Scott-Clary (makyo) wrote : | # |
Please take a look.
https:/
File quickstart/app.py (right):
https:/
quickstart/
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:/
quickstart/
On 2013/12/17 17:22:39, frankban wrote:
> Maybe ^C here too? <shrug>
Done.
https:/
quickstart/
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:/
quickstart/
utils.start_
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:/
quickstart/
utils.call(
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:/
quickstart/
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(
Done.
https:/
quickstart/
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.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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://
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.
.......
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://
Note the use of more usual opposites of automatic/manual, indentation to
help distinguish commands to run manually as a callout, etc.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Richard Harding (rharding) wrote : | # |
The QA was LGTM with the comments and discussion points in my previous
message.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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:/
Preview Diff
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)) |
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): tests/test_ app.py
A [revision details]
M quickstart/app.py
M quickstart/
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 join(os. path.expanduser ('~'), '.ssh') '/usr/bin/ find', ssh_dir, '-name', 'id_*.pub')[0]
'Would you like quickstart to create them for you? Note
'this will create a new SSH key with the passphrase you
'available to your SSH agent. ssh-keygen will ask for a '
'passphrase, but juju quickstart will not have access to
create_ ssh_keys( ) ssh_keys( )
'quickstart again:\n\n'
'ssh- keygen -b 4096 -t rsa')
=== 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.
- retcode = utils.call(
- if retcode > 0:
+ if check_ssh_keys():
print('No SSH keys were found in ~/.ssh\n\n'
that '
provide '
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':
+ elif create_keys.lower() == 'w':
+ watch_for_
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 '
+def check_ssh_keys(): join(os. path.expanduser ('~'), '.ssh') '/usr/bin/ find', ssh_dir, '-name', 'id_*.pub')[0]
+ ssh_dir = os.path.
+ return utils.call(
+
+
+def watch_for_ssh_ke...