Merge lp:~thumper/juju-core/juju-run-cli into lp:~go-bot/juju-core/trunk

Proposed by Tim Penhey
Status: Merged
Approved by: Tim Penhey
Approved revision: no longer in the source branch.
Merged at revision: 2193
Proposed branch: lp:~thumper/juju-core/juju-run-cli
Merge into: lp:~go-bot/juju-core/trunk
Prerequisite: lp:~thumper/juju-core/api-juju-run
Diff against target: 736 lines (+690/-0)
5 files modified
cmd/juju/main.go (+1/-0)
cmd/juju/main_test.go (+1/-0)
cmd/juju/run.go (+233/-0)
cmd/juju/run_test.go (+443/-0)
testing/environ.go (+12/-0)
To merge this branch: bzr merge lp:~thumper/juju-core/juju-run-cli
Reviewer Review Type Date Requested Status
Juju Engineering Pending
Review via email: mp+201124@code.launchpad.net

Commit message

Add the "run" command to the command line tool.

Hook up the Client Run and RunOnAllMachines API calls
to the command line tool.

https://codereview.appspot.com/50310043/

Description of the change

Add the "run" command to the command line tool.

Hook up the Client Run and RunOnAllMachines API calls
to the command line tool.

https://codereview.appspot.com/50310043/

To post a comment you must log in.
Revision history for this message
Tim Penhey (thumper) wrote :

Reviewers: mp+201124_code.launchpad.net,

Message:
Please take a look.

Description:
Add the "run" command to the command line tool.

Hook up the Client Run and RunOnAllMachines API calls
to the command line tool.

https://code.launchpad.net/~thumper/juju-core/juju-run-cli/+merge/201124

Requires:
https://code.launchpad.net/~thumper/juju-core/api-juju-run/+merge/201103

(do not edit description out of merge proposal)

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

Affected files (+652, -0 lines):
   A [revision details]
   M cmd/juju/main.go
   M cmd/juju/main_test.go
   A cmd/juju/run.go
   A cmd/juju/run_test.go
   M testing/environ.go

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

https://codereview.appspot.com/50310043/diff/1/cmd/juju/run.go
File cmd/juju/run.go (right):

https://codereview.appspot.com/50310043/diff/1/cmd/juju/run.go#newcode33
cmd/juju/run.go:33: Run the commands on the specified targets.
This just occurred to me: should we be supporting patterns, like in juju
status?
e.g. juju run --service='nova-*' <commands>

Just a thought.

https://codereview.appspot.com/50310043/diff/1/cmd/juju/run.go#newcode128
cmd/juju/run.go:128: var results = make([]interface{}, 0)
Why are you initialising this with 0 length, and then appending? Why not
initialise with len(runResults) and assign elements?

https://codereview.appspot.com/50310043/diff/1/cmd/juju/run.go#newcode139
cmd/juju/run.go:139: values["Stdout"] = string(result.Stdout)
You're assuming that stdout/stderr are UTF-8 here, which they may not
be. At the least, can we please use utf8.Valid before converting them to
strings?

https://codereview.appspot.com/50310043/diff/1/cmd/juju/run.go#newcode203
cmd/juju/run.go:203: type RunClient interface {
Nice

https://codereview.appspot.com/50310043/diff/1/cmd/juju/run_test.go
File cmd/juju/run_test.go (right):

https://codereview.appspot.com/50310043/diff/1/cmd/juju/run_test.go#newcode210
cmd/juju/run_test.go:210: },
In light of my comments in run.go, I think a test for binary
stdout/stderr is in order.

https://codereview.appspot.com/50310043/

Revision history for this message
Tim Penhey (thumper) wrote :

Please take a look.

https://codereview.appspot.com/50310043/diff/1/cmd/juju/run.go
File cmd/juju/run.go (right):

https://codereview.appspot.com/50310043/diff/1/cmd/juju/run.go#newcode33
cmd/juju/run.go:33: Run the commands on the specified targets.
On 2014/01/10 02:13:53, axw wrote:
> This just occurred to me: should we be supporting patterns, like in
juju status?
> e.g. juju run --service='nova-*' <commands>

> Just a thought.

I do think that this is a good idea, but not for the first release :-)

https://codereview.appspot.com/50310043/diff/1/cmd/juju/run.go#newcode128
cmd/juju/run.go:128: var results = make([]interface{}, 0)
On 2014/01/10 02:13:53, axw wrote:
> Why are you initialising this with 0 length, and then appending? Why
not
> initialise with len(runResults) and assign elements?

Hmm good point. Since we know the size, I'll do that.

https://codereview.appspot.com/50310043/diff/1/cmd/juju/run.go#newcode139
cmd/juju/run.go:139: values["Stdout"] = string(result.Stdout)
On 2014/01/10 02:13:53, axw wrote:
> You're assuming that stdout/stderr are UTF-8 here, which they may not
be. At the
> least, can we please use utf8.Valid before converting them to strings?

Now base64 encoding non-utf8 byte slice results.

https://codereview.appspot.com/50310043/diff/1/cmd/juju/run_test.go
File cmd/juju/run_test.go (right):

https://codereview.appspot.com/50310043/diff/1/cmd/juju/run_test.go#newcode210
cmd/juju/run_test.go:210: },
On 2014/01/10 02:13:53, axw wrote:
> In light of my comments in run.go, I think a test for binary
stdout/stderr is in
> order.

Added.

https://codereview.appspot.com/50310043/

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

On 2014/01/10 03:40:49, thumper wrote:
> Please take a look.

> https://codereview.appspot.com/50310043/diff/1/cmd/juju/run.go
> File cmd/juju/run.go (right):

https://codereview.appspot.com/50310043/diff/1/cmd/juju/run.go#newcode33
> cmd/juju/run.go:33: Run the commands on the specified targets.
> On 2014/01/10 02:13:53, axw wrote:
> > This just occurred to me: should we be supporting patterns, like in
juju
> status?
> > e.g. juju run --service='nova-*' <commands>
> >
> > Just a thought.

> I do think that this is a good idea, but not for the first release :-)

https://codereview.appspot.com/50310043/diff/1/cmd/juju/run.go#newcode128
> cmd/juju/run.go:128: var results = make([]interface{}, 0)
> On 2014/01/10 02:13:53, axw wrote:
> > Why are you initialising this with 0 length, and then appending? Why
not
> > initialise with len(runResults) and assign elements?

> Hmm good point. Since we know the size, I'll do that.

https://codereview.appspot.com/50310043/diff/1/cmd/juju/run.go#newcode139
> cmd/juju/run.go:139: values["Stdout"] = string(result.Stdout)
> On 2014/01/10 02:13:53, axw wrote:
> > You're assuming that stdout/stderr are UTF-8 here, which they may
not be. At
> the
> > least, can we please use utf8.Valid before converting them to
strings?

> Now base64 encoding non-utf8 byte slice results.

> https://codereview.appspot.com/50310043/diff/1/cmd/juju/run_test.go
> File cmd/juju/run_test.go (right):

https://codereview.appspot.com/50310043/diff/1/cmd/juju/run_test.go#newcode210
> cmd/juju/run_test.go:210: },
> On 2014/01/10 02:13:53, axw wrote:
> > In light of my comments in run.go, I think a test for binary
stdout/stderr is
> in
> > order.

> Added.

Thanks, LGTM

https://codereview.appspot.com/50310043/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'cmd/juju/main.go'
--- cmd/juju/main.go 2013-12-11 07:55:37 +0000
+++ cmd/juju/main.go 2014-01-12 20:39:24 +0000
@@ -86,6 +86,7 @@
86 jujucmd.Register(wrap(&EndpointCommand{}))86 jujucmd.Register(wrap(&EndpointCommand{}))
8787
88 // Error resolution and debugging commands.88 // Error resolution and debugging commands.
89 jujucmd.Register(wrap(&RunCommand{}))
89 jujucmd.Register(wrap(&SCPCommand{}))90 jujucmd.Register(wrap(&SCPCommand{}))
90 jujucmd.Register(wrap(&SSHCommand{}))91 jujucmd.Register(wrap(&SSHCommand{}))
91 jujucmd.Register(wrap(&ResolvedCommand{}))92 jujucmd.Register(wrap(&ResolvedCommand{}))
9293
=== modified file 'cmd/juju/main_test.go'
--- cmd/juju/main_test.go 2013-12-11 07:55:37 +0000
+++ cmd/juju/main_test.go 2014-01-12 20:39:24 +0000
@@ -244,6 +244,7 @@
244 "remove-relation", // alias for destroy-relation244 "remove-relation", // alias for destroy-relation
245 "remove-unit", // alias for destroy-unit245 "remove-unit", // alias for destroy-unit
246 "resolved",246 "resolved",
247 "run",
247 "scp",248 "scp",
248 "set",249 "set",
249 "set-constraints",250 "set-constraints",
250251
=== added file 'cmd/juju/run.go'
--- cmd/juju/run.go 1970-01-01 00:00:00 +0000
+++ cmd/juju/run.go 2014-01-12 20:39:24 +0000
@@ -0,0 +1,233 @@
1// Copyright 2013 Canonical Ltd.
2// Licensed under the AGPLv3, see LICENCE file for details.
3
4package main
5
6import (
7 "encoding/base64"
8 "errors"
9 "fmt"
10 "strings"
11 "time"
12 "unicode/utf8"
13
14 "launchpad.net/gnuflag"
15
16 "launchpad.net/juju-core/cmd"
17 "launchpad.net/juju-core/juju"
18 "launchpad.net/juju-core/names"
19 "launchpad.net/juju-core/state/api/params"
20)
21
22// RunCommand is responsible for running arbitrary commands on remote machines.
23type RunCommand struct {
24 cmd.EnvCommandBase
25 out cmd.Output
26 all bool
27 timeout time.Duration
28 machines []string
29 services []string
30 units []string
31 commands string
32}
33
34const runDoc = `
35Run the commands on the specified targets.
36
37Targets are specified using either machine ids, service names or unit
38names. At least one target specifier is needed.
39
40Multiple values can be set for --machine, --service, and --unit by using
41comma separated values.
42
43If the target is a machine, the command is run as the "ubuntu" user on
44the remote machine.
45
46If the target is a service, the command is run on all units for that
47service. For example, if there was a service "mysql" and that service
48had two units, "mysql/0" and "mysql/1", then
49 --service mysql
50is equivalent to
51 --unit mysql/0,mysql/1
52
53Commands run for services or units are executed in a 'hook context' for
54the unit.
55
56--all is provided as a simple way to run the command on all the machines
57in the environment. If you specify --all you cannot provide additional
58targets.
59
60`
61
62func (c *RunCommand) Info() *cmd.Info {
63 return &cmd.Info{
64 Name: "run",
65 Args: "<commands>",
66 Purpose: "run the commands on the remote targets specified",
67 Doc: runDoc,
68 }
69}
70
71func (c *RunCommand) SetFlags(f *gnuflag.FlagSet) {
72 c.EnvCommandBase.SetFlags(f)
73 c.out.AddFlags(f, "smart", cmd.DefaultFormatters)
74 f.BoolVar(&c.all, "all", false, "run the commands on all the machines")
75 f.DurationVar(&c.timeout, "timeout", 5*time.Minute, "how long to wait before the remote command is considered to have failed")
76 f.Var(cmd.NewStringsValue(nil, &c.machines), "machine", "one or more machine ids")
77 f.Var(cmd.NewStringsValue(nil, &c.services), "service", "one or more service names")
78 f.Var(cmd.NewStringsValue(nil, &c.units), "unit", "one or more unit ids")
79}
80
81func (c *RunCommand) Init(args []string) error {
82 if len(args) == 0 {
83 return errors.New("no commands specified")
84 }
85 c.commands, args = args[0], args[1:]
86
87 if c.all {
88 if len(c.machines) != 0 {
89 return fmt.Errorf("You cannot specify --all and individual machines")
90 }
91 if len(c.services) != 0 {
92 return fmt.Errorf("You cannot specify --all and individual services")
93 }
94 if len(c.units) != 0 {
95 return fmt.Errorf("You cannot specify --all and individual units")
96 }
97 } else {
98 if len(c.machines) == 0 && len(c.services) == 0 && len(c.units) == 0 {
99 return fmt.Errorf("You must specify a target, either through --all, --machine, --service or --unit")
100 }
101 }
102
103 var nameErrors []string
104 for _, machineId := range c.machines {
105 if !names.IsMachine(machineId) {
106 nameErrors = append(nameErrors, fmt.Sprintf(" %q is not a valid machine id", machineId))
107 }
108 }
109 for _, service := range c.services {
110 if !names.IsService(service) {
111 nameErrors = append(nameErrors, fmt.Sprintf(" %q is not a valid service name", service))
112 }
113 }
114 for _, unit := range c.units {
115 if !names.IsUnit(unit) {
116 nameErrors = append(nameErrors, fmt.Sprintf(" %q is not a valid unit name", unit))
117 }
118 }
119 if len(nameErrors) > 0 {
120 return fmt.Errorf("The following run targets are not valid:\n%s",
121 strings.Join(nameErrors, "\n"))
122 }
123
124 return cmd.CheckEmpty(args)
125}
126
127func encodeBytes(input []byte) (value string, encoding string) {
128 if utf8.Valid(input) {
129 value = string(input)
130 encoding = "utf8"
131 } else {
132 value = base64.StdEncoding.EncodeToString(input)
133 encoding = "base64"
134 }
135 return value, encoding
136}
137
138func storeOutput(values map[string]interface{}, key string, input []byte) {
139 value, encoding := encodeBytes(input)
140 values[key] = value
141 if encoding != "utf8" {
142 values[key+".encoding"] = encoding
143 }
144}
145
146// ConvertRunResults takes the results from the api and creates a map
147// suitable for format converstion to YAML or JSON.
148func ConvertRunResults(runResults []params.RunResult) interface{} {
149 var results = make([]interface{}, len(runResults))
150
151 for i, result := range runResults {
152 // We always want to have a string for stdout, but only show stderr,
153 // code and error if they are there.
154 values := make(map[string]interface{})
155 values["MachineId"] = result.MachineId
156 if result.UnitId != "" {
157 values["UnitId"] = result.UnitId
158
159 }
160 storeOutput(values, "Stdout", result.Stdout)
161 if len(result.Stderr) > 0 {
162 storeOutput(values, "Stderr", result.Stderr)
163 }
164 if result.Code != 0 {
165 values["ReturnCode"] = result.Code
166 }
167 if result.Error != "" {
168 values["Error"] = result.Error
169 }
170 results[i] = values
171 }
172
173 return results
174}
175
176func (c *RunCommand) Run(ctx *cmd.Context) error {
177 client, err := getAPIClient(c.EnvName)
178 if err != nil {
179 return err
180 }
181 defer client.Close()
182
183 var runResults []params.RunResult
184 if c.all {
185 runResults, err = client.RunOnAllMachines(c.commands, c.timeout)
186 } else {
187 params := params.RunParams{
188 Commands: c.commands,
189 Timeout: c.timeout,
190 Machines: c.machines,
191 Services: c.services,
192 Units: c.units,
193 }
194 runResults, err = client.Run(params)
195 }
196
197 if err != nil {
198 return err
199 }
200
201 // If we are just dealing with one result, AND we are using the smart
202 // format, then pretend we were running it locally.
203 if len(runResults) == 1 && c.out.Name() == "smart" {
204 result := runResults[0]
205 ctx.Stdout.Write(result.Stdout)
206 ctx.Stderr.Write(result.Stderr)
207 if result.Error != "" {
208 // Convert the error string back into an error object.
209 return fmt.Errorf("%s", result.Error)
210 }
211 if result.Code != 0 {
212 return cmd.NewRcPassthroughError(result.Code)
213 }
214 return nil
215 }
216
217 c.out.Write(ctx, ConvertRunResults(runResults))
218 return nil
219}
220
221// In order to be able to easily mock out the API side for testing,
222// the API client is got using a function.
223
224type RunClient interface {
225 Close() error
226 RunOnAllMachines(commands string, timeout time.Duration) ([]params.RunResult, error)
227 Run(run params.RunParams) ([]params.RunResult, error)
228}
229
230// Here we need the signature to be correct for the interface.
231var getAPIClient = func(name string) (RunClient, error) {
232 return juju.NewAPIClientFromName(name)
233}
0234
=== added file 'cmd/juju/run_test.go'
--- cmd/juju/run_test.go 1970-01-01 00:00:00 +0000
+++ cmd/juju/run_test.go 2014-01-12 20:39:24 +0000
@@ -0,0 +1,443 @@
1// Copyright 2013 Canonical Ltd.
2// Licensed under the AGPLv3, see LICENCE file for details.
3
4package main
5
6import (
7 "fmt"
8 "time"
9
10 gc "launchpad.net/gocheck"
11
12 "launchpad.net/juju-core/cmd"
13 "launchpad.net/juju-core/state/api/params"
14 "launchpad.net/juju-core/testing"
15 jc "launchpad.net/juju-core/testing/checkers"
16)
17
18type RunSuite struct {
19 testing.FakeHomeSuite
20}
21
22var _ = gc.Suite(&RunSuite{})
23
24func (*RunSuite) TestTargetArgParsing(c *gc.C) {
25 for i, test := range []struct {
26 message string
27 args []string
28 all bool
29 machines []string
30 units []string
31 services []string
32 commands string
33 errMatch string
34 }{{
35 message: "no args",
36 errMatch: "no commands specified",
37 }, {
38 message: "no target",
39 args: []string{"sudo reboot"},
40 errMatch: "You must specify a target, either through --all, --machine, --service or --unit",
41 }, {
42 message: "too many args",
43 args: []string{"--all", "sudo reboot", "oops"},
44 errMatch: `unrecognized args: \["oops"\]`,
45 }, {
46 message: "command to all machines",
47 args: []string{"--all", "sudo reboot"},
48 all: true,
49 commands: "sudo reboot",
50 }, {
51 message: "all and defined machines",
52 args: []string{"--all", "--machine=1,2", "sudo reboot"},
53 errMatch: `You cannot specify --all and individual machines`,
54 }, {
55 message: "command to machines 1, 2, and 1/kvm/0",
56 args: []string{"--machine=1,2,1/kvm/0", "sudo reboot"},
57 commands: "sudo reboot",
58 machines: []string{"1", "2", "1/kvm/0"},
59 }, {
60 message: "bad machine names",
61 args: []string{"--machine=foo,machine-2", "sudo reboot"},
62 errMatch: "" +
63 "The following run targets are not valid:\n" +
64 " \"foo\" is not a valid machine id\n" +
65 " \"machine-2\" is not a valid machine id",
66 }, {
67 message: "all and defined services",
68 args: []string{"--all", "--service=wordpress,mysql", "sudo reboot"},
69 errMatch: `You cannot specify --all and individual services`,
70 }, {
71 message: "command to services wordpress and mysql",
72 args: []string{"--service=wordpress,mysql", "sudo reboot"},
73 commands: "sudo reboot",
74 services: []string{"wordpress", "mysql"},
75 }, {
76 message: "bad service names",
77 args: []string{"--service", "foo,2,foo/0", "sudo reboot"},
78 errMatch: "" +
79 "The following run targets are not valid:\n" +
80 " \"2\" is not a valid service name\n" +
81 " \"foo/0\" is not a valid service name",
82 }, {
83 message: "all and defined units",
84 args: []string{"--all", "--unit=wordpress/0,mysql/1", "sudo reboot"},
85 errMatch: `You cannot specify --all and individual units`,
86 }, {
87 message: "command to valid units",
88 args: []string{"--unit=wordpress/0,wordpress/1,mysql/0", "sudo reboot"},
89 commands: "sudo reboot",
90 units: []string{"wordpress/0", "wordpress/1", "mysql/0"},
91 }, {
92 message: "bad unit names",
93 args: []string{"--unit", "foo,2,foo/0", "sudo reboot"},
94 errMatch: "" +
95 "The following run targets are not valid:\n" +
96 " \"foo\" is not a valid unit name\n" +
97 " \"2\" is not a valid unit name",
98 }, {
99 message: "command to mixed valid targets",
100 args: []string{"--machine=0", "--unit=wordpress/0,wordpress/1", "--service=mysql", "sudo reboot"},
101 commands: "sudo reboot",
102 machines: []string{"0"},
103 services: []string{"mysql"},
104 units: []string{"wordpress/0", "wordpress/1"},
105 }} {
106 c.Log(fmt.Sprintf("%v: %s", i, test.message))
107 runCmd := &RunCommand{}
108 testing.TestInit(c, runCmd, test.args, test.errMatch)
109 if test.errMatch == "" {
110 c.Check(runCmd.all, gc.Equals, test.all)
111 c.Check(runCmd.machines, gc.DeepEquals, test.machines)
112 c.Check(runCmd.services, gc.DeepEquals, test.services)
113 c.Check(runCmd.units, gc.DeepEquals, test.units)
114 c.Check(runCmd.commands, gc.Equals, test.commands)
115 }
116 }
117}
118
119func (*RunSuite) TestTimeoutArgParsing(c *gc.C) {
120 for i, test := range []struct {
121 message string
122 args []string
123 errMatch string
124 timeout time.Duration
125 }{{
126 message: "default time",
127 args: []string{"--all", "sudo reboot"},
128 timeout: 5 * time.Minute,
129 }, {
130 message: "invalid time",
131 args: []string{"--timeout=foo", "--all", "sudo reboot"},
132 errMatch: `invalid value "foo" for flag --timeout: time: invalid duration foo`,
133 }, {
134 message: "two hours",
135 args: []string{"--timeout=2h", "--all", "sudo reboot"},
136 timeout: 2 * time.Hour,
137 }, {
138 message: "3 minutes 30 seconds",
139 args: []string{"--timeout=3m30s", "--all", "sudo reboot"},
140 timeout: (3 * time.Minute) + (30 * time.Second),
141 }} {
142 c.Log(fmt.Sprintf("%v: %s", i, test.message))
143 runCmd := &RunCommand{}
144 testing.TestInit(c, runCmd, test.args, test.errMatch)
145 if test.errMatch == "" {
146 c.Check(runCmd.timeout, gc.Equals, test.timeout)
147 }
148 }
149}
150
151func (s *RunSuite) TestConvertRunResults(c *gc.C) {
152 for i, test := range []struct {
153 message string
154 results []params.RunResult
155 expected interface{}
156 }{{
157 message: "empty",
158 expected: []interface{}{},
159 }, {
160 message: "minimum is machine id and stdout",
161 results: []params.RunResult{
162 makeRunResult(mockResponse{machineId: "1"}),
163 },
164 expected: []interface{}{
165 map[string]interface{}{
166 "MachineId": "1",
167 "Stdout": "",
168 }},
169 }, {
170 message: "other fields are copied if there",
171 results: []params.RunResult{
172 makeRunResult(mockResponse{
173 machineId: "1",
174 stdout: "stdout",
175 stderr: "stderr",
176 code: 42,
177 unitId: "unit/0",
178 error: "error",
179 }),
180 },
181 expected: []interface{}{
182 map[string]interface{}{
183 "MachineId": "1",
184 "Stdout": "stdout",
185 "Stderr": "stderr",
186 "ReturnCode": 42,
187 "UnitId": "unit/0",
188 "Error": "error",
189 }},
190 }, {
191 message: "stdout and stderr are base64 encoded if not valid utf8",
192 results: []params.RunResult{
193 params.RunResult{
194 RemoteResponse: cmd.RemoteResponse{
195 Stdout: []byte{0xff},
196 Stderr: []byte{0xfe},
197 },
198 MachineId: "jake",
199 },
200 },
201 expected: []interface{}{
202 map[string]interface{}{
203 "MachineId": "jake",
204 "Stdout": "/w==",
205 "Stdout.encoding": "base64",
206 "Stderr": "/g==",
207 "Stderr.encoding": "base64",
208 }},
209 }, {
210 message: "more than one",
211 results: []params.RunResult{
212 makeRunResult(mockResponse{machineId: "1"}),
213 makeRunResult(mockResponse{machineId: "2"}),
214 makeRunResult(mockResponse{machineId: "3"}),
215 },
216 expected: []interface{}{
217 map[string]interface{}{
218 "MachineId": "1",
219 "Stdout": "",
220 },
221 map[string]interface{}{
222 "MachineId": "2",
223 "Stdout": "",
224 },
225 map[string]interface{}{
226 "MachineId": "3",
227 "Stdout": "",
228 },
229 },
230 }} {
231 c.Log(fmt.Sprintf("%v: %s", i, test.message))
232 result := ConvertRunResults(test.results)
233 c.Check(result, jc.DeepEquals, test.expected)
234 }
235}
236
237func (s *RunSuite) TestRunForMachineAndUnit(c *gc.C) {
238 mock := s.setupMockAPI()
239 machineResponse := mockResponse{
240 stdout: "megatron\n",
241 machineId: "0",
242 }
243 unitResponse := mockResponse{
244 stdout: "bumblebee",
245 machineId: "1",
246 unitId: "unit/0",
247 }
248 mock.setResponse("0", machineResponse)
249 mock.setResponse("unit/0", unitResponse)
250
251 unformatted := ConvertRunResults([]params.RunResult{
252 makeRunResult(machineResponse),
253 makeRunResult(unitResponse),
254 })
255
256 jsonFormatted, err := cmd.FormatJson(unformatted)
257 c.Assert(err, gc.IsNil)
258
259 context, err := testing.RunCommand(c, &RunCommand{}, []string{
260 "--format=json", "--machine=0", "--unit=unit/0", "hostname",
261 })
262 c.Assert(err, gc.IsNil)
263
264 c.Check(testing.Stdout(context), gc.Equals, string(jsonFormatted)+"\n")
265}
266
267func (s *RunSuite) TestAllMachines(c *gc.C) {
268 mock := s.setupMockAPI()
269 mock.setMachinesAlive("0", "1")
270 response0 := mockResponse{
271 stdout: "megatron\n",
272 machineId: "0",
273 }
274 response1 := mockResponse{
275 error: "command timed out",
276 machineId: "1",
277 }
278 mock.setResponse("0", response0)
279
280 unformatted := ConvertRunResults([]params.RunResult{
281 makeRunResult(response0),
282 makeRunResult(response1),
283 })
284
285 jsonFormatted, err := cmd.FormatJson(unformatted)
286 c.Assert(err, gc.IsNil)
287
288 context, err := testing.RunCommand(c, &RunCommand{}, []string{
289 "--format=json", "--all", "hostname",
290 })
291 c.Assert(err, gc.IsNil)
292
293 c.Check(testing.Stdout(context), gc.Equals, string(jsonFormatted)+"\n")
294}
295
296func (s *RunSuite) TestSingleResponse(c *gc.C) {
297 mock := s.setupMockAPI()
298 mock.setMachinesAlive("0")
299 mockResponse := mockResponse{
300 stdout: "stdout\n",
301 stderr: "stderr\n",
302 code: 42,
303 machineId: "0",
304 }
305 mock.setResponse("0", mockResponse)
306 unformatted := ConvertRunResults([]params.RunResult{
307 makeRunResult(mockResponse)})
308 yamlFormatted, err := cmd.FormatYaml(unformatted)
309 c.Assert(err, gc.IsNil)
310 jsonFormatted, err := cmd.FormatJson(unformatted)
311 c.Assert(err, gc.IsNil)
312
313 for i, test := range []struct {
314 message string
315 format string
316 stdout string
317 stderr string
318 errorMatch string
319 }{{
320 message: "smart (default)",
321 stdout: "stdout\n",
322 stderr: "stderr\n",
323 errorMatch: "rc: 42",
324 }, {
325 message: "yaml output",
326 format: "yaml",
327 stdout: string(yamlFormatted) + "\n",
328 }, {
329 message: "json output",
330 format: "json",
331 stdout: string(jsonFormatted) + "\n",
332 }} {
333 c.Log(fmt.Sprintf("%v: %s", i, test.message))
334 args := []string{}
335 if test.format != "" {
336 args = append(args, "--format", test.format)
337 }
338 args = append(args, "--all", "ignored")
339 context, err := testing.RunCommand(c, &RunCommand{}, args)
340 if test.errorMatch != "" {
341 c.Check(err, gc.ErrorMatches, test.errorMatch)
342 } else {
343 c.Check(err, gc.IsNil)
344 }
345 c.Check(testing.Stdout(context), gc.Equals, test.stdout)
346 c.Check(testing.Stderr(context), gc.Equals, test.stderr)
347 }
348}
349
350func (s *RunSuite) setupMockAPI() *mockRunAPI {
351 mock := &mockRunAPI{}
352 s.PatchValue(&getAPIClient, func(name string) (RunClient, error) {
353 return mock, nil
354 })
355 return mock
356}
357
358type mockRunAPI struct {
359 stdout string
360 stderr string
361 code int
362 // machines, services, units
363 machines map[string]bool
364 responses map[string]params.RunResult
365}
366
367type mockResponse struct {
368 stdout string
369 stderr string
370 code int
371 error string
372 machineId string
373 unitId string
374}
375
376var _ RunClient = (*mockRunAPI)(nil)
377
378func (m *mockRunAPI) setMachinesAlive(ids ...string) {
379 if m.machines == nil {
380 m.machines = make(map[string]bool)
381 }
382 for _, id := range ids {
383 m.machines[id] = true
384 }
385}
386
387func makeRunResult(mock mockResponse) params.RunResult {
388 return params.RunResult{
389 RemoteResponse: cmd.RemoteResponse{
390 Stdout: []byte(mock.stdout),
391 Stderr: []byte(mock.stderr),
392 Code: mock.code,
393 },
394 MachineId: mock.machineId,
395 UnitId: mock.unitId,
396 Error: mock.error,
397 }
398}
399
400func (m *mockRunAPI) setResponse(id string, mock mockResponse) {
401 if m.responses == nil {
402 m.responses = make(map[string]params.RunResult)
403 }
404 m.responses[id] = makeRunResult(mock)
405}
406
407func (*mockRunAPI) Close() error {
408 return nil
409}
410
411func (m *mockRunAPI) RunOnAllMachines(commands string, timeout time.Duration) ([]params.RunResult, error) {
412 var result []params.RunResult
413 for machine := range m.machines {
414 response, found := m.responses[machine]
415 if !found {
416 // Consider this a timeout
417 response = params.RunResult{MachineId: machine, Error: "command timed out"}
418 }
419 result = append(result, response)
420 }
421
422 return result, nil
423}
424
425func (m *mockRunAPI) Run(runParams params.RunParams) ([]params.RunResult, error) {
426 var result []params.RunResult
427 // Just add in ids that match in order.
428 for _, id := range runParams.Machines {
429 response, found := m.responses[id]
430 if found {
431 result = append(result, response)
432 }
433 }
434 // mock ignores services
435 for _, id := range runParams.Units {
436 response, found := m.responses[id]
437 if found {
438 result = append(result, response)
439 }
440 }
441
442 return result, nil
443}
0444
=== modified file 'testing/environ.go'
--- testing/environ.go 2013-12-10 17:17:31 +0000
+++ testing/environ.go 2014-01-12 20:39:24 +0000
@@ -12,6 +12,7 @@
1212
13 "launchpad.net/juju-core/environs/config"13 "launchpad.net/juju-core/environs/config"
14 "launchpad.net/juju-core/juju/osenv"14 "launchpad.net/juju-core/juju/osenv"
15 "launchpad.net/juju-core/testing/testbase"
15)16)
1617
17// FakeConfig() returns an environment configuration for a18// FakeConfig() returns an environment configuration for a
@@ -233,3 +234,14 @@
233func MakeMultipleEnvHome(c *gc.C) *FakeHome {234func MakeMultipleEnvHome(c *gc.C) *FakeHome {
234 return MakeFakeHome(c, MultipleEnvConfig, SampleCertName, SampleCertName+"-2")235 return MakeFakeHome(c, MultipleEnvConfig, SampleCertName, SampleCertName+"-2")
235}236}
237
238type FakeHomeSuite struct {
239 testbase.LoggingSuite
240 Home *FakeHome
241}
242
243func (s *FakeHomeSuite) SetUpTest(c *gc.C) {
244 s.LoggingSuite.SetUpTest(c)
245 s.Home = MakeSampleHome(c)
246 s.AddCleanup(func(*gc.C) { s.Home.Restore() })
247}

Subscribers

People subscribed via source and target branches

to status/vote changes: