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
=== modified file 'state/api/apiclient.go'
--- state/api/apiclient.go 2014-04-09 15:17:04 +0000
+++ state/api/apiclient.go 2014-04-10 09:38:03 +0000
@@ -56,6 +56,10 @@
56 // serverRoot holds the cached API server address and port we used56 // serverRoot holds the cached API server address and port we used
57 // to login, with a https:// prefix.57 // to login, with a https:// prefix.
58 serverRoot string58 serverRoot string
59
60 // certPool holds the cert pool that is used to authenticate the tls
61 // connections to the API.
62 certPool *x509.CertPool
59}63}
6064
61// Info encapsulates information about a server holding juju state and65// Info encapsulates information about a server holding juju state and
@@ -151,6 +155,7 @@
151 serverRoot: "https://" + conn.Config().Location.Host,155 serverRoot: "https://" + conn.Config().Location.Host,
152 tag: info.Tag,156 tag: info.Tag,
153 password: info.Password,157 password: info.Password,
158 certPool: pool,
154 }159 }
155 if info.Tag != "" || info.Password != "" {160 if info.Tag != "" || info.Password != "" {
156 if err := st.Login(info.Tag, info.Password, info.Nonce); err != nil {161 if err := st.Login(info.Tag, info.Password, info.Nonce); err != nil {
157162
=== modified file 'state/api/client.go'
--- state/api/client.go 2014-04-10 08:47:38 +0000
+++ state/api/client.go 2014-04-10 09:38:03 +0000
@@ -4,14 +4,20 @@
4package api4package api
55
6import (6import (
7 "crypto/tls"
7 "encoding/json"8 "encoding/json"
8 "fmt"9 "fmt"
10 "io"
9 "io/ioutil"11 "io/ioutil"
10 "net/http"12 "net/http"
13 "net/url"
11 "os"14 "os"
12 "strings"15 "strings"
13 "time"16 "time"
1417
18 "code.google.com/p/go.net/websocket"
19 "github.com/juju/loggo"
20
15 "launchpad.net/juju-core/charm"21 "launchpad.net/juju-core/charm"
16 "launchpad.net/juju-core/constraints"22 "launchpad.net/juju-core/constraints"
17 "launchpad.net/juju-core/instance"23 "launchpad.net/juju-core/instance"
@@ -703,3 +709,128 @@
703 }709 }
704 return c.call("EnsureAvailability", args, nil)710 return c.call("EnsureAvailability", args, nil)
705}711}
712
713// AgentVersion reports the version number of the api server.
714func (c *Client) AgentVersion() (version.Number, error) {
715 var result params.AgentVersionResult
716 if err := c.call("AgentVersion", nil, &result); err != nil {
717 return version.Number{}, err
718 }
719 return result.Version, nil
720}
721
722// websocketDialConfig is called instead of websocket.DialConfig so we can
723// override it in tests.
724var websocketDialConfig = func(config *websocket.Config) (io.ReadCloser, error) {
725 return websocket.DialConfig(config)
726}
727
728type connectionError struct {
729 error
730}
731
732// IsConnectionError reports whether the error is a connection error.
733func IsConnectionError(err error) bool {
734 _, ok := err.(*connectionError)
735 return ok
736}
737
738// DebugLogParams holds parameters for WatchDebugLog that control the
739// filtering of the log messages. If the structure is zero initialized, the
740// entire log file is sent back starting from the end, and until the user
741// closes the connection.
742type DebugLogParams struct {
743 // IncludeEntity lists entity tags to include in the response. Tags may
744 // finish with a '*' to match a prefix e.g.: unit-mysql-*, machine-2. If
745 // none are set, then all lines are considered included.
746 IncludeEntity []string
747 // IncludeModule lists logging modules to include in the response. If none
748 // are set all modules are considered included. If a module is specified,
749 // all the submodules also match.
750 IncludeModule []string
751 // ExcludeEntity lists entity tags to exclude from the response. As with
752 // IncludeEntity the values may finish with a '*'.
753 ExcludeEntity []string
754 // ExcludeModule lists logging modules to exclude from the resposne. If a
755 // module is specified, all the submodules are also excluded.
756 ExcludeModule []string
757 // Limit defines the maximum number of lines to return. Once this many
758 // have been sent, the socket is closed. If zero, all filtered lines are
759 // sent down the connection until the client closes the connection.
760 Limit uint
761 // Backlog tells the server to try to go back this many lines before
762 // starting filtering. If backlog is zero and replay is false, then there
763 // may be an initial delay until the next matching log message is written.
764 Backlog uint
765 // Level specifies the minimum logging level to be sent back in the response.
766 Level loggo.Level
767 // Replay tells the server to start at the start of the log file rather
768 // than the end. If replay is true, backlog is ignored.
769 Replay bool
770}
771
772// WatchDebugLog returns a ReadCloser that the caller can read the log lines
773// from. Only log lines that match the filtering specified in the
774// DebugLogParams are returned. It returns an error that satisfies
775// IsConnectionError when the connection cannot be made.
776func (c *Client) WatchDebugLog(args DebugLogParams) (io.ReadCloser, error) {
777 // The websocket connection just hangs if the server doesn't have the log
778 // end point. So do a version check, as version was added at the same time
779 // as the remote end point.
780 _, err := c.AgentVersion()
781 if err != nil {
782 return nil, &connectionError{fmt.Errorf("server doesn't support debug log websocket")}
783 }
784 // Prepare URL.
785 attrs := url.Values{}
786 if args.Replay {
787 attrs.Set("replay", fmt.Sprint(args.Replay))
788 }
789 if args.Limit > 0 {
790 attrs.Set("maxLines", fmt.Sprint(args.Limit))
791 }
792 if args.Backlog > 0 {
793 attrs.Set("backlog", fmt.Sprint(args.Backlog))
794 }
795 if args.Level != loggo.UNSPECIFIED {
796 attrs.Set("level", fmt.Sprint(args.Level))
797 }
798 attrs["includeEntity"] = args.IncludeEntity
799 attrs["includeModule"] = args.IncludeModule
800 attrs["excludeEntity"] = args.ExcludeEntity
801 attrs["excludeModule"] = args.ExcludeModule
802
803 target := url.URL{
804 Scheme: "wss",
805 Host: c.st.addr,
806 Path: "/log",
807 RawQuery: attrs.Encode(),
808 }
809 cfg, err := websocket.NewConfig(target.String(), "http://localhost/")
810 cfg.Header = utils.BasicAuthHeader(c.st.tag, c.st.password)
811 cfg.TlsConfig = &tls.Config{RootCAs: c.st.certPool, ServerName: "anything"}
812 connection, err := websocketDialConfig(cfg)
813 if err != nil {
814 return nil, &connectionError{err}
815 }
816 // Read the initial error and translate to a real error.
817 // Read up to the first new line character. We can't use bufio here as it
818 // reads too much from the reader.
819 line := make([]byte, 4096)
820 n, err := connection.Read(line)
821 if err != nil {
822 return nil, fmt.Errorf("unable to read initial response: %v", err)
823 }
824 line = line[0:n]
825
826 logger.Debugf("initial line: %q", line)
827 var errResult params.ErrorResult
828 err = json.Unmarshal(line, &errResult)
829 if err != nil {
830 return nil, fmt.Errorf("unable to unmarshal initial response: %v", err)
831 }
832 if errResult.Error != nil {
833 return nil, errResult.Error
834 }
835 return connection, nil
836}
706837
=== modified file 'state/api/client_test.go'
--- state/api/client_test.go 2014-04-01 15:28:49 +0000
+++ state/api/client_test.go 2014-04-10 09:38:03 +0000
@@ -4,10 +4,19 @@
4package api_test4package api_test
55
6import (6import (
7 "bufio"
8 "bytes"
9 "encoding/json"
7 "fmt"10 "fmt"
11 "io"
12 "io/ioutil"
8 "net"13 "net"
9 "net/http"14 "net/http"
15 "net/url"
16 "strings"
1017
18 "code.google.com/p/go.net/websocket"
19 "github.com/juju/loggo"
11 jc "github.com/juju/testing/checkers"20 jc "github.com/juju/testing/checkers"
12 gc "launchpad.net/gocheck"21 gc "launchpad.net/gocheck"
1322
@@ -83,3 +92,114 @@
83 _, err = client.AddLocalCharm(curl, charmArchive)92 _, err = client.AddLocalCharm(curl, charmArchive)
84 c.Assert(err, jc.Satisfies, params.IsCodeNotImplemented)93 c.Assert(err, jc.Satisfies, params.IsCodeNotImplemented)
85}94}
95
96func (s *clientSuite) TestWatchDebugLogConnected(c *gc.C) {
97 // Shows both the unmarshalling of a real error, and
98 // that the api server is connected.
99 client := s.APIState.Client()
100 reader, err := client.WatchDebugLog(api.DebugLogParams{})
101 c.Assert(err, gc.ErrorMatches, "cannot open log file: .*")
102 c.Assert(reader, gc.IsNil)
103}
104
105func (s *clientSuite) TestConnectionErrorBadConnection(c *gc.C) {
106 s.PatchValue(api.WebsocketDialConfig, func(_ *websocket.Config) (io.ReadCloser, error) {
107 return nil, fmt.Errorf("bad connection")
108 })
109 client := s.APIState.Client()
110 reader, err := client.WatchDebugLog(api.DebugLogParams{})
111 c.Assert(err, gc.ErrorMatches, "bad connection")
112 c.Assert(reader, gc.IsNil)
113}
114
115func (s *clientSuite) TestConnectionErrorNoData(c *gc.C) {
116 s.PatchValue(api.WebsocketDialConfig, func(_ *websocket.Config) (io.ReadCloser, error) {
117 return ioutil.NopCloser(&bytes.Buffer{}), nil
118 })
119 client := s.APIState.Client()
120 reader, err := client.WatchDebugLog(api.DebugLogParams{})
121 c.Assert(err, gc.ErrorMatches, "unable to read initial response: EOF")
122 c.Assert(reader, gc.IsNil)
123}
124
125func (s *clientSuite) TestConnectionErrorBadData(c *gc.C) {
126 s.PatchValue(api.WebsocketDialConfig, func(_ *websocket.Config) (io.ReadCloser, error) {
127 junk := strings.NewReader("junk\n")
128 return ioutil.NopCloser(junk), nil
129 })
130 client := s.APIState.Client()
131 reader, err := client.WatchDebugLog(api.DebugLogParams{})
132 c.Assert(err, gc.ErrorMatches, "unable to unmarshal initial response: .*")
133 c.Assert(reader, gc.IsNil)
134}
135
136func (s *clientSuite) TestConnectionErrorReadError(c *gc.C) {
137 s.PatchValue(api.WebsocketDialConfig, func(_ *websocket.Config) (io.ReadCloser, error) {
138 err := fmt.Errorf("bad read")
139 return ioutil.NopCloser(&badReader{err}), nil
140 })
141 client := s.APIState.Client()
142 reader, err := client.WatchDebugLog(api.DebugLogParams{})
143 c.Assert(err, gc.ErrorMatches, "unable to read initial response: bad read")
144 c.Assert(reader, gc.IsNil)
145}
146
147func (s *clientSuite) TestParamsEncoded(c *gc.C) {
148 s.PatchValue(api.WebsocketDialConfig, echoURL(c))
149
150 params := api.DebugLogParams{
151 IncludeEntity: []string{"a", "b"},
152 IncludeModule: []string{"c", "d"},
153 ExcludeEntity: []string{"e", "f"},
154 ExcludeModule: []string{"g", "h"},
155 Limit: 100,
156 Backlog: 200,
157 Level: loggo.ERROR,
158 Replay: true,
159 }
160
161 client := s.APIState.Client()
162 reader, err := client.WatchDebugLog(params)
163 c.Assert(err, gc.IsNil)
164
165 bufReader := bufio.NewReader(reader)
166 location, err := bufReader.ReadString('\n')
167 c.Assert(err, gc.IsNil)
168 connectUrl, err := url.Parse(strings.TrimSpace(location))
169 c.Assert(err, gc.IsNil)
170
171 values := connectUrl.Query()
172 c.Assert(values, jc.DeepEquals, url.Values{
173 "includeEntity": params.IncludeEntity,
174 "includeModule": params.IncludeModule,
175 "excludeEntity": params.ExcludeEntity,
176 "excludeModule": params.ExcludeModule,
177 "maxLines": {"100"},
178 "backlog": {"200"},
179 "level": {"ERROR"},
180 "replay": {"true"},
181 })
182}
183
184// badReader raises err when Read is called.
185type badReader struct {
186 err error
187}
188
189func (r *badReader) Read(p []byte) (n int, err error) {
190 return 0, r.err
191}
192
193func echoURL(c *gc.C) func(*websocket.Config) (io.ReadCloser, error) {
194 response := &params.ErrorResult{}
195 message, err := json.Marshal(response)
196 c.Assert(err, gc.IsNil)
197 return func(config *websocket.Config) (io.ReadCloser, error) {
198 pr, pw := io.Pipe()
199 go func() {
200 fmt.Fprintf(pw, "%s\n", message)
201 fmt.Fprintf(pw, "%s\n", config.Location)
202 }()
203 return pr, nil
204 }
205}
86206
=== modified file 'state/api/export_test.go'
--- state/api/export_test.go 2014-04-01 03:13:49 +0000
+++ state/api/export_test.go 2014-04-10 09:38:03 +0000
@@ -5,6 +5,8 @@
55
6var (6var (
7 NewWebsocketDialer = newWebsocketDialer7 NewWebsocketDialer = newWebsocketDialer
8
9 WebsocketDialConfig = &websocketDialConfig
8)10)
911
10// SetServerRoot allows changing the URL to the internal API server12// SetServerRoot allows changing the URL to the internal API server
1113
=== modified file 'state/api/params/internal.go'
--- state/api/params/internal.go 2014-04-09 15:08:51 +0000
+++ state/api/params/internal.go 2014-04-10 09:38:03 +0000
@@ -572,3 +572,9 @@
572type RunResults struct {572type RunResults struct {
573 Results []RunResult573 Results []RunResult
574}574}
575
576// AgentVersionResult is used to return the current version number of the
577// agent running the API server.
578type AgentVersionResult struct {
579 Version version.Number
580}
575581
=== modified file 'state/apiserver/client/client.go'
--- state/apiserver/client/client.go 2014-04-10 08:47:38 +0000
+++ state/apiserver/client/client.go 2014-04-10 09:38:03 +0000
@@ -28,6 +28,7 @@
28 "launchpad.net/juju-core/state/statecmd"28 "launchpad.net/juju-core/state/statecmd"
29 coretools "launchpad.net/juju-core/tools"29 coretools "launchpad.net/juju-core/tools"
30 "launchpad.net/juju-core/utils"30 "launchpad.net/juju-core/utils"
31 "launchpad.net/juju-core/version"
31)32)
3233
33var logger = loggo.GetLogger("juju.state.apiserver.client")34var logger = loggo.GetLogger("juju.state.apiserver.client")
@@ -782,6 +783,11 @@
782 return changes, nil783 return changes, nil
783}784}
784785
786// AgentVersion returns the current version that the API server is running.
787func (c *Client) AgentVersion() (params.AgentVersionResult, error) {
788 return params.AgentVersionResult{Version: version.Current.Number}, nil
789}
790
785// EnvironmentGet implements the server-side part of the791// EnvironmentGet implements the server-side part of the
786// get-environment CLI command.792// get-environment CLI command.
787func (c *Client) EnvironmentGet() (params.EnvironmentGetResults, error) {793func (c *Client) EnvironmentGet() (params.EnvironmentGetResults, error) {
788794
=== modified file 'state/apiserver/client/client_test.go'
--- state/apiserver/client/client_test.go 2014-04-10 08:47:38 +0000
+++ state/apiserver/client/client_test.go 2014-04-10 09:38:03 +0000
@@ -2211,3 +2211,11 @@
2211 c.Assert(err, gc.IsNil)2211 c.Assert(err, gc.IsNil)
2212 c.Assert(apiHostPorts, gc.DeepEquals, stateAPIHostPorts)2212 c.Assert(apiHostPorts, gc.DeepEquals, stateAPIHostPorts)
2213}2213}
2214
2215func (s *clientSuite) TestClientAgentVersion(c *gc.C) {
2216 current := version.MustParse("1.2.0")
2217 s.PatchValue(&version.Current.Number, current)
2218 result, err := s.APIState.Client().AgentVersion()
2219 c.Assert(err, gc.IsNil)
2220 c.Assert(result, gc.Equals, current)
2221}
22142222
=== modified file 'state/apiserver/debuglog.go'
--- state/apiserver/debuglog.go 2014-04-07 04:58:59 +0000
+++ state/apiserver/debuglog.go 2014-04-10 09:38:03 +0000
@@ -41,7 +41,7 @@
41// - as with include, it may finish with a '*'41// - as with include, it may finish with a '*'
42// excludeModule -> []string - lists logging modules to exclude from the response42// excludeModule -> []string - lists logging modules to exclude from the response
43// limit -> uint - show *at most* this many lines43// limit -> uint - show *at most* this many lines
44// backtrack -> uint44// backlog -> uint
45// - go back this many lines from the end before starting to filter45// - go back this many lines from the end before starting to filter
46// - has no meaning if 'replay' is true46// - has no meaning if 'replay' is true
47// level -> string one of [TRACE, DEBUG, INFO, WARNING, ERROR]47// level -> string one of [TRACE, DEBUG, INFO, WARNING, ERROR]
4848
=== modified file 'state/apiserver/debuglog_test.go'
--- state/apiserver/debuglog_test.go 2014-04-07 04:33:18 +0000
+++ state/apiserver/debuglog_test.go 2014-04-10 09:38:03 +0000
@@ -7,7 +7,6 @@
7 "bufio"7 "bufio"
8 "crypto/tls"8 "crypto/tls"
9 "crypto/x509"9 "crypto/x509"
10 "encoding/base64"
11 "encoding/json"10 "encoding/json"
12 "io"11 "io"
13 "net/http"12 "net/http"
@@ -22,6 +21,7 @@
2221
23 "launchpad.net/juju-core/state/api/params"22 "launchpad.net/juju-core/state/api/params"
24 "launchpad.net/juju-core/testing"23 "launchpad.net/juju-core/testing"
24 "launchpad.net/juju-core/utils"
25)25)
2626
27type debugLogSuite struct {27type debugLogSuite struct {
@@ -191,9 +191,7 @@
191}191}
192192
193func (s *debugLogSuite) dialWebsocket(c *gc.C, queryParams url.Values) (*websocket.Conn, error) {193func (s *debugLogSuite) dialWebsocket(c *gc.C, queryParams url.Values) (*websocket.Conn, error) {
194 header := http.Header{194 header := utils.BasicAuthHeader(s.userTag, s.password)
195 "Authorization": {"Basic " + base64.StdEncoding.EncodeToString([]byte(s.userTag+":"+s.password))},
196 }
197 return s.dialWebsocketInternal(c, queryParams, header)195 return s.dialWebsocketInternal(c, queryParams, header)
198}196}
199197
200198
=== modified file 'utils/http.go'
--- utils/http.go 2014-03-24 03:24:00 +0000
+++ utils/http.go 2014-04-10 09:38:03 +0000
@@ -5,6 +5,7 @@
55
6import (6import (
7 "crypto/tls"7 "crypto/tls"
8 "encoding/base64"
8 "net/http"9 "net/http"
9 "sync"10 "sync"
10)11)
@@ -83,3 +84,18 @@
83 registerFileProtocol(transport)84 registerFileProtocol(transport)
84 return transport85 return transport
85}86}
87
88// BasicAuthHeader creates a header that contains just the "Authorization"
89// entry. The implementation was originally taked from net/http but this is
90// needed externally from the http request object in order to use this with
91// our websockets. See 2 (end of page 4) http://www.ietf.org/rfc/rfc2617.txt
92// "To receive authorization, the client sends the userid and password,
93// separated by a single colon (":") character, within a base64 encoded string
94// in the credentials."
95func BasicAuthHeader(username, password string) http.Header {
96 auth := username + ":" + password
97 encoded := "Basic " + base64.StdEncoding.EncodeToString([]byte(auth))
98 return http.Header{
99 "Authorization": {encoded},
100 }
101}
86102
=== modified file 'utils/http_test.go'
--- utils/http_test.go 2014-03-24 00:23:19 +0000
+++ utils/http_test.go 2014-04-10 09:38:03 +0000
@@ -4,10 +4,12 @@
4package utils_test4package utils_test
55
6import (6import (
7 "encoding/base64"
7 "fmt"8 "fmt"
8 "io/ioutil"9 "io/ioutil"
9 "net/http"10 "net/http"
10 "net/http/httptest"11 "net/http/httptest"
12 "strings"
1113
12 gc "launchpad.net/gocheck"14 gc "launchpad.net/gocheck"
1315
@@ -79,3 +81,16 @@
79 client2 := utils.GetNonValidatingHTTPClient()81 client2 := utils.GetNonValidatingHTTPClient()
80 c.Check(client1, gc.Equals, client2)82 c.Check(client1, gc.Equals, client2)
81}83}
84
85func (s *httpSuite) TestBasicAuthHeader(c *gc.C) {
86 header := utils.BasicAuthHeader("eric", "sekrit")
87 c.Assert(len(header), gc.Equals, 1)
88 auth := header.Get("Authorization")
89 fields := strings.Fields(auth)
90 c.Assert(len(fields), gc.Equals, 2)
91 basic, encoded := fields[0], fields[1]
92 c.Assert(basic, gc.Equals, "Basic")
93 decoded, err := base64.StdEncoding.DecodeString(encoded)
94 c.Assert(err, gc.IsNil)
95 c.Assert(string(decoded), gc.Equals, "eric:sekrit")
96}

Subscribers

People subscribed via source and target branches

to status/vote changes: