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  }