Merge lp:~thumper/juju-core/jujuc-run 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: 2148
Proposed branch: lp:~thumper/juju-core/jujuc-run
Merge into: lp:~go-bot/juju-core/trunk
Prerequisite: lp:~thumper/juju-core/uniter-run-commands
Diff against target: 345 lines (+278/-0)
6 files modified
cmd/cmd.go (+23/-0)
cmd/jujud/main.go (+2/-0)
cmd/jujud/run.go (+108/-0)
cmd/jujud/run_test.go (+135/-0)
environs/cloudinit/cloudinit.go (+8/-0)
environs/cloudinit/cloudinit_test.go (+2/-0)
To merge this branch: bzr merge lp:~thumper/juju-core/jujuc-run
Reviewer Review Type Date Requested Status
Juju Engineering Pending
Review via email: mp+198865@code.launchpad.net

Commit message

Implement the juju-run symlink in jujud

Adds a command in the jujud executable so if the executable is called as
juju-run, it will attempt to connect to the rpc server being run by the unit
specified as the first arg.

In order to get the return code passed out of the command infrastructure I
added a new error type in the cmd package called "rcPassthroughError", which
has a return code in it.

We use the jujud from the machine agent as the target of the juju-run symlink
as we can add the symlink using cloud-init and we know there is only one
machine agent.

https://codereview.appspot.com/41660044/

Description of the change

Implement the juju-run symlink in jujud

Adds a command in the jujud executable so if the executable is called as
juju-run, it will attempt to connect to the rpc server being run by the unit
specified as the first arg.

In order to get the return code passed out of the command infrastructure I
added a new error type in the cmd package called "rcPassthroughError", which
has a return code in it.

We use the jujud from the machine agent as the target of the juju-run symlink
as we can add the symlink using cloud-init and we know there is only one
machine agent.

https://codereview.appspot.com/41660044/

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

Reviewers: mp+198865_code.launchpad.net,

Message:
Please take a look.

Description:
Implement the juju-run symlink in jujud

Adds a command in the jujud executable so if the executable is called as
juju-run, it will attempt to connect to the rpc server being run by the
unit
specified as the first arg.

In order to get the return code passed out of the command infrastructure
I
added a new error type in the cmd package called "rcPassthroughError",
which
has a return code in it.

We use the jujud from the machine agent as the target of the juju-run
symlink
as we can add the symlink using cloud-init and we know there is only one
machine agent.

https://code.launchpad.net/~thumper/juju-core/jujuc-run/+merge/198865

Requires:
https://code.launchpad.net/~thumper/juju-core/uniter-run-commands/+merge/198830

(do not edit description out of merge proposal)

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

Affected files (+269, -0 lines):
   A [revision details]
   M cmd/cmd.go
   M cmd/jujud/main.go
   A cmd/jujud/run.go
   A cmd/jujud/run_test.go
   M environs/cloudinit/cloudinit.go
   M environs/cloudinit/cloudinit_test.go

Revision history for this message
Ian Booth (wallyworld) wrote :

LGTM with some tweaks and a suggestion

https://codereview.appspot.com/41660044/diff/1/cmd/cmd.go
File cmd/cmd.go (right):

https://codereview.appspot.com/41660044/diff/1/cmd/cmd.go#newcode20
cmd/cmd.go:20: type rcPassthroughError struct {
Why not use a type alias?

https://codereview.appspot.com/41660044/diff/1/cmd/cmd.go#newcode33
cmd/cmd.go:33: func NewRcPassthroughError(code int) error {
Need doc here and/or the struct definition to say what it is for

https://codereview.appspot.com/41660044/diff/1/cmd/jujud/main.go
File cmd/jujud/main.go (right):

https://codereview.appspot.com/41660044/diff/1/cmd/jujud/main.go#newcode118
cmd/jujud/main.go:118: code = cmd.Main(&RunCommand{},
cmd.DefaultContext(), args[1:])
Should we follow the pattern used for jujuc and jujud and have a
jujurunMain method?

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

https://codereview.appspot.com/41660044/diff/1/cmd/jujud/run.go#newcode67
cmd/jujud/run.go:67: c.unit = names.UnitTag(c.unit)
can we have a comment here explaining what is being done ie unit can be
either a name or tag?

https://codereview.appspot.com/41660044/diff/1/environs/cloudinit/cloudinit.go
File environs/cloudinit/cloudinit.go (right):

https://codereview.appspot.com/41660044/diff/1/environs/cloudinit/cloudinit.go#newcode242
environs/cloudinit/cloudinit.go:242: fmt.Sprintf("ln -s
%s/tools/%s/jujud /usr/local/bin/juju-run", cfg.DataDir, machineTag),
please use cfg.jujuTools()

https://codereview.appspot.com/41660044/

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

Please take a look.

https://codereview.appspot.com/41660044/diff/1/cmd/cmd.go
File cmd/cmd.go (right):

https://codereview.appspot.com/41660044/diff/1/cmd/cmd.go#newcode20
cmd/cmd.go:20: type rcPassthroughError struct {
On 2013/12/13 04:17:33, wallyworld wrote:
> Why not use a type alias?

I think because I want a real struct here. A type alias to int is just
weird.

https://codereview.appspot.com/41660044/diff/1/cmd/cmd.go#newcode33
cmd/cmd.go:33: func NewRcPassthroughError(code int) error {
On 2013/12/13 04:17:33, wallyworld wrote:
> Need doc here and/or the struct definition to say what it is for

Done.

https://codereview.appspot.com/41660044/diff/1/cmd/jujud/main.go
File cmd/jujud/main.go (right):

https://codereview.appspot.com/41660044/diff/1/cmd/jujud/main.go#newcode118
cmd/jujud/main.go:118: code = cmd.Main(&RunCommand{},
cmd.DefaultContext(), args[1:])
On 2013/12/13 04:17:33, wallyworld wrote:
> Should we follow the pattern used for jujuc and jujud and have a
jujurunMain
> method?

We could, but it would just be one line... so I chose not to.

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

https://codereview.appspot.com/41660044/diff/1/cmd/jujud/run.go#newcode67
cmd/jujud/run.go:67: c.unit = names.UnitTag(c.unit)
On 2013/12/13 04:17:33, wallyworld wrote:
> can we have a comment here explaining what is being done ie unit can
be either a
> name or tag?

Done.

https://codereview.appspot.com/41660044/diff/1/environs/cloudinit/cloudinit.go
File environs/cloudinit/cloudinit.go (right):

https://codereview.appspot.com/41660044/diff/1/environs/cloudinit/cloudinit.go#newcode242
environs/cloudinit/cloudinit.go:242: fmt.Sprintf("ln -s
%s/tools/%s/jujud /usr/local/bin/juju-run", cfg.DataDir, machineTag),
On 2013/12/13 04:17:33, wallyworld wrote:
> please use cfg.jujuTools()

No, but comment added to explain why.

https://codereview.appspot.com/41660044/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'cmd/cmd.go'
2--- cmd/cmd.go 2013-12-11 09:21:56 +0000
3+++ cmd/cmd.go 2013-12-13 04:41:06 +0000
4@@ -17,6 +17,26 @@
5 "launchpad.net/gnuflag"
6 )
7
8+type rcPassthroughError struct {
9+ code int
10+}
11+
12+func (e *rcPassthroughError) Error() string {
13+ return fmt.Sprintf("rc: %v", e.code)
14+}
15+
16+func IsRcPassthroughError(err error) bool {
17+ _, ok := err.(*rcPassthroughError)
18+ return ok
19+}
20+
21+// NewRcPassthroughError creates an error that will have the code used at the
22+// return code from the cmd.Main function rather than the default of 1 if
23+// there is an error.
24+func NewRcPassthroughError(code int) error {
25+ return &rcPassthroughError{code}
26+}
27+
28 func init() {
29 // Don't replace the default transport as other init blocks
30 // register protocols.
31@@ -173,6 +193,9 @@
32 return rc
33 }
34 if err := c.Run(ctx); err != nil {
35+ if IsRcPassthroughError(err) {
36+ return err.(*rcPassthroughError).code
37+ }
38 if err != ErrSilent {
39 fmt.Fprintf(ctx.Stderr, "error: %v\n", err)
40 }
41
42=== modified file 'cmd/jujud/main.go'
43--- cmd/jujud/main.go 2013-12-12 04:31:01 +0000
44+++ cmd/jujud/main.go 2013-12-13 04:41:06 +0000
45@@ -114,6 +114,8 @@
46 fmt.Fprint(os.Stderr, jujudDoc)
47 code = 2
48 err = fmt.Errorf("jujuc should not be called directly")
49+ } else if commandName == "juju-run" {
50+ code = cmd.Main(&RunCommand{}, cmd.DefaultContext(), args[1:])
51 } else {
52 code, err = jujuCMain(commandName, args)
53 }
54
55=== added file 'cmd/jujud/run.go'
56--- cmd/jujud/run.go 1970-01-01 00:00:00 +0000
57+++ cmd/jujud/run.go 2013-12-13 04:41:06 +0000
58@@ -0,0 +1,108 @@
59+// Copyright 2013 Canonical Ltd.
60+// Licensed under the AGPLv3, see LICENCE file for details.
61+
62+package main
63+
64+import (
65+ "fmt"
66+ "net/rpc"
67+ "os"
68+ "path/filepath"
69+
70+ "launchpad.net/gnuflag"
71+
72+ "launchpad.net/juju-core/cmd"
73+ "launchpad.net/juju-core/names"
74+ "launchpad.net/juju-core/worker/uniter"
75+)
76+
77+var AgentDir = "/var/lib/juju/agents"
78+
79+type RunCommand struct {
80+ cmd.CommandBase
81+ unit string
82+ commands string
83+ showHelp bool
84+}
85+
86+const runCommandDoc = `
87+Run the specified commands in the hook context for the unit.
88+
89+unit-name can be either the unit tag:
90+ i.e. unit-ubuntu-0
91+or the unit id:
92+ i.e. ubuntu/0
93+
94+The commands are executed with '/bin/bash -s', and the output returned.
95+`
96+
97+// Info returns usage information for the command.
98+func (c *RunCommand) Info() *cmd.Info {
99+ return &cmd.Info{
100+ Name: "juju-run",
101+ Args: "<unit-name> <commands>",
102+ Purpose: "run commands in a unit's hook context",
103+ Doc: runCommandDoc,
104+ }
105+}
106+
107+func (c *RunCommand) SetFlags(f *gnuflag.FlagSet) {
108+ f.BoolVar(&c.showHelp, "h", false, "show help on juju-run")
109+ f.BoolVar(&c.showHelp, "help", false, "")
110+}
111+
112+func (c *RunCommand) Init(args []string) error {
113+ // make sure we aren't in an existing hook context
114+ if contextId, err := getenv("JUJU_CONTEXT_ID"); err == nil && contextId != "" {
115+ return fmt.Errorf("juju-run cannot be called from within a hook, have context %q", contextId)
116+ }
117+ if len(args) < 1 {
118+ return fmt.Errorf("missing unit-name")
119+ }
120+ if len(args) < 2 {
121+ return fmt.Errorf("missing commands")
122+ }
123+ c.unit, args = args[0], args[1:]
124+ // If the command line param is a unit id (like service/2) we need to
125+ // change it to the unit tag as that is the format of the agent directory
126+ // on disk (unit-service-2).
127+ if names.IsUnit(c.unit) {
128+ c.unit = names.UnitTag(c.unit)
129+ }
130+ c.commands, args = args[0], args[1:]
131+ return cmd.CheckEmpty(args)
132+}
133+
134+func (c *RunCommand) Run(ctx *cmd.Context) error {
135+ if c.showHelp {
136+ return gnuflag.ErrHelp
137+ }
138+
139+ unitDir := filepath.Join(AgentDir, c.unit)
140+ logger.Debugf("looking for unit dir %s", unitDir)
141+ // make sure the unit exists
142+ _, err := os.Stat(unitDir)
143+ if os.IsNotExist(err) {
144+ return fmt.Errorf("unit %q not found on this machine", c.unit)
145+ } else if err != nil {
146+ return err
147+ }
148+
149+ socketPath := filepath.Join(unitDir, uniter.RunListenerFile)
150+ // make sure the socket exists
151+ client, err := rpc.Dial(uniter.RunListenerNetType, socketPath)
152+ if err != nil {
153+ return err
154+ }
155+ defer client.Close()
156+
157+ var result cmd.RemoteResponse
158+ err = client.Call(uniter.JujuRunEndpoint, c.commands, &result)
159+ if err != nil {
160+ return err
161+ }
162+
163+ ctx.Stdout.Write(result.Stdout)
164+ ctx.Stderr.Write(result.Stderr)
165+ return cmd.NewRcPassthroughError(result.Code)
166+}
167
168=== added file 'cmd/jujud/run_test.go'
169--- cmd/jujud/run_test.go 1970-01-01 00:00:00 +0000
170+++ cmd/jujud/run_test.go 2013-12-13 04:41:06 +0000
171@@ -0,0 +1,135 @@
172+// Copyright 2012, 2013 Canonical Ltd.
173+// Licensed under the AGPLv3, see LICENCE file for details.
174+
175+package main
176+
177+import (
178+ "fmt"
179+ "os"
180+ "path/filepath"
181+
182+ gc "launchpad.net/gocheck"
183+ "launchpad.net/loggo"
184+
185+ "launchpad.net/juju-core/cmd"
186+ "launchpad.net/juju-core/testing"
187+ jc "launchpad.net/juju-core/testing/checkers"
188+ "launchpad.net/juju-core/testing/testbase"
189+ "launchpad.net/juju-core/worker/uniter"
190+)
191+
192+type RunTestSuite struct {
193+ testbase.LoggingSuite
194+}
195+
196+var _ = gc.Suite(&RunTestSuite{})
197+
198+func (*RunTestSuite) TestWrongArgs(c *gc.C) {
199+ for i, test := range []struct {
200+ title string
201+ args []string
202+ errMatch string
203+ unit string
204+ commands string
205+ }{{
206+ title: "no args",
207+ errMatch: "missing unit-name",
208+ }, {
209+ title: "one arg",
210+ args: []string{"foo"},
211+ errMatch: "missing commands",
212+ }, {
213+ title: "more than two arg",
214+ args: []string{"foo", "bar", "baz"},
215+ errMatch: `unrecognized args: \["baz"\]`,
216+ }, {
217+ title: "unit and command assignment",
218+ args: []string{"unit-name", "command"},
219+ unit: "unit-name",
220+ commands: "command",
221+ }, {
222+ title: "unit id converted to tag",
223+ args: []string{"foo/1", "command"},
224+ unit: "unit-foo-1",
225+ commands: "command",
226+ },
227+ } {
228+ c.Log(fmt.Sprintf("\n%d: %s", i, test.title))
229+ runCommand := &RunCommand{}
230+ err := runCommand.Init(test.args)
231+ if test.errMatch == "" {
232+ c.Assert(err, gc.IsNil)
233+ c.Assert(runCommand.unit, gc.Equals, test.unit)
234+ c.Assert(runCommand.commands, gc.Equals, test.commands)
235+ } else {
236+ c.Assert(err, gc.ErrorMatches, test.errMatch)
237+ }
238+ }
239+}
240+
241+func (s *RunTestSuite) TestInsideContext(c *gc.C) {
242+ s.PatchEnvironment("JUJU_CONTEXT_ID", "fake-id")
243+ runCommand := &RunCommand{}
244+ err := runCommand.Init([]string{"foo", "bar"})
245+ c.Assert(err, gc.ErrorMatches, "juju-run cannot be called from within a hook.*")
246+}
247+
248+func (s *RunTestSuite) TestMissingAgent(c *gc.C) {
249+ agentDir := c.MkDir()
250+ s.PatchValue(&AgentDir, agentDir)
251+
252+ _, err := testing.RunCommand(c, &RunCommand{}, []string{"foo", "bar"})
253+ c.Assert(err, gc.ErrorMatches, `unit "foo" not found on this machine`)
254+}
255+
256+func (s *RunTestSuite) TestMissingSocket(c *gc.C) {
257+ s.PatchValue(&AgentDir, c.MkDir())
258+ testAgentDir := filepath.Join(AgentDir, "foo")
259+ err := os.Mkdir(testAgentDir, 0755)
260+ c.Assert(err, gc.IsNil)
261+
262+ _, err = testing.RunCommand(c, &RunCommand{}, []string{"foo", "bar"})
263+ c.Assert(err, gc.ErrorMatches, `dial unix .*/run.socket: no such file or directory`)
264+}
265+
266+func (s *RunTestSuite) TestRunning(c *gc.C) {
267+ loggo.GetLogger("worker.uniter").SetLogLevel(loggo.TRACE)
268+ s.runListenerForAgent(c, "foo")
269+
270+ ctx, err := testing.RunCommand(c, &RunCommand{}, []string{"foo", "bar"})
271+ c.Check(cmd.IsRcPassthroughError(err), jc.IsTrue)
272+ c.Assert(err, gc.ErrorMatches, "rc: 42")
273+ c.Assert(testing.Stdout(ctx), gc.Equals, "bar stdout")
274+ c.Assert(testing.Stderr(ctx), gc.Equals, "bar stderr")
275+}
276+
277+func (s *RunTestSuite) runListenerForAgent(c *gc.C, agent string) {
278+ s.PatchValue(&AgentDir, c.MkDir())
279+
280+ testAgentDir := filepath.Join(AgentDir, agent)
281+ err := os.Mkdir(testAgentDir, 0755)
282+ c.Assert(err, gc.IsNil)
283+
284+ socketPath := filepath.Join(testAgentDir, uniter.RunListenerFile)
285+ listener, err := uniter.NewRunListener(&mockRunner{c}, "unix", socketPath)
286+ c.Assert(err, gc.IsNil)
287+ c.Assert(listener, gc.NotNil)
288+ s.AddCleanup(func(*gc.C) {
289+ listener.Close()
290+ })
291+}
292+
293+type mockRunner struct {
294+ c *gc.C
295+}
296+
297+var _ uniter.CommandRunner = (*mockRunner)(nil)
298+
299+func (r *mockRunner) RunCommands(commands string) (results *cmd.RemoteResponse, err error) {
300+ r.c.Log("mock runner: " + commands)
301+ return &cmd.RemoteResponse{
302+ Code: 42,
303+ Stdout: []byte(commands + " stdout"),
304+ Stderr: []byte(commands + " stderr"),
305+ }, nil
306+}
307
308=== modified file 'environs/cloudinit/cloudinit.go'
309--- environs/cloudinit/cloudinit.go 2013-12-04 04:36:32 +0000
310+++ environs/cloudinit/cloudinit.go 2013-12-13 04:41:06 +0000
311@@ -238,6 +238,14 @@
312 if err != nil {
313 return err
314 }
315+ c.AddScripts(
316+ // We specifically make the symlink here to the machine's current
317+ // tools, not to the specific version tool directory (from
318+ // cfg.jujuTools()), as we want the jujud that is linked to in
319+ // /usr/local/bin to also upgrade when the machine agent upgrades its
320+ // tools and changes the tools directory that it is using.
321+ fmt.Sprintf("ln -s %s/tools/%s/jujud /usr/local/bin/juju-run", cfg.DataDir, machineTag),
322+ )
323
324 // Add the cloud archive cloud-tools pocket to apt sources
325 // for series that need it. This gives us up-to-date LXC,
326
327=== modified file 'environs/cloudinit/cloudinit_test.go'
328--- environs/cloudinit/cloudinit_test.go 2013-12-03 06:04:43 +0000
329+++ environs/cloudinit/cloudinit_test.go 2013-12-13 04:41:06 +0000
330@@ -109,6 +109,7 @@
331 printf '%s\\n' '.*' > '/var/lib/juju/agents/machine-0/format'
332 install -m 600 /dev/null '/var/lib/juju/agents/machine-0/agent\.conf'
333 printf '%s\\n' '.*' > '/var/lib/juju/agents/machine-0/agent\.conf'
334+ln -s /var/lib/juju/tools/machine-0/jujud /usr/local/bin/juju-run
335 install -m 600 /dev/null '/var/lib/juju/server\.pem'
336 printf '%s\\n' 'SERVER CERT\\n[^']*SERVER KEY\\n[^']*' > '/var/lib/juju/server\.pem'
337 mkdir -p /var/lib/juju/db/journal
338@@ -219,6 +220,7 @@
339 printf '%s\\n' '.*' > '/var/lib/juju/agents/machine-99/format'
340 install -m 600 /dev/null '/var/lib/juju/agents/machine-99/agent\.conf'
341 printf '%s\\n' '.*' > '/var/lib/juju/agents/machine-99/agent\.conf'
342+ln -s /var/lib/juju/tools/machine-99/jujud /usr/local/bin/juju-run
343 ln -s 1\.2\.3-linux-amd64 '/var/lib/juju/tools/machine-99'
344 echo 'Starting Juju machine agent \(jujud-machine-99\)'.*
345 cat >> /etc/init/jujud-machine-99\.conf << 'EOF'\\ndescription "juju machine-99 agent"\\nauthor "Juju Team <juju@lists\.ubuntu\.com>"\\nstart on runlevel \[2345\]\\nstop on runlevel \[!2345\]\\nrespawn\\nnormal exit 0\\n\\nlimit nofile 20000 20000\\n\\nexec /var/lib/juju/tools/machine-99/jujud machine --data-dir '/var/lib/juju' --machine-id 99 --debug >> /var/log/juju/machine-99\.log 2>&1\\nEOF\\n

Subscribers

People subscribed via source and target branches

to status/vote changes: