github.com/mhilton/juju-juju@v0.0.0-20150901100907-a94dd2c73455/juju/api.go (about) 1 // Copyright 2013 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package juju 5 6 import ( 7 "fmt" 8 "io" 9 "time" 10 11 "github.com/juju/errors" 12 "github.com/juju/loggo" 13 "github.com/juju/names" 14 "github.com/juju/utils/parallel" 15 16 "github.com/juju/juju/api" 17 "github.com/juju/juju/environs" 18 "github.com/juju/juju/environs/config" 19 "github.com/juju/juju/environs/configstore" 20 "github.com/juju/juju/network" 21 ) 22 23 var logger = loggo.GetLogger("juju.api") 24 25 // The following are variables so that they can be 26 // changed by tests. 27 var ( 28 providerConnectDelay = 2 * time.Second 29 ) 30 31 type apiStateCachedInfo struct { 32 api.Connection 33 // If cachedInfo is non-nil, it indicates that the info has been 34 // newly retrieved, and should be cached in the config store. 35 cachedInfo *api.Info 36 } 37 38 var errAborted = fmt.Errorf("aborted") 39 40 // NewAPIState creates an api.State object from an Environ 41 // This is almost certainly the wrong thing to do as it assumes 42 // the old admin password (stored as admin-secret in the config). 43 func NewAPIState(user names.UserTag, environ environs.Environ, dialOpts api.DialOpts) (api.Connection, error) { 44 info, err := environAPIInfo(environ, user) 45 if err != nil { 46 return nil, err 47 } 48 49 st, err := api.Open(info, dialOpts) 50 if err != nil { 51 return nil, err 52 } 53 return st, nil 54 } 55 56 // NewAPIClientFromName returns an api.Client connected to the API Server for 57 // the named environment. If envName is "", the default environment 58 // will be used. 59 func NewAPIClientFromName(envName string) (*api.Client, error) { 60 st, err := newAPIClient(envName) 61 if err != nil { 62 return nil, err 63 } 64 return st.Client(), nil 65 } 66 67 // NewAPIFromName returns an api.State connected to the API Server for 68 // the named environment. If envName is "", the default environment will 69 // be used. 70 func NewAPIFromName(envName string) (api.Connection, error) { 71 return newAPIClient(envName) 72 } 73 74 var defaultAPIOpen = api.Open 75 76 func newAPIClient(envName string) (api.Connection, error) { 77 store, err := configstore.Default() 78 if err != nil { 79 return nil, errors.Trace(err) 80 } 81 st, err := newAPIFromStore(envName, store, defaultAPIOpen) 82 if err != nil { 83 return nil, errors.Trace(err) 84 } 85 return st, nil 86 } 87 88 // serverAddress returns the given string address:port as network.HostPort. 89 var serverAddress = func(hostPort string) (network.HostPort, error) { 90 addrConnectedTo, err := network.ParseHostPorts(hostPort) 91 if err != nil { 92 // Should never happen, since we've just connected with it. 93 return network.HostPort{}, errors.Annotatef(err, "invalid API address %q", hostPort) 94 } 95 return addrConnectedTo[0], nil 96 } 97 98 // newAPIFromStore implements the bulk of NewAPIClientFromName 99 // but is separate for testing purposes. 100 func newAPIFromStore(envName string, store configstore.Storage, apiOpen api.OpenFunc) (api.Connection, error) { 101 // Try to read the default environment configuration file. 102 // If it doesn't exist, we carry on in case 103 // there's some environment info for that environment. 104 // This enables people to copy environment files 105 // into their .juju/environments directory and have 106 // them be directly useful with no further configuration changes. 107 envs, err := environs.ReadEnvirons("") 108 if err == nil { 109 if envName == "" { 110 envName = envs.Default 111 } 112 if envName == "" { 113 return nil, fmt.Errorf("no default environment found") 114 } 115 } else if !environs.IsNoEnv(err) { 116 return nil, err 117 } 118 119 // Try to connect to the API concurrently using two different 120 // possible sources of truth for the API endpoint. Our 121 // preference is for the API endpoint cached in the API info, 122 // because we know that without needing to access any remote 123 // provider. However, the addresses stored there may no longer 124 // be current (and the network connection may take a very long 125 // time to time out) so we also try to connect using information 126 // found from the provider. We only start to make that 127 // connection after some suitable delay, so that in the 128 // hopefully usual case, we will make the connection to the API 129 // and never hit the provider. By preference we use provider 130 // attributes from the config store, but for backward 131 // compatibility reasons, we fall back to information from 132 // ReadEnvirons if that does not exist. 133 chooseError := func(err0, err1 error) error { 134 if err0 == nil { 135 return err1 136 } 137 if errorImportance(err0) < errorImportance(err1) { 138 err0, err1 = err1, err0 139 } 140 logger.Warningf("discarding API open error: %v", err1) 141 return err0 142 } 143 try := parallel.NewTry(0, chooseError) 144 145 info, err := store.ReadInfo(envName) 146 if err != nil && !errors.IsNotFound(err) { 147 return nil, err 148 } 149 var delay time.Duration 150 if info != nil && len(info.APIEndpoint().Addresses) > 0 { 151 logger.Debugf( 152 "trying cached API connection settings - endpoints %v", 153 info.APIEndpoint().Addresses, 154 ) 155 try.Start(func(stop <-chan struct{}) (io.Closer, error) { 156 return apiInfoConnect(info, apiOpen, stop) 157 }) 158 // Delay the config connection until we've spent 159 // some time trying to connect to the cached info. 160 delay = providerConnectDelay 161 } else { 162 logger.Debugf("no cached API connection settings found") 163 } 164 try.Start(func(stop <-chan struct{}) (io.Closer, error) { 165 cfg, err := getConfig(info, envs, envName) 166 if err != nil { 167 return nil, err 168 } 169 return apiConfigConnect(cfg, apiOpen, stop, delay, environInfoUserTag(info)) 170 }) 171 try.Close() 172 val0, err := try.Result() 173 if err != nil { 174 if ierr, ok := err.(*infoConnectError); ok { 175 // lose error encapsulation: 176 err = ierr.error 177 } 178 return nil, err 179 } 180 181 st := val0.(api.Connection) 182 addrConnectedTo, err := serverAddress(st.Addr()) 183 if err != nil { 184 return nil, err 185 } 186 // Even though we are about to update API addresses based on 187 // APIHostPorts in cacheChangedAPIInfo, we first cache the 188 // addresses based on the provider lookup. This is because older API 189 // servers didn't return their HostPort information on Login, and we 190 // still want to cache our connection information to them. 191 if cachedInfo, ok := st.(apiStateCachedInfo); ok { 192 st = cachedInfo.Connection 193 if cachedInfo.cachedInfo != nil && info != nil { 194 // Cache the connection settings only if we used the 195 // environment config, but any errors are just logged 196 // as warnings, because they're not fatal. 197 err = cacheAPIInfo(st, info, cachedInfo.cachedInfo) 198 if err != nil { 199 logger.Warningf("cannot cache API connection settings: %v", err.Error()) 200 } else { 201 logger.Infof("updated API connection settings cache") 202 } 203 addrConnectedTo, err = serverAddress(st.Addr()) 204 if err != nil { 205 return nil, err 206 } 207 } 208 } 209 // Update API addresses if they've changed. Error is non-fatal. 210 // For older servers, the environ tag or server tag may not be set. 211 // if they are not, we store empty values. 212 var environUUID string 213 var serverUUID string 214 if envTag, err := st.EnvironTag(); err == nil { 215 environUUID = envTag.Id() 216 } 217 if serverTag, err := st.ServerTag(); err == nil { 218 serverUUID = serverTag.Id() 219 } 220 if localerr := cacheChangedAPIInfo(info, st.APIHostPorts(), addrConnectedTo, environUUID, serverUUID); localerr != nil { 221 logger.Warningf("cannot cache API addresses: %v", localerr) 222 } 223 return st, nil 224 } 225 226 func errorImportance(err error) int { 227 if err == nil { 228 return 0 229 } 230 if errors.IsNotFound(err) { 231 // An error from an actual connection attempt 232 // is more interesting than the fact that there's 233 // no environment info available. 234 return 2 235 } 236 if _, ok := err.(*infoConnectError); ok { 237 // A connection to a potentially stale cached address 238 // is less important than a connection from fresh info. 239 return 1 240 } 241 return 3 242 } 243 244 type infoConnectError struct { 245 error 246 } 247 248 func environInfoUserTag(info configstore.EnvironInfo) names.UserTag { 249 var username string 250 if info != nil { 251 username = info.APICredentials().User 252 } 253 if username == "" { 254 username = configstore.DefaultAdminUsername 255 } 256 return names.NewUserTag(username) 257 } 258 259 // apiInfoConnect looks for endpoint on the given environment and 260 // tries to connect to it, sending the result on the returned channel. 261 func apiInfoConnect(info configstore.EnvironInfo, apiOpen api.OpenFunc, stop <-chan struct{}) (api.Connection, error) { 262 endpoint := info.APIEndpoint() 263 if info == nil || len(endpoint.Addresses) == 0 { 264 return nil, &infoConnectError{fmt.Errorf("no cached addresses")} 265 } 266 logger.Infof("connecting to API addresses: %v", endpoint.Addresses) 267 var environTag names.EnvironTag 268 if names.IsValidEnvironment(endpoint.EnvironUUID) { 269 environTag = names.NewEnvironTag(endpoint.EnvironUUID) 270 } 271 272 apiInfo := &api.Info{ 273 Addrs: endpoint.Addresses, 274 CACert: endpoint.CACert, 275 Tag: environInfoUserTag(info), 276 Password: info.APICredentials().Password, 277 EnvironTag: environTag, 278 } 279 st, err := apiOpen(apiInfo, api.DefaultDialOpts()) 280 if err != nil { 281 return nil, &infoConnectError{err} 282 } 283 return st, nil 284 } 285 286 // apiConfigConnect looks for configuration info on the given environment, 287 // and tries to use an Environ constructed from that to connect to 288 // its endpoint. It only starts the attempt after the given delay, 289 // to allow the faster apiInfoConnect to hopefully succeed first. 290 // It returns nil if there was no configuration information found. 291 func apiConfigConnect(cfg *config.Config, apiOpen api.OpenFunc, stop <-chan struct{}, delay time.Duration, user names.UserTag) (api.Connection, error) { 292 select { 293 case <-time.After(delay): 294 case <-stop: 295 return nil, errAborted 296 } 297 environ, err := environs.New(cfg) 298 if err != nil { 299 return nil, err 300 } 301 apiInfo, err := environAPIInfo(environ, user) 302 if err != nil { 303 return nil, err 304 } 305 306 st, err := apiOpen(apiInfo, api.DefaultDialOpts()) 307 // TODO(rog): handle errUnauthorized when the API handles passwords. 308 if err != nil { 309 return nil, err 310 } 311 return apiStateCachedInfo{st, apiInfo}, nil 312 } 313 314 // getConfig looks for configuration info on the given environment 315 func getConfig(info configstore.EnvironInfo, envs *environs.Environs, envName string) (*config.Config, error) { 316 if info != nil && len(info.BootstrapConfig()) > 0 { 317 cfg, err := config.New(config.NoDefaults, info.BootstrapConfig()) 318 if err != nil { 319 logger.Warningf("failed to parse bootstrap-config: %v", err) 320 } 321 return cfg, err 322 } 323 if envs != nil { 324 cfg, err := envs.Config(envName) 325 if err != nil && !errors.IsNotFound(err) { 326 logger.Warningf("failed to get config for environment %q: %v", envName, err) 327 } 328 return cfg, err 329 } 330 return nil, errors.NotFoundf("environment %q", envName) 331 } 332 333 func environAPIInfo(environ environs.Environ, user names.UserTag) (*api.Info, error) { 334 config := environ.Config() 335 password := config.AdminSecret() 336 if password == "" { 337 return nil, fmt.Errorf("cannot connect to API servers without admin-secret") 338 } 339 info, err := environs.APIInfo(environ) 340 if err != nil { 341 return nil, err 342 } 343 info.Tag = user 344 info.Password = password 345 return info, nil 346 } 347 348 // cacheAPIInfo updates the local environment settings (.jenv file) 349 // with the provided apiInfo, assuming we've just successfully 350 // connected to the API server. 351 func cacheAPIInfo(st api.Connection, info configstore.EnvironInfo, apiInfo *api.Info) (err error) { 352 defer errors.DeferredAnnotatef(&err, "failed to cache API credentials") 353 var environUUID string 354 if names.IsValidEnvironment(apiInfo.EnvironTag.Id()) { 355 environUUID = apiInfo.EnvironTag.Id() 356 } else { 357 // For backwards-compatibility, we have to allow connections 358 // with an empty UUID. Login will work for the same reasons. 359 logger.Warningf("ignoring invalid cached API endpoint environment UUID %v", apiInfo.EnvironTag.Id()) 360 } 361 hostPorts, err := network.ParseHostPorts(apiInfo.Addrs...) 362 if err != nil { 363 return errors.Annotatef(err, "invalid API addresses %v", apiInfo.Addrs) 364 } 365 addrConnectedTo, err := network.ParseHostPorts(st.Addr()) 366 if err != nil { 367 // Should never happen, since we've just connected with it. 368 return errors.Annotatef(err, "invalid API address %q", st.Addr()) 369 } 370 addrs, hostnames, addrsChanged := PrepareEndpointsForCaching( 371 info, [][]network.HostPort{hostPorts}, addrConnectedTo[0], 372 ) 373 374 endpoint := configstore.APIEndpoint{ 375 CACert: string(apiInfo.CACert), 376 EnvironUUID: environUUID, 377 } 378 if addrsChanged { 379 endpoint.Addresses = addrs 380 endpoint.Hostnames = hostnames 381 } 382 info.SetAPIEndpoint(endpoint) 383 tag, ok := apiInfo.Tag.(names.UserTag) 384 if !ok { 385 return errors.Errorf("apiInfo.Tag was of type %T, expecting names.UserTag", apiInfo.Tag) 386 } 387 info.SetAPICredentials(configstore.APICredentials{ 388 // This looks questionable. We have a tag, say "user-admin", but then only 389 // the Id portion of the tag is recorded, "admin", so this is really a 390 // username, not a tag, and cannot be reconstructed accurately. 391 User: tag.Id(), 392 Password: apiInfo.Password, 393 }) 394 return info.Write() 395 } 396 397 var maybePreferIPv6 = func(info configstore.EnvironInfo) bool { 398 // BootstrapConfig will exist in production environments after 399 // bootstrap, but for testing it's easier to mock this function. 400 cfg := info.BootstrapConfig() 401 result := false 402 if cfg != nil { 403 if val, ok := cfg["prefer-ipv6"]; ok { 404 // It's optional, so if missing assume false. 405 result, _ = val.(bool) 406 } 407 } 408 return result 409 } 410 411 var resolveOrDropHostnames = network.ResolveOrDropHostnames 412 413 // PrepareEndpointsForCaching performs the necessary operations on the 414 // given API hostPorts so they are suitable for caching into the 415 // environment's .jenv file, taking into account the addrConnectedTo 416 // and the existing config store info: 417 // 418 // 1. Collapses hostPorts into a single slice. 419 // 2. Filters out machine-local and link-local addresses. 420 // 3. Removes any duplicates 421 // 4. Call network.SortHostPorts() on the list, respecing prefer-ipv6 422 // flag. 423 // 5. Puts the addrConnectedTo on top. 424 // 6. Compares the result against info.APIEndpoint.Hostnames. 425 // 7. If the addresses differ, call network.ResolveOrDropHostnames() 426 // on the list and perform all steps again from step 1. 427 // 8. Compare the list of resolved addresses against the cached info 428 // APIEndpoint.Addresses, and if changed return both addresses and 429 // hostnames as strings (so they can be cached on APIEndpoint) and 430 // set haveChanged to true. 431 // 9. If the hostnames haven't changed, return two empty slices and set 432 // haveChanged to false. No DNS resolution is performed to save time. 433 // 434 // This is used right after bootstrap to cache the initial API 435 // endpoints, as well as on each CLI connection to verify if the 436 // cached endpoints need updating. 437 func PrepareEndpointsForCaching(info configstore.EnvironInfo, hostPorts [][]network.HostPort, addrConnectedTo network.HostPort) (addresses, hostnames []string, haveChanged bool) { 438 processHostPorts := func(allHostPorts [][]network.HostPort) []network.HostPort { 439 collapsedHPs := network.CollapseHostPorts(allHostPorts) 440 filteredHPs := network.FilterUnusableHostPorts(collapsedHPs) 441 uniqueHPs := network.DropDuplicatedHostPorts(filteredHPs) 442 // Sort the result to prefer public IPs on top (when prefer-ipv6 443 // is true, IPv6 addresses of the same scope will come before IPv4 444 // ones). 445 preferIPv6 := maybePreferIPv6(info) 446 network.SortHostPorts(uniqueHPs, preferIPv6) 447 448 if addrConnectedTo.Value != "" { 449 return network.EnsureFirstHostPort(addrConnectedTo, uniqueHPs) 450 } 451 // addrConnectedTo can be empty only right after bootstrap. 452 return uniqueHPs 453 } 454 455 apiHosts := processHostPorts(hostPorts) 456 hostsStrings := network.HostPortsToStrings(apiHosts) 457 endpoint := info.APIEndpoint() 458 needResolving := false 459 460 // Verify if the unresolved addresses have changed. 461 if len(apiHosts) > 0 && len(endpoint.Hostnames) > 0 { 462 if addrsChanged(hostsStrings, endpoint.Hostnames) { 463 logger.Debugf( 464 "API hostnames changed from %v to %v - resolving hostnames", 465 endpoint.Hostnames, hostsStrings, 466 ) 467 needResolving = true 468 } 469 } else if len(apiHosts) > 0 { 470 // No cached hostnames, most likely right after bootstrap. 471 logger.Debugf("API hostnames %v - resolving hostnames", hostsStrings) 472 needResolving = true 473 } 474 if !needResolving { 475 // We're done - nothing changed. 476 logger.Debugf("API hostnames unchanged - not resolving") 477 return nil, nil, false 478 } 479 // Perform DNS resolution and check against APIEndpoints.Addresses. 480 resolved := resolveOrDropHostnames(apiHosts) 481 apiAddrs := processHostPorts([][]network.HostPort{resolved}) 482 addrsStrings := network.HostPortsToStrings(apiAddrs) 483 if len(apiAddrs) > 0 && len(endpoint.Addresses) > 0 { 484 if addrsChanged(addrsStrings, endpoint.Addresses) { 485 logger.Infof( 486 "API addresses changed from %v to %v", 487 endpoint.Addresses, addrsStrings, 488 ) 489 return addrsStrings, hostsStrings, true 490 } 491 } else if len(apiAddrs) > 0 { 492 // No cached addresses, most likely right after bootstrap. 493 logger.Infof("new API addresses to cache %v", addrsStrings) 494 return addrsStrings, hostsStrings, true 495 } 496 // No changes. 497 logger.Debugf("API addresses unchanged") 498 return nil, nil, false 499 } 500 501 // cacheChangedAPIInfo updates the local environment settings (.jenv file) 502 // with the provided API server addresses if they have changed. It will also 503 // save the environment tag if it is available. 504 func cacheChangedAPIInfo(info configstore.EnvironInfo, hostPorts [][]network.HostPort, addrConnectedTo network.HostPort, environUUID, serverUUID string) error { 505 addrs, hosts, addrsChanged := PrepareEndpointsForCaching(info, hostPorts, addrConnectedTo) 506 logger.Debugf("cacheChangedAPIInfo: serverUUID=%q", serverUUID) 507 endpoint := info.APIEndpoint() 508 needCaching := false 509 if endpoint.EnvironUUID != environUUID && environUUID != "" { 510 endpoint.EnvironUUID = environUUID 511 needCaching = true 512 } 513 if endpoint.ServerUUID != serverUUID && serverUUID != "" { 514 endpoint.ServerUUID = serverUUID 515 needCaching = true 516 } 517 if addrsChanged { 518 endpoint.Addresses = addrs 519 endpoint.Hostnames = hosts 520 needCaching = true 521 } 522 if !needCaching { 523 return nil 524 } 525 info.SetAPIEndpoint(endpoint) 526 if err := info.Write(); err != nil { 527 return err 528 } 529 logger.Infof("updated API connection settings cache - endpoints %v", endpoint.Addresses) 530 return nil 531 } 532 533 // addrsChanged returns true iff the two 534 // slices are not equal. Order is important. 535 func addrsChanged(a, b []string) bool { 536 if len(a) != len(b) { 537 return true 538 } 539 for i := range a { 540 if a[i] != b[i] { 541 return true 542 } 543 } 544 return false 545 }