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
1=== modified file 'cmd/juju/scp.go'
2--- cmd/juju/scp.go 2014-03-20 02:30:15 +0000
3+++ cmd/juju/scp.go 2014-04-10 20:59:22 +0000
4@@ -35,12 +35,12 @@
5 Copy 2 files from two units to the local backup/ directory, passing -v
6 to scp as an extra argument:
7
8- juju scp ubuntu/0:/path/file1 ubuntu/1:/path/file2 backup/ -v
9+ juju scp -v ubuntu/0:/path/file1 ubuntu/1:/path/file2 backup/
10
11 Recursively copy the directory /var/log/mongodb/ on the first mongodb
12 server to the local directory remote-logs:
13
14- juju scp mongodb/0:/var/log/mongodb/ remote-logs/ -r
15+ juju scp -r mongodb/0:/var/log/mongodb/ remote-logs/
16
17 Copy a local file to the second apache unit of the environment "testing":
18
19@@ -64,6 +64,35 @@
20 return nil
21 }
22
23+// expandArgs takes a list of arguments and looks for ones in the form of
24+// 0:some/path or service/0:some/path, and translates them into
25+// ubuntu@machine:some/path so they can be passed as arguments to scp, and pass
26+// the rest verbatim on to scp
27+func expandArgs(args []string, hostFromTarget func(string) (string, error)) ([]string, error) {
28+ outArgs := make([]string, len(args))
29+ for i, arg := range args {
30+ v := strings.SplitN(arg, ":", 2)
31+ if strings.HasPrefix(arg, "-") || len(v) <= 1 {
32+ // Can't be an interesting target, so just pass it along
33+ outArgs[i] = arg
34+ continue
35+ }
36+ host, err := hostFromTarget(v[0])
37+ if err != nil {
38+ return nil, err
39+ }
40+ // To ensure this works with IPv6 addresses, we need to
41+ // wrap the host with \[..\], so the colons inside will be
42+ // interpreted as part of the address and the last one as
43+ // separator between host and remote path.
44+ if strings.Contains(host, ":") {
45+ host = fmt.Sprintf(`\[%s\]`, host)
46+ }
47+ outArgs[i] = "ubuntu@" + host + ":" + v[1]
48+ }
49+ return outArgs, nil
50+}
51+
52 // Run resolves c.Target to a machine, or host of a unit and
53 // forks ssh with c.Args, if provided.
54 func (c *SCPCommand) Run(ctx *cmd.Context) error {
55@@ -73,38 +102,9 @@
56 return err
57 }
58 defer c.apiClient.Close()
59-
60- // Parse all arguments, translating those in the form 0:/somepath
61- // or service/0:/somepath into ubuntu@machine:/somepath so they
62- // can be given to scp as targets (source(s) and destination(s)),
63- // and passing any others that look like extra arguments (starting
64- // with "-") verbatim to scp.
65- var targets, extraArgs []string
66- for i, arg := range c.Args {
67- if v := strings.SplitN(arg, ":", 2); len(v) > 1 {
68- host, err := c.hostFromTarget(v[0])
69- if err != nil {
70- return err
71- }
72- // To ensure this works with IPv6 addresses, we need to
73- // wrap the host with \[..\], so the colons inside will be
74- // interpreted as part of the address and the last one as
75- // separator between host and remote path.
76- if strings.Contains(host, ":") {
77- host = fmt.Sprintf(`\[%s\]`, host)
78- }
79- targets = append(targets, "ubuntu@"+host+":"+v[1])
80- continue
81- }
82- if strings.HasPrefix(arg, "-") {
83- if i != len(c.Args)-1 {
84- return fmt.Errorf("unexpected argument %q; extra arguments must be last", arg)
85- }
86- extraArgs = append(extraArgs, arg)
87- } else {
88- // Local path
89- targets = append(targets, arg)
90- }
91+ args, err := expandArgs(c.Args, c.hostFromTarget)
92+ if err != nil {
93+ return err
94 }
95- return ssh.Copy(targets, extraArgs, nil)
96+ return ssh.Copy(args, nil)
97 }
98
99=== modified file 'cmd/juju/scp_test.go'
100--- cmd/juju/scp_test.go 2014-03-20 02:30:15 +0000
101+++ cmd/juju/scp_test.go 2014-04-10 20:59:22 +0000
102@@ -9,7 +9,9 @@
103 "io/ioutil"
104 "net/url"
105 "path/filepath"
106+ "strings"
107
108+ jc "github.com/juju/testing/checkers"
109 gc "launchpad.net/gocheck"
110
111 "launchpad.net/juju-core/charm"
112@@ -18,11 +20,14 @@
113 )
114
115 var _ = gc.Suite(&SCPSuite{})
116+var _ = gc.Suite(&expandArgsSuite{})
117
118 type SCPSuite struct {
119 SSHCommonSuite
120 }
121
122+type expandArgsSuite struct{}
123+
124 var scpTests = []struct {
125 about string
126 args []string
127@@ -34,60 +39,61 @@
128 []string{"0:foo", "."},
129 commonArgs + "ubuntu@dummyenv-0.dns:foo .\n",
130 "",
131- },
132- {
133+ }, {
134 "scp from machine 0 to current dir with extra args",
135- []string{"0:foo", ".", "-rv -o SomeOption"},
136- commonArgs + "-rv -o SomeOption ubuntu@dummyenv-0.dns:foo .\n",
137+ []string{"0:foo", ".", "-rv", "-o", "SomeOption"},
138+ commonArgs + "ubuntu@dummyenv-0.dns:foo . -rv -o SomeOption\n",
139 "",
140- },
141- {
142+ }, {
143 "scp from current dir to machine 0",
144 []string{"foo", "0:"},
145 commonArgs + "foo ubuntu@dummyenv-0.dns:\n",
146 "",
147- },
148- {
149+ }, {
150 "scp from current dir to machine 0 with extra args",
151- []string{"foo", "0:", "-r -v"},
152- commonArgs + "-r -v foo ubuntu@dummyenv-0.dns:\n",
153+ []string{"foo", "0:", "-r", "-v"},
154+ commonArgs + "foo ubuntu@dummyenv-0.dns: -r -v\n",
155 "",
156- },
157- {
158+ }, {
159 "scp from machine 0 to unit mysql/0",
160 []string{"0:foo", "mysql/0:/foo"},
161 commonArgs + "ubuntu@dummyenv-0.dns:foo ubuntu@dummyenv-0.dns:/foo\n",
162 "",
163- },
164- {
165+ }, {
166 "scp from machine 0 to unit mysql/0 and extra args",
167 []string{"0:foo", "mysql/0:/foo", "-q"},
168- commonArgs + "-q ubuntu@dummyenv-0.dns:foo ubuntu@dummyenv-0.dns:/foo\n",
169+ commonArgs + "ubuntu@dummyenv-0.dns:foo ubuntu@dummyenv-0.dns:/foo -q\n",
170 "",
171- },
172- {
173+ }, {
174 "scp from machine 0 to unit mysql/0 and extra args before",
175 []string{"-q", "-r", "0:foo", "mysql/0:/foo"},
176+ commonArgs + "-q -r ubuntu@dummyenv-0.dns:foo ubuntu@dummyenv-0.dns:/foo\n",
177 "",
178- `unexpected argument "-q"; extra arguments must be last`,
179- },
180- {
181+ }, {
182 "scp two local files to unit mysql/0",
183 []string{"file1", "file2", "mysql/0:/foo/"},
184 commonArgs + "file1 file2 ubuntu@dummyenv-0.dns:/foo/\n",
185 "",
186- },
187- {
188+ }, {
189 "scp from unit mongodb/1 to unit mongodb/0 and multiple extra args",
190- []string{"mongodb/1:foo", "mongodb/0:", "-r -v -q -l5"},
191- commonArgs + "-r -v -q -l5 ubuntu@dummyenv-2.dns:foo ubuntu@dummyenv-1.dns:\n",
192- "",
193- },
194- {
195+ []string{"mongodb/1:foo", "mongodb/0:", "-r", "-v", "-q", "-l5"},
196+ commonArgs + "ubuntu@dummyenv-2.dns:foo ubuntu@dummyenv-1.dns: -r -v -q -l5\n",
197+ "",
198+ }, {
199+ "scp from unit mongodb/1 to unit mongodb/0 with a --",
200+ []string{"--", "-r", "-v", "mongodb/1:foo", "mongodb/0:", "-q", "-l5"},
201+ commonArgs + "-- -r -v ubuntu@dummyenv-2.dns:foo ubuntu@dummyenv-1.dns: -q -l5\n",
202+ "",
203+ }, {
204 "scp works with IPv6 addresses",
205 []string{"ipv6-svc/0:foo", "bar"},
206 commonArgs + `ubuntu@\[2001:db8::\]:foo bar` + "\n",
207 "",
208+ }, {
209+ "scp with no such machine",
210+ []string{"5:foo", "bar"},
211+ "",
212+ "machine 5 not found",
213 },
214 }
215
216@@ -140,3 +146,70 @@
217 }
218 }
219 }
220+
221+var hostsFromTargets = map[string]string{
222+ "0": "dummyenv-0.dns",
223+ "mysql/0": "dummyenv-0.dns",
224+ "mongodb/0": "dummyenv-1.dns",
225+ "mongodb/1": "dummyenv-2.dns",
226+ "ipv6-svc/0": "2001:db8::",
227+}
228+
229+func dummyHostsFromTarget(target string) (string, error) {
230+ if res, ok := hostsFromTargets[target]; ok {
231+ return res, nil
232+ }
233+ return target, nil
234+}
235+
236+func (s *expandArgsSuite) TestSCPExpandArgs(c *gc.C) {
237+ for i, t := range scpTests {
238+ if t.error != "" {
239+ // We are just running a focused set of tests on
240+ // expandArgs, we aren't implementing the full
241+ // hostsFromTargets to actually trigger errors
242+ continue
243+ }
244+ c.Logf("test %d: %s -> %s\n", i, t.about, t.args)
245+ // expandArgs doesn't add the commonArgs prefix, so strip it
246+ // off, along with the trailing '\n'
247+ c.Check(strings.HasPrefix(t.result, commonArgs), jc.IsTrue)
248+ argString := t.result[len(commonArgs):]
249+ c.Check(strings.HasSuffix(argString, "\n"), jc.IsTrue)
250+ argString = argString[:len(argString)-1]
251+ args := strings.Split(argString, " ")
252+ expanded, err := expandArgs(t.args, dummyHostsFromTarget)
253+ c.Check(err, gc.IsNil)
254+ c.Check(expanded, gc.DeepEquals, args)
255+ }
256+}
257+
258+var expandTests = []struct {
259+ about string
260+ args []string
261+ result []string
262+}{
263+ {
264+ "don't expand params that start with '-'",
265+ []string{"-0:stuff", "0:foo", "."},
266+ []string{"-0:stuff", "ubuntu@dummyenv-0.dns:foo", "."},
267+ },
268+}
269+
270+func (s *expandArgsSuite) TestExpandArgs(c *gc.C) {
271+ for i, t := range expandTests {
272+ c.Logf("test %d: %s -> %s\n", i, t.about, t.args)
273+ expanded, err := expandArgs(t.args, dummyHostsFromTarget)
274+ c.Check(err, gc.IsNil)
275+ c.Check(expanded, gc.DeepEquals, t.result)
276+ }
277+}
278+
279+func (s *expandArgsSuite) TestExpandArgsPropagatesErrors(c *gc.C) {
280+ erroringHostFromTargets := func(string) (string, error) {
281+ return "", fmt.Errorf("this is my error")
282+ }
283+ expanded, err := expandArgs([]string{"foo:1", "bar"}, erroringHostFromTargets)
284+ c.Assert(err, gc.ErrorMatches, "this is my error")
285+ c.Check(expanded, gc.IsNil)
286+}
287
288=== modified file 'utils/ssh/ssh.go'
289--- utils/ssh/ssh.go 2014-03-19 03:18:41 +0000
290+++ utils/ssh/ssh.go 2014-04-10 20:59:22 +0000
291@@ -81,7 +81,7 @@
292 // Paths are specified in the scp format, [[user@]host:]path. If
293 // any extra arguments are specified in extraArgs, they are passed
294 // verbatim.
295- Copy(targets, extraArgs []string, options *Options) error
296+ Copy(args []string, options *Options) error
297 }
298
299 // Cmd represents a command to be (or being) executed
300@@ -236,7 +236,7 @@
301 }
302
303 // Copy is a short-cut for DefaultClient.Copy.
304-func Copy(targets, extraArgs []string, options *Options) error {
305+func Copy(args []string, options *Options) error {
306 logger.Debugf("using %s ssh client", chosenClient)
307- return DefaultClient.Copy(targets, extraArgs, options)
308+ return DefaultClient.Copy(args, options)
309 }
310
311=== modified file 'utils/ssh/ssh_gocrypto.go'
312--- utils/ssh/ssh_gocrypto.go 2014-02-24 16:21:43 +0000
313+++ utils/ssh/ssh_gocrypto.go 2014-04-10 20:59:22 +0000
314@@ -62,7 +62,7 @@
315 // Copy implements Client.Copy.
316 //
317 // Copy is currently unimplemented, and will always return an error.
318-func (c *GoCryptoClient) Copy(targets, extraArgs []string, options *Options) error {
319+func (c *GoCryptoClient) Copy(args []string, options *Options) error {
320 return fmt.Errorf("scp command is not implemented (OpenSSH scp not available in PATH)")
321 }
322
323
324=== modified file 'utils/ssh/ssh_gocrypto_test.go'
325--- utils/ssh/ssh_gocrypto_test.go 2014-03-13 07:54:56 +0000
326+++ utils/ssh/ssh_gocrypto_test.go 2014-04-10 20:59:22 +0000
327@@ -145,6 +145,6 @@
328 func (s *SSHGoCryptoCommandSuite) TestCopy(c *gc.C) {
329 client, err := ssh.NewGoCryptoClient()
330 c.Assert(err, gc.IsNil)
331- err = client.Copy([]string{"0.1.2.3:b", c.MkDir()}, nil, nil)
332+ err = client.Copy([]string{"0.1.2.3:b", c.MkDir()}, nil)
333 c.Assert(err, gc.ErrorMatches, `scp command is not implemented \(OpenSSH scp not available in PATH\)`)
334 }
335
336=== modified file 'utils/ssh/ssh_openssh.go'
337--- utils/ssh/ssh_openssh.go 2014-03-27 02:42:08 +0000
338+++ utils/ssh/ssh_openssh.go 2014-04-10 20:59:22 +0000
339@@ -124,17 +124,16 @@
340 }
341
342 // Copy implements Client.Copy.
343-func (c *OpenSSHClient) Copy(targets, extraArgs []string, userOptions *Options) error {
344+func (c *OpenSSHClient) Copy(args []string, userOptions *Options) error {
345 var options Options
346 if userOptions != nil {
347 options = *userOptions
348 options.allocatePTY = false // doesn't make sense for scp
349 }
350- args := opensshOptions(&options, scpKind)
351- args = append(args, extraArgs...)
352- args = append(args, targets...)
353- bin, args := sshpassWrap("scp", args)
354- cmd := exec.Command(bin, args...)
355+ allArgs := opensshOptions(&options, scpKind)
356+ allArgs = append(allArgs, args...)
357+ bin, allArgs := sshpassWrap("scp", allArgs)
358+ cmd := exec.Command(bin, allArgs...)
359 var stderr bytes.Buffer
360 cmd.Stderr = &stderr
361 logger.Debugf("running: %s %s", bin, utils.CommandString(args...))
362
363=== modified file 'utils/ssh/ssh_test.go'
364--- utils/ssh/ssh_test.go 2014-02-22 13:45:10 +0000
365+++ utils/ssh/ssh_test.go 2014-04-10 20:59:22 +0000
366@@ -129,7 +129,7 @@
367 opts.AllowPasswordAuthentication()
368 opts.SetIdentities("x", "y")
369 opts.SetPort(2022)
370- err := s.client.Copy([]string{"/tmp/blah", "foo@bar.com:baz"}, nil, &opts)
371+ err := s.client.Copy([]string{"/tmp/blah", "foo@bar.com:baz"}, &opts)
372 c.Assert(err, gc.IsNil)
373 out, err := ioutil.ReadFile(s.fakescp + ".args")
374 c.Assert(err, gc.IsNil)
375@@ -137,11 +137,18 @@
376 c.Assert(string(out), gc.Equals, s.fakescp+" -o StrictHostKeyChecking no -i x -i y -P 2022 /tmp/blah foo@bar.com:baz\n")
377
378 // Try passing extra args
379- err = s.client.Copy([]string{"/tmp/blah", "foo@bar.com:baz"}, []string{"-r", "-v"}, &opts)
380- c.Assert(err, gc.IsNil)
381- out, err = ioutil.ReadFile(s.fakescp + ".args")
382- c.Assert(err, gc.IsNil)
383- 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")
384+ err = s.client.Copy([]string{"/tmp/blah", "foo@bar.com:baz", "-r", "-v"}, &opts)
385+ c.Assert(err, gc.IsNil)
386+ out, err = ioutil.ReadFile(s.fakescp + ".args")
387+ c.Assert(err, gc.IsNil)
388+ 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")
389+
390+ // Try interspersing extra args
391+ err = s.client.Copy([]string{"-r", "/tmp/blah", "-v", "foo@bar.com:baz"}, &opts)
392+ c.Assert(err, gc.IsNil)
393+ out, err = ioutil.ReadFile(s.fakescp + ".args")
394+ c.Assert(err, gc.IsNil)
395+ 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")
396 }
397
398 func (s *SSHCommandSuite) TestCommandClientKeys(c *gc.C) {

Subscribers

People subscribed via source and target branches

to all changes: