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  }