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