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