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