launchpad.net/~rogpeppe/juju-core/500-errgo-fix@v0.0.0-20140213181702-000000002356/state/api/apiclient.go (about) 1 // Copyright 2012, 2013 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package api 5 6 import ( 7 "crypto/tls" 8 "crypto/x509" 9 "time" 10 11 "code.google.com/p/go.net/websocket" 12 13 "launchpad.net/errgo/errors" 14 "launchpad.net/juju-core/cert" 15 "launchpad.net/juju-core/log" 16 "launchpad.net/juju-core/rpc" 17 "launchpad.net/juju-core/rpc/jsoncodec" 18 "launchpad.net/juju-core/state/api/params" 19 "launchpad.net/juju-core/utils" 20 ) 21 22 var mask = errors.Mask 23 24 // PingPeriod defines how often the internal connection health check 25 // will run. It's a variable so it can be changed in tests. 26 var PingPeriod = 1 * time.Minute 27 28 type State struct { 29 client *rpc.Conn 30 conn *websocket.Conn 31 32 // authTag holds the authenticated entity's tag after login. 33 authTag string 34 35 // broken is a channel that gets closed when the connection is 36 // broken. 37 broken chan struct{} 38 39 // tag and password hold the cached login credentials. 40 tag string 41 password string 42 // serverRoot holds the cached API server address and port we used 43 // to login, with a https:// prefix. 44 serverRoot string 45 } 46 47 // Info encapsulates information about a server holding juju state and 48 // can be used to make a connection to it. 49 type Info struct { 50 // Addrs holds the addresses of the state servers. 51 Addrs []string 52 53 // CACert holds the CA certificate that will be used 54 // to validate the state server's certificate, in PEM format. 55 CACert []byte 56 57 // Tag holds the name of the entity that is connecting. 58 // If this and the password are empty, no login attempt will be made 59 // (this is to allow tests to access the API to check that operations 60 // fail when not logged in). 61 Tag string 62 63 // Password holds the password for the administrator or connecting entity. 64 Password string 65 66 // Nonce holds the nonce used when provisioning the machine. Used 67 // only by the machine agent. 68 Nonce string `yaml:",omitempty"` 69 } 70 71 var openAttempt = utils.AttemptStrategy{ 72 Total: 5 * time.Minute, 73 Delay: 500 * time.Millisecond, 74 } 75 76 // DialOpts holds configuration parameters that control the 77 // Dialing behavior when connecting to a state server. 78 type DialOpts struct { 79 // Timeout is the amount of time to wait contacting 80 // a state server. 81 Timeout time.Duration 82 83 // RetryDelay is the amount of time to wait between 84 // unsucssful connection attempts. 85 RetryDelay time.Duration 86 } 87 88 // DefaultDialOpts returns a DialOpts representing the default 89 // parameters for contacting a state server. 90 func DefaultDialOpts() DialOpts { 91 return DialOpts{ 92 Timeout: 10 * time.Minute, 93 RetryDelay: 2 * time.Second, 94 } 95 } 96 97 func Open(info *Info, opts DialOpts) (*State, error) { 98 // TODO Select a random address from info.Addrs 99 // and only fail when we've tried all the addresses. 100 // TODO what does "origin" really mean, and is localhost always ok? 101 cfg, err := websocket.NewConfig("wss://"+info.Addrs[0]+"/", "http://localhost/") 102 if err != nil { 103 return nil, mask(err) 104 } 105 pool := x509.NewCertPool() 106 xcert, err := cert.ParseCert(info.CACert) 107 if err != nil { 108 return nil, mask(err) 109 } 110 pool.AddCert(xcert) 111 cfg.TlsConfig = &tls.Config{ 112 RootCAs: pool, 113 ServerName: "anything", 114 } 115 var conn *websocket.Conn 116 openAttempt := utils.AttemptStrategy{ 117 Total: opts.Timeout, 118 Delay: opts.RetryDelay, 119 } 120 for a := openAttempt.Start(); a.Next(); { 121 log.Infof("state/api: dialing %q", cfg.Location) 122 conn, err = websocket.DialConfig(cfg) 123 if err == nil { 124 break 125 } 126 log.Errorf("state/api: %v", err) 127 } 128 if err != nil { 129 return nil, mask(err) 130 } 131 log.Infof("state/api: connection established") 132 133 client := rpc.NewConn(jsoncodec.NewWebsocket(conn), nil) 134 client.Start() 135 st := &State{ 136 client: client, 137 conn: conn, 138 serverRoot: "https://" + cfg.Location.Host, 139 tag: info.Tag, 140 password: info.Password, 141 } 142 if info.Tag != "" || info.Password != "" { 143 if err := st.Login(info.Tag, info.Password, info.Nonce); err != nil { 144 conn.Close() 145 return nil, err 146 } 147 } 148 st.broken = make(chan struct{}) 149 go st.heartbeatMonitor() 150 return st, nil 151 } 152 153 func (s *State) heartbeatMonitor() { 154 for { 155 if err := s.Ping(); err != nil { 156 close(s.broken) 157 return 158 } 159 time.Sleep(PingPeriod) 160 } 161 } 162 163 func (s *State) Ping() error { 164 return s.Call("Pinger", "", "Ping", nil, nil) 165 } 166 167 // Call invokes a low-level RPC method of the given objType, id, and 168 // request, passing the given parameters and filling in the response 169 // results. This should not be used directly by clients. 170 // TODO (dimitern) Add tests for all client-facing objects to verify 171 // we return the correct error when invoking Call("Object", 172 // "non-empty-id",...) 173 func (s *State) Call(objType, id, request string, args, response interface{}) error { 174 err := s.client.Call(rpc.Request{ 175 Type: objType, 176 Id: id, 177 Action: request, 178 }, args, response) 179 return params.ClientError(err) 180 } 181 182 func (s *State) Close() error { 183 return s.client.Close() 184 } 185 186 // Broken returns a channel that's closed when the connection is broken. 187 func (s *State) Broken() <-chan struct{} { 188 return s.broken 189 } 190 191 // RPCClient returns the RPC client for the state, so that testing 192 // functions can tickle parts of the API that the conventional entry 193 // points don't reach. This is exported for testing purposes only. 194 func (s *State) RPCClient() *rpc.Conn { 195 return s.client 196 }