github.com/altoros/juju-vmware@v0.0.0-20150312064031-f19ae857ccca/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 "fmt" 10 "io" 11 "strings" 12 "time" 13 14 "code.google.com/p/go.net/websocket" 15 "github.com/juju/loggo" 16 "github.com/juju/names" 17 "github.com/juju/utils" 18 "github.com/juju/utils/parallel" 19 20 "github.com/juju/juju/apiserver/params" 21 "github.com/juju/juju/cert" 22 "github.com/juju/juju/network" 23 "github.com/juju/juju/rpc" 24 "github.com/juju/juju/rpc/jsoncodec" 25 ) 26 27 var logger = loggo.GetLogger("juju.api") 28 29 // PingPeriod defines how often the internal connection health check 30 // will run. It's a variable so it can be changed in tests. 31 var PingPeriod = 1 * time.Minute 32 33 type State struct { 34 client *rpc.Conn 35 conn *websocket.Conn 36 37 // addr is the address used to connect to the API server. 38 addr string 39 40 // environTag holds the environment tag once we're connected 41 environTag string 42 43 // hostPorts is the API server addresses returned from Login, 44 // which the client may cache and use for failover. 45 hostPorts [][]network.HostPort 46 47 // facadeVersions holds the versions of all facades as reported by 48 // Login 49 facadeVersions map[string][]int 50 51 // authTag holds the authenticated entity's tag after login. 52 authTag names.Tag 53 54 // broken is a channel that gets closed when the connection is 55 // broken. 56 broken chan struct{} 57 58 // closed is a channel that gets closed when State.Close is called. 59 closed chan struct{} 60 61 // tag and password hold the cached login credentials. 62 tag string 63 password string 64 65 // serverRoot holds the cached API server address and port we used 66 // to login, with a https:// prefix. 67 serverRoot string 68 69 // certPool holds the cert pool that is used to authenticate the tls 70 // connections to the API. 71 certPool *x509.CertPool 72 } 73 74 // Info encapsulates information about a server holding juju state and 75 // can be used to make a connection to it. 76 type Info struct { 77 // Addrs holds the addresses of the state servers. 78 Addrs []string 79 80 // CACert holds the CA certificate that will be used 81 // to validate the state server's certificate, in PEM format. 82 CACert string 83 84 // Tag holds the name of the entity that is connecting. 85 // If this is nil, and the password are empty, no login attempt will be made. 86 // (this is to allow tests to access the API to check that operations 87 // fail when not logged in). 88 Tag names.Tag 89 90 // Password holds the password for the administrator or connecting entity. 91 Password string 92 93 // Nonce holds the nonce used when provisioning the machine. Used 94 // only by the machine agent. 95 Nonce string `yaml:",omitempty"` 96 97 // EnvironTag holds the environ tag for the environment we are 98 // trying to connect to. 99 EnvironTag names.EnvironTag 100 } 101 102 // DialOpts holds configuration parameters that control the 103 // Dialing behavior when connecting to a state server. 104 type DialOpts struct { 105 // DialAddressInterval is the amount of time to wait 106 // before starting to dial another address. 107 DialAddressInterval time.Duration 108 109 // Timeout is the amount of time to wait contacting 110 // a state server. 111 Timeout time.Duration 112 113 // RetryDelay is the amount of time to wait between 114 // unsucssful connection attempts. 115 RetryDelay time.Duration 116 } 117 118 // DefaultDialOpts returns a DialOpts representing the default 119 // parameters for contacting a state server. 120 func DefaultDialOpts() DialOpts { 121 return DialOpts{ 122 DialAddressInterval: 50 * time.Millisecond, 123 Timeout: 10 * time.Minute, 124 RetryDelay: 2 * time.Second, 125 } 126 } 127 128 func Open(info *Info, opts DialOpts) (*State, error) { 129 if len(info.Addrs) == 0 { 130 return nil, fmt.Errorf("no API addresses to connect to") 131 } 132 pool := x509.NewCertPool() 133 xcert, err := cert.ParseCert(info.CACert) 134 if err != nil { 135 return nil, err 136 } 137 pool.AddCert(xcert) 138 139 var environUUID string 140 if info.EnvironTag.Id() != "" { 141 environUUID = info.EnvironTag.Id() 142 } 143 // Dial all addresses at reasonable intervals. 144 try := parallel.NewTry(0, nil) 145 defer try.Kill() 146 var addrs []string 147 for _, addr := range info.Addrs { 148 if strings.HasPrefix(addr, "localhost:") { 149 addrs = append(addrs, addr) 150 break 151 } 152 } 153 if len(addrs) == 0 { 154 addrs = info.Addrs 155 } 156 for _, addr := range addrs { 157 err := dialWebsocket(addr, environUUID, opts, pool, try) 158 if err == parallel.ErrStopped { 159 break 160 } 161 if err != nil { 162 return nil, err 163 } 164 select { 165 case <-time.After(opts.DialAddressInterval): 166 case <-try.Dead(): 167 } 168 } 169 try.Close() 170 result, err := try.Result() 171 if err != nil { 172 return nil, err 173 } 174 conn := result.(*websocket.Conn) 175 logger.Infof("connection established to %q", conn.RemoteAddr()) 176 177 client := rpc.NewConn(jsoncodec.NewWebsocket(conn), nil) 178 client.Start() 179 st := &State{ 180 client: client, 181 conn: conn, 182 addr: conn.Config().Location.Host, 183 serverRoot: "https://" + conn.Config().Location.Host, 184 // why are the contents of the tag (username and password) written into the 185 // state structure BEFORE login ?!? 186 tag: toString(info.Tag), 187 password: info.Password, 188 certPool: pool, 189 } 190 if info.Tag != nil || info.Password != "" { 191 if err := st.Login(info.Tag.String(), info.Password, info.Nonce); err != nil { 192 conn.Close() 193 return nil, err 194 } 195 } 196 st.broken = make(chan struct{}) 197 st.closed = make(chan struct{}) 198 go st.heartbeatMonitor() 199 return st, nil 200 } 201 202 // toString returns the value of a tag's String method, or "" if the tag is nil. 203 func toString(tag names.Tag) string { 204 if tag == nil { 205 return "" 206 } 207 return tag.String() 208 } 209 210 func dialWebsocket(addr, environUUID string, opts DialOpts, rootCAs *x509.CertPool, try *parallel.Try) error { 211 cfg, err := setUpWebsocket(addr, environUUID, rootCAs) 212 if err != nil { 213 return err 214 } 215 return try.Start(newWebsocketDialer(cfg, opts)) 216 } 217 218 func setUpWebsocket(addr, environUUID string, rootCAs *x509.CertPool) (*websocket.Config, error) { 219 // origin is required by the WebSocket API, used for "origin policy" 220 // in websockets. We pass localhost to satisfy the API; it is 221 // inconsequential to us. 222 const origin = "http://localhost/" 223 tail := "/" 224 if environUUID != "" { 225 tail = "/environment/" + environUUID + "/api" 226 } 227 cfg, err := websocket.NewConfig("wss://"+addr+tail, origin) 228 if err != nil { 229 return nil, err 230 } 231 cfg.TlsConfig = &tls.Config{ 232 RootCAs: rootCAs, 233 ServerName: "juju-apiserver", 234 } 235 return cfg, nil 236 } 237 238 // newWebsocketDialer returns a function that 239 // can be passed to utils/parallel.Try.Start. 240 func newWebsocketDialer(cfg *websocket.Config, opts DialOpts) func(<-chan struct{}) (io.Closer, error) { 241 openAttempt := utils.AttemptStrategy{ 242 Total: opts.Timeout, 243 Delay: opts.RetryDelay, 244 } 245 return func(stop <-chan struct{}) (io.Closer, error) { 246 for a := openAttempt.Start(); a.Next(); { 247 select { 248 case <-stop: 249 return nil, parallel.ErrStopped 250 default: 251 } 252 logger.Infof("dialing %q", cfg.Location) 253 conn, err := websocket.DialConfig(cfg) 254 if err == nil { 255 return conn, nil 256 } 257 if a.HasNext() { 258 logger.Debugf("error dialing %q, will retry: %v", cfg.Location, err) 259 } else { 260 logger.Infof("error dialing %q: %v", cfg.Location, err) 261 return nil, fmt.Errorf("unable to connect to %q", cfg.Location) 262 } 263 } 264 panic("unreachable") 265 } 266 } 267 268 func (s *State) heartbeatMonitor() { 269 for { 270 if err := s.Ping(); err != nil { 271 close(s.broken) 272 return 273 } 274 select { 275 case <-time.After(PingPeriod): 276 case <-s.closed: 277 } 278 } 279 } 280 281 func (s *State) Ping() error { 282 return s.APICall("Pinger", s.BestFacadeVersion("Pinger"), "", "Ping", nil, nil) 283 } 284 285 // APICall places a call to the remote machine. 286 // 287 // This fills out the rpc.Request on the given facade, version for a given 288 // object id, and the specific RPC method. It marshalls the Arguments, and will 289 // unmarshall the result into the response object that is supplied. 290 func (s *State) APICall(facade string, version int, id, method string, args, response interface{}) error { 291 err := s.client.Call(rpc.Request{ 292 Type: facade, 293 Version: version, 294 Id: id, 295 Action: method, 296 }, args, response) 297 return params.ClientError(err) 298 } 299 300 func (s *State) Close() error { 301 err := s.client.Close() 302 select { 303 case <-s.closed: 304 default: 305 close(s.closed) 306 } 307 <-s.broken 308 return err 309 } 310 311 // Broken returns a channel that's closed when the connection is broken. 312 func (s *State) Broken() <-chan struct{} { 313 return s.broken 314 } 315 316 // RPCClient returns the RPC client for the state, so that testing 317 // functions can tickle parts of the API that the conventional entry 318 // points don't reach. This is exported for testing purposes only. 319 func (s *State) RPCClient() *rpc.Conn { 320 return s.client 321 } 322 323 // Addr returns the address used to connect to the API server. 324 func (s *State) Addr() string { 325 return s.addr 326 } 327 328 // EnvironTag returns the tag of the environment we are connected to. 329 func (s *State) EnvironTag() (names.EnvironTag, error) { 330 return names.ParseEnvironTag(s.environTag) 331 } 332 333 // APIHostPorts returns addresses that may be used to connect 334 // to the API server, including the address used to connect. 335 // 336 // The addresses are scoped (public, cloud-internal, etc.), so 337 // the client may choose which addresses to attempt. For the 338 // Juju CLI, all addresses must be attempted, as the CLI may 339 // be invoked both within and outside the environment (think 340 // private clouds). 341 func (s *State) APIHostPorts() [][]network.HostPort { 342 hostPorts := make([][]network.HostPort, len(s.hostPorts)) 343 for i, server := range s.hostPorts { 344 hostPorts[i] = append([]network.HostPort{}, server...) 345 } 346 return hostPorts 347 } 348 349 // AllFacadeVersions returns what versions we know about for all facades 350 func (s *State) AllFacadeVersions() map[string][]int { 351 facades := make(map[string][]int, len(s.facadeVersions)) 352 for name, versions := range s.facadeVersions { 353 facades[name] = append([]int{}, versions...) 354 } 355 return facades 356 } 357 358 // BestFacadeVersion compares the versions of facades that we know about, and 359 // the versions available from the server, and reports back what version is the 360 // 'best available' to use. 361 // TODO(jam) this is the eventual implementation of what version of a given 362 // Facade we will want to use. It needs to line up the versions that the server 363 // reports to us, with the versions that our client knows how to use. 364 func (s *State) BestFacadeVersion(facade string) int { 365 return bestVersion(facadeVersions[facade], s.facadeVersions[facade]) 366 }