Merge lp:~thumper/juju-core/debug-log-api 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: 2613
Proposed branch: lp:~thumper/juju-core/debug-log-api
Merge into: lp:~go-bot/juju-core/trunk
Diff against target: 605 lines (+322/-131)
8 files modified
cmd/args.go (+23/-0)
cmd/args_test.go (+23/-0)
cmd/juju/debuglog.go (+94/-61)
cmd/juju/debuglog_test.go (+149/-56)
cmd/juju/main.go (+1/-1)
errors/errors.go (+22/-0)
errors/errors_test.go (+6/-0)
state/api/client.go (+4/-13)
To merge this branch: bzr merge lp:~thumper/juju-core/debug-log-api
Reviewer Review Type Date Requested Status
Juju Engineering Pending
Review via email: mp+215323@code.launchpad.net

Commit message

Hook up the debug-log command line to the API.

Have 'juju debug-log' try to use the API first. If that
fails, it falls back to tailing the log over ssh.

https://codereview.appspot.com/85570044/

Description of the change

Hook up the debug-log command line to the API.

Have 'juju debug-log' try to use the API first. If that
fails, it falls back to tailing the log over ssh.

https://codereview.appspot.com/85570044/

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

Reviewers: mp+215323_code.launchpad.net,

Message:
Please take a look.

Description:
Hook up the debug-log command line to the API.

Have 'juju debug-log' try to use the API first. If that
fails, it falls back to tailing the log over ssh.

https://code.launchpad.net/~thumper/juju-core/debug-log-api/+merge/215323

(do not edit description out of merge proposal)

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

Affected files (+325, -131 lines):
   A [revision details]
   M cmd/args.go
   M cmd/args_test.go
   M cmd/juju/debuglog.go
   M cmd/juju/debuglog_test.go
   M cmd/juju/main.go
   M errors/errors.go
   M errors/errors_test.go
   M state/api/client.go

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

LGTM, not sure about the need for a different "api not supported"
mechanism though.

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

https://codereview.appspot.com/85570044/diff/1/cmd/args.go#newcode46
cmd/args.go:46: // f.Var(cmd.NewAppendStringsValue(defaultValue,
&someMember), "name", "help")
The above comment is wrong

https://codereview.appspot.com/85570044/diff/1/cmd/args.go#newcode59
cmd/args.go:59: return fmt.Sprint(*v)
Could we do:

return strings.Join(*v, ",")

to be consistent with StringsValue

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

https://codereview.appspot.com/85570044/diff/1/cmd/args_test.go#newcode161
cmd/args_test.go:161: message: "no value set by args",
I don't understand the message. Shouldn't it be "value set by args"?

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

https://codereview.appspot.com/85570044/diff/1/cmd/juju/debuglog.go#newcode74
cmd/juju/debuglog.go:74: c.level, loggo.TRACE, loggo.DEBUG, loggo.INFO,
loggo.WARNING, loggo.ERROR)
Would be nice if there were a loggo helper ValidLevels()

https://codereview.appspot.com/85570044/diff/1/state/api/client.go
File state/api/client.go (right):

https://codereview.appspot.com/85570044/diff/1/state/api/client.go#newcode773
state/api/client.go:773: return nil,
errors.NewNotSupportedError("WatchDebugLog")
Till now, CodeNotImplemented has been used to flag an incompatible API
call. I'm not sure of the value of introducing something new here,
instead of just going with what's already been done.

https://codereview.appspot.com/85570044/

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

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

https://codereview.appspot.com/85570044/diff/1/cmd/args.go#newcode46
cmd/args.go:46: // f.Var(cmd.NewAppendStringsValue(defaultValue,
&someMember), "name", "help")
On 2014/04/11 00:54:43, wallyworld wrote:
> The above comment is wrong

Done.

https://codereview.appspot.com/85570044/diff/1/cmd/args.go#newcode59
cmd/args.go:59: return fmt.Sprint(*v)
On 2014/04/11 00:54:43, wallyworld wrote:
> Could we do:

> return strings.Join(*v, ",")

> to be consistent with StringsValue

Done.

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

https://codereview.appspot.com/85570044/diff/1/cmd/args_test.go#newcode161
cmd/args_test.go:161: message: "no value set by args",
On 2014/04/11 00:54:43, wallyworld wrote:
> I don't understand the message. Shouldn't it be "value set by args"?

yes

https://codereview.appspot.com/85570044/diff/1/state/api/client.go
File state/api/client.go (right):

https://codereview.appspot.com/85570044/diff/1/state/api/client.go#newcode773
state/api/client.go:773: return nil,
errors.NewNotSupportedError("WatchDebugLog")
On 2014/04/11 00:54:43, wallyworld wrote:
> Till now, CodeNotImplemented has been used to flag an incompatible API
call. I'm
> not sure of the value of introducing something new here, instead of
just going
> with what's already been done.

Talked with Roger about this last night and we agreed to change it to
this.

https://codereview.appspot.com/85570044/

Revision history for this message
Go Bot (go-bot) wrote :
Download full text (154.2 KiB)

The attempt to merge lp:~thumper/juju-core/debug-log-api into lp:juju-core failed. Below is the output from the failed tests.

ok launchpad.net/juju-core 0.014s
ok launchpad.net/juju-core/agent 1.181s
ok launchpad.net/juju-core/agent/mongo 1.129s
ok launchpad.net/juju-core/agent/tools 0.186s
ok launchpad.net/juju-core/bzr 5.118s
ok launchpad.net/juju-core/cert 2.939s
ok launchpad.net/juju-core/charm 0.411s
? launchpad.net/juju-core/charm/hooks [no test files]
? launchpad.net/juju-core/charm/testing [no test files]
ok launchpad.net/juju-core/cloudinit 0.027s
ok launchpad.net/juju-core/cloudinit/sshinit 0.745s
ok launchpad.net/juju-core/cmd 0.171s
ok launchpad.net/juju-core/cmd/charm-admin 0.765s
? launchpad.net/juju-core/cmd/charmd [no test files]
? launchpad.net/juju-core/cmd/charmload [no test files]
ok launchpad.net/juju-core/cmd/envcmd 0.182s
ok launchpad.net/juju-core/cmd/juju 221.263s
ok launchpad.net/juju-core/cmd/jujud 79.785s
ok launchpad.net/juju-core/cmd/plugins/juju-metadata 9.623s
? launchpad.net/juju-core/cmd/plugins/juju-restore [no test files]
ok launchpad.net/juju-core/cmd/plugins/local 0.209s
? launchpad.net/juju-core/cmd/plugins/local/juju-local [no test files]
ok launchpad.net/juju-core/constraints 0.023s
ok launchpad.net/juju-core/container 0.038s
ok launchpad.net/juju-core/container/factory 0.041s
ok launchpad.net/juju-core/container/kvm 0.219s
ok launchpad.net/juju-core/container/kvm/mock 0.032s
? launchpad.net/juju-core/container/kvm/testing [no test files]
ok launchpad.net/juju-core/container/lxc 3.328s
? launchpad.net/juju-core/container/lxc/mock [no test files]
? launchpad.net/juju-core/container/lxc/testing [no test files]
? launchpad.net/juju-core/container/testing [no test files]
ok launchpad.net/juju-core/downloader 5.247s
ok launchpad.net/juju-core/environs 2.270s
ok launchpad.net/juju-core/environs/bootstrap 11.102s
ok launchpad.net/juju-core/environs/cloudinit 0.448s
ok launchpad.net/juju-core/environs/config 1.969s
ok launchpad.net/juju-core/environs/configstore 0.033s
ok launchpad.net/juju-core/environs/filestorage 0.027s
ok launchpad.net/juju-core/environs/httpstorage 0.768s
ok launchpad.net/juju-core/environs/imagemetadata 0.523s
? launchpad.net/juju-core/environs/imagemetadata/testing [no test files]
ok launchpad.net/juju-core/environs/instances 0.041s
ok launchpad.net/juju-core/environs/jujutest 0.242s
ok launchpad.net/juju-core/environs/manual 10.860s
ok launchpad.net/juju-core/environs/simplestreams 0.282s
? launchpad.net/juju-core/environs/simplestreams/testing [no test files]
ok launchpad.net/juju-core/environs/sshstorage 0.918s
ok launchpad.net/juju-core/environs/storage 0.916s
ok launchpad.net/juju-core/environs/sync 48.850s
ok launchpad.net/juju-core/environs/testing 0.121s
ok launchpad.net/juju-core/environs/tools 4.561s
? launchpad.net/juju-core/environs/tools/testing [no test files]
ok launchpad.net/juju-core/errors 0.011s
ok launchpad.net/juju-core/instance 0.018s
? launchpad.net/juju-core/instance/testing [no test files]
ok launchpad.net/juju-core/juju 20.543s
ok l...

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'cmd/args.go'
2--- cmd/args.go 2014-01-05 22:53:25 +0000
3+++ cmd/args.go 2014-04-11 01:07:04 +0000
4@@ -34,3 +34,26 @@
5 func (v *StringsValue) String() string {
6 return strings.Join(*v, ",")
7 }
8+
9+// AppendStringsValue implements gnuflag.Value for a value that can be set
10+// multiple times, and it appends each value to the slice.
11+type AppendStringsValue []string
12+
13+var _ gnuflag.Value = (*AppendStringsValue)(nil)
14+
15+// NewAppendStringsValue is used to create the type passed into the gnuflag.FlagSet Var function.
16+// f.Var(cmd.NewAppendStringsValue(&someMember), "name", "help")
17+func NewAppendStringsValue(target *[]string) *AppendStringsValue {
18+ return (*AppendStringsValue)(target)
19+}
20+
21+// Implements gnuflag.Value Set.
22+func (v *AppendStringsValue) Set(s string) error {
23+ *v = append(*v, s)
24+ return nil
25+}
26+
27+// Implements gnuflag.Value String.
28+func (v *AppendStringsValue) String() string {
29+ return strings.Join(*v, ",")
30+}
31
32=== modified file 'cmd/args_test.go'
33--- cmd/args_test.go 2014-01-05 22:53:25 +0000
34+++ cmd/args_test.go 2014-04-11 01:07:04 +0000
35@@ -149,3 +149,26 @@
36 c.Assert(value.String(), gc.Equals, test.expected)
37 }
38 }
39+
40+func (*ArgsSuite) TestAppendStringsUsage(c *gc.C) {
41+ for i, test := range []struct {
42+ message string
43+ args []string
44+ expectedValue []string
45+ }{{
46+ message: "no args",
47+ }, {
48+ message: "value set by args",
49+ args: []string{"--value", "foo", "--value=bar"},
50+ expectedValue: []string{"foo", "bar"},
51+ }} {
52+ c.Log(fmt.Sprintf("%v: %s", i, test.message))
53+ f := gnuflag.NewFlagSet("test", gnuflag.ContinueOnError)
54+ f.SetOutput(ioutil.Discard)
55+ var value []string
56+ f.Var(cmd.NewAppendStringsValue(&value), "value", "help")
57+ err := f.Parse(false, test.args)
58+ c.Check(err, gc.IsNil)
59+ c.Check(value, gc.DeepEquals, test.expectedValue)
60+ }
61+}
62
63=== modified file 'cmd/juju/debuglog.go'
64--- cmd/juju/debuglog.go 2013-12-13 02:18:39 +0000
65+++ cmd/juju/debuglog.go 2014-04-11 01:07:04 +0000
66@@ -1,64 +1,38 @@
67-// Copyright 2013 Canonical Ltd.
68+// Copyright 2013, 2014 Canonical Ltd.
69 // Licensed under the AGPLv3, see LICENCE file for details.
70
71 package main
72
73 import (
74 "fmt"
75- "strconv"
76+ "io"
77
78+ "github.com/juju/loggo"
79 "launchpad.net/gnuflag"
80
81 "launchpad.net/juju-core/cmd"
82+ "launchpad.net/juju-core/cmd/envcmd"
83+ "launchpad.net/juju-core/errors"
84+ "launchpad.net/juju-core/juju"
85+ "launchpad.net/juju-core/state/api"
86 )
87
88 type DebugLogCommand struct {
89- cmd.CommandBase
90- // The debug log command simply invokes juju ssh with the required arguments.
91- sshCmd cmd.Command
92- lines linesValue
93+ envcmd.EnvCommandBase
94+
95+ level string
96+ params api.DebugLogParams
97 }
98
99+var DefaultLogLocation = "/var/log/juju/all-machines.log"
100+
101 // defaultLineCount is the default number of lines to
102 // display, from the end of the consolidated log.
103 const defaultLineCount = 10
104
105-// linesValue implements gnuflag.Value, and represents
106-// a -n/--lines flag value compatible with "tail".
107-//
108-// A negative value (-K) corresponds to --lines=K,
109-// i.e. the last K lines; a positive value (+K)
110-// corresponds to --lines=+K, i.e. from line K onwards.
111-type linesValue int
112-
113-func (v *linesValue) String() string {
114- if *v > 0 {
115- return fmt.Sprintf("+%d", *v)
116- }
117- return fmt.Sprint(-*v)
118-}
119-
120-func (v *linesValue) Set(value string) error {
121- if len(value) > 0 {
122- sign := -1
123- if value[0] == '+' {
124- value = value[1:]
125- sign = 1
126- }
127- n, err := strconv.ParseInt(value, 10, 0)
128- if err == nil && n > 0 {
129- *v = linesValue(sign * int(n))
130- return nil
131- }
132- // err is quite verbose, and doesn't convey
133- // any additional useful information.
134- }
135- return fmt.Errorf("invalid number of lines")
136-}
137-
138 const debuglogDoc = `
139-Launch an ssh shell on the state server machine and tail the consolidated log file.
140-The consolidated log file contains log messages from all nodes in the environment.
141+Stream the consolidated debug log file. This file contains the log messages
142+from all nodes in the environment.
143 `
144
145 func (c *DebugLogCommand) Info() *cmd.Info {
146@@ -70,27 +44,86 @@
147 }
148
149 func (c *DebugLogCommand) SetFlags(f *gnuflag.FlagSet) {
150- c.sshCmd.SetFlags(f)
151-
152- c.lines = -defaultLineCount
153- f.Var(&c.lines, "n", "output the last K lines; or use -n +K to output lines starting with the Kth")
154- f.Var(&c.lines, "lines", "")
155-}
156-
157-func (c *DebugLogCommand) AllowInterspersedFlags() bool {
158- return true
159+ c.EnvCommandBase.SetFlags(f)
160+
161+ f.Var(cmd.NewAppendStringsValue(&c.params.IncludeEntity), "i", "only show log messages for these entities")
162+ f.Var(cmd.NewAppendStringsValue(&c.params.IncludeEntity), "include", "only show log messages for these entities")
163+ f.Var(cmd.NewAppendStringsValue(&c.params.ExcludeEntity), "x", "only show log messages for these entities")
164+ f.Var(cmd.NewAppendStringsValue(&c.params.ExcludeEntity), "exclude", "only show log messages for these entities")
165+ f.Var(cmd.NewAppendStringsValue(&c.params.IncludeModule), "include-module", "only show log messages for these logging modules")
166+ f.Var(cmd.NewAppendStringsValue(&c.params.ExcludeModule), "exclude-module", "do not show log messages for these logging modules")
167+
168+ f.StringVar(&c.level, "l", "", "log level to show, one of [TRACE, DEBUG, INFO, WARNING, ERROR]")
169+ f.StringVar(&c.level, "level", "", "")
170+
171+ f.UintVar(&c.params.Backlog, "n", defaultLineCount, "go back this many lines from the end before starting to filter")
172+ f.UintVar(&c.params.Backlog, "lines", defaultLineCount, "")
173+ f.UintVar(&c.params.Limit, "limit", 0, "show at most this many lines")
174+ f.BoolVar(&c.params.Replay, "replay", false, "start filtering from the start")
175 }
176
177 func (c *DebugLogCommand) Init(args []string) error {
178- tailcmd := fmt.Sprintf("tail -n %s -f /var/log/juju/all-machines.log", &c.lines)
179- args = append([]string{"0"}, args...)
180- args = append(args, tailcmd)
181- return c.sshCmd.Init(args)
182-}
183-
184-// Run uses "juju ssh" to log into the state server node
185-// and tails the consolidated log file which captures log
186-// messages from all nodes.
187-func (c *DebugLogCommand) Run(ctx *cmd.Context) error {
188- return c.sshCmd.Run(ctx)
189+ err := c.EnvCommandBase.Init()
190+ if err != nil {
191+ return err
192+ }
193+ if c.level != "" {
194+ level, ok := loggo.ParseLevel(c.level)
195+ if !ok || level < loggo.TRACE || level > loggo.ERROR {
196+ return fmt.Errorf("level value %q is not one of %q, %q, %q, %q, %q",
197+ c.level, loggo.TRACE, loggo.DEBUG, loggo.INFO, loggo.WARNING, loggo.ERROR)
198+ }
199+ c.params.Level = level
200+ }
201+ return cmd.CheckEmpty(args)
202+}
203+
204+type DebugLogAPI interface {
205+ WatchDebugLog(params api.DebugLogParams) (io.ReadCloser, error)
206+ Close() error
207+}
208+
209+var getDebugLogAPI = func(envName string) (DebugLogAPI, error) {
210+ return juju.NewAPIClientFromName(envName)
211+}
212+
213+// Run retrieves the debug log via the API.
214+func (c *DebugLogCommand) Run(ctx *cmd.Context) (err error) {
215+ client, err := getDebugLogAPI(c.EnvName)
216+ if err != nil {
217+ return err
218+ }
219+ defer client.Close()
220+ debugLog, err := client.WatchDebugLog(c.params)
221+ if err != nil {
222+ if errors.IsNotSupportedError(err) {
223+ return c.watchDebugLog1dot18(ctx)
224+ }
225+ return err
226+ }
227+ defer debugLog.Close()
228+ _, err = io.Copy(ctx.Stdout, debugLog)
229+ return err
230+}
231+
232+var runSSHCommand = func(sshCmd *SSHCommand, ctx *cmd.Context) error {
233+ return sshCmd.Run(ctx)
234+}
235+
236+// watchDebugLog1dot18 runs in case of an older API server and uses ssh
237+// but with server-side grep.
238+func (c *DebugLogCommand) watchDebugLog1dot18(ctx *cmd.Context) error {
239+ ctx.Infof("Server does not support new stream log, falling back to tail")
240+ ctx.Verbosef("filters are not supported with tail")
241+ sshCmd := &SSHCommand{}
242+ tailCmd := fmt.Sprintf("tail -n -%d -f %s", c.params.Backlog, DefaultLogLocation)
243+ // If the api doesn't support WatchDebugLog, then it won't be running in
244+ // HA either, so machine 0 is where it is all at.
245+ args := []string{"0", tailCmd}
246+ err := sshCmd.Init(args)
247+ if err != nil {
248+ return err
249+ }
250+ sshCmd.EnvName = c.EnvName
251+ return runSSHCommand(sshCmd, ctx)
252 }
253
254=== modified file 'cmd/juju/debuglog_test.go'
255--- cmd/juju/debuglog_test.go 2014-03-11 09:29:33 +0000
256+++ cmd/juju/debuglog_test.go 2014-04-11 01:07:04 +0000
257@@ -4,72 +4,165 @@
258 package main
259
260 import (
261+ "fmt"
262+ "io"
263+ "io/ioutil"
264+ "strings"
265+
266+ "github.com/juju/loggo"
267+ jc "github.com/juju/testing/checkers"
268 gc "launchpad.net/gocheck"
269
270 "launchpad.net/juju-core/cmd"
271+ "launchpad.net/juju-core/errors"
272+ "launchpad.net/juju-core/state/api"
273 "launchpad.net/juju-core/testing"
274 )
275
276 type DebugLogSuite struct {
277+ testing.FakeHomeSuite
278 }
279
280 var _ = gc.Suite(&DebugLogSuite{})
281
282-func runDebugLog(c *gc.C, args ...string) (*DebugLogCommand, error) {
283- cmd := &DebugLogCommand{
284- sshCmd: &dummySSHCommand{},
285- }
286- _, err := testing.RunCommand(c, cmd, args)
287- return cmd, err
288-}
289-
290-type dummySSHCommand struct {
291- SSHCommand
292- runCalled bool
293-}
294-
295-func (c *dummySSHCommand) Run(ctx *cmd.Context) error {
296- c.runCalled = true
297+func (s *DebugLogSuite) TestArgParsing(c *gc.C) {
298+ for i, test := range []struct {
299+ args []string
300+ expected api.DebugLogParams
301+ errMatch string
302+ }{
303+ {
304+ expected: api.DebugLogParams{
305+ Backlog: 10,
306+ },
307+ }, {
308+ args: []string{"-n0"},
309+ }, {
310+ args: []string{"--lines=50"},
311+ expected: api.DebugLogParams{
312+ Backlog: 50,
313+ },
314+ }, {
315+ args: []string{"-l", "foo"},
316+ errMatch: `level value "foo" is not one of "TRACE", "DEBUG", "INFO", "WARNING", "ERROR"`,
317+ }, {
318+ args: []string{"--level=INFO"},
319+ expected: api.DebugLogParams{
320+ Backlog: 10,
321+ Level: loggo.INFO,
322+ },
323+ }, {
324+ args: []string{"--include", "machine-1", "-i", "machine-2"},
325+ expected: api.DebugLogParams{
326+ IncludeEntity: []string{"machine-1", "machine-2"},
327+ Backlog: 10,
328+ },
329+ }, {
330+ args: []string{"--exclude", "machine-1", "-x", "machine-2"},
331+ expected: api.DebugLogParams{
332+ ExcludeEntity: []string{"machine-1", "machine-2"},
333+ Backlog: 10,
334+ },
335+ }, {
336+ args: []string{"--include-module", "juju.foo", "--include-module", "unit"},
337+ expected: api.DebugLogParams{
338+ IncludeModule: []string{"juju.foo", "unit"},
339+ Backlog: 10,
340+ },
341+ }, {
342+ args: []string{"--exclude-module", "juju.foo", "--exclude-module", "unit"},
343+ expected: api.DebugLogParams{
344+ ExcludeModule: []string{"juju.foo", "unit"},
345+ Backlog: 10,
346+ },
347+ }, {
348+ args: []string{"--replay"},
349+ expected: api.DebugLogParams{
350+ Backlog: 10,
351+ Replay: true,
352+ },
353+ }, {
354+ args: []string{"--limit", "100"},
355+ expected: api.DebugLogParams{
356+ Backlog: 10,
357+ Limit: 100,
358+ },
359+ },
360+ } {
361+ c.Logf("test %v", i)
362+ command := &DebugLogCommand{}
363+ err := testing.InitCommand(command, test.args)
364+ if test.errMatch == "" {
365+ c.Check(err, gc.IsNil)
366+ c.Check(command.params, jc.DeepEquals, test.expected)
367+ } else {
368+ c.Check(err, gc.ErrorMatches, test.errMatch)
369+ }
370+ }
371+}
372+
373+func (s *DebugLogSuite) TestParamsPassed(c *gc.C) {
374+ fake := &fakeDebugLogAPI{}
375+ s.PatchValue(&getDebugLogAPI, func(envName string) (DebugLogAPI, error) {
376+ return fake, nil
377+ })
378+ _, err := testing.RunCommand(c, &DebugLogCommand{}, []string{
379+ "-i", "machine-1*", "-x", "machine-1-lxc-1",
380+ "--include-module=juju.provisioner",
381+ "--lines=500",
382+ "--level=WARNING",
383+ })
384+ c.Assert(err, gc.IsNil)
385+ c.Assert(fake.params, gc.DeepEquals, api.DebugLogParams{
386+ IncludeEntity: []string{"machine-1*"},
387+ IncludeModule: []string{"juju.provisioner"},
388+ ExcludeEntity: []string{"machine-1-lxc-1"},
389+ Backlog: 500,
390+ Level: loggo.WARNING,
391+ })
392+}
393+
394+func (s *DebugLogSuite) TestLogOutput(c *gc.C) {
395+ s.PatchValue(&getDebugLogAPI, func(envName string) (DebugLogAPI, error) {
396+ return &fakeDebugLogAPI{log: "this is the log output"}, nil
397+ })
398+ ctx, err := testing.RunCommand(c, &DebugLogCommand{}, nil)
399+ c.Assert(err, gc.IsNil)
400+ c.Assert(testing.Stdout(ctx), gc.Equals, "this is the log output")
401+}
402+
403+func (s *DebugLogSuite) TestTailFallback(c *gc.C) {
404+ s.PatchValue(&runSSHCommand, func(sshCmd *SSHCommand, ctx *cmd.Context) error {
405+ fmt.Fprintf(ctx.Stdout, "%s", sshCmd.Args)
406+ return nil
407+ })
408+ s.PatchValue(&getDebugLogAPI, func(envName string) (DebugLogAPI, error) {
409+ return &fakeDebugLogAPI{err: errors.NewNotSupportedError("testing")}, nil
410+ })
411+ ctx, err := testing.RunCommand(c, &DebugLogCommand{}, []string{"-n", "100"})
412+ c.Assert(err, gc.IsNil)
413+ c.Check(testing.Stderr(ctx), gc.Equals, "Server does not support new stream log, falling back to tail\n")
414+ c.Check(testing.Stdout(ctx), gc.Equals, "[tail -n -100 -f /var/log/juju/all-machines.log]")
415+}
416+
417+func newFakeDebugLogAPI(log string) DebugLogAPI {
418+ return &fakeDebugLogAPI{log: log}
419+}
420+
421+type fakeDebugLogAPI struct {
422+ log string
423+ params api.DebugLogParams
424+ err error
425+}
426+
427+func (fake *fakeDebugLogAPI) WatchDebugLog(params api.DebugLogParams) (io.ReadCloser, error) {
428+ if fake.err != nil {
429+ return nil, fake.err
430+ }
431+ fake.params = params
432+ return ioutil.NopCloser(strings.NewReader(fake.log)), nil
433+}
434+
435+func (fake *fakeDebugLogAPI) Close() error {
436 return nil
437 }
438-
439-// debug-log is implemented by invoking juju ssh with the correct arguments.
440-// This test helper checks for the expected invocation.
441-func (s *DebugLogSuite) assertDebugLogInvokesSSHCommand(c *gc.C, expected string, args ...string) {
442- defer testing.MakeSampleHome(c).Restore()
443- debugLogCmd, err := runDebugLog(c, args...)
444- c.Assert(err, gc.IsNil)
445- debugCmd := debugLogCmd.sshCmd.(*dummySSHCommand)
446- c.Assert(debugCmd.runCalled, gc.Equals, true)
447- c.Assert(debugCmd.Target, gc.Equals, "0")
448- c.Assert([]string{expected}, gc.DeepEquals, debugCmd.Args)
449-}
450-
451-const logLocation = "/var/log/juju/all-machines.log"
452-
453-func (s *DebugLogSuite) TestDebugLog(c *gc.C) {
454- const expected = "tail -n 10 -f " + logLocation
455- s.assertDebugLogInvokesSSHCommand(c, expected)
456-}
457-
458-func (s *DebugLogSuite) TestDebugLogFrom(c *gc.C) {
459- const expected = "tail -n +1 -f " + logLocation
460- s.assertDebugLogInvokesSSHCommand(c, expected, "-n", "+1")
461- s.assertDebugLogInvokesSSHCommand(c, expected, "--lines=+1")
462-}
463-
464-func (s *DebugLogSuite) TestDebugLogLast(c *gc.C) {
465- const expected = "tail -n 100 -f " + logLocation
466- s.assertDebugLogInvokesSSHCommand(c, expected, "-n", "100")
467- s.assertDebugLogInvokesSSHCommand(c, expected, "--lines=100")
468-}
469-
470-func (s *DebugLogSuite) TestDebugLogValidation(c *gc.C) {
471- defer testing.MakeSampleHome(c).Restore()
472- _, err := runDebugLog(c, "-n", "0")
473- c.Assert(err, gc.ErrorMatches, "invalid value \"0\" for flag -n: invalid number of lines")
474- _, err = runDebugLog(c, "-n", "-1")
475- c.Assert(err, gc.ErrorMatches, "invalid value \"-1\" for flag -n: invalid number of lines")
476- _, err = runDebugLog(c, "-n", "fnord")
477- c.Assert(err, gc.ErrorMatches, "invalid value \"fnord\" for flag -n: invalid number of lines")
478-}
479
480=== modified file 'cmd/juju/main.go'
481--- cmd/juju/main.go 2014-03-31 00:12:13 +0000
482+++ cmd/juju/main.go 2014-04-11 01:07:04 +0000
483@@ -96,7 +96,7 @@
484 jujucmd.Register(wrap(&SCPCommand{}))
485 jujucmd.Register(wrap(&SSHCommand{}))
486 jujucmd.Register(wrap(&ResolvedCommand{}))
487- jujucmd.Register(wrap(&DebugLogCommand{sshCmd: &SSHCommand{}}))
488+ jujucmd.Register(wrap(&DebugLogCommand{}))
489 jujucmd.Register(wrap(&DebugHooksCommand{}))
490 jujucmd.Register(wrap(&RetryProvisioningCommand{}))
491
492
493=== modified file 'errors/errors.go'
494--- errors/errors.go 2014-04-08 10:20:15 +0000
495+++ errors/errors.go 2014-04-11 01:07:04 +0000
496@@ -127,3 +127,25 @@
497 _, ok := err.(*alreadyExistsError)
498 return ok
499 }
500+
501+type notSupportedError struct {
502+ what string
503+}
504+
505+// NewNotSupportedError returns an error signifying that something is not
506+// supported. For example a client API call to a server that does not support
507+// the action.
508+func NewNotSupportedError(what string) error {
509+ return &notSupportedError{what: what}
510+}
511+
512+func (e *notSupportedError) Error() string {
513+ return e.what + " not supported"
514+}
515+
516+// IsNotSupportedError reports whether the error
517+// was created with NewNotSupportedError.
518+func IsNotSupportedError(err error) bool {
519+ _, ok := err.(*notSupportedError)
520+ return ok
521+}
522
523=== modified file 'errors/errors_test.go'
524--- errors/errors_test.go 2014-04-08 10:20:15 +0000
525+++ errors/errors_test.go 2014-04-11 01:07:04 +0000
526@@ -38,11 +38,13 @@
527 isUnauthorizedError := &errorSatisfier{errors.IsUnauthorizedError}
528 isNotImplementedError := &errorSatisfier{errors.IsNotImplementedError}
529 isAlreadyExistsError := &errorSatisfier{errors.IsAlreadyExistsError}
530+ isNotSupportedError := &errorSatisfier{errors.IsNotSupportedError}
531 satisfiers := []*errorSatisfier{
532 isNotFoundError,
533 isUnauthorizedError,
534 isNotImplementedError,
535 isAlreadyExistsError,
536+ isNotSupportedError,
537 }
538
539 // make some errors, and record the errorSatsifier
540@@ -88,6 +90,10 @@
541 errors.NewAlreadyExistsError("something"),
542 "something already exists",
543 isAlreadyExistsError,
544+ }, {
545+ errors.NewNotSupportedError("something"),
546+ "something not supported",
547+ isNotSupportedError,
548 }}
549
550 for i, t := range errorTests {
551
552=== modified file 'state/api/client.go'
553--- state/api/client.go 2014-04-10 10:00:50 +0000
554+++ state/api/client.go 2014-04-11 01:07:04 +0000
555@@ -20,6 +20,7 @@
556
557 "launchpad.net/juju-core/charm"
558 "launchpad.net/juju-core/constraints"
559+ "launchpad.net/juju-core/errors"
560 "launchpad.net/juju-core/instance"
561 "launchpad.net/juju-core/state/api/params"
562 "launchpad.net/juju-core/tools"
563@@ -725,16 +726,6 @@
564 return websocket.DialConfig(config)
565 }
566
567-type connectionError struct {
568- error
569-}
570-
571-// IsConnectionError reports whether the error is a connection error.
572-func IsConnectionError(err error) bool {
573- _, ok := err.(*connectionError)
574- return ok
575-}
576-
577 // DebugLogParams holds parameters for WatchDebugLog that control the
578 // filtering of the log messages. If the structure is zero initialized, the
579 // entire log file is sent back starting from the end, and until the user
580@@ -772,14 +763,14 @@
581 // WatchDebugLog returns a ReadCloser that the caller can read the log lines
582 // from. Only log lines that match the filtering specified in the
583 // DebugLogParams are returned. It returns an error that satisfies
584-// IsConnectionError when the connection cannot be made.
585+// IsNotSupportedError when the API server does not support the end-point.
586 func (c *Client) WatchDebugLog(args DebugLogParams) (io.ReadCloser, error) {
587 // The websocket connection just hangs if the server doesn't have the log
588 // end point. So do a version check, as version was added at the same time
589 // as the remote end point.
590 _, err := c.AgentVersion()
591 if err != nil {
592- return nil, &connectionError{fmt.Errorf("server doesn't support debug log websocket")}
593+ return nil, errors.NewNotSupportedError("WatchDebugLog")
594 }
595 // Prepare URL.
596 attrs := url.Values{}
597@@ -811,7 +802,7 @@
598 cfg.TlsConfig = &tls.Config{RootCAs: c.st.certPool, ServerName: "anything"}
599 connection, err := websocketDialConfig(cfg)
600 if err != nil {
601- return nil, &connectionError{err}
602+ return nil, err
603 }
604 // Read the initial error and translate to a real error.
605 // Read up to the first new line character. We can't use bufio here as it

Subscribers

People subscribed via source and target branches

to status/vote changes: