Merge lp:~dimitern/juju-core/302-lp-1281577-ssh-command-extra-args into lp:~go-bot/juju-core/trunk

Proposed by Dimiter Naydenov
Status: Merged
Approved by: Dimiter Naydenov
Approved revision: no longer in the source branch.
Merged at revision: 2348
Proposed branch: lp:~dimitern/juju-core/302-lp-1281577-ssh-command-extra-args
Merge into: lp:~go-bot/juju-core/trunk
Prerequisite: lp:~dimitern/juju-core/301-lp-1282553-use-curl-not-wget-precise
Diff against target: 662 lines (+185/-87)
12 files modified
cmd/juju/addmachine.go (+1/-0)
cmd/juju/debughooks_test.go (+2/-2)
cmd/juju/scp.go (+34/-12)
cmd/juju/scp_test.go (+40/-11)
cmd/juju/ssh.go (+10/-8)
cmd/juju/ssh_test.go (+12/-18)
utils/ssh/run_test.go (+1/-1)
utils/ssh/ssh.go (+15/-7)
utils/ssh/ssh_gocrypto.go (+2/-1)
utils/ssh/ssh_gocrypto_test.go (+1/-1)
utils/ssh/ssh_openssh.go (+48/-14)
utils/ssh/ssh_test.go (+19/-12)
To merge this branch: bzr merge lp:~dimitern/juju-core/302-lp-1281577-ssh-command-extra-args
Reviewer Review Type Date Requested Status
Adam Collard (community) Needs Fixing
Juju Engineering Pending
Review via email: mp+207498@code.launchpad.net

Commit message

cmd/juju: ssh/scp commands extra arguments; fixes

Removed the extraneous "--" that the ssh command
adds to the passed arguments, so any extra arguments
after the command will be forwarded to ssh properly.
It worked in 1.16, so this fixes a regression.
This fixes bug #1281577.

The scp command claims in its help that it supports
-- to pass extra arguments to scp, but it does not
and also does not support multiple local/remote
targets to be specified, as OpenSSH scp command does.
This fixes bug #1283412.

Also, improved a lot of ssh-related tests in both
utils/ssh/ and in cmd/juju/, adding new tests as
needed for the extra arguments.

Improved help texts for ssh, scp and add-machine
commands (included examples for the extra args
and multiple targets for scp, added ssh: example
for add-machine).

All changes are tested live:
 - bootstrapping on EC2 and running ssh or scp
 to test extra args and multiple targets;
 - manual bootstrapping
 - add-machine ssh:user@host to an existing env;
 - using juju on Windows, to test the embedded
 go.crypto ssh command.

https://codereview.appspot.com/66340045/

R=axwalk, gz

Description of the change

cmd/juju: ssh/scp commands extra arguments; fixes

Removed the extraneous "--" that the ssh command
adds to the passed arguments, so any extra arguments
after the command will be forwarded to ssh properly.
It worked in 1.16, so this fixes a regression.
This fixes bug #1281577.

The scp command claims in its help that it supports
-- to pass extra arguments to scp, but it does not
and also does not support multiple local/remote
targets to be specified, as OpenSSH scp command does.
This fixes bug #1283412.

Also, improved a lot of ssh-related tests in both
utils/ssh/ and in cmd/juju/, adding new tests as
needed for the extra arguments.

Improved help texts for ssh, scp and add-machine
commands (included examples for the extra args
and multiple targets for scp, added ssh: example
for add-machine).

All changes are tested live:
 - bootstrapping on EC2 and running ssh or scp
 to test extra args and multiple targets;
 - manual bootstrapping
 - add-machine ssh:user@host to an existing env;
 - using juju on Windows, to test the embedded
 go.crypto ssh command.

https://codereview.appspot.com/66340045/

To post a comment you must log in.
Revision history for this message
Dimiter Naydenov (dimitern) wrote :

Reviewers: mp+207498_code.launchpad.net,

Message:
Please take a look.

Description:
ssh: Fixed bug #1281577 - ssh command-line opts

Removed the extraneous "--" that the ssh command
adds to the passed arguments, so any extra arguments
after the command will be forwarded to ssh properly.
It worked in 1.16, so this fixes a regression.

https://code.launchpad.net/~dimitern/juju-core/302-lp-1281577-ssh-command-extra-args/+merge/207498

Requires:
https://code.launchpad.net/~dimitern/juju-core/301-lp-1282553-use-curl-not-wget-precise/+merge/207443

(do not edit description out of merge proposal)

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

Affected files (+3, -8 lines):
   A [revision details]
   M cmd/juju/ssh.go
   M utils/ssh/ssh_openssh.go

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: cmd/juju/ssh.go
=== modified file 'cmd/juju/ssh.go'
--- cmd/juju/ssh.go 2014-01-13 06:25:28 +0000
+++ cmd/juju/ssh.go 2014-02-20 16:46:11 +0000
@@ -82,15 +82,9 @@
   if err != nil {
    return err
   }
- args := c.Args
- if len(args) > 0 && args[0] == "--" {
- // utils/ssh adds "--"; we will continue to accept
- // it from the CLI for backwards compatibility.
- args = args[1:]
- }
   var options ssh.Options
   options.EnablePTY()
- cmd := ssh.Command("ubuntu@"+host, args, &options)
+ cmd := ssh.Command("ubuntu@"+host, c.Args, &options)
   cmd.Stdin = ctx.Stdin
   cmd.Stdout = ctx.Stdout
   cmd.Stderr = ctx.Stderr

Index: utils/ssh/ssh_openssh.go
=== modified file 'utils/ssh/ssh_openssh.go'
--- utils/ssh/ssh_openssh.go 2014-02-05 08:48:40 +0000
+++ utils/ssh/ssh_openssh.go 2014-02-20 16:46:11 +0000
@@ -113,7 +113,6 @@
   args := opensshOptions(options, sshKind)
   args = append(args, host)
   if len(command) > 0 {
- args = append(args, "--")
    args = append(args, command...)
   }
   bin, args := sshpassWrap("ssh", args)

Revision history for this message
Martin Packman (gz) wrote :

LGTM, but there are a few other questionable bits involved with the
original change in r2116 that rely on sane -- behaviour - perhaps we
should bug Andrew as well.

https://codereview.appspot.com/66340045/

Revision history for this message
Andrew Wilkins (axwalk) wrote :

On 2014/02/20 17:14:00, gz wrote:
> LGTM, but there are a few other questionable bits involved with the
original
> change in r2116 that rely on sane -- behaviour - perhaps we should bug
Andrew as
> well.

Which bits?

I don't recall my rationale, but it was probably something like: this is
relying on implementation details, namely that we're using OpenSSH.
We're not necessarily using OpenSSH now, so those args passed through
will break if the embedded client is used. I suppose it's not a big
deal.

https://codereview.appspot.com/66340045/

Revision history for this message
Dimiter Naydenov (dimitern) wrote :
Revision history for this message
Andrew Wilkins (axwalk) wrote :

https://codereview.appspot.com/66340045/diff/20001/cmd/juju/scp.go
File cmd/juju/scp.go (left):

https://codereview.appspot.com/66340045/diff/20001/cmd/juju/scp.go#oldcode73
cmd/juju/scp.go:73: // BUG(dfc) This will not work for IPv6 addresses
like 2001:db8::1:2:/somepath.
The BUG is still relevant, no?

https://codereview.appspot.com/66340045/diff/20001/cmd/juju/scp.go
File cmd/juju/scp.go (right):

https://codereview.appspot.com/66340045/diff/20001/cmd/juju/scp.go#newcode32
cmd/juju/scp.go:32: Copy 2 files from two units to the local backup/
directory, passig -v
s/passig/passing/

https://codereview.appspot.com/66340045/diff/20001/cmd/juju/scp.go#newcode35
cmd/juju/scp.go:35: juju scp ubuntu/0:/path/file1 -- -v
ubuntu/1:/path/file2 backup/
While it may be possible to do this, is it useful to highlight it as an
example? It seems a bit pathological, and not actually useful, so I'd
just move "-- -v" to the end.

Also, we shouldn't *need* -- at all if the options are at the end,
right? It would good if you could just do:
     juju scp 0:/remote/dir /local/dir -r

We may want to explicitly state that options are not supported unless
OpenSSH is available on the system... although, Copy isn't even
implemented using go.crypto at the moment.

Frankly, I think it's a bad idea to rely on implementation details like
this (which is not your fault of course). It would be better if we just
implemented a subset of the scp options and passed them through. Then
we're more easily able to implement those for the go.crypto client.

https://codereview.appspot.com/66340045/diff/20001/cmd/juju/scp.go#newcode91
cmd/juju/scp.go:91: if strings.HasPrefix(arg, "-") {
Actually, one of the more common usages of pass-through args that I've
seen so far is "-o", "option". This won't work, since the option value
will end up in the wrong place.

https://codereview.appspot.com/66340045/diff/20001/utils/ssh/ssh.go
File utils/ssh/ssh.go (right):

https://codereview.appspot.com/66340045/diff/20001/utils/ssh/ssh.go#newcode72
utils/ssh/ssh.go:72: // Copy copies file(s) between the local host a
remote host(s).
s/a /and /
(or just "local and remote")

https://codereview.appspot.com/66340045/diff/20001/utils/ssh/ssh.go#newcode74
utils/ssh/ssh.go:74: // any extra arguments are specified in extraArgs,
they are passed
Client is implementation agnostic. Talking about scp here is wrong.

https://codereview.appspot.com/66340045/diff/20001/utils/ssh/ssh_openssh.go
File utils/ssh/ssh_openssh.go (right):

https://codereview.appspot.com/66340045/diff/20001/utils/ssh/ssh_openssh.go#newcode17
utils/ssh/ssh_openssh.go:17: var opensshCommonOptions =
map[string][]string{"-o": []string{"StrictHostKeyChecking no"}}
What's the point of this change? To group options? Why? Seems to be
complicating things for no practical benefit.

https://codereview.appspot.com/66340045/

Revision history for this message
Adam Collard (adam-collard) wrote :

https://codereview.appspot.com/66340045/diff/20001/cmd/juju/scp.go#newcode32
cmd/juju/scp.go:32: Copy 2 files from two units to the local backup/
directory, passig -v
s/passig/passing/

It doesn't look like this comment got addressed.

http://bazaar.launchpad.net/~go-bot/juju-core/trunk/view/head:/cmd/juju/scp.go#L32

Shows it still says "passig"

review: Needs Fixing
Revision history for this message
Dimiter Naydenov (dimitern) wrote :

I'm about to propose a follow-up with the last review's suggestions, as the branch landed before that.

Revision history for this message
Dimiter Naydenov (dimitern) wrote :
Download full text (4.4 KiB)

Thanks for the review!
Find below my answers, and please review the follow-up which implements
the suggestions, here: https://codereview.appspot.com/67820048

https://codereview.appspot.com/66340045/diff/20001/cmd/juju/scp.go
File cmd/juju/scp.go (left):

https://codereview.appspot.com/66340045/diff/20001/cmd/juju/scp.go#oldcode73
cmd/juju/scp.go:73: // BUG(dfc) This will not work for IPv6 addresses
like 2001:db8::1:2:/somepath.
On 2014/02/24 04:29:36, axw wrote:
> The BUG is still relevant, no?

I thought I fixed it, but I forgot. According to
http://marc.info/?l=openssh-unix-dev&m=112975365929888 we need to add
`\[`+host+`\]:` so that IPv6 addresses will be properly separated from
the path. Fixed and added a test for it.

https://codereview.appspot.com/66340045/diff/20001/cmd/juju/scp.go
File cmd/juju/scp.go (right):

https://codereview.appspot.com/66340045/diff/20001/cmd/juju/scp.go#newcode32
cmd/juju/scp.go:32: Copy 2 files from two units to the local backup/
directory, passig -v
On 2014/02/24 04:29:36, axw wrote:
> s/passig/passing/

Done.

https://codereview.appspot.com/66340045/diff/20001/cmd/juju/scp.go#newcode35
cmd/juju/scp.go:35: juju scp ubuntu/0:/path/file1 -- -v
ubuntu/1:/path/file2 backup/
On 2014/02/24 04:29:36, axw wrote:
> While it may be possible to do this, is it useful to highlight it as
an example?
> It seems a bit pathological, and not actually useful, so I'd just move
"-- -v"
> to the end.

> Also, we shouldn't *need* -- at all if the options are at the end,
right? It
> would good if you could just do:
> juju scp 0:/remote/dir /local/dir -r

Good point, I've changed the implementation, help text, and tests to
accept extra arguments at the end only and -- is no longer needed or
allowed.

> We may want to explicitly state that options are not supported unless
OpenSSH is
> available on the system... although, Copy isn't even implemented using
go.crypto
> at the moment.

Mentioning OpenSSH is useful, added.

Also improved the error message given by go.crypto's Copy, because:
$ juju scp xx yy
ERROR Copy is not implemented

> Frankly, I think it's a bad idea to rely on implementation details
like this
> (which is not your fault of course). It would be better if we just
implemented a
> subset of the scp options and passed them through. Then we're more
easily able
> to implement those for the go.crypto client.

Yeah, that's definitely a future improvement worth doing, but not now.

https://codereview.appspot.com/66340045/diff/20001/cmd/juju/scp.go#newcode91
cmd/juju/scp.go:91: if strings.HasPrefix(arg, "-") {
On 2014/02/24 04:29:36, axw wrote:
> Actually, one of the more common usages of pass-through args that I've
seen so
> far is "-o", "option". This won't work, since the option value will
end up in
> the wrong place.

It works actually, added -o SomeOption in the extra args tests.

https://codereview.appspot.com/66340045/diff/20001/utils/ssh/ssh.go
File utils/ssh/ssh.go (right):

https://codereview.appspot.com/66340045/diff/20001/utils/ssh/ssh.go#newcode72
utils/ssh/ssh.go:72: // Copy copies file(s) between the local host a
remote host(s).
On 2014/02/24 04:29:36, axw wrote:
> s/a /and /
> (or just "local and remote"...

Read more...

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'cmd/juju/addmachine.go'
2--- cmd/juju/addmachine.go 2013-12-20 09:25:57 +0000
3+++ cmd/juju/addmachine.go 2014-02-22 15:58:57 +0000
4@@ -48,6 +48,7 @@
5 juju add-machine lxc (starts a new machine with an lxc container)
6 juju add-machine lxc:4 (starts a new lxc container on machine 4)
7 juju add-machine --constraints mem=8G (starts a machine with at least 8GB RAM)
8+ juju add-machine ssh:user@10.10.0.3 (manually provisions a machine with ssh)
9
10 See Also:
11 juju help constraints
12
13=== modified file 'cmd/juju/debughooks_test.go'
14--- cmd/juju/debughooks_test.go 2013-12-03 14:19:47 +0000
15+++ cmd/juju/debughooks_test.go 2014-02-22 15:58:57 +0000
16@@ -29,10 +29,10 @@
17 stderr string
18 }{{
19 args: []string{"mysql/0"},
20- result: regexp.QuoteMeta(debugHooksArgs + "ubuntu@dummyenv-0.dns -- sudo /bin/bash -c 'F=$(mktemp); echo IyEvYmluL2Jhc2gKKAojIExvY2sgdGhlIGp1anUtPHVuaXQ+LWRlYnVnIGxvY2tmaWxlLgpmbG9jayAtbiA4IHx8IChlY2hvICJGYWlsZWQgdG8gYWNxdWlyZSAvdG1wL2p1anUtdW5pdC1teXNxbC0wLWRlYnVnLWhvb2tzOiB1bml0IGlzIGFscmVhZHkgYmVpbmcgZGVidWdnZWQiIDI+JjE7IGV4aXQgMSkKKAojIENsb3NlIHRoZSBpbmhlcml0ZWQgbG9jayBGRCwgb3IgdG11eCB3aWxsIGtlZXAgaXQgb3Blbi4KZXhlYyA4PiYtCgojIFdyaXRlIG91dCB0aGUgZGVidWctaG9va3MgYXJncy4KZWNobyAiZTMwSyIgfCBiYXNlNjQgLWQgPiAvdG1wL2p1anUtdW5pdC1teXNxbC0wLWRlYnVnLWhvb2tzCgojIExvY2sgdGhlIGp1anUtPHVuaXQ+LWRlYnVnLWV4aXQgbG9ja2ZpbGUuCmZsb2NrIC1uIDkgfHwgZXhpdCAxCgojIFdhaXQgZm9yIHRtdXggdG8gYmUgaW5zdGFsbGVkLgp3aGlsZSBbICEgLWYgL3Vzci9iaW4vdG11eCBdOyBkbwogICAgc2xlZXAgMQpkb25lCgppZiBbICEgLWYgfi8udG11eC5jb25mIF07IHRoZW4KICAgICAgICBpZiBbIC1mIC91c3Ivc2hhcmUvYnlvYnUvcHJvZmlsZXMvdG11eCBdOyB0aGVuCiAgICAgICAgICAgICAgICAjIFVzZSBieW9idS90bXV4IHByb2ZpbGUgZm9yIGZhbWlsaWFyIGtleWJpbmRpbmdzIGFuZCBicmFuZGluZwogICAgICAgICAgICAgICAgZWNobyAic291cmNlLWZpbGUgL3Vzci9zaGFyZS9ieW9idS9wcm9maWxlcy90bXV4IiA+IH4vLnRtdXguY29uZgogICAgICAgIGVsc2UKICAgICAgICAgICAgICAgICMgT3RoZXJ3aXNlLCB1c2UgdGhlIGxlZ2FjeSBqdWp1L3RtdXggY29uZmlndXJhdGlvbgogICAgICAgICAgICAgICAgY2F0ID4gfi8udG11eC5jb25mIDw8RU5ECiAgICAgICAgICAgICAgICAKIyBTdGF0dXMgYmFyCnNldC1vcHRpb24gLWcgc3RhdHVzLWJnIGJsYWNrCnNldC1vcHRpb24gLWcgc3RhdHVzLWZnIHdoaXRlCgpzZXQtd2luZG93LW9wdGlvbiAtZyB3aW5kb3ctc3RhdHVzLWN1cnJlbnQtYmcgcmVkCnNldC13aW5kb3ctb3B0aW9uIC1nIHdpbmRvdy1zdGF0dXMtY3VycmVudC1hdHRyIGJyaWdodAoKc2V0LW9wdGlvbiAtZyBzdGF0dXMtcmlnaHQgJycKCiMgUGFuZXMKc2V0LW9wdGlvbiAtZyBwYW5lLWJvcmRlci1mZyB3aGl0ZQpzZXQtb3B0aW9uIC1nIHBhbmUtYWN0aXZlLWJvcmRlci1mZyB3aGl0ZQoKIyBNb25pdG9yIGFjdGl2aXR5IG9uIHdpbmRvd3MKc2V0LXdpbmRvdy1vcHRpb24gLWcgbW9uaXRvci1hY3Rpdml0eSBvbgoKIyBTY3JlZW4gYmluZGluZ3MsIHNpbmNlIHBlb3BsZSBhcmUgbW9yZSBmYW1pbGlhciB3aXRoIHRoYXQuCnNldC1vcHRpb24gLWcgcHJlZml4IEMtYQpiaW5kIEMtYSBsYXN0LXdpbmRvdwpiaW5kIGEgc2VuZC1rZXkgQy1hCgpiaW5kIHwgc3BsaXQtd2luZG93IC1oCmJpbmQgLSBzcGxpdC13aW5kb3cgLXYKCiMgRml4IENUUkwtUEdVUC9QR0RPV04gZm9yIHZpbQpzZXQtd2luZG93LW9wdGlvbiAtZyB4dGVybS1rZXlzIG9uCgojIFByZXZlbnQgRVNDIGtleSBmcm9tIGFkZGluZyBkZWxheSBhbmQgYnJlYWtpbmcgVmltJ3MgRVNDID4gYXJyb3cga2V5CnNldC1vcHRpb24gLXMgZXNjYXBlLXRpbWUgMAoKRU5ECiAgICAgICAgZmkKZmkKCigKICAgICMgQ2xvc2UgdGhlIGluaGVyaXRlZCBsb2NrIEZELCBvciB0bXV4IHdpbGwga2VlcCBpdCBvcGVuLgogICAgZXhlYyA5PiYtCiAgICBleGVjIHRtdXggbmV3LXNlc3Npb24gLXMgbXlzcWwvMAopCikgOT4vdG1wL2p1anUtdW5pdC1teXNxbC0wLWRlYnVnLWhvb2tzLWV4aXQKKSA4Pi90bXAvanVqdS11bml0LW15c3FsLTAtZGVidWctaG9va3MKZXhpdCAkPwo= | base64 -d > $F; . $F'\n"),
21+ result: regexp.QuoteMeta(debugHooksArgs + "ubuntu@dummyenv-0.dns sudo /bin/bash -c 'F=$(mktemp); echo IyEvYmluL2Jhc2gKKAojIExvY2sgdGhlIGp1anUtPHVuaXQ+LWRlYnVnIGxvY2tmaWxlLgpmbG9jayAtbiA4IHx8IChlY2hvICJGYWlsZWQgdG8gYWNxdWlyZSAvdG1wL2p1anUtdW5pdC1teXNxbC0wLWRlYnVnLWhvb2tzOiB1bml0IGlzIGFscmVhZHkgYmVpbmcgZGVidWdnZWQiIDI+JjE7IGV4aXQgMSkKKAojIENsb3NlIHRoZSBpbmhlcml0ZWQgbG9jayBGRCwgb3IgdG11eCB3aWxsIGtlZXAgaXQgb3Blbi4KZXhlYyA4PiYtCgojIFdyaXRlIG91dCB0aGUgZGVidWctaG9va3MgYXJncy4KZWNobyAiZTMwSyIgfCBiYXNlNjQgLWQgPiAvdG1wL2p1anUtdW5pdC1teXNxbC0wLWRlYnVnLWhvb2tzCgojIExvY2sgdGhlIGp1anUtPHVuaXQ+LWRlYnVnLWV4aXQgbG9ja2ZpbGUuCmZsb2NrIC1uIDkgfHwgZXhpdCAxCgojIFdhaXQgZm9yIHRtdXggdG8gYmUgaW5zdGFsbGVkLgp3aGlsZSBbICEgLWYgL3Vzci9iaW4vdG11eCBdOyBkbwogICAgc2xlZXAgMQpkb25lCgppZiBbICEgLWYgfi8udG11eC5jb25mIF07IHRoZW4KICAgICAgICBpZiBbIC1mIC91c3Ivc2hhcmUvYnlvYnUvcHJvZmlsZXMvdG11eCBdOyB0aGVuCiAgICAgICAgICAgICAgICAjIFVzZSBieW9idS90bXV4IHByb2ZpbGUgZm9yIGZhbWlsaWFyIGtleWJpbmRpbmdzIGFuZCBicmFuZGluZwogICAgICAgICAgICAgICAgZWNobyAic291cmNlLWZpbGUgL3Vzci9zaGFyZS9ieW9idS9wcm9maWxlcy90bXV4IiA+IH4vLnRtdXguY29uZgogICAgICAgIGVsc2UKICAgICAgICAgICAgICAgICMgT3RoZXJ3aXNlLCB1c2UgdGhlIGxlZ2FjeSBqdWp1L3RtdXggY29uZmlndXJhdGlvbgogICAgICAgICAgICAgICAgY2F0ID4gfi8udG11eC5jb25mIDw8RU5ECiAgICAgICAgICAgICAgICAKIyBTdGF0dXMgYmFyCnNldC1vcHRpb24gLWcgc3RhdHVzLWJnIGJsYWNrCnNldC1vcHRpb24gLWcgc3RhdHVzLWZnIHdoaXRlCgpzZXQtd2luZG93LW9wdGlvbiAtZyB3aW5kb3ctc3RhdHVzLWN1cnJlbnQtYmcgcmVkCnNldC13aW5kb3ctb3B0aW9uIC1nIHdpbmRvdy1zdGF0dXMtY3VycmVudC1hdHRyIGJyaWdodAoKc2V0LW9wdGlvbiAtZyBzdGF0dXMtcmlnaHQgJycKCiMgUGFuZXMKc2V0LW9wdGlvbiAtZyBwYW5lLWJvcmRlci1mZyB3aGl0ZQpzZXQtb3B0aW9uIC1nIHBhbmUtYWN0aXZlLWJvcmRlci1mZyB3aGl0ZQoKIyBNb25pdG9yIGFjdGl2aXR5IG9uIHdpbmRvd3MKc2V0LXdpbmRvdy1vcHRpb24gLWcgbW9uaXRvci1hY3Rpdml0eSBvbgoKIyBTY3JlZW4gYmluZGluZ3MsIHNpbmNlIHBlb3BsZSBhcmUgbW9yZSBmYW1pbGlhciB3aXRoIHRoYXQuCnNldC1vcHRpb24gLWcgcHJlZml4IEMtYQpiaW5kIEMtYSBsYXN0LXdpbmRvdwpiaW5kIGEgc2VuZC1rZXkgQy1hCgpiaW5kIHwgc3BsaXQtd2luZG93IC1oCmJpbmQgLSBzcGxpdC13aW5kb3cgLXYKCiMgRml4IENUUkwtUEdVUC9QR0RPV04gZm9yIHZpbQpzZXQtd2luZG93LW9wdGlvbiAtZyB4dGVybS1rZXlzIG9uCgojIFByZXZlbnQgRVNDIGtleSBmcm9tIGFkZGluZyBkZWxheSBhbmQgYnJlYWtpbmcgVmltJ3MgRVNDID4gYXJyb3cga2V5CnNldC1vcHRpb24gLXMgZXNjYXBlLXRpbWUgMAoKRU5ECiAgICAgICAgZmkKZmkKCigKICAgICMgQ2xvc2UgdGhlIGluaGVyaXRlZCBsb2NrIEZELCBvciB0bXV4IHdpbGwga2VlcCBpdCBvcGVuLgogICAgZXhlYyA5PiYtCiAgICBleGVjIHRtdXggbmV3LXNlc3Npb24gLXMgbXlzcWwvMAopCikgOT4vdG1wL2p1anUtdW5pdC1teXNxbC0wLWRlYnVnLWhvb2tzLWV4aXQKKSA4Pi90bXAvanVqdS11bml0LW15c3FsLTAtZGVidWctaG9va3MKZXhpdCAkPwo= | base64 -d > $F; . $F'\n"),
22 }, {
23 args: []string{"mongodb/1"},
24- result: regexp.QuoteMeta(debugHooksArgs + "ubuntu@dummyenv-2.dns -- sudo /bin/bash -c 'F=$(mktemp); echo IyEvYmluL2Jhc2gKKAojIExvY2sgdGhlIGp1anUtPHVuaXQ+LWRlYnVnIGxvY2tmaWxlLgpmbG9jayAtbiA4IHx8IChlY2hvICJGYWlsZWQgdG8gYWNxdWlyZSAvdG1wL2p1anUtdW5pdC1tb25nb2RiLTEtZGVidWctaG9va3M6IHVuaXQgaXMgYWxyZWFkeSBiZWluZyBkZWJ1Z2dlZCIgMj4mMTsgZXhpdCAxKQooCiMgQ2xvc2UgdGhlIGluaGVyaXRlZCBsb2NrIEZELCBvciB0bXV4IHdpbGwga2VlcCBpdCBvcGVuLgpleGVjIDg+Ji0KCiMgV3JpdGUgb3V0IHRoZSBkZWJ1Zy1ob29rcyBhcmdzLgplY2hvICJlMzBLIiB8IGJhc2U2NCAtZCA+IC90bXAvanVqdS11bml0LW1vbmdvZGItMS1kZWJ1Zy1ob29rcwoKIyBMb2NrIHRoZSBqdWp1LTx1bml0Pi1kZWJ1Zy1leGl0IGxvY2tmaWxlLgpmbG9jayAtbiA5IHx8IGV4aXQgMQoKIyBXYWl0IGZvciB0bXV4IHRvIGJlIGluc3RhbGxlZC4Kd2hpbGUgWyAhIC1mIC91c3IvYmluL3RtdXggXTsgZG8KICAgIHNsZWVwIDEKZG9uZQoKaWYgWyAhIC1mIH4vLnRtdXguY29uZiBdOyB0aGVuCiAgICAgICAgaWYgWyAtZiAvdXNyL3NoYXJlL2J5b2J1L3Byb2ZpbGVzL3RtdXggXTsgdGhlbgogICAgICAgICAgICAgICAgIyBVc2UgYnlvYnUvdG11eCBwcm9maWxlIGZvciBmYW1pbGlhciBrZXliaW5kaW5ncyBhbmQgYnJhbmRpbmcKICAgICAgICAgICAgICAgIGVjaG8gInNvdXJjZS1maWxlIC91c3Ivc2hhcmUvYnlvYnUvcHJvZmlsZXMvdG11eCIgPiB+Ly50bXV4LmNvbmYKICAgICAgICBlbHNlCiAgICAgICAgICAgICAgICAjIE90aGVyd2lzZSwgdXNlIHRoZSBsZWdhY3kganVqdS90bXV4IGNvbmZpZ3VyYXRpb24KICAgICAgICAgICAgICAgIGNhdCA+IH4vLnRtdXguY29uZiA8PEVORAogICAgICAgICAgICAgICAgCiMgU3RhdHVzIGJhcgpzZXQtb3B0aW9uIC1nIHN0YXR1cy1iZyBibGFjawpzZXQtb3B0aW9uIC1nIHN0YXR1cy1mZyB3aGl0ZQoKc2V0LXdpbmRvdy1vcHRpb24gLWcgd2luZG93LXN0YXR1cy1jdXJyZW50LWJnIHJlZApzZXQtd2luZG93LW9wdGlvbiAtZyB3aW5kb3ctc3RhdHVzLWN1cnJlbnQtYXR0ciBicmlnaHQKCnNldC1vcHRpb24gLWcgc3RhdHVzLXJpZ2h0ICcnCgojIFBhbmVzCnNldC1vcHRpb24gLWcgcGFuZS1ib3JkZXItZmcgd2hpdGUKc2V0LW9wdGlvbiAtZyBwYW5lLWFjdGl2ZS1ib3JkZXItZmcgd2hpdGUKCiMgTW9uaXRvciBhY3Rpdml0eSBvbiB3aW5kb3dzCnNldC13aW5kb3ctb3B0aW9uIC1nIG1vbml0b3ItYWN0aXZpdHkgb24KCiMgU2NyZWVuIGJpbmRpbmdzLCBzaW5jZSBwZW9wbGUgYXJlIG1vcmUgZmFtaWxpYXIgd2l0aCB0aGF0LgpzZXQtb3B0aW9uIC1nIHByZWZpeCBDLWEKYmluZCBDLWEgbGFzdC13aW5kb3cKYmluZCBhIHNlbmQta2V5IEMtYQoKYmluZCB8IHNwbGl0LXdpbmRvdyAtaApiaW5kIC0gc3BsaXQtd2luZG93IC12CgojIEZpeCBDVFJMLVBHVVAvUEdET1dOIGZvciB2aW0Kc2V0LXdpbmRvdy1vcHRpb24gLWcgeHRlcm0ta2V5cyBvbgoKIyBQcmV2ZW50IEVTQyBrZXkgZnJvbSBhZGRpbmcgZGVsYXkgYW5kIGJyZWFraW5nIFZpbSdzIEVTQyA+IGFycm93IGtleQpzZXQtb3B0aW9uIC1zIGVzY2FwZS10aW1lIDAKCkVORAogICAgICAgIGZpCmZpCgooCiAgICAjIENsb3NlIHRoZSBpbmhlcml0ZWQgbG9jayBGRCwgb3IgdG11eCB3aWxsIGtlZXAgaXQgb3Blbi4KICAgIGV4ZWMgOT4mLQogICAgZXhlYyB0bXV4IG5ldy1zZXNzaW9uIC1zIG1vbmdvZGIvMQopCikgOT4vdG1wL2p1anUtdW5pdC1tb25nb2RiLTEtZGVidWctaG9va3MtZXhpdAopIDg+L3RtcC9qdWp1LXVuaXQtbW9uZ29kYi0xLWRlYnVnLWhvb2tzCmV4aXQgJD8K | base64 -d > $F; . $F'\n"),
25+ result: regexp.QuoteMeta(debugHooksArgs + "ubuntu@dummyenv-2.dns sudo /bin/bash -c 'F=$(mktemp); echo IyEvYmluL2Jhc2gKKAojIExvY2sgdGhlIGp1anUtPHVuaXQ+LWRlYnVnIGxvY2tmaWxlLgpmbG9jayAtbiA4IHx8IChlY2hvICJGYWlsZWQgdG8gYWNxdWlyZSAvdG1wL2p1anUtdW5pdC1tb25nb2RiLTEtZGVidWctaG9va3M6IHVuaXQgaXMgYWxyZWFkeSBiZWluZyBkZWJ1Z2dlZCIgMj4mMTsgZXhpdCAxKQooCiMgQ2xvc2UgdGhlIGluaGVyaXRlZCBsb2NrIEZELCBvciB0bXV4IHdpbGwga2VlcCBpdCBvcGVuLgpleGVjIDg+Ji0KCiMgV3JpdGUgb3V0IHRoZSBkZWJ1Zy1ob29rcyBhcmdzLgplY2hvICJlMzBLIiB8IGJhc2U2NCAtZCA+IC90bXAvanVqdS11bml0LW1vbmdvZGItMS1kZWJ1Zy1ob29rcwoKIyBMb2NrIHRoZSBqdWp1LTx1bml0Pi1kZWJ1Zy1leGl0IGxvY2tmaWxlLgpmbG9jayAtbiA5IHx8IGV4aXQgMQoKIyBXYWl0IGZvciB0bXV4IHRvIGJlIGluc3RhbGxlZC4Kd2hpbGUgWyAhIC1mIC91c3IvYmluL3RtdXggXTsgZG8KICAgIHNsZWVwIDEKZG9uZQoKaWYgWyAhIC1mIH4vLnRtdXguY29uZiBdOyB0aGVuCiAgICAgICAgaWYgWyAtZiAvdXNyL3NoYXJlL2J5b2J1L3Byb2ZpbGVzL3RtdXggXTsgdGhlbgogICAgICAgICAgICAgICAgIyBVc2UgYnlvYnUvdG11eCBwcm9maWxlIGZvciBmYW1pbGlhciBrZXliaW5kaW5ncyBhbmQgYnJhbmRpbmcKICAgICAgICAgICAgICAgIGVjaG8gInNvdXJjZS1maWxlIC91c3Ivc2hhcmUvYnlvYnUvcHJvZmlsZXMvdG11eCIgPiB+Ly50bXV4LmNvbmYKICAgICAgICBlbHNlCiAgICAgICAgICAgICAgICAjIE90aGVyd2lzZSwgdXNlIHRoZSBsZWdhY3kganVqdS90bXV4IGNvbmZpZ3VyYXRpb24KICAgICAgICAgICAgICAgIGNhdCA+IH4vLnRtdXguY29uZiA8PEVORAogICAgICAgICAgICAgICAgCiMgU3RhdHVzIGJhcgpzZXQtb3B0aW9uIC1nIHN0YXR1cy1iZyBibGFjawpzZXQtb3B0aW9uIC1nIHN0YXR1cy1mZyB3aGl0ZQoKc2V0LXdpbmRvdy1vcHRpb24gLWcgd2luZG93LXN0YXR1cy1jdXJyZW50LWJnIHJlZApzZXQtd2luZG93LW9wdGlvbiAtZyB3aW5kb3ctc3RhdHVzLWN1cnJlbnQtYXR0ciBicmlnaHQKCnNldC1vcHRpb24gLWcgc3RhdHVzLXJpZ2h0ICcnCgojIFBhbmVzCnNldC1vcHRpb24gLWcgcGFuZS1ib3JkZXItZmcgd2hpdGUKc2V0LW9wdGlvbiAtZyBwYW5lLWFjdGl2ZS1ib3JkZXItZmcgd2hpdGUKCiMgTW9uaXRvciBhY3Rpdml0eSBvbiB3aW5kb3dzCnNldC13aW5kb3ctb3B0aW9uIC1nIG1vbml0b3ItYWN0aXZpdHkgb24KCiMgU2NyZWVuIGJpbmRpbmdzLCBzaW5jZSBwZW9wbGUgYXJlIG1vcmUgZmFtaWxpYXIgd2l0aCB0aGF0LgpzZXQtb3B0aW9uIC1nIHByZWZpeCBDLWEKYmluZCBDLWEgbGFzdC13aW5kb3cKYmluZCBhIHNlbmQta2V5IEMtYQoKYmluZCB8IHNwbGl0LXdpbmRvdyAtaApiaW5kIC0gc3BsaXQtd2luZG93IC12CgojIEZpeCBDVFJMLVBHVVAvUEdET1dOIGZvciB2aW0Kc2V0LXdpbmRvdy1vcHRpb24gLWcgeHRlcm0ta2V5cyBvbgoKIyBQcmV2ZW50IEVTQyBrZXkgZnJvbSBhZGRpbmcgZGVsYXkgYW5kIGJyZWFraW5nIFZpbSdzIEVTQyA+IGFycm93IGtleQpzZXQtb3B0aW9uIC1zIGVzY2FwZS10aW1lIDAKCkVORAogICAgICAgIGZpCmZpCgooCiAgICAjIENsb3NlIHRoZSBpbmhlcml0ZWQgbG9jayBGRCwgb3IgdG11eCB3aWxsIGtlZXAgaXQgb3Blbi4KICAgIGV4ZWMgOT4mLQogICAgZXhlYyB0bXV4IG5ldy1zZXNzaW9uIC1zIG1vbmdvZGIvMQopCikgOT4vdG1wL2p1anUtdW5pdC1tb25nb2RiLTEtZGVidWctaG9va3MtZXhpdAopIDg+L3RtcC9qdWp1LXVuaXQtbW9uZ29kYi0xLWRlYnVnLWhvb2tzCmV4aXQgJD8K | base64 -d > $F; . $F'\n"),
26 }, {
27 info: `"*" is a valid hook name: it means hook everything`,
28 args: []string{"mysql/0", "*"},
29
30=== modified file 'cmd/juju/scp.go'
31--- cmd/juju/scp.go 2014-01-14 02:53:16 +0000
32+++ cmd/juju/scp.go 2014-02-22 15:58:57 +0000
33@@ -17,17 +17,23 @@
34 }
35
36 const scpDoc = `
37-Lauch an scp command to copy files. <to> and <from> are either local
38-file paths or remote locations of the form <target>:<path>, where
39-<target> can be either a machine id as listed by "juju status" in the
40+Launch an scp command to copy files. Each argument <file1> ... <file2>
41+is either local file path or remote locations of the form <target>:<path>,
42+where <target> can be either a machine id as listed by "juju status" in the
43 "machines" section or a unit name as listed in the "services" section.
44+Any extra arguments to scp can be passed after --, in any place.
45
46-Examples
47+Examples:
48
49 Copy a single file from machine 2 to the local machine:
50
51 juju scp 2:/var/log/syslog .
52
53+Copy 2 files from two units to the local backup/ directory, passig -v
54+to scp as an extra argument:
55+
56+ juju scp ubuntu/0:/path/file1 -- -v ubuntu/1:/path/file2 backup/
57+
58 Recursively copy the directory /var/log/mongodb/ on the first mongodb
59 server to the local directory remote-logs:
60
61@@ -41,7 +47,7 @@
62 func (c *SCPCommand) Info() *cmd.Info {
63 return &cmd.Info{
64 Name: "scp",
65- Args: "[-- scp-option...] <from> <to>",
66+ Args: "[-- scp-option...] <file1> ... <file2> [-- scp-option...]",
67 Purpose: "launch a scp command to copy files to/from remote machine(s)",
68 Doc: scpDoc,
69 }
70@@ -67,17 +73,33 @@
71 }
72 defer c.apiClient.Close()
73
74- // translate arguments in the form 0:/somepath or service/0:/somepath into
75- // ubuntu@machine:/somepath so they can be presented to scp.
76- for i := range c.Args {
77- // BUG(dfc) This will not work for IPv6 addresses like 2001:db8::1:2:/somepath.
78- if v := strings.SplitN(c.Args[i], ":", 2); len(v) > 1 {
79+ // Parse all arguments, translating those in the form 0:/somepath
80+ // or service/0:/somepath into ubuntu@machine:/somepath so they
81+ // can be given to scp as targets (source(s) and destination(s)),
82+ // and passing any others that look like extra arguments (starting
83+ // with "-") verbatim to scp.
84+ var targets, extraArgs []string
85+ for _, arg := range c.Args {
86+ if v := strings.SplitN(arg, ":", 2); len(v) > 1 {
87 host, err := c.hostFromTarget(v[0])
88 if err != nil {
89 return err
90 }
91- c.Args[i] = "ubuntu@" + host + ":" + v[1]
92+ targets = append(targets, "ubuntu@"+host+":"+v[1])
93+ continue
94+ }
95+ if strings.HasPrefix(arg, "-") {
96+ // Allow -- to be specified last, in which case
97+ // we need to strip it.
98+ arg = strings.TrimSpace(strings.TrimPrefix(arg, "--"))
99+ if arg != "" {
100+ // Extra argument(s).
101+ extraArgs = append(extraArgs, arg)
102+ }
103+ } else {
104+ // Local path
105+ targets = append(targets, arg)
106 }
107 }
108- return ssh.Copy(c.Args[0], c.Args[1], nil)
109+ return ssh.Copy(targets, extraArgs, nil)
110 }
111
112=== modified file 'cmd/juju/scp_test.go'
113--- cmd/juju/scp_test.go 2014-01-13 06:25:28 +0000
114+++ cmd/juju/scp_test.go 2014-02-22 15:58:57 +0000
115@@ -13,7 +13,6 @@
116 gc "launchpad.net/gocheck"
117
118 "launchpad.net/juju-core/charm"
119- "launchpad.net/juju-core/cmd"
120 coretesting "launchpad.net/juju-core/testing"
121 )
122
123@@ -24,28 +23,54 @@
124 }
125
126 var scpTests = []struct {
127+ about string
128 args []string
129 result string
130 }{
131 {
132+ "scp from machine 0 to current dir",
133 []string{"0:foo", "."},
134 commonArgs + "ubuntu@dummyenv-0.dns:foo .\n",
135 },
136 {
137+ "scp from machine 0 to current dir with extra args before",
138+ []string{"-- -rv", "0:foo", "."},
139+ commonArgs + "-rv ubuntu@dummyenv-0.dns:foo .\n",
140+ },
141+ {
142+ "scp from machine 0 to current dir with extra args after",
143+ []string{"0:foo", ".", "-- -rv"},
144+ commonArgs + "-rv ubuntu@dummyenv-0.dns:foo .\n",
145+ },
146+ {
147+ "scp from current dir to machine 0",
148 []string{"foo", "0:"},
149 commonArgs + "foo ubuntu@dummyenv-0.dns:\n",
150 },
151 {
152+ "scp from current dir to machine 0 with extra args in the middle",
153+ []string{"foo", "-- -r -v", "0:"},
154+ commonArgs + "-r -v foo ubuntu@dummyenv-0.dns:\n",
155+ },
156+ {
157+ "scp from machine 0 to unit mysql/0",
158 []string{"0:foo", "mysql/0:/foo"},
159 commonArgs + "ubuntu@dummyenv-0.dns:foo ubuntu@dummyenv-0.dns:/foo\n",
160 },
161 {
162- []string{"a", "b", "mysql/0"},
163- commonArgs + "a b\n",
164- },
165- {
166- []string{"mongodb/1:foo", "mongodb/0:"},
167- commonArgs + "ubuntu@dummyenv-2.dns:foo ubuntu@dummyenv-1.dns:\n",
168+ "scp from machine 0 to unit mysql/0 and extra args",
169+ []string{"-- -q", "0:foo", "mysql/0:/foo"},
170+ commonArgs + "-q ubuntu@dummyenv-0.dns:foo ubuntu@dummyenv-0.dns:/foo\n",
171+ },
172+ {
173+ "scp two local files to unit mysql/0",
174+ []string{"file1", "file2", "mysql/0:/foo/"},
175+ commonArgs + "file1 file2 ubuntu@dummyenv-0.dns:/foo/\n",
176+ },
177+ {
178+ "scp from unit mongodb/1 to unit mongodb/0 and multiple extra args",
179+ []string{"-- -r", "mongodb/1:foo", "-- -v", "mongodb/0:", "-- -q -l5"},
180+ commonArgs + "-r -v -q -l5 ubuntu@dummyenv-2.dns:foo ubuntu@dummyenv-1.dns:\n",
181 },
182 }
183
184@@ -66,11 +91,15 @@
185 s.addUnit(srv, m[1], c)
186 s.addUnit(srv, m[2], c)
187
188- for _, t := range scpTests {
189- c.Logf("testing juju scp %s", t.args)
190+ for i, t := range scpTests {
191+ c.Logf("test %d: %s -> %s\n", i, t.about, t.args)
192 ctx := coretesting.Context(c)
193- code := cmd.Main(&SCPCommand{}, ctx, t.args)
194- c.Check(code, gc.Equals, 0)
195+ scpcmd := &SCPCommand{}
196+
197+ err := scpcmd.Init(t.args)
198+ c.Check(err, gc.IsNil)
199+ err = scpcmd.Run(ctx)
200+ c.Check(err, gc.IsNil)
201 // we suppress stdout from scp
202 c.Check(ctx.Stderr.(*bytes.Buffer).String(), gc.Equals, "")
203 c.Check(ctx.Stdout.(*bytes.Buffer).String(), gc.Equals, "")
204
205=== modified file 'cmd/juju/ssh.go'
206--- cmd/juju/ssh.go 2014-01-13 06:25:28 +0000
207+++ cmd/juju/ssh.go 2014-02-22 15:58:57 +0000
208@@ -39,15 +39,23 @@
209 "machines" section or a unit name as listed in the "services" section.
210 Any extra parameters are passsed as extra parameters to the ssh command.
211
212-Examples
213+Examples:
214
215 Connect to machine 0:
216
217 juju ssh 0
218
219+Connect to machine 1 and run 'uname -a':
220+
221+ juju ssh 1 uname -a
222+
223 Connect to the first mysql unit:
224
225 juju ssh mysql/0
226+
227+Connect to the first mysql unit and run 'ls -la /var/log/juju':
228+
229+ juju ssh mysql/0 ls -la /var/log/juju
230 `
231
232 func (c *SSHCommand) Info() *cmd.Info {
233@@ -82,15 +90,9 @@
234 if err != nil {
235 return err
236 }
237- args := c.Args
238- if len(args) > 0 && args[0] == "--" {
239- // utils/ssh adds "--"; we will continue to accept
240- // it from the CLI for backwards compatibility.
241- args = args[1:]
242- }
243 var options ssh.Options
244 options.EnablePTY()
245- cmd := ssh.Command("ubuntu@"+host, args, &options)
246+ cmd := ssh.Command("ubuntu@"+host, c.Args, &options)
247 cmd.Stdin = ctx.Stdin
248 cmd.Stdout = ctx.Stdout
249 cmd.Stderr = ctx.Stderr
250
251=== modified file 'cmd/juju/ssh_test.go'
252--- cmd/juju/ssh_test.go 2014-02-18 17:08:55 +0000
253+++ cmd/juju/ssh_test.go 2014-02-22 15:58:57 +0000
254@@ -54,39 +54,33 @@
255
256 const (
257 commonArgs = `-o StrictHostKeyChecking no -o PasswordAuthentication no `
258- sshArgs = commonArgs + `-t `
259+ sshArgs = commonArgs + `-t -t `
260 )
261
262 var sshTests = []struct {
263+ about string
264 args []string
265 result string
266 }{
267 {
268+ "connect to machine 0",
269 []string{"ssh", "0"},
270 sshArgs + "ubuntu@dummyenv-0.dns\n",
271 },
272- // juju ssh 0 'uname -a'
273- {
274- []string{"ssh", "0", "uname -a"},
275- sshArgs + "ubuntu@dummyenv-0.dns -- uname -a\n",
276- },
277- // juju ssh 0 -- uname -a
278- {
279- []string{"ssh", "0", "--", "uname", "-a"},
280- sshArgs + "ubuntu@dummyenv-0.dns -- uname -a\n",
281- },
282- // juju ssh 0 uname -a
283- {
284+ {
285+ "connect to machine 0 and pass extra arguments",
286 []string{"ssh", "0", "uname", "-a"},
287- sshArgs + "ubuntu@dummyenv-0.dns -- uname -a\n",
288+ sshArgs + "ubuntu@dummyenv-0.dns uname -a\n",
289 },
290 {
291+ "connect to unit mysql/0",
292 []string{"ssh", "mysql/0"},
293 sshArgs + "ubuntu@dummyenv-0.dns\n",
294 },
295 {
296- []string{"ssh", "mongodb/1"},
297- sshArgs + "ubuntu@dummyenv-2.dns\n",
298+ "connect to unit mongodb/1 and pass extra arguments",
299+ []string{"ssh", "mongodb/1", "ls", "/"},
300+ sshArgs + "ubuntu@dummyenv-2.dns ls /\n",
301 },
302 }
303
304@@ -107,8 +101,8 @@
305 s.addUnit(srv, m[1], c)
306 s.addUnit(srv, m[2], c)
307
308- for _, t := range sshTests {
309- c.Logf("testing juju ssh %s", t.args)
310+ for i, t := range sshTests {
311+ c.Logf("test %d: %s -> %s\n", i, t.about, t.args)
312 ctx := coretesting.Context(c)
313 jujucmd := cmd.NewSuperCommand(cmd.SuperCommandParams{})
314 jujucmd.Register(&SSHCommand{})
315
316=== modified file 'utils/ssh/run_test.go'
317--- utils/ssh/run_test.go 2014-02-18 17:08:55 +0000
318+++ utils/ssh/run_test.go 2014-02-22 15:58:57 +0000
319@@ -48,7 +48,7 @@
320 c.Assert(response.Code, gc.Equals, 0)
321 c.Assert(string(response.Stdout), gc.Equals, "sudo apt-get update\nsudo apt-get upgrade\n")
322 c.Assert(string(response.Stderr), gc.Equals,
323- "-o StrictHostKeyChecking no -o PasswordAuthentication no hostname -- /bin/bash -s\n")
324+ "-o StrictHostKeyChecking no -o PasswordAuthentication no hostname /bin/bash -s\n")
325 }
326
327 func (s *ExecuteSSHCommandSuite) TestIdentityFile(c *gc.C) {
328
329=== modified file 'utils/ssh/ssh.go'
330--- utils/ssh/ssh.go 2014-02-07 11:06:43 +0000
331+++ utils/ssh/ssh.go 2014-02-22 15:58:57 +0000
332@@ -5,7 +5,6 @@
333 // key management, and so on. All SSH-based command executions in
334 // Juju should use the Command/ScpCommand functions in this package.
335 //
336-// TODO(axw) fallback to go.crypto/ssh if no native client is available.
337 package ssh
338
339 import (
340@@ -70,10 +69,11 @@
341 // Host is specified in the format [user@]host.
342 Command(host string, command []string, options *Options) *Cmd
343
344- // Copy copies a file between the local host and
345- // target host. Paths are specified in the scp format,
346- // [[user@]host:]path.
347- Copy(source, dest string, options *Options) error
348+ // Copy copies file(s) between the local host a remote host(s).
349+ // Paths are specified in the scp format, [[user@]host:]path. If
350+ // any extra arguments are specified in extraArgs, they are passed
351+ // to scp verbatim.
352+ Copy(targets, extraArgs []string, options *Options) error
353 }
354
355 // Cmd represents a command to be (or being) executed
356@@ -203,6 +203,10 @@
357 // an embedded client based on go.crypto/ssh.
358 var DefaultClient Client
359
360+// chosenClient holds the type of SSH client created for
361+// DefaultClient, so that we can log it in Command or Copy.
362+var chosenClient string
363+
364 func init() {
365 initDefaultClient()
366 }
367@@ -210,17 +214,21 @@
368 func initDefaultClient() {
369 if client, err := NewOpenSSHClient(); err == nil {
370 DefaultClient = client
371+ chosenClient = "OpenSSH"
372 } else if client, err := NewGoCryptoClient(); err == nil {
373 DefaultClient = client
374+ chosenClient = "go.crypto (embedded)"
375 }
376 }
377
378 // Command is a short-cut for DefaultClient.Command.
379 func Command(host string, command []string, options *Options) *Cmd {
380+ logger.Debugf("using %s ssh client", chosenClient)
381 return DefaultClient.Command(host, command, options)
382 }
383
384 // Copy is a short-cut for DefaultClient.Copy.
385-func Copy(source, dest string, options *Options) error {
386- return DefaultClient.Copy(source, dest, options)
387+func Copy(targets, extraArgs []string, options *Options) error {
388+ logger.Debugf("using %s ssh client", chosenClient)
389+ return DefaultClient.Copy(targets, extraArgs, options)
390 }
391
392=== modified file 'utils/ssh/ssh_gocrypto.go'
393--- utils/ssh/ssh_gocrypto.go 2014-02-14 04:58:10 +0000
394+++ utils/ssh/ssh_gocrypto.go 2014-02-22 15:58:57 +0000
395@@ -50,6 +50,7 @@
396 port = options.port
397 }
398 }
399+ logger.Debugf(`running (equivalent of): ssh "%s@%s" -p %d '%s'`, user, host, port, shellCommand)
400 return &Cmd{impl: &goCryptoCommand{
401 signers: signers,
402 user: user,
403@@ -61,7 +62,7 @@
404 // Copy implements Client.Copy.
405 //
406 // Copy is currently unimplemented, and will always return an error.
407-func (c *GoCryptoClient) Copy(source, dest string, options *Options) error {
408+func (c *GoCryptoClient) Copy(targets, extraArgs []string, options *Options) error {
409 return fmt.Errorf("Copy is not implemented")
410 }
411
412
413=== modified file 'utils/ssh/ssh_gocrypto_test.go'
414--- utils/ssh/ssh_gocrypto_test.go 2014-02-14 04:58:10 +0000
415+++ utils/ssh/ssh_gocrypto_test.go 2014-02-22 15:58:57 +0000
416@@ -145,6 +145,6 @@
417 func (s *SSHGoCryptoCommandSuite) TestCopy(c *gc.C) {
418 client, err := ssh.NewGoCryptoClient()
419 c.Assert(err, gc.IsNil)
420- err = client.Copy("0.1.2.3:b", c.MkDir(), nil)
421+ err = client.Copy([]string{"0.1.2.3:b", c.MkDir()}, nil, nil)
422 c.Assert(err, gc.ErrorMatches, "Copy is not implemented")
423 }
424
425=== modified file 'utils/ssh/ssh_openssh.go'
426--- utils/ssh/ssh_openssh.go 2014-02-05 08:48:40 +0000
427+++ utils/ssh/ssh_openssh.go 2014-02-22 15:58:57 +0000
428@@ -14,7 +14,7 @@
429 "launchpad.net/juju-core/utils"
430 )
431
432-var opensshCommonOptions = []string{"-o", "StrictHostKeyChecking no"}
433+var opensshCommonOptions = map[string][]string{"-o": []string{"StrictHostKeyChecking no"}}
434
435 // default identities will not be attempted if
436 // -i is specified and they are not explcitly
437@@ -63,16 +63,19 @@
438 return &c, nil
439 }
440
441-func opensshOptions(options *Options, commandKind opensshCommandKind) []string {
442- args := append([]string{}, opensshCommonOptions...)
443+func opensshOptions(options *Options, commandKind opensshCommandKind) map[string][]string {
444+ args := make(map[string][]string)
445+ for k, v := range opensshCommonOptions {
446+ args[k] = v
447+ }
448 if options == nil {
449 options = &Options{}
450 }
451 if !options.passwordAuthAllowed {
452- args = append(args, "-o", "PasswordAuthentication no")
453+ args["-o"] = append(args["-o"], "PasswordAuthentication no")
454 }
455 if options.allocatePTY {
456- args = append(args, "-t")
457+ args["-t"] = []string{}
458 }
459 identities := append([]string{}, options.identities...)
460 if pk := PrivateKeyFiles(); len(pk) > 0 {
461@@ -94,45 +97,76 @@
462 }
463 }
464 for _, identity := range identities {
465- args = append(args, "-i", identity)
466+ args["-i"] = append(args["-i"], identity)
467 }
468 if options.port != 0 {
469+ port := fmt.Sprint(options.port)
470 if commandKind == scpKind {
471 // scp uses -P instead of -p (-p means preserve).
472- args = append(args, "-P")
473+ args["-P"] = []string{port}
474 } else {
475- args = append(args, "-p")
476+ args["-p"] = []string{port}
477 }
478- args = append(args, fmt.Sprint(options.port))
479 }
480 return args
481 }
482
483+func expandArgs(args map[string][]string, quote bool) []string {
484+ var list []string
485+ for opt, vals := range args {
486+ if len(vals) == 0 {
487+ list = append(list, opt)
488+ if opt == "-t" {
489+ // In order to force a PTY to be allocated, we need to
490+ // pass -t twice.
491+ list = append(list, opt)
492+ }
493+ }
494+ for _, val := range vals {
495+ list = append(list, opt)
496+ if quote {
497+ val = fmt.Sprintf("%q", val)
498+ }
499+ list = append(list, val)
500+ }
501+ }
502+ return list
503+}
504+
505 // Command implements Client.Command.
506 func (c *OpenSSHClient) Command(host string, command []string, options *Options) *Cmd {
507- args := opensshOptions(options, sshKind)
508+ opts := opensshOptions(options, sshKind)
509+ args := expandArgs(opts, false)
510 args = append(args, host)
511 if len(command) > 0 {
512- args = append(args, "--")
513 args = append(args, command...)
514 }
515 bin, args := sshpassWrap("ssh", args)
516+ optsList := strings.Join(expandArgs(opts, true), " ")
517+ fullCommand := strings.Join(command, " ")
518+ logger.Debugf("running: %s %s %q '%s'", bin, optsList, host, fullCommand)
519 return &Cmd{impl: &opensshCmd{exec.Command(bin, args...)}}
520 }
521
522 // Copy implements Client.Copy.
523-func (c *OpenSSHClient) Copy(source, dest string, userOptions *Options) error {
524+func (c *OpenSSHClient) Copy(targets, extraArgs []string, userOptions *Options) error {
525 var options Options
526 if userOptions != nil {
527 options = *userOptions
528 options.allocatePTY = false // doesn't make sense for scp
529 }
530- args := opensshOptions(&options, scpKind)
531- args = append(args, source, dest)
532+ opts := opensshOptions(&options, scpKind)
533+ args := expandArgs(opts, false)
534+ args = append(args, extraArgs...)
535+ args = append(args, targets...)
536 bin, args := sshpassWrap("scp", args)
537 cmd := exec.Command(bin, args...)
538 var stderr bytes.Buffer
539 cmd.Stderr = &stderr
540+ allOpts := append(expandArgs(opts, true), extraArgs...)
541+ optsList := strings.Join(allOpts, " ")
542+ targetList := `"` + strings.Join(targets, `" "`) + `"`
543+ logger.Debugf("running: %s %s %s", bin, optsList, targetList)
544 if err := cmd.Run(); err != nil {
545 stderr := strings.TrimSpace(stderr.String())
546 if len(stderr) > 0 {
547
548=== modified file 'utils/ssh/ssh_test.go'
549--- utils/ssh/ssh_test.go 2014-02-18 17:08:55 +0000
550+++ utils/ssh/ssh_test.go 2014-02-22 15:58:57 +0000
551@@ -70,24 +70,24 @@
552 fakesshpass := filepath.Join(s.testbin, "sshpass")
553 err := ioutil.WriteFile(fakesshpass, []byte(echoCommandScript), 0755)
554 s.assertCommandArgs(c, s.command("echo", "123"),
555- s.fakessh+" -o StrictHostKeyChecking no -o PasswordAuthentication no localhost -- echo 123",
556+ s.fakessh+" -o StrictHostKeyChecking no -o PasswordAuthentication no localhost echo 123",
557 )
558 // Now set $SSHPASS.
559 s.PatchEnvironment("SSHPASS", "anyoldthing")
560 s.assertCommandArgs(c, s.command("echo", "123"),
561- fakesshpass+" -e ssh -o StrictHostKeyChecking no -o PasswordAuthentication no localhost -- echo 123",
562+ fakesshpass+" -e ssh -o StrictHostKeyChecking no -o PasswordAuthentication no localhost echo 123",
563 )
564 // Finally, remove sshpass from $PATH.
565 err = os.Remove(fakesshpass)
566 c.Assert(err, gc.IsNil)
567 s.assertCommandArgs(c, s.command("echo", "123"),
568- s.fakessh+" -o StrictHostKeyChecking no -o PasswordAuthentication no localhost -- echo 123",
569+ s.fakessh+" -o StrictHostKeyChecking no -o PasswordAuthentication no localhost echo 123",
570 )
571 }
572
573 func (s *SSHCommandSuite) TestCommand(c *gc.C) {
574 s.assertCommandArgs(c, s.command("echo", "123"),
575- s.fakessh+" -o StrictHostKeyChecking no -o PasswordAuthentication no localhost -- echo 123",
576+ s.fakessh+" -o StrictHostKeyChecking no -o PasswordAuthentication no localhost echo 123",
577 )
578 }
579
580@@ -95,7 +95,7 @@
581 var opts ssh.Options
582 opts.EnablePTY()
583 s.assertCommandArgs(c, s.commandOptions([]string{"echo", "123"}, &opts),
584- s.fakessh+" -o StrictHostKeyChecking no -o PasswordAuthentication no -t localhost -- echo 123",
585+ s.fakessh+" -o StrictHostKeyChecking no -o PasswordAuthentication no -t -t localhost echo 123",
586 )
587 }
588
589@@ -103,7 +103,7 @@
590 var opts ssh.Options
591 opts.AllowPasswordAuthentication()
592 s.assertCommandArgs(c, s.commandOptions([]string{"echo", "123"}, &opts),
593- s.fakessh+" -o StrictHostKeyChecking no localhost -- echo 123",
594+ s.fakessh+" -o StrictHostKeyChecking no localhost echo 123",
595 )
596 }
597
598@@ -111,7 +111,7 @@
599 var opts ssh.Options
600 opts.SetIdentities("x", "y")
601 s.assertCommandArgs(c, s.commandOptions([]string{"echo", "123"}, &opts),
602- s.fakessh+" -o StrictHostKeyChecking no -o PasswordAuthentication no -i x -i y localhost -- echo 123",
603+ s.fakessh+" -o StrictHostKeyChecking no -o PasswordAuthentication no -i x -i y localhost echo 123",
604 )
605 }
606
607@@ -119,7 +119,7 @@
608 var opts ssh.Options
609 opts.SetPort(2022)
610 s.assertCommandArgs(c, s.commandOptions([]string{"echo", "123"}, &opts),
611- s.fakessh+" -o StrictHostKeyChecking no -o PasswordAuthentication no -p 2022 localhost -- echo 123",
612+ s.fakessh+" -o StrictHostKeyChecking no -o PasswordAuthentication no -p 2022 localhost echo 123",
613 )
614 }
615
616@@ -129,12 +129,19 @@
617 opts.AllowPasswordAuthentication()
618 opts.SetIdentities("x", "y")
619 opts.SetPort(2022)
620- err := s.client.Copy("/tmp/blah", "foo@bar.com:baz", &opts)
621+ err := s.client.Copy([]string{"/tmp/blah", "foo@bar.com:baz"}, nil, &opts)
622 c.Assert(err, gc.IsNil)
623 out, err := ioutil.ReadFile(s.fakescp + ".args")
624 c.Assert(err, gc.IsNil)
625 // EnablePTY has no effect for Copy
626 c.Assert(string(out), gc.Equals, s.fakescp+" -o StrictHostKeyChecking no -i x -i y -P 2022 /tmp/blah foo@bar.com:baz\n")
627+
628+ // Try passing extra args
629+ err = s.client.Copy([]string{"/tmp/blah", "foo@bar.com:baz"}, []string{"-r", "-v"}, &opts)
630+ c.Assert(err, gc.IsNil)
631+ out, err = ioutil.ReadFile(s.fakescp + ".args")
632+ c.Assert(err, gc.IsNil)
633+ c.Assert(string(out), gc.Equals, s.fakescp+" -o StrictHostKeyChecking no -i x -i y -P 2022 -r -v /tmp/blah foo@bar.com:baz\n")
634 }
635
636 func (s *SSHCommandSuite) TestCommandClientKeys(c *gc.C) {
637@@ -146,7 +153,7 @@
638 var opts ssh.Options
639 opts.SetIdentities("x", "y")
640 s.assertCommandArgs(c, s.commandOptions([]string{"echo", "123"}, &opts),
641- s.fakessh+" -o StrictHostKeyChecking no -o PasswordAuthentication no -i x -i y -i "+ck+" localhost -- echo 123",
642+ s.fakessh+" -o StrictHostKeyChecking no -o PasswordAuthentication no -i x -i y -i "+ck+" localhost echo 123",
643 )
644 }
645
646@@ -167,7 +174,7 @@
647 s.PatchValue(ssh.DefaultIdentities, []string{def1, def2})
648 // If no identities are specified, then the defaults aren't added.
649 s.assertCommandArgs(c, s.commandOptions([]string{"echo", "123"}, &opts),
650- s.fakessh+" -o StrictHostKeyChecking no -o PasswordAuthentication no localhost -- echo 123",
651+ s.fakessh+" -o StrictHostKeyChecking no -o PasswordAuthentication no localhost echo 123",
652 )
653 // If identities are specified, then the defaults are must added.
654 // Only the defaults that exist on disk will be added.
655@@ -175,6 +182,6 @@
656 c.Assert(err, gc.IsNil)
657 opts.SetIdentities("x", "y")
658 s.assertCommandArgs(c, s.commandOptions([]string{"echo", "123"}, &opts),
659- s.fakessh+" -o StrictHostKeyChecking no -o PasswordAuthentication no -i x -i y -i "+def2+" localhost -- echo 123",
660+ s.fakessh+" -o StrictHostKeyChecking no -o PasswordAuthentication no -i x -i y -i "+def2+" localhost echo 123",
661 )
662 }

Subscribers

People subscribed via source and target branches

to status/vote changes: