Merge lp:~thumper/juju-core/debug-log-api-client into lp:~go-bot/juju-core/trunk
- debug-log-api-client
- Merge into trunk
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 |
Related bugs: |
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.
Tim Penhey (thumper) wrote : | # |
Tim Penhey (thumper) wrote : | # |
Please take a look.
Roger Peppe (rogpeppe) wrote : | # |
Looks great in general. Quite a few minor points but nothing too
substantial.
https:/
File state/api/client.go (right):
https:/
state/api/
error) {
I think I'd call this AgentVersion.
We might have different ideas of API version later.
https:/
state/api/
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:/
state/api/
I'd be tempted to name this websocketDialCo
we're
mocking.
// websocketDialConfig is called instead of websocket.
// so we can override it in tests.
?
https:/
state/api/
is a connection error.
// IsConnectionError reports whether the error is a connection
// error.
https:/
state/api/
Please can we document this type and its fields?
This is the public face of the Go Juju API.
https:/
state/api/
the given number of
The last sentence doesn't seem to fit here.
https:/
state/api/
// 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:/
state/api/
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:/
state/api/
[]string{
attrs.Set(
https:/
state/api/
[]string{
attrs.Set(
etc
https:/
state/api/
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...
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.
Tim Penhey (thumper) wrote : | # |
Please take a look.
https:/
File state/api/client.go (right):
https:/
state/api/
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:/
state/api/
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:/
state/api/
On 2014/04/09 10:28:40, rog wrote:
> I'd be tempted to name this websocketDialCo
we're
> mocking.
> // websocketDialConfig is called instead of websocket.
> // so we can override it in tests.
> ?
Done.
https:/
state/api/
is a connection error.
On 2014/04/09 10:28:40, rog wrote:
> // IsConnectionError reports whether the error is a connection
> // error.
Done.
https:/
state/api/
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:/
state/api/
the given number of
On 2014/04/09 10:28:40, rog wrote:
> The last sentence doesn't seem to fit here.
Rewritten
https:/
state/api/
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:/
state/api/
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...
Tim Penhey (thumper) wrote : | # |
Please take a look.
Roger Peppe (rogpeppe) wrote : | # |
LGTM with a few trivials.
Thanks!
https:/
File state/api/
https:/
state/api/
func(*websocket
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
{
> > message, err := json.Marshal(
> > 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:/
File state/api/client.go (right):
https:/
state/api/
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:/
state/api/
included.
s/ / /
Does the inclusion of a logging module here imply all its sub-modules
too?
https:/
state/api/
exclude from the resposne.
ditto
https:/
state/api/
// If zero, there is no limit.
?
https:/
state/api/
ignored.
Nice. I started commenting on Backlog to say "but what if I want no
lines of backlog?" - then I saw this.
https:/
state/api/
Just realised - we probably want %q here so that we don't get a blank
line in the log.
https:/
File state/api/
https:/
state/api/
err}, nil
or ioutil.
then you can lose the ReadCloser field inside badReader.
https:/
state/api/
func(*websocket
Tim Penhey (thumper) wrote : | # |
Please take a look.
https:/
File state/api/client.go (right):
https:/
state/api/
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:/
state/api/
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:/
state/api/
On 2014/04/10 07:04:18, rog wrote:
> // If zero, there is no limit.
> ?
Updated.
https:/
state/api/
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:/
File state/api/
https:/
state/api/
err}, nil
On 2014/04/10 07:04:18, rog wrote:
> or ioutil.
> then you can lose the ReadCloser field inside badReader.
Good call.
https:/
state/api/
func(*websocket
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:/
File state/api/
https:/
state/api/
On 2014/04/10 07:04:18, rog wrote:
> s/apiserver/agent running the API server./
> ?
Done.
https:/
File state/apiserver
https:/
state/apiserver
current version that the api server is running.
On 2014/04/10 07:04:18, rog wrote:
> s/api/API/
Done.
Preview Diff
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 := ¶ms.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 | +} |
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): apiclient. go client_ test.go export_ test.go /client/ client. go /client/ client_ test.go /debuglog_ test.go
A [revision details]
M state/api/
M state/api/client.go
M state/api/
M state/api/
M state/apiserver
M state/apiserver
M state/apiserver
M utils/http.go
M utils/http_test.go