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