github.com/cloudbase/juju-core@v0.0.0-20140504232958-a7271ac7912f/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/loggo"
    12  
    13  	"launchpad.net/juju-core/environs"
    14  	"launchpad.net/juju-core/environs/config"
    15  	"launchpad.net/juju-core/environs/configstore"
    16  	"launchpad.net/juju-core/errors"
    17  	"launchpad.net/juju-core/juju/osenv"
    18  	"launchpad.net/juju-core/names"
    19  	"launchpad.net/juju-core/state/api"
    20  	"launchpad.net/juju-core/state/api/keymanager"
    21  	"launchpad.net/juju-core/utils/parallel"
    22  )
    23  
    24  var logger = loggo.GetLogger("juju")
    25  
    26  // The following are variables so that they can be
    27  // changed by tests.
    28  var (
    29  	apiOpen              = api.Open
    30  	apiClose             = (*api.State).Close
    31  	providerConnectDelay = 2 * time.Second
    32  )
    33  
    34  // apiState wraps an api.State, redefining its Close method
    35  // so we can abuse it for testing purposes.
    36  type apiState struct {
    37  	st *api.State
    38  	// If cachedInfo is non-nil, it indicates that the info has been
    39  	// newly retrieved, and should be cached in the config store.
    40  	cachedInfo *api.Info
    41  }
    42  
    43  func (st apiState) Close() error {
    44  	return apiClose(st.st)
    45  }
    46  
    47  // APIConn holds a connection to a juju environment and its
    48  // associated state through its API interface.
    49  type APIConn struct {
    50  	Environ environs.Environ
    51  	State   *api.State
    52  }
    53  
    54  var errAborted = fmt.Errorf("aborted")
    55  
    56  // NewAPIConn returns a new Conn that uses the
    57  // given environment. The environment must have already
    58  // been bootstrapped.
    59  func NewAPIConn(environ environs.Environ, dialOpts api.DialOpts) (*APIConn, error) {
    60  	info, err := environAPIInfo(environ)
    61  	if err != nil {
    62  		return nil, err
    63  	}
    64  
    65  	st, err := apiOpen(info, dialOpts)
    66  	// TODO(rog): handle errUnauthorized when the API handles passwords.
    67  	if err != nil {
    68  		return nil, err
    69  	}
    70  	return &APIConn{
    71  		Environ: environ,
    72  		State:   st,
    73  	}, nil
    74  }
    75  
    76  // Close terminates the connection to the environment and releases
    77  // any associated resources.
    78  func (c *APIConn) Close() error {
    79  	return apiClose(c.State)
    80  }
    81  
    82  // NewAPIClientFromName returns an api.Client connected to the API Server for
    83  // the named environment. If envName is "", the default environment
    84  // will be used.
    85  func NewAPIClientFromName(envName string) (*api.Client, error) {
    86  	st, err := newAPIClient(envName)
    87  	if err != nil {
    88  		return nil, err
    89  	}
    90  	return st.Client(), nil
    91  }
    92  
    93  // NewKeyManagerClient returns an api.keymanager.Client connected to the API Server for
    94  // the named environment. If envName is "", the default environment will be used.
    95  func NewKeyManagerClient(envName string) (*keymanager.Client, error) {
    96  	st, err := newAPIClient(envName)
    97  	if err != nil {
    98  		return nil, err
    99  	}
   100  	return keymanager.NewClient(st), nil
   101  }
   102  
   103  func newAPIClient(envName string) (*api.State, error) {
   104  	store, err := configstore.NewDisk(osenv.JujuHome())
   105  	if err != nil {
   106  		return nil, err
   107  	}
   108  	return newAPIFromName(envName, store)
   109  }
   110  
   111  // newAPIFromName implements the bulk of NewAPIClientFromName
   112  // but is separate for testing purposes.
   113  func newAPIFromName(envName string, store configstore.Storage) (*api.State, 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.IsNotFoundError(err) {
   160  		return nil, err
   161  	}
   162  	var delay time.Duration
   163  	if info != nil && len(info.APIEndpoint().Addresses) > 0 {
   164  		logger.Debugf("trying cached API connection settings")
   165  		try.Start(func(stop <-chan struct{}) (io.Closer, error) {
   166  			return apiInfoConnect(store, info, stop)
   167  		})
   168  		// Delay the config connection until we've spent
   169  		// some time trying to connect to the cached info.
   170  		delay = providerConnectDelay
   171  	} else {
   172  		logger.Debugf("no cached API connection settings found")
   173  	}
   174  	try.Start(func(stop <-chan struct{}) (io.Closer, error) {
   175  		return apiConfigConnect(info, envs, envName, stop, delay)
   176  	})
   177  	try.Close()
   178  	val0, err := try.Result()
   179  	if err != nil {
   180  		if ierr, ok := err.(*infoConnectError); ok {
   181  			// lose error encapsulation:
   182  			err = ierr.error
   183  		}
   184  		return nil, err
   185  	}
   186  	val := val0.(apiState)
   187  
   188  	if val.cachedInfo != nil && info != nil {
   189  		// Cache the connection settings only if we used the
   190  		// environment config, but any errors are just logged
   191  		// as warnings, because they're not fatal.
   192  		err = cacheAPIInfo(info, val.cachedInfo)
   193  		if err != nil {
   194  			logger.Warningf(err.Error())
   195  		} else {
   196  			logger.Debugf("updated API connection settings cache")
   197  		}
   198  	}
   199  	return val.st, nil
   200  }
   201  
   202  func errorImportance(err error) int {
   203  	if err == nil {
   204  		return 0
   205  	}
   206  	if errors.IsNotFoundError(err) {
   207  		// An error from an actual connection attempt
   208  		// is more interesting than the fact that there's
   209  		// no environment info available.
   210  		return 1
   211  	}
   212  	if _, ok := err.(*infoConnectError); ok {
   213  		// A connection to a potentially stale cached address
   214  		// is less important than a connection from fresh info.
   215  		return 2
   216  	}
   217  	return 3
   218  }
   219  
   220  type infoConnectError struct {
   221  	error
   222  }
   223  
   224  // apiInfoConnect looks for endpoint on the given environment and
   225  // tries to connect to it, sending the result on the returned channel.
   226  func apiInfoConnect(store configstore.Storage, info configstore.EnvironInfo, stop <-chan struct{}) (apiState, error) {
   227  	endpoint := info.APIEndpoint()
   228  	if info == nil || len(endpoint.Addresses) == 0 {
   229  		return apiState{}, &infoConnectError{fmt.Errorf("no cached addresses")}
   230  	}
   231  	logger.Infof("connecting to API addresses: %v", endpoint.Addresses)
   232  	apiInfo := &api.Info{
   233  		Addrs:    endpoint.Addresses,
   234  		CACert:   []byte(endpoint.CACert),
   235  		Tag:      names.UserTag(info.APICredentials().User),
   236  		Password: info.APICredentials().Password,
   237  	}
   238  	st, err := apiOpen(apiInfo, api.DefaultDialOpts())
   239  	if err != nil {
   240  		return apiState{}, &infoConnectError{err}
   241  	}
   242  	return apiState{st, nil}, err
   243  }
   244  
   245  // apiConfigConnect looks for configuration info on the given environment,
   246  // and tries to use an Environ constructed from that to connect to
   247  // its endpoint. It only starts the attempt after the given delay,
   248  // to allow the faster apiInfoConnect to hopefully succeed first.
   249  // It returns nil if there was no configuration information found.
   250  func apiConfigConnect(info configstore.EnvironInfo, envs *environs.Environs, envName string, stop <-chan struct{}, delay time.Duration) (apiState, error) {
   251  	var cfg *config.Config
   252  	var err error
   253  	if info != nil && len(info.BootstrapConfig()) > 0 {
   254  		cfg, err = config.New(config.NoDefaults, info.BootstrapConfig())
   255  	} else if envs != nil {
   256  		cfg, err = envs.Config(envName)
   257  		if errors.IsNotFoundError(err) {
   258  			return apiState{}, err
   259  		}
   260  	} else {
   261  		return apiState{}, errors.NotFoundf("environment %q", envName)
   262  	}
   263  	select {
   264  	case <-time.After(delay):
   265  	case <-stop:
   266  		return apiState{}, errAborted
   267  	}
   268  	environ, err := environs.New(cfg)
   269  	if err != nil {
   270  		return apiState{}, err
   271  	}
   272  	apiInfo, err := environAPIInfo(environ)
   273  	if err != nil {
   274  		return apiState{}, err
   275  	}
   276  	st, err := apiOpen(apiInfo, api.DefaultDialOpts())
   277  	// TODO(rog): handle errUnauthorized when the API handles passwords.
   278  	if err != nil {
   279  		return apiState{}, err
   280  	}
   281  	return apiState{st, apiInfo}, nil
   282  }
   283  
   284  func environAPIInfo(environ environs.Environ) (*api.Info, error) {
   285  	_, info, err := environ.StateInfo()
   286  	if err != nil {
   287  		return nil, err
   288  	}
   289  	info.Tag = "user-admin"
   290  	password := environ.Config().AdminSecret()
   291  	if password == "" {
   292  		return nil, fmt.Errorf("cannot connect without admin-secret")
   293  	}
   294  	info.Password = password
   295  	return info, nil
   296  }
   297  
   298  // cacheAPIInfo updates the local environment settings (.jenv file)
   299  // with the provided apiInfo, assuming we've just successfully
   300  // connected to the API server.
   301  func cacheAPIInfo(info configstore.EnvironInfo, apiInfo *api.Info) error {
   302  	info.SetAPIEndpoint(configstore.APIEndpoint{
   303  		Addresses: apiInfo.Addrs,
   304  		CACert:    string(apiInfo.CACert),
   305  	})
   306  	_, username, err := names.ParseTag(apiInfo.Tag, names.UserTagKind)
   307  	if err != nil {
   308  		return fmt.Errorf("not caching API connection settings: invalid API user tag: %v", err)
   309  	}
   310  	info.SetAPICredentials(configstore.APICredentials{
   311  		User:     username,
   312  		Password: apiInfo.Password,
   313  	})
   314  	if err := info.Write(); err != nil {
   315  		return fmt.Errorf("cannot cache API connection settings: %v", err)
   316  	}
   317  	return nil
   318  }