Permalink
Cannot retrieve contributors at this time
Fetching contributors…
| // Copyright 2013 Canonical Ltd. | |
| // Licensed under the AGPLv3, see LICENCE file for details. | |
| package juju | |
| import ( | |
| "time" | |
| "github.com/juju/errors" | |
| "github.com/juju/loggo" | |
| "gopkg.in/juju/names.v2" | |
| "github.com/juju/juju/api" | |
| "github.com/juju/juju/jujuclient" | |
| "github.com/juju/juju/network" | |
| ) | |
| var logger = loggo.GetLogger("juju.juju") | |
| // The following are variables so that they can be | |
| // changed by tests. | |
| var ( | |
| providerConnectDelay = 2 * time.Second | |
| ) | |
| type apiStateCachedInfo struct { | |
| api.Connection | |
| // If cachedInfo is non-nil, it indicates that the info has been | |
| // newly retrieved, and should be cached in the config store. | |
| cachedInfo *api.Info | |
| } | |
| // NewAPIConnectionParams contains the parameters for creating a new Juju API | |
| // connection. | |
| type NewAPIConnectionParams struct { | |
| // ControllerName is the name of the controller to connect to. | |
| ControllerName string | |
| // Store is the jujuclient.ClientStore from which the controller's | |
| // details will be fetched, and updated on address changes. | |
| Store jujuclient.ClientStore | |
| // OpenAPI is the function that will be used to open API connections. | |
| OpenAPI api.OpenFunc | |
| // DialOpts contains the options used to dial the API connection. | |
| DialOpts api.DialOpts | |
| // AccountDetails contains the account details to use for logging | |
| // in to the Juju API. If this is nil, then no login will take | |
| // place. If AccountDetails.Password and AccountDetails.Macaroon | |
| // are zero, the login will be as an external user. | |
| AccountDetails *jujuclient.AccountDetails | |
| // ModelUUID is an optional model UUID. If specified, the API connection | |
| // will be scoped to the model with that UUID; otherwise it will be | |
| // scoped to the controller. | |
| ModelUUID string | |
| } | |
| // NewAPIConnection returns an api.Connection to the specified Juju controller, | |
| // with specified account credentials, optionally scoped to the specified model | |
| // name. | |
| func NewAPIConnection(args NewAPIConnectionParams) (api.Connection, error) { | |
| apiInfo, controller, err := connectionInfo(args) | |
| if err != nil { | |
| return nil, errors.Annotatef(err, "cannot work out how to connect") | |
| } | |
| if len(apiInfo.Addrs) == 0 { | |
| return nil, errors.New("no API addresses") | |
| } | |
| logger.Infof("connecting to API addresses: %v", apiInfo.Addrs) | |
| st, err := args.OpenAPI(apiInfo, args.DialOpts) | |
| if err != nil { | |
| redirErr, ok := errors.Cause(err).(*api.RedirectError) | |
| if !ok { | |
| return nil, errors.Trace(err) | |
| } | |
| // We've been told to connect to a different API server, | |
| // so do so. Note that we don't copy the account details | |
| // because the account on the redirected server may well | |
| // be different - we'll use macaroon authentication | |
| // directly without sending account details. | |
| // Copy the API info because it's possible that the | |
| // apiConfigConnect is still using it concurrently. | |
| apiInfo = &api.Info{ | |
| ModelTag: apiInfo.ModelTag, | |
| Addrs: network.HostPortsToStrings(usableHostPorts(redirErr.Servers)), | |
| CACert: redirErr.CACert, | |
| } | |
| st, err = args.OpenAPI(apiInfo, args.DialOpts) | |
| if err != nil { | |
| return nil, errors.Annotatef(err, "cannot connect to redirected address") | |
| } | |
| // TODO(rog) update cached model addresses. | |
| // TODO(rog) should we do something with the logged-in username? | |
| return st, nil | |
| } | |
| addrConnectedTo, err := serverAddress(st.Addr()) | |
| if err != nil { | |
| return nil, errors.Trace(err) | |
| } | |
| // Update API addresses if they've changed. Error is non-fatal. | |
| // Note that in the redirection case, we won't update the addresses | |
| // of the controller we first connected to. This shouldn't be | |
| // a problem in practice because the intended scenario for | |
| // controllers that redirect involves them having well known | |
| // public addresses that won't change over time. | |
| hostPorts := st.APIHostPorts() | |
| agentVersion := "" | |
| if v, ok := st.ServerVersion(); ok { | |
| agentVersion = v.String() | |
| } | |
| params := UpdateControllerParams{ | |
| AgentVersion: agentVersion, | |
| AddrConnectedTo: []network.HostPort{addrConnectedTo}, | |
| CurrentHostPorts: hostPorts, | |
| } | |
| err = updateControllerDetailsFromLogin(args.Store, args.ControllerName, controller, params) | |
| if err != nil { | |
| logger.Errorf("cannot cache API addresses: %v", err) | |
| } | |
| // Process the account details obtained from login. | |
| var accountDetails *jujuclient.AccountDetails | |
| user, ok := st.AuthTag().(names.UserTag) | |
| if !apiInfo.SkipLogin { | |
| if ok { | |
| if accountDetails, err = args.Store.AccountDetails(args.ControllerName); err != nil { | |
| if !errors.IsNotFound(err) { | |
| logger.Errorf("cannot load local account information: %v", err) | |
| } | |
| } else { | |
| accountDetails.LastKnownAccess = st.ControllerAccess() | |
| } | |
| } | |
| if ok && !user.IsLocal() && apiInfo.Tag == nil { | |
| // We used macaroon auth to login; save the username | |
| // that we've logged in as. | |
| accountDetails = &jujuclient.AccountDetails{ | |
| User: user.Id(), | |
| LastKnownAccess: st.ControllerAccess(), | |
| } | |
| } else if apiInfo.Tag == nil { | |
| logger.Errorf("unexpected logged-in username %v", st.AuthTag()) | |
| } | |
| } | |
| if accountDetails != nil { | |
| if err := args.Store.UpdateAccount(args.ControllerName, *accountDetails); err != nil { | |
| logger.Errorf("cannot update account information: %v", err) | |
| } | |
| } | |
| return st, nil | |
| } | |
| // connectionInfo returns connection information suitable for | |
| // connecting to the controller and model specified in the given | |
| // parameters. If there are no addresses known for the controller, | |
| // it may return a *api.Info with no APIEndpoints, but all other | |
| // information will be populated. | |
| func connectionInfo(args NewAPIConnectionParams) (*api.Info, *jujuclient.ControllerDetails, error) { | |
| controller, err := args.Store.ControllerByName(args.ControllerName) | |
| if err != nil { | |
| return nil, nil, errors.Annotate(err, "cannot get controller details") | |
| } | |
| apiInfo := &api.Info{ | |
| Addrs: controller.APIEndpoints, | |
| CACert: controller.CACert, | |
| } | |
| if args.ModelUUID != "" { | |
| apiInfo.ModelTag = names.NewModelTag(args.ModelUUID) | |
| } | |
| if args.AccountDetails == nil { | |
| apiInfo.SkipLogin = true | |
| return apiInfo, controller, nil | |
| } | |
| account := args.AccountDetails | |
| if account.User != "" { | |
| userTag := names.NewUserTag(account.User) | |
| if userTag.IsLocal() { | |
| apiInfo.Tag = userTag | |
| } | |
| } | |
| if args.AccountDetails.Password != "" { | |
| // If a password is available, we always use that. | |
| // If no password is recorded, we'll attempt to | |
| // authenticate using macaroons. | |
| apiInfo.Password = account.Password | |
| } | |
| return apiInfo, controller, nil | |
| } | |
| func isAPIError(err error) bool { | |
| type errorCoder interface { | |
| ErrorCode() string | |
| } | |
| _, ok := errors.Cause(err).(errorCoder) | |
| return ok | |
| } | |
| var resolveOrDropHostnames = network.ResolveOrDropHostnames | |
| // PrepareEndpointsForCaching performs the necessary operations on the | |
| // given API hostPorts so they are suitable for saving into the | |
| // controller.yaml file, taking into account the addrConnectedTo | |
| // and the existing config store info: | |
| // | |
| // 1. Collapses hostPorts into a single slice. | |
| // 2. Filters out machine-local and link-local addresses. | |
| // 3. Removes any duplicates | |
| // 4. Call network.SortHostPorts() on the list. | |
| // 5. Puts the addrConnectedTo on top. | |
| // 6. Compares the result against info.APIEndpoint.Hostnames. | |
| // 7. If the addresses differ, call network.ResolveOrDropHostnames() | |
| // on the list and perform all steps again from step 1. | |
| // 8. Compare the list of resolved addresses against the cached info | |
| // APIEndpoint.Addresses, and if changed return both addresses and | |
| // hostnames as strings (so they can be cached on APIEndpoint) and | |
| // set haveChanged to true. | |
| // 9. If the hostnames haven't changed, return two empty slices and set | |
| // haveChanged to false. No DNS resolution is performed to save time. | |
| // | |
| // This is used right after bootstrap to saved the initial API | |
| // endpoints, as well as on each CLI connection to verify if the | |
| // saved endpoints need updating. | |
| // | |
| // TODO(rogpeppe) this function mixes too many concerns - the | |
| // logic is difficult to follow and has non-obvious properties. | |
| func PrepareEndpointsForCaching( | |
| controllerDetails jujuclient.ControllerDetails, | |
| hostPorts [][]network.HostPort, | |
| addrConnectedTo ...network.HostPort, | |
| ) (addrs, unresolvedAddrs []string, haveChanged bool) { | |
| processHostPorts := func(allHostPorts [][]network.HostPort) []network.HostPort { | |
| uniqueHPs := usableHostPorts(allHostPorts) | |
| network.SortHostPorts(uniqueHPs) | |
| for _, addr := range addrConnectedTo { | |
| uniqueHPs = network.EnsureFirstHostPort(addr, uniqueHPs) | |
| } | |
| return uniqueHPs | |
| } | |
| apiHosts := processHostPorts(hostPorts) | |
| hostsStrings := network.HostPortsToStrings(apiHosts) | |
| needResolving := false | |
| // Verify if the unresolved addresses have changed. | |
| if len(apiHosts) > 0 && len(controllerDetails.UnresolvedAPIEndpoints) > 0 { | |
| if addrsChanged(hostsStrings, controllerDetails.UnresolvedAPIEndpoints) { | |
| logger.Debugf( | |
| "API hostnames changed from %v to %v - resolving hostnames", | |
| controllerDetails.UnresolvedAPIEndpoints, hostsStrings, | |
| ) | |
| needResolving = true | |
| } | |
| } else if len(apiHosts) > 0 { | |
| // No cached hostnames, most likely right after bootstrap. | |
| logger.Debugf("API hostnames %v - resolving hostnames", hostsStrings) | |
| needResolving = true | |
| } | |
| if !needResolving { | |
| // We're done - nothing changed. | |
| logger.Debugf("API hostnames unchanged - not resolving") | |
| return nil, nil, false | |
| } | |
| // Perform DNS resolution and check against APIEndpoints.Addresses. | |
| resolved := resolveOrDropHostnames(apiHosts) | |
| apiAddrs := processHostPorts([][]network.HostPort{resolved}) | |
| addrsStrings := network.HostPortsToStrings(apiAddrs) | |
| if len(apiAddrs) > 0 && len(controllerDetails.APIEndpoints) > 0 { | |
| if addrsChanged(addrsStrings, controllerDetails.APIEndpoints) { | |
| logger.Infof( | |
| "API addresses changed from %v to %v", | |
| controllerDetails.APIEndpoints, addrsStrings, | |
| ) | |
| return addrsStrings, hostsStrings, true | |
| } | |
| } else if len(apiAddrs) > 0 { | |
| // No cached addresses, most likely right after bootstrap. | |
| logger.Infof("new API addresses to cache %v", addrsStrings) | |
| return addrsStrings, hostsStrings, true | |
| } | |
| // No changes. | |
| logger.Debugf("API addresses unchanged") | |
| return nil, nil, false | |
| } | |
| // usableHostPorts returns hps with unusable and non-unique | |
| // host-ports filtered out. | |
| func usableHostPorts(hps [][]network.HostPort) []network.HostPort { | |
| collapsed := network.CollapseHostPorts(hps) | |
| usable := network.FilterUnusableHostPorts(collapsed) | |
| unique := network.UniqueHostPorts(usable) | |
| return unique | |
| } | |
| // addrsChanged returns true iff the two | |
| // slices are not equal. Order is important. | |
| func addrsChanged(a, b []string) bool { | |
| if len(a) != len(b) { | |
| return true | |
| } | |
| for i := range a { | |
| if a[i] != b[i] { | |
| return true | |
| } | |
| } | |
| return false | |
| } | |
| // UpdateControllerParams holds values used to update a controller details | |
| // after bootstrap or a login operation. | |
| type UpdateControllerParams struct { | |
| // AgentVersion is the version of the controller agent. | |
| AgentVersion string | |
| // CurrentHostPorts are the available api addresses. | |
| CurrentHostPorts [][]network.HostPort | |
| // AddrConnectedTo are the previously known api addresses. | |
| AddrConnectedTo []network.HostPort | |
| // ModelCount (when set) is the number of models visible to the user. | |
| ModelCount *int | |
| // ControllerMachineCount (when set) is the total number of controller machines in the environment. | |
| ControllerMachineCount *int | |
| // MachineCount (when set) is the total number of machines in the models. | |
| MachineCount *int | |
| } | |
| // UpdateControllerDetailsFromLogin writes any new api addresses and other relevant details | |
| // to the client controller file. | |
| // Controller may be specified by a UUID or name, and must already exist. | |
| func UpdateControllerDetailsFromLogin( | |
| store jujuclient.ControllerStore, controllerName string, | |
| params UpdateControllerParams, | |
| ) error { | |
| controllerDetails, err := store.ControllerByName(controllerName) | |
| if err != nil { | |
| return errors.Trace(err) | |
| } | |
| return updateControllerDetailsFromLogin(store, controllerName, controllerDetails, params) | |
| } | |
| func updateControllerDetailsFromLogin( | |
| store jujuclient.ControllerStore, | |
| controllerName string, controllerDetails *jujuclient.ControllerDetails, | |
| params UpdateControllerParams, | |
| ) error { | |
| // Get the new endpoint addresses. | |
| addrs, unresolvedAddrs, addrsChanged := PrepareEndpointsForCaching(*controllerDetails, params.CurrentHostPorts, params.AddrConnectedTo...) | |
| agentChanged := params.AgentVersion != controllerDetails.AgentVersion | |
| if !addrsChanged && !agentChanged && params.ModelCount == nil && params.MachineCount == nil && params.ControllerMachineCount == nil { | |
| return nil | |
| } | |
| // Write the new controller data. | |
| if addrsChanged { | |
| controllerDetails.APIEndpoints = addrs | |
| controllerDetails.UnresolvedAPIEndpoints = unresolvedAddrs | |
| } | |
| if agentChanged { | |
| controllerDetails.AgentVersion = params.AgentVersion | |
| } | |
| if params.ModelCount != nil { | |
| controllerDetails.ModelCount = params.ModelCount | |
| } | |
| if params.MachineCount != nil { | |
| controllerDetails.MachineCount = params.MachineCount | |
| } | |
| if params.ControllerMachineCount != nil { | |
| controllerDetails.ControllerMachineCount = *params.ControllerMachineCount | |
| } | |
| err := store.UpdateController(controllerName, *controllerDetails) | |
| return errors.Trace(err) | |
| } | |
| // serverAddress returns the given string address:port as network.HostPort. | |
| // | |
| // TODO(axw) fix the tests that pass invalid addresses, and drop this. | |
| var serverAddress = func(hostPort string) (network.HostPort, error) { | |
| addrConnectedTo, err := network.ParseHostPorts(hostPort) | |
| if err != nil { | |
| // Should never happen, since we've just connected with it. | |
| return network.HostPort{}, errors.Annotatef(err, "invalid API address %q", hostPort) | |
| } | |
| return addrConnectedTo[0], nil | |
| } |