Merge lp:~thumper/juju-core/debug-log-api-client 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: 2604
Proposed branch: lp:~thumper/juju-core/debug-log-api-client
Merge into: lp:~go-bot/juju-core/trunk
Diff against target: 491 lines (+312/-5)
11 files modified
state/api/apiclient.go (+5/-0)
state/api/client.go (+131/-0)
state/api/client_test.go (+120/-0)
state/api/export_test.go (+2/-0)
state/api/params/internal.go (+6/-0)
state/apiserver/client/client.go (+6/-0)
state/apiserver/client/client_test.go (+8/-0)
state/apiserver/debuglog.go (+1/-1)
state/apiserver/debuglog_test.go (+2/-4)
utils/http.go (+16/-0)
utils/http_test.go (+15/-0)
To merge this branch: bzr merge lp:~thumper/juju-core/debug-log-api-client
Reviewer Review Type Date Requested Status
Juju Engineering Pending
Review via email: mp+214875@code.launchpad.net

Commit message

Add the debug-log client API

The client connection now caches the x509 pool so it can
be passed along with the websocket connection for the debug
log call.

Description of the change

Add the debug-log client api

This involved a little more work than I expected.
The client connection now caches the x509 pool so it can
be passed along with the websocket connection for the debug
log call.

A key point of interest, is that if the remote api server
does not have a websocket listener on '/log', the dial config
just blocks and never returns, so we can't wait for an error
on that to determine that the remote api server doesn't support
the api yet. I thought it would be good to be able to ask
the api server what version it was running, so in future, if we
hit this again, we can at least have a version check. The mere
existance of the version call is enough for us to determine that
the remote server supports debug-log, so we don't care about
the actual value.

Initially I used a bufio scanner to read the first line of the
response, however that reads 4k into a buffer, which killed all
the tests. I now read a byte at a time to get the json encoded
error. It would probably be much better to use the websocket
methods to read messages, but not sure how that would impact the
gui.

https://codereview.appspot.com/85850043/

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

Reviewers: mp+214875_code.launchpad.net,

Message:
Please take a look.

Description:
Add the debug-log client api

This involved a little more work than I expected.
The client connection now caches the x509 pool so it can
be passed along with the websocket connection for the debug
log call.

A key point of interest, is that if the remote api server
does not have a websocket listener on '/log', the dial config
just blocks and never returns, so we can't wait for an error
on that to determine that the remote api server doesn't support
the api yet. I thought it would be good to be able to ask
the api server what version it was running, so in future, if we
hit this again, we can at least have a version check. The mere
existance of the version call is enough for us to determine that
the remote server supports debug-log, so we don't care about
the actual value.

Initially I used a bufio scanner to read the first line of the
response, however that reads 4k into a buffer, which killed all
the tests. I now read a byte at a time to get the json encoded
error. It would probably be much better to use the websocket
methods to read messages, but not sure how that would impact the
gui.

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

(do not edit description out of merge proposal)

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

Affected files (+322, -4 lines):
   A [revision details]
   M state/api/apiclient.go
   M state/api/client.go
   M state/api/client_test.go
   M state/api/export_test.go
   M state/apiserver/client/client.go
   M state/apiserver/client/client_test.go
   M state/apiserver/debuglog_test.go
   M utils/http.go
   M utils/http_test.go

Revision history for this message
Tim Penhey (thumper) wrote :
Revision history for this message
Roger Peppe (rogpeppe) wrote :
Download full text (7.2 KiB)

Looks great in general. Quite a few minor points but nothing too
substantial.

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

https://codereview.appspot.com/85850043/diff/20001/state/api/client.go#newcode715
state/api/client.go:715: func (c *Client) Version() (version.Number,
error) {
I think I'd call this AgentVersion.
We might have different ideas of API version later.

https://codereview.appspot.com/85850043/diff/20001/state/api/client.go#newcode720
state/api/client.go:720: if result.Error != nil {
This isn't possible. The Error field is only there for bulk version
results.
I'd either ignore the field, or make a new type in params
(e.g. type AgentVersionResult {Version version.Number})

https://codereview.appspot.com/85850043/diff/20001/state/api/client.go#newcode726
state/api/client.go:726: // Allow overriding in tests.
I'd be tempted to name this websocketDialConfig, because that's what
we're
mocking.

// websocketDialConfig is called instead of websocket.DialConfig
// so we can override it in tests.
?

https://codereview.appspot.com/85850043/diff/20001/state/api/client.go#newcode735
state/api/client.go:735: // IsConnectionError returns true if the error
is a connection error.
// IsConnectionError reports whether the error is a connection
// error.

https://codereview.appspot.com/85850043/diff/20001/state/api/client.go#newcode741
state/api/client.go:741: // Params for WatchDebugLog
Please can we document this type and its fields?
This is the public face of the Go Juju API.

https://codereview.appspot.com/85850043/diff/20001/state/api/client.go#newcode755
state/api/client.go:755: // machines or units. The watching is started
the given number of
The last sentence doesn't seem to fit here.

https://codereview.appspot.com/85850043/diff/20001/state/api/client.go#newcode756
state/api/client.go:756: // matching lines back in history.

// It returns an error that satisfies IsConnectionError
// when the connection cannot be made.

?

I'm not quite sure why we want to distinguish a connection
error from the other kinds of errors though.

https://codereview.appspot.com/85850043/diff/20001/state/api/client.go#newcode759
state/api/client.go:759: // end point (not sure why). So do a version
check, as version was added
The reason that the websocket connection hangs is that
the server is serving the API on every URL (that was
a mistake that I should have fixed earlier, sorry),
so you're waiting for an RPC reply without sending
any request.

https://codereview.appspot.com/85850043/diff/20001/state/api/client.go#newcode768
state/api/client.go:768: attrs["replay"] =
[]string{strconv.FormatBool(args.Replay)}
attrs.Set(fmt.Sprint(args.Replay))

https://codereview.appspot.com/85850043/diff/20001/state/api/client.go#newcode771
state/api/client.go:771: attrs["maxLines"] =
[]string{fmt.Sprint(args.Limit)}
attrs.Set(fmt.Sprint(args.Limit))
etc

https://codereview.appspot.com/85850043/diff/20001/state/api/client.go#newcode780
state/api/client.go:780: if len(arg) > 0 {
Is this actually necessary. If there's a zero-length slice in the
attributes, does it matter if it's nil or empty?

I think just
attrs["inc...

Read more...

Revision history for this message
John A Meinel (jameinel) wrote :

I believe old servers respond to plain API requests on any URL (so /log
is just another API endpoint). So I think you *could* issue a request
there and see if it responds? Or try to log and then if we haven't
gotten any data after X seconds, issue a request.

https://codereview.appspot.com/85850043/

Revision history for this message
Tim Penhey (thumper) wrote :
Download full text (9.5 KiB)

Please take a look.

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

https://codereview.appspot.com/85850043/diff/20001/state/api/client.go#newcode715
state/api/client.go:715: func (c *Client) Version() (version.Number,
error) {
On 2014/04/09 10:28:40, rog wrote:
> I think I'd call this AgentVersion.
> We might have different ideas of API version later.

Done.

https://codereview.appspot.com/85850043/diff/20001/state/api/client.go#newcode720
state/api/client.go:720: if result.Error != nil {
On 2014/04/09 10:28:40, rog wrote:
> This isn't possible. The Error field is only there for bulk version
results.
> I'd either ignore the field, or make a new type in params
> (e.g. type AgentVersionResult {Version version.Number})

I don't like ignoring it, so I'll change the response type.

https://codereview.appspot.com/85850043/diff/20001/state/api/client.go#newcode726
state/api/client.go:726: // Allow overriding in tests.
On 2014/04/09 10:28:40, rog wrote:
> I'd be tempted to name this websocketDialConfig, because that's what
we're
> mocking.

> // websocketDialConfig is called instead of websocket.DialConfig
> // so we can override it in tests.
> ?

Done.

https://codereview.appspot.com/85850043/diff/20001/state/api/client.go#newcode735
state/api/client.go:735: // IsConnectionError returns true if the error
is a connection error.
On 2014/04/09 10:28:40, rog wrote:
> // IsConnectionError reports whether the error is a connection
> // error.

Done.

https://codereview.appspot.com/85850043/diff/20001/state/api/client.go#newcode741
state/api/client.go:741: // Params for WatchDebugLog
On 2014/04/09 10:28:40, rog wrote:
> Please can we document this type and its fields?
> This is the public face of the Go Juju API.

Done.

https://codereview.appspot.com/85850043/diff/20001/state/api/client.go#newcode755
state/api/client.go:755: // machines or units. The watching is started
the given number of
On 2014/04/09 10:28:40, rog wrote:
> The last sentence doesn't seem to fit here.

Rewritten

https://codereview.appspot.com/85850043/diff/20001/state/api/client.go#newcode756
state/api/client.go:756: // matching lines back in history.
On 2014/04/09 10:28:40, rog wrote:

> // It returns an error that satisfies IsConnectionError
> // when the connection cannot be made.

> ?

> I'm not quite sure why we want to distinguish a connection
> error from the other kinds of errors though.

The next branch uses the connection error to determine whether to just
report the error and finish, or to try tailing the debug log file over
ssh like we did before as a fallback for older servers.

https://codereview.appspot.com/85850043/diff/20001/state/api/client.go#newcode759
state/api/client.go:759: // end point (not sure why). So do a version
check, as version was added
On 2014/04/09 10:28:40, rog wrote:
> The reason that the websocket connection hangs is that
> the server is serving the API on every URL (that was
> a mistake that I should have fixed earlier, sorry),
> so you're waiting for an RPC reply without sending
> any request.

Ah.. thanks for the explanation. I had guessed something like this but
wasn't entirely sure.

h...

Read more...

Revision history for this message
Tim Penhey (thumper) wrote :
Revision history for this message
Roger Peppe (rogpeppe) wrote :
Download full text (3.9 KiB)

LGTM with a few trivials.
Thanks!

https://codereview.appspot.com/85850043/diff/20001/state/api/client_test.go
File state/api/client_test.go (right):

https://codereview.appspot.com/85850043/diff/20001/state/api/client_test.go#newcode211
state/api/client_test.go:211: func echoUrl(c *gc.C)
func(*websocket.Config) (io.ReadCloser, error) {
On 2014/04/10 03:59:50, thumper wrote:
> On 2014/04/09 10:28:40, rog wrote:
> > Here's the source of your buffering problem.
> > This fixes it:
> >
> > func echoUrl(c *gc.C) func(*websocket.Config) (io.ReadCloser, error)
{
> > message, err := json.Marshal(&params.ErrorResult{})
> > c.Assert(err, gc.IsNil)
> > return func(config *websocket.Config) (io.ReadCloser, error) {
> > pr, pw := io.Pipe()
> > go func() {
> > fmt.Fprintf(pw, "%s\n", message)
> > fmt.Fprintf(pw, "%s\n", config.Location)
> > }()
> > return pr, nil
> > }
> > }

> That is pretty nice. I take it io.Pipe also does the reading message
boundaries?

yup, io.Pipe does no buffering at all - each write corresponds exactly
to each read.

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

https://codereview.appspot.com/85850043/diff/60001/state/api/client.go#newcode738
state/api/client.go:738: // Params for WatchDebugLog controls the
filtering of the log messages. If the
// DebugLogParams holds parameters for WatchDebugLog that
// control the filtering of the log messages.

?

Thanks a lot for documenting these, BTW. It makes a big difference.

https://codereview.appspot.com/85850043/diff/60001/state/api/client.go#newcode747
state/api/client.go:747: // are set all modules are considered
included.
s/ / /

Does the inclusion of a logging module here imply all its sub-modules
too?

https://codereview.appspot.com/85850043/diff/60001/state/api/client.go#newcode752
state/api/client.go:752: // ExcludeModule lists logging modules to
exclude from the resposne.
ditto

https://codereview.appspot.com/85850043/diff/60001/state/api/client.go#newcode755
state/api/client.go:755: // have been sent, the socket is closed.
// If zero, there is no limit.
?

https://codereview.appspot.com/85850043/diff/60001/state/api/client.go#newcode763
state/api/client.go:763: // than the end. If replay is true, backlog is
ignored.
Nice. I started commenting on Backlog to say "but what if I want no
lines of backlog?" - then I saw this.

https://codereview.appspot.com/85850043/diff/60001/state/api/client.go#newcode821
state/api/client.go:821: logger.Debugf("initial line: %s", line)
Just realised - we probably want %q here so that we don't get a blank
line in the log.

https://codereview.appspot.com/85850043/diff/60001/state/api/client_test.go
File state/api/client_test.go (right):

https://codereview.appspot.com/85850043/diff/60001/state/api/client_test.go#newcode140
state/api/client_test.go:140: return &badReader{ioutil.NopCloser(junk),
err}, nil
or ioutil.NopCloser(&badReader{err})

then you can lose the ReadCloser field inside badReader.

https://codereview.appspot.com/85850043/diff/60001/state/api/client_test.go#newcode195
state/api/client_test.go:195: func echoUrl(c *gc.C)
func(*websocket.Config) (io.ReadClos...

Read more...

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

Please take a look.

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

https://codereview.appspot.com/85850043/diff/60001/state/api/client.go#newcode738
state/api/client.go:738: // Params for WatchDebugLog controls the
filtering of the log messages. If the
On 2014/04/10 07:04:18, rog wrote:
> // DebugLogParams holds parameters for WatchDebugLog that
> // control the filtering of the log messages.

> ?

> Thanks a lot for documenting these, BTW. It makes a big difference.

Done.

https://codereview.appspot.com/85850043/diff/60001/state/api/client.go#newcode747
state/api/client.go:747: // are set all modules are considered
included.
On 2014/04/10 07:04:18, rog wrote:
> s/ / /

> Does the inclusion of a logging module here imply all its sub-modules
too?

Yes, updated.

https://codereview.appspot.com/85850043/diff/60001/state/api/client.go#newcode755
state/api/client.go:755: // have been sent, the socket is closed.
On 2014/04/10 07:04:18, rog wrote:
> // If zero, there is no limit.
> ?

Updated.

https://codereview.appspot.com/85850043/diff/60001/state/api/client.go#newcode821
state/api/client.go:821: logger.Debugf("initial line: %s", line)
On 2014/04/10 07:04:18, rog wrote:
> Just realised - we probably want %q here so that we don't get a blank
line in
> the log.

Done.

https://codereview.appspot.com/85850043/diff/60001/state/api/client_test.go
File state/api/client_test.go (right):

https://codereview.appspot.com/85850043/diff/60001/state/api/client_test.go#newcode140
state/api/client_test.go:140: return &badReader{ioutil.NopCloser(junk),
err}, nil
On 2014/04/10 07:04:18, rog wrote:
> or ioutil.NopCloser(&badReader{err})

> then you can lose the ReadCloser field inside badReader.

Good call.

https://codereview.appspot.com/85850043/diff/60001/state/api/client_test.go#newcode195
state/api/client_test.go:195: func echoUrl(c *gc.C)
func(*websocket.Config) (io.ReadCloser, error) {
On 2014/04/10 07:04:18, rog wrote:
> s/echoUrl/echoURL/

> (Go convention is to capitalise either all or none of an acronym)

Done.

https://codereview.appspot.com/85850043/diff/60001/state/api/params/internal.go
File state/api/params/internal.go (right):

https://codereview.appspot.com/85850043/diff/60001/state/api/params/internal.go#newcode563
state/api/params/internal.go:563: // apiserver.
On 2014/04/10 07:04:18, rog wrote:
> s/apiserver/agent running the API server./
> ?

Done.

https://codereview.appspot.com/85850043/diff/60001/state/apiserver/client/client.go
File state/apiserver/client/client.go (right):

https://codereview.appspot.com/85850043/diff/60001/state/apiserver/client/client.go#newcode785
state/apiserver/client/client.go:785: // AgentVersion returns the
current version that the api server is running.
On 2014/04/10 07:04:18, rog wrote:
> s/api/API/

Done.

https://codereview.appspot.com/85850043/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'state/api/apiclient.go'
2--- state/api/apiclient.go 2014-04-09 15:17:04 +0000
3+++ state/api/apiclient.go 2014-04-10 09:38:03 +0000
4@@ -56,6 +56,10 @@
5 // serverRoot holds the cached API server address and port we used
6 // to login, with a https:// prefix.
7 serverRoot string
8+
9+ // certPool holds the cert pool that is used to authenticate the tls
10+ // connections to the API.
11+ certPool *x509.CertPool
12 }
13
14 // Info encapsulates information about a server holding juju state and
15@@ -151,6 +155,7 @@
16 serverRoot: "https://" + conn.Config().Location.Host,
17 tag: info.Tag,
18 password: info.Password,
19+ certPool: pool,
20 }
21 if info.Tag != "" || info.Password != "" {
22 if err := st.Login(info.Tag, info.Password, info.Nonce); err != nil {
23
24=== modified file 'state/api/client.go'
25--- state/api/client.go 2014-04-10 08:47:38 +0000
26+++ state/api/client.go 2014-04-10 09:38:03 +0000
27@@ -4,14 +4,20 @@
28 package api
29
30 import (
31+ "crypto/tls"
32 "encoding/json"
33 "fmt"
34+ "io"
35 "io/ioutil"
36 "net/http"
37+ "net/url"
38 "os"
39 "strings"
40 "time"
41
42+ "code.google.com/p/go.net/websocket"
43+ "github.com/juju/loggo"
44+
45 "launchpad.net/juju-core/charm"
46 "launchpad.net/juju-core/constraints"
47 "launchpad.net/juju-core/instance"
48@@ -703,3 +709,128 @@
49 }
50 return c.call("EnsureAvailability", args, nil)
51 }
52+
53+// AgentVersion reports the version number of the api server.
54+func (c *Client) AgentVersion() (version.Number, error) {
55+ var result params.AgentVersionResult
56+ if err := c.call("AgentVersion", nil, &result); err != nil {
57+ return version.Number{}, err
58+ }
59+ return result.Version, nil
60+}
61+
62+// websocketDialConfig is called instead of websocket.DialConfig so we can
63+// override it in tests.
64+var websocketDialConfig = func(config *websocket.Config) (io.ReadCloser, error) {
65+ return websocket.DialConfig(config)
66+}
67+
68+type connectionError struct {
69+ error
70+}
71+
72+// IsConnectionError reports whether the error is a connection error.
73+func IsConnectionError(err error) bool {
74+ _, ok := err.(*connectionError)
75+ return ok
76+}
77+
78+// DebugLogParams holds parameters for WatchDebugLog that control the
79+// filtering of the log messages. If the structure is zero initialized, the
80+// entire log file is sent back starting from the end, and until the user
81+// closes the connection.
82+type DebugLogParams struct {
83+ // IncludeEntity lists entity tags to include in the response. Tags may
84+ // finish with a '*' to match a prefix e.g.: unit-mysql-*, machine-2. If
85+ // none are set, then all lines are considered included.
86+ IncludeEntity []string
87+ // IncludeModule lists logging modules to include in the response. If none
88+ // are set all modules are considered included. If a module is specified,
89+ // all the submodules also match.
90+ IncludeModule []string
91+ // ExcludeEntity lists entity tags to exclude from the response. As with
92+ // IncludeEntity the values may finish with a '*'.
93+ ExcludeEntity []string
94+ // ExcludeModule lists logging modules to exclude from the resposne. If a
95+ // module is specified, all the submodules are also excluded.
96+ ExcludeModule []string
97+ // Limit defines the maximum number of lines to return. Once this many
98+ // have been sent, the socket is closed. If zero, all filtered lines are
99+ // sent down the connection until the client closes the connection.
100+ Limit uint
101+ // Backlog tells the server to try to go back this many lines before
102+ // starting filtering. If backlog is zero and replay is false, then there
103+ // may be an initial delay until the next matching log message is written.
104+ Backlog uint
105+ // Level specifies the minimum logging level to be sent back in the response.
106+ Level loggo.Level
107+ // Replay tells the server to start at the start of the log file rather
108+ // than the end. If replay is true, backlog is ignored.
109+ Replay bool
110+}
111+
112+// WatchDebugLog returns a ReadCloser that the caller can read the log lines
113+// from. Only log lines that match the filtering specified in the
114+// DebugLogParams are returned. It returns an error that satisfies
115+// IsConnectionError when the connection cannot be made.
116+func (c *Client) WatchDebugLog(args DebugLogParams) (io.ReadCloser, error) {
117+ // The websocket connection just hangs if the server doesn't have the log
118+ // end point. So do a version check, as version was added at the same time
119+ // as the remote end point.
120+ _, err := c.AgentVersion()
121+ if err != nil {
122+ return nil, &connectionError{fmt.Errorf("server doesn't support debug log websocket")}
123+ }
124+ // Prepare URL.
125+ attrs := url.Values{}
126+ if args.Replay {
127+ attrs.Set("replay", fmt.Sprint(args.Replay))
128+ }
129+ if args.Limit > 0 {
130+ attrs.Set("maxLines", fmt.Sprint(args.Limit))
131+ }
132+ if args.Backlog > 0 {
133+ attrs.Set("backlog", fmt.Sprint(args.Backlog))
134+ }
135+ if args.Level != loggo.UNSPECIFIED {
136+ attrs.Set("level", fmt.Sprint(args.Level))
137+ }
138+ attrs["includeEntity"] = args.IncludeEntity
139+ attrs["includeModule"] = args.IncludeModule
140+ attrs["excludeEntity"] = args.ExcludeEntity
141+ attrs["excludeModule"] = args.ExcludeModule
142+
143+ target := url.URL{
144+ Scheme: "wss",
145+ Host: c.st.addr,
146+ Path: "/log",
147+ RawQuery: attrs.Encode(),
148+ }
149+ cfg, err := websocket.NewConfig(target.String(), "http://localhost/")
150+ cfg.Header = utils.BasicAuthHeader(c.st.tag, c.st.password)
151+ cfg.TlsConfig = &tls.Config{RootCAs: c.st.certPool, ServerName: "anything"}
152+ connection, err := websocketDialConfig(cfg)
153+ if err != nil {
154+ return nil, &connectionError{err}
155+ }
156+ // Read the initial error and translate to a real error.
157+ // Read up to the first new line character. We can't use bufio here as it
158+ // reads too much from the reader.
159+ line := make([]byte, 4096)
160+ n, err := connection.Read(line)
161+ if err != nil {
162+ return nil, fmt.Errorf("unable to read initial response: %v", err)
163+ }
164+ line = line[0:n]
165+
166+ logger.Debugf("initial line: %q", line)
167+ var errResult params.ErrorResult
168+ err = json.Unmarshal(line, &errResult)
169+ if err != nil {
170+ return nil, fmt.Errorf("unable to unmarshal initial response: %v", err)
171+ }
172+ if errResult.Error != nil {
173+ return nil, errResult.Error
174+ }
175+ return connection, nil
176+}
177
178=== modified file 'state/api/client_test.go'
179--- state/api/client_test.go 2014-04-01 15:28:49 +0000
180+++ state/api/client_test.go 2014-04-10 09:38:03 +0000
181@@ -4,10 +4,19 @@
182 package api_test
183
184 import (
185+ "bufio"
186+ "bytes"
187+ "encoding/json"
188 "fmt"
189+ "io"
190+ "io/ioutil"
191 "net"
192 "net/http"
193+ "net/url"
194+ "strings"
195
196+ "code.google.com/p/go.net/websocket"
197+ "github.com/juju/loggo"
198 jc "github.com/juju/testing/checkers"
199 gc "launchpad.net/gocheck"
200
201@@ -83,3 +92,114 @@
202 _, err = client.AddLocalCharm(curl, charmArchive)
203 c.Assert(err, jc.Satisfies, params.IsCodeNotImplemented)
204 }
205+
206+func (s *clientSuite) TestWatchDebugLogConnected(c *gc.C) {
207+ // Shows both the unmarshalling of a real error, and
208+ // that the api server is connected.
209+ client := s.APIState.Client()
210+ reader, err := client.WatchDebugLog(api.DebugLogParams{})
211+ c.Assert(err, gc.ErrorMatches, "cannot open log file: .*")
212+ c.Assert(reader, gc.IsNil)
213+}
214+
215+func (s *clientSuite) TestConnectionErrorBadConnection(c *gc.C) {
216+ s.PatchValue(api.WebsocketDialConfig, func(_ *websocket.Config) (io.ReadCloser, error) {
217+ return nil, fmt.Errorf("bad connection")
218+ })
219+ client := s.APIState.Client()
220+ reader, err := client.WatchDebugLog(api.DebugLogParams{})
221+ c.Assert(err, gc.ErrorMatches, "bad connection")
222+ c.Assert(reader, gc.IsNil)
223+}
224+
225+func (s *clientSuite) TestConnectionErrorNoData(c *gc.C) {
226+ s.PatchValue(api.WebsocketDialConfig, func(_ *websocket.Config) (io.ReadCloser, error) {
227+ return ioutil.NopCloser(&bytes.Buffer{}), nil
228+ })
229+ client := s.APIState.Client()
230+ reader, err := client.WatchDebugLog(api.DebugLogParams{})
231+ c.Assert(err, gc.ErrorMatches, "unable to read initial response: EOF")
232+ c.Assert(reader, gc.IsNil)
233+}
234+
235+func (s *clientSuite) TestConnectionErrorBadData(c *gc.C) {
236+ s.PatchValue(api.WebsocketDialConfig, func(_ *websocket.Config) (io.ReadCloser, error) {
237+ junk := strings.NewReader("junk\n")
238+ return ioutil.NopCloser(junk), nil
239+ })
240+ client := s.APIState.Client()
241+ reader, err := client.WatchDebugLog(api.DebugLogParams{})
242+ c.Assert(err, gc.ErrorMatches, "unable to unmarshal initial response: .*")
243+ c.Assert(reader, gc.IsNil)
244+}
245+
246+func (s *clientSuite) TestConnectionErrorReadError(c *gc.C) {
247+ s.PatchValue(api.WebsocketDialConfig, func(_ *websocket.Config) (io.ReadCloser, error) {
248+ err := fmt.Errorf("bad read")
249+ return ioutil.NopCloser(&badReader{err}), nil
250+ })
251+ client := s.APIState.Client()
252+ reader, err := client.WatchDebugLog(api.DebugLogParams{})
253+ c.Assert(err, gc.ErrorMatches, "unable to read initial response: bad read")
254+ c.Assert(reader, gc.IsNil)
255+}
256+
257+func (s *clientSuite) TestParamsEncoded(c *gc.C) {
258+ s.PatchValue(api.WebsocketDialConfig, echoURL(c))
259+
260+ params := api.DebugLogParams{
261+ IncludeEntity: []string{"a", "b"},
262+ IncludeModule: []string{"c", "d"},
263+ ExcludeEntity: []string{"e", "f"},
264+ ExcludeModule: []string{"g", "h"},
265+ Limit: 100,
266+ Backlog: 200,
267+ Level: loggo.ERROR,
268+ Replay: true,
269+ }
270+
271+ client := s.APIState.Client()
272+ reader, err := client.WatchDebugLog(params)
273+ c.Assert(err, gc.IsNil)
274+
275+ bufReader := bufio.NewReader(reader)
276+ location, err := bufReader.ReadString('\n')
277+ c.Assert(err, gc.IsNil)
278+ connectUrl, err := url.Parse(strings.TrimSpace(location))
279+ c.Assert(err, gc.IsNil)
280+
281+ values := connectUrl.Query()
282+ c.Assert(values, jc.DeepEquals, url.Values{
283+ "includeEntity": params.IncludeEntity,
284+ "includeModule": params.IncludeModule,
285+ "excludeEntity": params.ExcludeEntity,
286+ "excludeModule": params.ExcludeModule,
287+ "maxLines": {"100"},
288+ "backlog": {"200"},
289+ "level": {"ERROR"},
290+ "replay": {"true"},
291+ })
292+}
293+
294+// badReader raises err when Read is called.
295+type badReader struct {
296+ err error
297+}
298+
299+func (r *badReader) Read(p []byte) (n int, err error) {
300+ return 0, r.err
301+}
302+
303+func echoURL(c *gc.C) func(*websocket.Config) (io.ReadCloser, error) {
304+ response := &params.ErrorResult{}
305+ message, err := json.Marshal(response)
306+ c.Assert(err, gc.IsNil)
307+ return func(config *websocket.Config) (io.ReadCloser, error) {
308+ pr, pw := io.Pipe()
309+ go func() {
310+ fmt.Fprintf(pw, "%s\n", message)
311+ fmt.Fprintf(pw, "%s\n", config.Location)
312+ }()
313+ return pr, nil
314+ }
315+}
316
317=== modified file 'state/api/export_test.go'
318--- state/api/export_test.go 2014-04-01 03:13:49 +0000
319+++ state/api/export_test.go 2014-04-10 09:38:03 +0000
320@@ -5,6 +5,8 @@
321
322 var (
323 NewWebsocketDialer = newWebsocketDialer
324+
325+ WebsocketDialConfig = &websocketDialConfig
326 )
327
328 // SetServerRoot allows changing the URL to the internal API server
329
330=== modified file 'state/api/params/internal.go'
331--- state/api/params/internal.go 2014-04-09 15:08:51 +0000
332+++ state/api/params/internal.go 2014-04-10 09:38:03 +0000
333@@ -572,3 +572,9 @@
334 type RunResults struct {
335 Results []RunResult
336 }
337+
338+// AgentVersionResult is used to return the current version number of the
339+// agent running the API server.
340+type AgentVersionResult struct {
341+ Version version.Number
342+}
343
344=== modified file 'state/apiserver/client/client.go'
345--- state/apiserver/client/client.go 2014-04-10 08:47:38 +0000
346+++ state/apiserver/client/client.go 2014-04-10 09:38:03 +0000
347@@ -28,6 +28,7 @@
348 "launchpad.net/juju-core/state/statecmd"
349 coretools "launchpad.net/juju-core/tools"
350 "launchpad.net/juju-core/utils"
351+ "launchpad.net/juju-core/version"
352 )
353
354 var logger = loggo.GetLogger("juju.state.apiserver.client")
355@@ -782,6 +783,11 @@
356 return changes, nil
357 }
358
359+// AgentVersion returns the current version that the API server is running.
360+func (c *Client) AgentVersion() (params.AgentVersionResult, error) {
361+ return params.AgentVersionResult{Version: version.Current.Number}, nil
362+}
363+
364 // EnvironmentGet implements the server-side part of the
365 // get-environment CLI command.
366 func (c *Client) EnvironmentGet() (params.EnvironmentGetResults, error) {
367
368=== modified file 'state/apiserver/client/client_test.go'
369--- state/apiserver/client/client_test.go 2014-04-10 08:47:38 +0000
370+++ state/apiserver/client/client_test.go 2014-04-10 09:38:03 +0000
371@@ -2211,3 +2211,11 @@
372 c.Assert(err, gc.IsNil)
373 c.Assert(apiHostPorts, gc.DeepEquals, stateAPIHostPorts)
374 }
375+
376+func (s *clientSuite) TestClientAgentVersion(c *gc.C) {
377+ current := version.MustParse("1.2.0")
378+ s.PatchValue(&version.Current.Number, current)
379+ result, err := s.APIState.Client().AgentVersion()
380+ c.Assert(err, gc.IsNil)
381+ c.Assert(result, gc.Equals, current)
382+}
383
384=== modified file 'state/apiserver/debuglog.go'
385--- state/apiserver/debuglog.go 2014-04-07 04:58:59 +0000
386+++ state/apiserver/debuglog.go 2014-04-10 09:38:03 +0000
387@@ -41,7 +41,7 @@
388 // - as with include, it may finish with a '*'
389 // excludeModule -> []string - lists logging modules to exclude from the response
390 // limit -> uint - show *at most* this many lines
391-// backtrack -> uint
392+// backlog -> uint
393 // - go back this many lines from the end before starting to filter
394 // - has no meaning if 'replay' is true
395 // level -> string one of [TRACE, DEBUG, INFO, WARNING, ERROR]
396
397=== modified file 'state/apiserver/debuglog_test.go'
398--- state/apiserver/debuglog_test.go 2014-04-07 04:33:18 +0000
399+++ state/apiserver/debuglog_test.go 2014-04-10 09:38:03 +0000
400@@ -7,7 +7,6 @@
401 "bufio"
402 "crypto/tls"
403 "crypto/x509"
404- "encoding/base64"
405 "encoding/json"
406 "io"
407 "net/http"
408@@ -22,6 +21,7 @@
409
410 "launchpad.net/juju-core/state/api/params"
411 "launchpad.net/juju-core/testing"
412+ "launchpad.net/juju-core/utils"
413 )
414
415 type debugLogSuite struct {
416@@ -191,9 +191,7 @@
417 }
418
419 func (s *debugLogSuite) dialWebsocket(c *gc.C, queryParams url.Values) (*websocket.Conn, error) {
420- header := http.Header{
421- "Authorization": {"Basic " + base64.StdEncoding.EncodeToString([]byte(s.userTag+":"+s.password))},
422- }
423+ header := utils.BasicAuthHeader(s.userTag, s.password)
424 return s.dialWebsocketInternal(c, queryParams, header)
425 }
426
427
428=== modified file 'utils/http.go'
429--- utils/http.go 2014-03-24 03:24:00 +0000
430+++ utils/http.go 2014-04-10 09:38:03 +0000
431@@ -5,6 +5,7 @@
432
433 import (
434 "crypto/tls"
435+ "encoding/base64"
436 "net/http"
437 "sync"
438 )
439@@ -83,3 +84,18 @@
440 registerFileProtocol(transport)
441 return transport
442 }
443+
444+// BasicAuthHeader creates a header that contains just the "Authorization"
445+// entry. The implementation was originally taked from net/http but this is
446+// needed externally from the http request object in order to use this with
447+// our websockets. See 2 (end of page 4) http://www.ietf.org/rfc/rfc2617.txt
448+// "To receive authorization, the client sends the userid and password,
449+// separated by a single colon (":") character, within a base64 encoded string
450+// in the credentials."
451+func BasicAuthHeader(username, password string) http.Header {
452+ auth := username + ":" + password
453+ encoded := "Basic " + base64.StdEncoding.EncodeToString([]byte(auth))
454+ return http.Header{
455+ "Authorization": {encoded},
456+ }
457+}
458
459=== modified file 'utils/http_test.go'
460--- utils/http_test.go 2014-03-24 00:23:19 +0000
461+++ utils/http_test.go 2014-04-10 09:38:03 +0000
462@@ -4,10 +4,12 @@
463 package utils_test
464
465 import (
466+ "encoding/base64"
467 "fmt"
468 "io/ioutil"
469 "net/http"
470 "net/http/httptest"
471+ "strings"
472
473 gc "launchpad.net/gocheck"
474
475@@ -79,3 +81,16 @@
476 client2 := utils.GetNonValidatingHTTPClient()
477 c.Check(client1, gc.Equals, client2)
478 }
479+
480+func (s *httpSuite) TestBasicAuthHeader(c *gc.C) {
481+ header := utils.BasicAuthHeader("eric", "sekrit")
482+ c.Assert(len(header), gc.Equals, 1)
483+ auth := header.Get("Authorization")
484+ fields := strings.Fields(auth)
485+ c.Assert(len(fields), gc.Equals, 2)
486+ basic, encoded := fields[0], fields[1]
487+ c.Assert(basic, gc.Equals, "Basic")
488+ decoded, err := base64.StdEncoding.DecodeString(encoded)
489+ c.Assert(err, gc.IsNil)
490+ c.Assert(string(decoded), gc.Equals, "eric:sekrit")
491+}

Subscribers

People subscribed via source and target branches

to status/vote changes: