Merge lp:~jameinel/juju-core/1.18-scp-multiple-args-1306208 into lp:juju-core/1.18

Proposed by John A Meinel
Status: Merged
Merged at revision: 2268
Proposed branch: lp:~jameinel/juju-core/1.18-scp-multiple-args-1306208
Merge into: lp:juju-core/1.18
Diff against target: 398 lines (+158/-79)
7 files modified
cmd/juju/scp.go (+35/-35)
cmd/juju/scp_test.go (+100/-27)
utils/ssh/ssh.go (+3/-3)
utils/ssh/ssh_gocrypto.go (+1/-1)
utils/ssh/ssh_gocrypto_test.go (+1/-1)
utils/ssh/ssh_openssh.go (+5/-6)
utils/ssh/ssh_test.go (+13/-6)
To merge this branch: bzr merge lp:~jameinel/juju-core/1.18-scp-multiple-args-1306208
Reviewer Review Type Date Requested Status
Ian Booth Approve
Review via email: mp+215301@code.launchpad.net

Commit message

This changes how "juju scp" parses its arguments, to restore the ability to put flags in more places on the command line.

This does the 'expand' logic differently. If something looks like it starts with an identifier: then it tries to expand it (like it did previously). But it allows that to happen anywhere in the args list, rather than all but the last arg.

The one thing we don't support (but we didn't support before, either), is "juju scp 0:foo myhost:bar", because we always lookup the stuff before the : as a PublicAddress lookup.

I'm not sure that this is 100% perfect, but I do think it is a step in the right direction.

I'm targetting 1.18.1 because I think the behavior of 1.18 is a regression from 1.16 since old command lines no longer work.

Description of the change

This changes how "juju scp" parses its arguments, to restore the ability to put flags in more places on the command line.

This does the 'expand' logic differently. If something looks like it starts with an identifier: then it tries to expand it (like it did previously). But it allows that to happen anywhere in the args list, rather than all but the last arg.

The one thing we don't support (but we didn't support before, either), is "juju scp 0:foo myhost:bar", because we always lookup the stuff before the : as a PublicAddress lookup.

I'm not sure that this is 100% perfect, but I do think it is a step in the right direction.

I'm targetting 1.18.1 because I think the behavior of 1.18 is a regression from 1.16 since old command lines no longer work.

To post a comment you must log in.
Revision history for this message
Go Bot (go-bot) wrote :

The attempt to merge lp:~jameinel/juju-core/1.18-scp-multiple-args-1306208 into lp:juju-core/1.18 failed. Below is the output from the failed tests.

# launchpad.net/juju-core/cmd/plugins/juju-restore
cmd/plugins/juju-restore/restore.go:437: too many arguments in call to "launchpad.net/juju-core/utils/ssh".Copy
mongod: no process found

Revision history for this message
Ian Booth (wallyworld) :
review: Approve
Revision history for this message
Go Bot (go-bot) wrote :

The attempt to merge lp:~jameinel/juju-core/1.18-scp-multiple-args-1306208 into lp:juju-core/1.18 failed. Below is the output from the failed tests.

# launchpad.net/juju-core/cmd/plugins/juju-restore
cmd/plugins/juju-restore/restore.go:437: too many arguments in call to "launchpad.net/juju-core/utils/ssh".Copy
mongod: no process found

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'cmd/juju/scp.go'
--- cmd/juju/scp.go 2014-03-20 02:30:15 +0000
+++ cmd/juju/scp.go 2014-04-10 20:59:22 +0000
@@ -35,12 +35,12 @@
35Copy 2 files from two units to the local backup/ directory, passing -v35Copy 2 files from two units to the local backup/ directory, passing -v
36to scp as an extra argument:36to scp as an extra argument:
3737
38 juju scp ubuntu/0:/path/file1 ubuntu/1:/path/file2 backup/ -v38 juju scp -v ubuntu/0:/path/file1 ubuntu/1:/path/file2 backup/
3939
40Recursively copy the directory /var/log/mongodb/ on the first mongodb40Recursively copy the directory /var/log/mongodb/ on the first mongodb
41server to the local directory remote-logs:41server to the local directory remote-logs:
4242
43 juju scp mongodb/0:/var/log/mongodb/ remote-logs/ -r43 juju scp -r mongodb/0:/var/log/mongodb/ remote-logs/
4444
45Copy a local file to the second apache unit of the environment "testing":45Copy a local file to the second apache unit of the environment "testing":
4646
@@ -64,6 +64,35 @@
64 return nil64 return nil
65}65}
6666
67// expandArgs takes a list of arguments and looks for ones in the form of
68// 0:some/path or service/0:some/path, and translates them into
69// ubuntu@machine:some/path so they can be passed as arguments to scp, and pass
70// the rest verbatim on to scp
71func expandArgs(args []string, hostFromTarget func(string) (string, error)) ([]string, error) {
72 outArgs := make([]string, len(args))
73 for i, arg := range args {
74 v := strings.SplitN(arg, ":", 2)
75 if strings.HasPrefix(arg, "-") || len(v) <= 1 {
76 // Can't be an interesting target, so just pass it along
77 outArgs[i] = arg
78 continue
79 }
80 host, err := hostFromTarget(v[0])
81 if err != nil {
82 return nil, err
83 }
84 // To ensure this works with IPv6 addresses, we need to
85 // wrap the host with \[..\], so the colons inside will be
86 // interpreted as part of the address and the last one as
87 // separator between host and remote path.
88 if strings.Contains(host, ":") {
89 host = fmt.Sprintf(`\[%s\]`, host)
90 }
91 outArgs[i] = "ubuntu@" + host + ":" + v[1]
92 }
93 return outArgs, nil
94}
95
67// Run resolves c.Target to a machine, or host of a unit and96// Run resolves c.Target to a machine, or host of a unit and
68// forks ssh with c.Args, if provided.97// forks ssh with c.Args, if provided.
69func (c *SCPCommand) Run(ctx *cmd.Context) error {98func (c *SCPCommand) Run(ctx *cmd.Context) error {
@@ -73,38 +102,9 @@
73 return err102 return err
74 }103 }
75 defer c.apiClient.Close()104 defer c.apiClient.Close()
76105 args, err := expandArgs(c.Args, c.hostFromTarget)
77 // Parse all arguments, translating those in the form 0:/somepath106 if err != nil {
78 // or service/0:/somepath into ubuntu@machine:/somepath so they107 return err
79 // can be given to scp as targets (source(s) and destination(s)),
80 // and passing any others that look like extra arguments (starting
81 // with "-") verbatim to scp.
82 var targets, extraArgs []string
83 for i, arg := range c.Args {
84 if v := strings.SplitN(arg, ":", 2); len(v) > 1 {
85 host, err := c.hostFromTarget(v[0])
86 if err != nil {
87 return err
88 }
89 // To ensure this works with IPv6 addresses, we need to
90 // wrap the host with \[..\], so the colons inside will be
91 // interpreted as part of the address and the last one as
92 // separator between host and remote path.
93 if strings.Contains(host, ":") {
94 host = fmt.Sprintf(`\[%s\]`, host)
95 }
96 targets = append(targets, "ubuntu@"+host+":"+v[1])
97 continue
98 }
99 if strings.HasPrefix(arg, "-") {
100 if i != len(c.Args)-1 {
101 return fmt.Errorf("unexpected argument %q; extra arguments must be last", arg)
102 }
103 extraArgs = append(extraArgs, arg)
104 } else {
105 // Local path
106 targets = append(targets, arg)
107 }
108 }108 }
109 return ssh.Copy(targets, extraArgs, nil)109 return ssh.Copy(args, nil)
110}110}
111111
=== modified file 'cmd/juju/scp_test.go'
--- cmd/juju/scp_test.go 2014-03-20 02:30:15 +0000
+++ cmd/juju/scp_test.go 2014-04-10 20:59:22 +0000
@@ -9,7 +9,9 @@
9 "io/ioutil"9 "io/ioutil"
10 "net/url"10 "net/url"
11 "path/filepath"11 "path/filepath"
12 "strings"
1213
14 jc "github.com/juju/testing/checkers"
13 gc "launchpad.net/gocheck"15 gc "launchpad.net/gocheck"
1416
15 "launchpad.net/juju-core/charm"17 "launchpad.net/juju-core/charm"
@@ -18,11 +20,14 @@
18)20)
1921
20var _ = gc.Suite(&SCPSuite{})22var _ = gc.Suite(&SCPSuite{})
23var _ = gc.Suite(&expandArgsSuite{})
2124
22type SCPSuite struct {25type SCPSuite struct {
23 SSHCommonSuite26 SSHCommonSuite
24}27}
2528
29type expandArgsSuite struct{}
30
26var scpTests = []struct {31var scpTests = []struct {
27 about string32 about string
28 args []string33 args []string
@@ -34,60 +39,61 @@
34 []string{"0:foo", "."},39 []string{"0:foo", "."},
35 commonArgs + "ubuntu@dummyenv-0.dns:foo .\n",40 commonArgs + "ubuntu@dummyenv-0.dns:foo .\n",
36 "",41 "",
37 },42 }, {
38 {
39 "scp from machine 0 to current dir with extra args",43 "scp from machine 0 to current dir with extra args",
40 []string{"0:foo", ".", "-rv -o SomeOption"},44 []string{"0:foo", ".", "-rv", "-o", "SomeOption"},
41 commonArgs + "-rv -o SomeOption ubuntu@dummyenv-0.dns:foo .\n",45 commonArgs + "ubuntu@dummyenv-0.dns:foo . -rv -o SomeOption\n",
42 "",46 "",
43 },47 }, {
44 {
45 "scp from current dir to machine 0",48 "scp from current dir to machine 0",
46 []string{"foo", "0:"},49 []string{"foo", "0:"},
47 commonArgs + "foo ubuntu@dummyenv-0.dns:\n",50 commonArgs + "foo ubuntu@dummyenv-0.dns:\n",
48 "",51 "",
49 },52 }, {
50 {
51 "scp from current dir to machine 0 with extra args",53 "scp from current dir to machine 0 with extra args",
52 []string{"foo", "0:", "-r -v"},54 []string{"foo", "0:", "-r", "-v"},
53 commonArgs + "-r -v foo ubuntu@dummyenv-0.dns:\n",55 commonArgs + "foo ubuntu@dummyenv-0.dns: -r -v\n",
54 "",56 "",
55 },57 }, {
56 {
57 "scp from machine 0 to unit mysql/0",58 "scp from machine 0 to unit mysql/0",
58 []string{"0:foo", "mysql/0:/foo"},59 []string{"0:foo", "mysql/0:/foo"},
59 commonArgs + "ubuntu@dummyenv-0.dns:foo ubuntu@dummyenv-0.dns:/foo\n",60 commonArgs + "ubuntu@dummyenv-0.dns:foo ubuntu@dummyenv-0.dns:/foo\n",
60 "",61 "",
61 },62 }, {
62 {
63 "scp from machine 0 to unit mysql/0 and extra args",63 "scp from machine 0 to unit mysql/0 and extra args",
64 []string{"0:foo", "mysql/0:/foo", "-q"},64 []string{"0:foo", "mysql/0:/foo", "-q"},
65 commonArgs + "-q ubuntu@dummyenv-0.dns:foo ubuntu@dummyenv-0.dns:/foo\n",65 commonArgs + "ubuntu@dummyenv-0.dns:foo ubuntu@dummyenv-0.dns:/foo -q\n",
66 "",66 "",
67 },67 }, {
68 {
69 "scp from machine 0 to unit mysql/0 and extra args before",68 "scp from machine 0 to unit mysql/0 and extra args before",
70 []string{"-q", "-r", "0:foo", "mysql/0:/foo"},69 []string{"-q", "-r", "0:foo", "mysql/0:/foo"},
70 commonArgs + "-q -r ubuntu@dummyenv-0.dns:foo ubuntu@dummyenv-0.dns:/foo\n",
71 "",71 "",
72 `unexpected argument "-q"; extra arguments must be last`,72 }, {
73 },
74 {
75 "scp two local files to unit mysql/0",73 "scp two local files to unit mysql/0",
76 []string{"file1", "file2", "mysql/0:/foo/"},74 []string{"file1", "file2", "mysql/0:/foo/"},
77 commonArgs + "file1 file2 ubuntu@dummyenv-0.dns:/foo/\n",75 commonArgs + "file1 file2 ubuntu@dummyenv-0.dns:/foo/\n",
78 "",76 "",
79 },77 }, {
80 {
81 "scp from unit mongodb/1 to unit mongodb/0 and multiple extra args",78 "scp from unit mongodb/1 to unit mongodb/0 and multiple extra args",
82 []string{"mongodb/1:foo", "mongodb/0:", "-r -v -q -l5"},79 []string{"mongodb/1:foo", "mongodb/0:", "-r", "-v", "-q", "-l5"},
83 commonArgs + "-r -v -q -l5 ubuntu@dummyenv-2.dns:foo ubuntu@dummyenv-1.dns:\n",80 commonArgs + "ubuntu@dummyenv-2.dns:foo ubuntu@dummyenv-1.dns: -r -v -q -l5\n",
84 "",81 "",
85 },82 }, {
86 {83 "scp from unit mongodb/1 to unit mongodb/0 with a --",
84 []string{"--", "-r", "-v", "mongodb/1:foo", "mongodb/0:", "-q", "-l5"},
85 commonArgs + "-- -r -v ubuntu@dummyenv-2.dns:foo ubuntu@dummyenv-1.dns: -q -l5\n",
86 "",
87 }, {
87 "scp works with IPv6 addresses",88 "scp works with IPv6 addresses",
88 []string{"ipv6-svc/0:foo", "bar"},89 []string{"ipv6-svc/0:foo", "bar"},
89 commonArgs + `ubuntu@\[2001:db8::\]:foo bar` + "\n",90 commonArgs + `ubuntu@\[2001:db8::\]:foo bar` + "\n",
90 "",91 "",
92 }, {
93 "scp with no such machine",
94 []string{"5:foo", "bar"},
95 "",
96 "machine 5 not found",
91 },97 },
92}98}
9399
@@ -140,3 +146,70 @@
140 }146 }
141 }147 }
142}148}
149
150var hostsFromTargets = map[string]string{
151 "0": "dummyenv-0.dns",
152 "mysql/0": "dummyenv-0.dns",
153 "mongodb/0": "dummyenv-1.dns",
154 "mongodb/1": "dummyenv-2.dns",
155 "ipv6-svc/0": "2001:db8::",
156}
157
158func dummyHostsFromTarget(target string) (string, error) {
159 if res, ok := hostsFromTargets[target]; ok {
160 return res, nil
161 }
162 return target, nil
163}
164
165func (s *expandArgsSuite) TestSCPExpandArgs(c *gc.C) {
166 for i, t := range scpTests {
167 if t.error != "" {
168 // We are just running a focused set of tests on
169 // expandArgs, we aren't implementing the full
170 // hostsFromTargets to actually trigger errors
171 continue
172 }
173 c.Logf("test %d: %s -> %s\n", i, t.about, t.args)
174 // expandArgs doesn't add the commonArgs prefix, so strip it
175 // off, along with the trailing '\n'
176 c.Check(strings.HasPrefix(t.result, commonArgs), jc.IsTrue)
177 argString := t.result[len(commonArgs):]
178 c.Check(strings.HasSuffix(argString, "\n"), jc.IsTrue)
179 argString = argString[:len(argString)-1]
180 args := strings.Split(argString, " ")
181 expanded, err := expandArgs(t.args, dummyHostsFromTarget)
182 c.Check(err, gc.IsNil)
183 c.Check(expanded, gc.DeepEquals, args)
184 }
185}
186
187var expandTests = []struct {
188 about string
189 args []string
190 result []string
191}{
192 {
193 "don't expand params that start with '-'",
194 []string{"-0:stuff", "0:foo", "."},
195 []string{"-0:stuff", "ubuntu@dummyenv-0.dns:foo", "."},
196 },
197}
198
199func (s *expandArgsSuite) TestExpandArgs(c *gc.C) {
200 for i, t := range expandTests {
201 c.Logf("test %d: %s -> %s\n", i, t.about, t.args)
202 expanded, err := expandArgs(t.args, dummyHostsFromTarget)
203 c.Check(err, gc.IsNil)
204 c.Check(expanded, gc.DeepEquals, t.result)
205 }
206}
207
208func (s *expandArgsSuite) TestExpandArgsPropagatesErrors(c *gc.C) {
209 erroringHostFromTargets := func(string) (string, error) {
210 return "", fmt.Errorf("this is my error")
211 }
212 expanded, err := expandArgs([]string{"foo:1", "bar"}, erroringHostFromTargets)
213 c.Assert(err, gc.ErrorMatches, "this is my error")
214 c.Check(expanded, gc.IsNil)
215}
143216
=== modified file 'utils/ssh/ssh.go'
--- utils/ssh/ssh.go 2014-03-19 03:18:41 +0000
+++ utils/ssh/ssh.go 2014-04-10 20:59:22 +0000
@@ -81,7 +81,7 @@
81 // Paths are specified in the scp format, [[user@]host:]path. If81 // Paths are specified in the scp format, [[user@]host:]path. If
82 // any extra arguments are specified in extraArgs, they are passed82 // any extra arguments are specified in extraArgs, they are passed
83 // verbatim.83 // verbatim.
84 Copy(targets, extraArgs []string, options *Options) error84 Copy(args []string, options *Options) error
85}85}
8686
87// Cmd represents a command to be (or being) executed87// Cmd represents a command to be (or being) executed
@@ -236,7 +236,7 @@
236}236}
237237
238// Copy is a short-cut for DefaultClient.Copy.238// Copy is a short-cut for DefaultClient.Copy.
239func Copy(targets, extraArgs []string, options *Options) error {239func Copy(args []string, options *Options) error {
240 logger.Debugf("using %s ssh client", chosenClient)240 logger.Debugf("using %s ssh client", chosenClient)
241 return DefaultClient.Copy(targets, extraArgs, options)241 return DefaultClient.Copy(args, options)
242}242}
243243
=== modified file 'utils/ssh/ssh_gocrypto.go'
--- utils/ssh/ssh_gocrypto.go 2014-02-24 16:21:43 +0000
+++ utils/ssh/ssh_gocrypto.go 2014-04-10 20:59:22 +0000
@@ -62,7 +62,7 @@
62// Copy implements Client.Copy.62// Copy implements Client.Copy.
63//63//
64// Copy is currently unimplemented, and will always return an error.64// Copy is currently unimplemented, and will always return an error.
65func (c *GoCryptoClient) Copy(targets, extraArgs []string, options *Options) error {65func (c *GoCryptoClient) Copy(args []string, options *Options) error {
66 return fmt.Errorf("scp command is not implemented (OpenSSH scp not available in PATH)")66 return fmt.Errorf("scp command is not implemented (OpenSSH scp not available in PATH)")
67}67}
6868
6969
=== modified file 'utils/ssh/ssh_gocrypto_test.go'
--- utils/ssh/ssh_gocrypto_test.go 2014-03-13 07:54:56 +0000
+++ utils/ssh/ssh_gocrypto_test.go 2014-04-10 20:59:22 +0000
@@ -145,6 +145,6 @@
145func (s *SSHGoCryptoCommandSuite) TestCopy(c *gc.C) {145func (s *SSHGoCryptoCommandSuite) TestCopy(c *gc.C) {
146 client, err := ssh.NewGoCryptoClient()146 client, err := ssh.NewGoCryptoClient()
147 c.Assert(err, gc.IsNil)147 c.Assert(err, gc.IsNil)
148 err = client.Copy([]string{"0.1.2.3:b", c.MkDir()}, nil, nil)148 err = client.Copy([]string{"0.1.2.3:b", c.MkDir()}, nil)
149 c.Assert(err, gc.ErrorMatches, `scp command is not implemented \(OpenSSH scp not available in PATH\)`)149 c.Assert(err, gc.ErrorMatches, `scp command is not implemented \(OpenSSH scp not available in PATH\)`)
150}150}
151151
=== modified file 'utils/ssh/ssh_openssh.go'
--- utils/ssh/ssh_openssh.go 2014-03-27 02:42:08 +0000
+++ utils/ssh/ssh_openssh.go 2014-04-10 20:59:22 +0000
@@ -124,17 +124,16 @@
124}124}
125125
126// Copy implements Client.Copy.126// Copy implements Client.Copy.
127func (c *OpenSSHClient) Copy(targets, extraArgs []string, userOptions *Options) error {127func (c *OpenSSHClient) Copy(args []string, userOptions *Options) error {
128 var options Options128 var options Options
129 if userOptions != nil {129 if userOptions != nil {
130 options = *userOptions130 options = *userOptions
131 options.allocatePTY = false // doesn't make sense for scp131 options.allocatePTY = false // doesn't make sense for scp
132 }132 }
133 args := opensshOptions(&options, scpKind)133 allArgs := opensshOptions(&options, scpKind)
134 args = append(args, extraArgs...)134 allArgs = append(allArgs, args...)
135 args = append(args, targets...)135 bin, allArgs := sshpassWrap("scp", allArgs)
136 bin, args := sshpassWrap("scp", args)136 cmd := exec.Command(bin, allArgs...)
137 cmd := exec.Command(bin, args...)
138 var stderr bytes.Buffer137 var stderr bytes.Buffer
139 cmd.Stderr = &stderr138 cmd.Stderr = &stderr
140 logger.Debugf("running: %s %s", bin, utils.CommandString(args...))139 logger.Debugf("running: %s %s", bin, utils.CommandString(args...))
141140
=== modified file 'utils/ssh/ssh_test.go'
--- utils/ssh/ssh_test.go 2014-02-22 13:45:10 +0000
+++ utils/ssh/ssh_test.go 2014-04-10 20:59:22 +0000
@@ -129,7 +129,7 @@
129 opts.AllowPasswordAuthentication()129 opts.AllowPasswordAuthentication()
130 opts.SetIdentities("x", "y")130 opts.SetIdentities("x", "y")
131 opts.SetPort(2022)131 opts.SetPort(2022)
132 err := s.client.Copy([]string{"/tmp/blah", "foo@bar.com:baz"}, nil, &opts)132 err := s.client.Copy([]string{"/tmp/blah", "foo@bar.com:baz"}, &opts)
133 c.Assert(err, gc.IsNil)133 c.Assert(err, gc.IsNil)
134 out, err := ioutil.ReadFile(s.fakescp + ".args")134 out, err := ioutil.ReadFile(s.fakescp + ".args")
135 c.Assert(err, gc.IsNil)135 c.Assert(err, gc.IsNil)
@@ -137,11 +137,18 @@
137 c.Assert(string(out), gc.Equals, s.fakescp+" -o StrictHostKeyChecking no -i x -i y -P 2022 /tmp/blah foo@bar.com:baz\n")137 c.Assert(string(out), gc.Equals, s.fakescp+" -o StrictHostKeyChecking no -i x -i y -P 2022 /tmp/blah foo@bar.com:baz\n")
138138
139 // Try passing extra args139 // Try passing extra args
140 err = s.client.Copy([]string{"/tmp/blah", "foo@bar.com:baz"}, []string{"-r", "-v"}, &opts)140 err = s.client.Copy([]string{"/tmp/blah", "foo@bar.com:baz", "-r", "-v"}, &opts)
141 c.Assert(err, gc.IsNil)141 c.Assert(err, gc.IsNil)
142 out, err = ioutil.ReadFile(s.fakescp + ".args")142 out, err = ioutil.ReadFile(s.fakescp + ".args")
143 c.Assert(err, gc.IsNil)143 c.Assert(err, gc.IsNil)
144 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")144 c.Assert(string(out), gc.Equals, s.fakescp+" -o StrictHostKeyChecking no -i x -i y -P 2022 /tmp/blah foo@bar.com:baz -r -v\n")
145
146 // Try interspersing extra args
147 err = s.client.Copy([]string{"-r", "/tmp/blah", "-v", "foo@bar.com:baz"}, &opts)
148 c.Assert(err, gc.IsNil)
149 out, err = ioutil.ReadFile(s.fakescp + ".args")
150 c.Assert(err, gc.IsNil)
151 c.Assert(string(out), gc.Equals, s.fakescp+" -o StrictHostKeyChecking no -i x -i y -P 2022 -r /tmp/blah -v foo@bar.com:baz\n")
145}152}
146153
147func (s *SSHCommandSuite) TestCommandClientKeys(c *gc.C) {154func (s *SSHCommandSuite) TestCommandClientKeys(c *gc.C) {

Subscribers

People subscribed via source and target branches

to all changes: