github.com/wallyworld/juju@v0.0.0-20161013125918-6cf1bc9d917a/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  	"time"
     8  
     9  	"github.com/juju/errors"
    10  	"github.com/juju/loggo"
    11  	"gopkg.in/juju/names.v2"
    12  
    13  	"github.com/juju/juju/api"
    14  	"github.com/juju/juju/jujuclient"
    15  	"github.com/juju/juju/network"
    16  )
    17  
    18  var logger = loggo.GetLogger("juju.juju")
    19  
    20  // The following are variables so that they can be
    21  // changed by tests.
    22  var (
    23  	providerConnectDelay = 2 * time.Second
    24  )
    25  
    26  type apiStateCachedInfo struct {
    27  	api.Connection
    28  	// If cachedInfo is non-nil, it indicates that the info has been
    29  	// newly retrieved, and should be cached in the config store.
    30  	cachedInfo *api.Info
    31  }
    32  
    33  // NewAPIConnectionParams contains the parameters for creating a new Juju API
    34  // connection.
    35  type NewAPIConnectionParams struct {
    36  	// ControllerName is the name of the controller to connect to.
    37  	ControllerName string
    38  
    39  	// Store is the jujuclient.ClientStore from which the controller's
    40  	// details will be fetched, and updated on address changes.
    41  	Store jujuclient.ClientStore
    42  
    43  	// OpenAPI is the function that will be used to open API connections.
    44  	OpenAPI api.OpenFunc
    45  
    46  	// DialOpts contains the options used to dial the API connection.
    47  	DialOpts api.DialOpts
    48  
    49  	// AccountDetails contains the account details to use for logging
    50  	// in to the Juju API. If this is nil, then no login will take
    51  	// place. If AccountDetails.Password and AccountDetails.Macaroon
    52  	// are zero, the login will be as an external user.
    53  	AccountDetails *jujuclient.AccountDetails
    54  
    55  	// ModelUUID is an optional model UUID. If specified, the API connection
    56  	// will be scoped to the model with that UUID; otherwise it will be
    57  	// scoped to the controller.
    58  	ModelUUID string
    59  }
    60  
    61  // NewAPIConnection returns an api.Connection to the specified Juju controller,
    62  // with specified account credentials, optionally scoped to the specified model
    63  // name.
    64  func NewAPIConnection(args NewAPIConnectionParams) (api.Connection, error) {
    65  	apiInfo, controller, err := connectionInfo(args)
    66  	if err != nil {
    67  		return nil, errors.Annotatef(err, "cannot work out how to connect")
    68  	}
    69  	if len(apiInfo.Addrs) == 0 {
    70  		return nil, errors.New("no API addresses")
    71  	}
    72  	logger.Infof("connecting to API addresses: %v", apiInfo.Addrs)
    73  	st, err := args.OpenAPI(apiInfo, args.DialOpts)
    74  	if err != nil {
    75  		redirErr, ok := errors.Cause(err).(*api.RedirectError)
    76  		if !ok {
    77  			return nil, errors.Trace(err)
    78  		}
    79  		// We've been told to connect to a different API server,
    80  		// so do so. Note that we don't copy the account details
    81  		// because the account on the redirected server may well
    82  		// be different - we'll use macaroon authentication
    83  		// directly without sending account details.
    84  		// Copy the API info because it's possible that the
    85  		// apiConfigConnect is still using it concurrently.
    86  		apiInfo = &api.Info{
    87  			ModelTag: apiInfo.ModelTag,
    88  			Addrs:    network.HostPortsToStrings(usableHostPorts(redirErr.Servers)),
    89  			CACert:   redirErr.CACert,
    90  		}
    91  		st, err = args.OpenAPI(apiInfo, args.DialOpts)
    92  		if err != nil {
    93  			return nil, errors.Annotatef(err, "cannot connect to redirected address")
    94  		}
    95  		// TODO(rog) update cached model addresses.
    96  		// TODO(rog) should we do something with the logged-in username?
    97  		return st, nil
    98  	}
    99  	addrConnectedTo, err := serverAddress(st.Addr())
   100  	if err != nil {
   101  		return nil, errors.Trace(err)
   102  	}
   103  	// Update API addresses if they've changed. Error is non-fatal.
   104  	// Note that in the redirection case, we won't update the addresses
   105  	// of the controller we first connected to. This shouldn't be
   106  	// a problem in practice because the intended scenario for
   107  	// controllers that redirect involves them having well known
   108  	// public addresses that won't change over time.
   109  	hostPorts := st.APIHostPorts()
   110  	agentVersion := ""
   111  	if v, ok := st.ServerVersion(); ok {
   112  		agentVersion = v.String()
   113  	}
   114  	params := UpdateControllerParams{
   115  		AgentVersion:     agentVersion,
   116  		AddrConnectedTo:  []network.HostPort{addrConnectedTo},
   117  		CurrentHostPorts: hostPorts,
   118  	}
   119  	err = updateControllerDetailsFromLogin(args.Store, args.ControllerName, controller, params)
   120  	if err != nil {
   121  		logger.Errorf("cannot cache API addresses: %v", err)
   122  	}
   123  
   124  	// Process the account details obtained from login.
   125  	var accountDetails *jujuclient.AccountDetails
   126  	user, ok := st.AuthTag().(names.UserTag)
   127  	if !apiInfo.SkipLogin {
   128  		if ok {
   129  			if accountDetails, err = args.Store.AccountDetails(args.ControllerName); err != nil {
   130  				if !errors.IsNotFound(err) {
   131  					logger.Errorf("cannot load local account information: %v", err)
   132  				}
   133  			} else {
   134  				accountDetails.LastKnownAccess = st.ControllerAccess()
   135  			}
   136  		}
   137  		if ok && !user.IsLocal() && apiInfo.Tag == nil {
   138  			// We used macaroon auth to login; save the username
   139  			// that we've logged in as.
   140  			accountDetails = &jujuclient.AccountDetails{
   141  				User:            user.Id(),
   142  				LastKnownAccess: st.ControllerAccess(),
   143  			}
   144  		} else if apiInfo.Tag == nil {
   145  			logger.Errorf("unexpected logged-in username %v", st.AuthTag())
   146  		}
   147  	}
   148  	if accountDetails != nil {
   149  		if err := args.Store.UpdateAccount(args.ControllerName, *accountDetails); err != nil {
   150  			logger.Errorf("cannot update account information: %v", err)
   151  		}
   152  	}
   153  	return st, nil
   154  }
   155  
   156  // connectionInfo returns connection information suitable for
   157  // connecting to the controller and model specified in the given
   158  // parameters. If there are no addresses known for the controller,
   159  // it may return a *api.Info with no APIEndpoints, but all other
   160  // information will be populated.
   161  func connectionInfo(args NewAPIConnectionParams) (*api.Info, *jujuclient.ControllerDetails, error) {
   162  	controller, err := args.Store.ControllerByName(args.ControllerName)
   163  	if err != nil {
   164  		return nil, nil, errors.Annotate(err, "cannot get controller details")
   165  	}
   166  	apiInfo := &api.Info{
   167  		Addrs:  controller.APIEndpoints,
   168  		CACert: controller.CACert,
   169  	}
   170  	if args.ModelUUID != "" {
   171  		apiInfo.ModelTag = names.NewModelTag(args.ModelUUID)
   172  	}
   173  	if args.AccountDetails == nil {
   174  		apiInfo.SkipLogin = true
   175  		return apiInfo, controller, nil
   176  	}
   177  	account := args.AccountDetails
   178  	if account.User != "" {
   179  		userTag := names.NewUserTag(account.User)
   180  		if userTag.IsLocal() {
   181  			apiInfo.Tag = userTag
   182  		}
   183  	}
   184  	if args.AccountDetails.Password != "" {
   185  		// If a password is available, we always use that.
   186  		// If no password is recorded, we'll attempt to
   187  		// authenticate using macaroons.
   188  		apiInfo.Password = account.Password
   189  	}
   190  	return apiInfo, controller, nil
   191  }
   192  
   193  func isAPIError(err error) bool {
   194  	type errorCoder interface {
   195  		ErrorCode() string
   196  	}
   197  	_, ok := errors.Cause(err).(errorCoder)
   198  	return ok
   199  }
   200  
   201  var resolveOrDropHostnames = network.ResolveOrDropHostnames
   202  
   203  // PrepareEndpointsForCaching performs the necessary operations on the
   204  // given API hostPorts so they are suitable for saving into the
   205  // controller.yaml file, taking into account the addrConnectedTo
   206  // and the existing config store info:
   207  //
   208  // 1. Collapses hostPorts into a single slice.
   209  // 2. Filters out machine-local and link-local addresses.
   210  // 3. Removes any duplicates
   211  // 4. Call network.SortHostPorts() on the list.
   212  // 5. Puts the addrConnectedTo on top.
   213  // 6. Compares the result against info.APIEndpoint.Hostnames.
   214  // 7. If the addresses differ, call network.ResolveOrDropHostnames()
   215  // on the list and perform all steps again from step 1.
   216  // 8. Compare the list of resolved addresses against the cached info
   217  // APIEndpoint.Addresses, and if changed return both addresses and
   218  // hostnames as strings (so they can be cached on APIEndpoint) and
   219  // set haveChanged to true.
   220  // 9. If the hostnames haven't changed, return two empty slices and set
   221  // haveChanged to false. No DNS resolution is performed to save time.
   222  //
   223  // This is used right after bootstrap to saved the initial API
   224  // endpoints, as well as on each CLI connection to verify if the
   225  // saved endpoints need updating.
   226  //
   227  // TODO(rogpeppe) this function mixes too many concerns - the
   228  // logic is difficult to follow and has non-obvious properties.
   229  func PrepareEndpointsForCaching(
   230  	controllerDetails jujuclient.ControllerDetails,
   231  	hostPorts [][]network.HostPort,
   232  	addrConnectedTo ...network.HostPort,
   233  ) (addrs, unresolvedAddrs []string, haveChanged bool) {
   234  	processHostPorts := func(allHostPorts [][]network.HostPort) []network.HostPort {
   235  		uniqueHPs := usableHostPorts(allHostPorts)
   236  		network.SortHostPorts(uniqueHPs)
   237  		for _, addr := range addrConnectedTo {
   238  			uniqueHPs = network.EnsureFirstHostPort(addr, uniqueHPs)
   239  		}
   240  		return uniqueHPs
   241  	}
   242  
   243  	apiHosts := processHostPorts(hostPorts)
   244  	hostsStrings := network.HostPortsToStrings(apiHosts)
   245  	needResolving := false
   246  
   247  	// Verify if the unresolved addresses have changed.
   248  	if len(apiHosts) > 0 && len(controllerDetails.UnresolvedAPIEndpoints) > 0 {
   249  		if addrsChanged(hostsStrings, controllerDetails.UnresolvedAPIEndpoints) {
   250  			logger.Debugf(
   251  				"API hostnames changed from %v to %v - resolving hostnames",
   252  				controllerDetails.UnresolvedAPIEndpoints, hostsStrings,
   253  			)
   254  			needResolving = true
   255  		}
   256  	} else if len(apiHosts) > 0 {
   257  		// No cached hostnames, most likely right after bootstrap.
   258  		logger.Debugf("API hostnames %v - resolving hostnames", hostsStrings)
   259  		needResolving = true
   260  	}
   261  	if !needResolving {
   262  		// We're done - nothing changed.
   263  		logger.Debugf("API hostnames unchanged - not resolving")
   264  		return nil, nil, false
   265  	}
   266  	// Perform DNS resolution and check against APIEndpoints.Addresses.
   267  	resolved := resolveOrDropHostnames(apiHosts)
   268  	apiAddrs := processHostPorts([][]network.HostPort{resolved})
   269  	addrsStrings := network.HostPortsToStrings(apiAddrs)
   270  	if len(apiAddrs) > 0 && len(controllerDetails.APIEndpoints) > 0 {
   271  		if addrsChanged(addrsStrings, controllerDetails.APIEndpoints) {
   272  			logger.Infof(
   273  				"API addresses changed from %v to %v",
   274  				controllerDetails.APIEndpoints, addrsStrings,
   275  			)
   276  			return addrsStrings, hostsStrings, true
   277  		}
   278  	} else if len(apiAddrs) > 0 {
   279  		// No cached addresses, most likely right after bootstrap.
   280  		logger.Infof("new API addresses to cache %v", addrsStrings)
   281  		return addrsStrings, hostsStrings, true
   282  	}
   283  	// No changes.
   284  	logger.Debugf("API addresses unchanged")
   285  	return nil, nil, false
   286  }
   287  
   288  // usableHostPorts returns hps with unusable and non-unique
   289  // host-ports filtered out.
   290  func usableHostPorts(hps [][]network.HostPort) []network.HostPort {
   291  	collapsed := network.CollapseHostPorts(hps)
   292  	usable := network.FilterUnusableHostPorts(collapsed)
   293  	unique := network.DropDuplicatedHostPorts(usable)
   294  	return unique
   295  }
   296  
   297  // addrsChanged returns true iff the two
   298  // slices are not equal. Order is important.
   299  func addrsChanged(a, b []string) bool {
   300  	if len(a) != len(b) {
   301  		return true
   302  	}
   303  	for i := range a {
   304  		if a[i] != b[i] {
   305  			return true
   306  		}
   307  	}
   308  	return false
   309  }
   310  
   311  // UpdateControllerParams holds values used to update a controller details
   312  // after bootstrap or a login operation.
   313  type UpdateControllerParams struct {
   314  	// AgentVersion is the version of the controller agent.
   315  	AgentVersion string
   316  
   317  	// CurrentHostPorts are the available api addresses.
   318  	CurrentHostPorts [][]network.HostPort
   319  
   320  	// AddrConnectedTo are the previously known api addresses.
   321  	AddrConnectedTo []network.HostPort
   322  
   323  	// ModelCount (when set) is the number of models visible to the user.
   324  	ModelCount *int
   325  
   326  	// ControllerMachineCount (when set) is the total number of controller machines in the environment.
   327  	ControllerMachineCount *int
   328  
   329  	// MachineCount (when set) is the total number of machines in the models.
   330  	MachineCount *int
   331  }
   332  
   333  // UpdateControllerDetailsFromLogin writes any new api addresses and other relevant details
   334  // to the client controller file.
   335  // Controller may be specified by a UUID or name, and must already exist.
   336  func UpdateControllerDetailsFromLogin(
   337  	store jujuclient.ControllerStore, controllerName string,
   338  	params UpdateControllerParams,
   339  ) error {
   340  	controllerDetails, err := store.ControllerByName(controllerName)
   341  	if err != nil {
   342  		return errors.Trace(err)
   343  	}
   344  	return updateControllerDetailsFromLogin(store, controllerName, controllerDetails, params)
   345  }
   346  
   347  func updateControllerDetailsFromLogin(
   348  	store jujuclient.ControllerStore,
   349  	controllerName string, controllerDetails *jujuclient.ControllerDetails,
   350  	params UpdateControllerParams,
   351  ) error {
   352  	// Get the new endpoint addresses.
   353  	addrs, unresolvedAddrs, addrsChanged := PrepareEndpointsForCaching(*controllerDetails, params.CurrentHostPorts, params.AddrConnectedTo...)
   354  	agentChanged := params.AgentVersion != controllerDetails.AgentVersion
   355  	if !addrsChanged && !agentChanged && params.ModelCount == nil && params.MachineCount == nil && params.ControllerMachineCount == nil {
   356  		return nil
   357  	}
   358  
   359  	// Write the new controller data.
   360  	if addrsChanged {
   361  		controllerDetails.APIEndpoints = addrs
   362  		controllerDetails.UnresolvedAPIEndpoints = unresolvedAddrs
   363  	}
   364  	if agentChanged {
   365  		controllerDetails.AgentVersion = params.AgentVersion
   366  	}
   367  	if params.ModelCount != nil {
   368  		controllerDetails.ModelCount = params.ModelCount
   369  	}
   370  	if params.MachineCount != nil {
   371  		controllerDetails.MachineCount = params.MachineCount
   372  	}
   373  	if params.ControllerMachineCount != nil {
   374  		controllerDetails.ControllerMachineCount = *params.ControllerMachineCount
   375  	}
   376  	err := store.UpdateController(controllerName, *controllerDetails)
   377  	return errors.Trace(err)
   378  }
   379  
   380  // serverAddress returns the given string address:port as network.HostPort.
   381  //
   382  // TODO(axw) fix the tests that pass invalid addresses, and drop this.
   383  var serverAddress = func(hostPort string) (network.HostPort, error) {
   384  	addrConnectedTo, err := network.ParseHostPorts(hostPort)
   385  	if err != nil {
   386  		// Should never happen, since we've just connected with it.
   387  		return network.HostPort{}, errors.Annotatef(err, "invalid API address %q", hostPort)
   388  	}
   389  	return addrConnectedTo[0], nil
   390  }