func (s *baseLoginSuite) openAPIWithoutLogin(c *gc.C, info *api.Info) api.Connection { info.Tag = nil info.Password = "" st, err := api.Open(info, fastDialOpts) c.Assert(err, jc.ErrorIsNil) return st }
// connectFallback opens an API connection using the supplied info, // or a copy using the fallbackPassword; blocks for up to 5 minutes // if it encounters a CodeNotProvisioned error, periodically retrying; // and eventually, having either succeeded, failed, or timed out, returns: // // * (if successful) the connection, and whether the fallback was used // * (otherwise) whatever error it most recently encountered // // It's clear that it still has machine-agent concerns still baked in, // but there's no obvious practical path to separating those entirely at // the moment. // // (The right answer is probably to treat CodeNotProvisioned as a normal // error and depend on (currently nonexistent) exponential backoff in // the framework: either it'll work soon enough, or the controller will // spot the error and nuke the machine anyway. No harm leaving the local // agent running and occasionally polling for changes -- it won't do much // until it's managed to log in, and any suicide-cutoff point we pick here // will be objectively bad in some circumstances.) func connectFallback( apiOpen api.OpenFunc, info *api.Info, fallbackPassword string, ) ( conn api.Connection, didFallback bool, err error, ) { // We expect to assign to `conn`, `err`, *and* `info` in // the course of this operation: wrapping this repeated // atom in a func currently seems to be less treacherous // than the alternatives. var tryConnect = func() { conn, err = apiOpen(info, api.DialOpts{}) } // Try to connect, trying both the primary and fallback // passwords if necessary; and update info, and remember // which password we used. tryConnect() if params.IsCodeUnauthorized(err) { // We've perhaps used the wrong password, so // try again with the fallback password. infoCopy := *info info = &infoCopy info.Password = fallbackPassword didFallback = true tryConnect() } // We might be a machine agent that's started before its // provisioner has had a chance to report instance data // to the machine; wait a fair while to ensure we really // are in the (expected rare) provisioner-crash situation // that would cause permanent CodeNotProvisioned (which // indicates that the controller has forgotten about us, // and is provisioning a new instance, so we really should // uninstall). // // Yes, it's dumb that this can't be interrupted, and that // it's not configurable without patching. if params.IsCodeNotProvisioned(err) { for a := checkProvisionedStrategy.Start(); a.Next(); { tryConnect() if !params.IsCodeNotProvisioned(err) { break } } } // At this point we've run out of reasons to retry connecting, // and just go with whatever error we last saw (if any). if err != nil { return nil, false, errors.Trace(err) } return conn, didFallback, nil }
func commonConnect( apiOpen api.OpenFunc, apiInfo *api.Info, accountDetails *jujuclient.AccountDetails, modelUUID string, dialOpts api.DialOpts, ) (api.Connection, error) { if accountDetails != nil { // We only set the tag if either a password or // macaroon is found in the accounts.yaml file. // If neither is found, we'll use external // macaroon authentication which requires that // no tag be specified. userTag := names.NewUserTag(accountDetails.User) if accountDetails.Password != "" { // If a password is available, we always use // that. // // TODO(axw) make it invalid to store both // password and macaroon in accounts.yaml? apiInfo.Tag = userTag apiInfo.Password = accountDetails.Password } else if accountDetails.Macaroon != "" { var m macaroon.Macaroon if err := json.Unmarshal([]byte(accountDetails.Macaroon), &m); err != nil { return nil, errors.Trace(err) } apiInfo.Tag = userTag apiInfo.Macaroons = []macaroon.Slice{{&m}} } } if modelUUID != "" { apiInfo.ModelTag = names.NewModelTag(modelUUID) } st, err := apiOpen(apiInfo, dialOpts) return st, errors.Trace(err) }
func (s *macaroonLoginSuite) login(c *gc.C, info *api.Info) (params.LoginResult, error) { info.SkipLogin = true cookieJar := apitesting.NewClearableCookieJar() client := s.OpenAPI(c, info, cookieJar) defer client.Close() var ( // Remote users start with an empty login request. request params.LoginRequest result params.LoginResult ) err := client.APICall("Admin", 3, "", "Login", &request, &result) c.Assert(err, jc.ErrorIsNil) cookieURL := &url.URL{ Scheme: "https", Host: "localhost", Path: "/", } bakeryClient := httpbakery.NewClient() err = bakeryClient.HandleError(cookieURL, &httpbakery.Error{ Message: result.DischargeRequiredReason, Code: httpbakery.ErrDischargeRequired, Info: &httpbakery.ErrorInfo{ Macaroon: result.DischargeRequired, MacaroonPath: "/", }, }) c.Assert(err, jc.ErrorIsNil) // Add the macaroons that have been saved by HandleError to our login request. request.Macaroons = httpbakery.MacaroonsForURL(bakeryClient.Client.Jar, cookieURL) err = client.APICall("Admin", 3, "", "Login", &request, &result) return result, err }
func openAPIStateUsingInfo(info *api.Info, oldPassword string) (api.Connection, bool, error) { // We let the API dial fail immediately because the // runner's loop outside the caller of openAPIState will // keep on retrying. If we block for ages here, // then the worker that's calling this cannot // be interrupted. st, err := apiOpen(info, api.DialOpts{}) usedOldPassword := false if params.IsCodeUnauthorized(err) { // We've perhaps used the wrong password, so // try again with the fallback password. infoCopy := *info info = &infoCopy info.Password = oldPassword usedOldPassword = true st, err = apiOpen(info, api.DialOpts{}) } // The provisioner may take some time to record the agent's // machine instance ID, so wait until it does so. if params.IsCodeNotProvisioned(err) { for a := checkProvisionedStrategy.Start(); a.Next(); { st, err = apiOpen(info, api.DialOpts{}) if !params.IsCodeNotProvisioned(err) { break } } } if err != nil { if params.IsCodeNotProvisioned(err) || params.IsCodeUnauthorized(err) { logger.Errorf("agent terminating due to error returned during API open: %v", err) return nil, false, worker.ErrTerminateAgent } return nil, false, err } return st, usedOldPassword, nil }
// Run implements Command.Run func (c *loginCommand) Run(ctx *cmd.Context) error { if c.loginAPIOpen == nil { c.loginAPIOpen = c.JujuCommandBase.APIOpen } // TODO(thumper): as we support the user and address // change this check here. if c.Server.Path == "" { return errors.New("no server file specified") } serverYAML, err := c.Server.Read(ctx) if err != nil { return errors.Trace(err) } var serverDetails envcmd.ServerFile if err := goyaml.Unmarshal(serverYAML, &serverDetails); err != nil { return errors.Trace(err) } info := api.Info{ Addrs: serverDetails.Addresses, CACert: serverDetails.CACert, } var userTag names.UserTag if serverDetails.Username != "" { // Construct the api.Info struct from the provided values // and attempt to connect to the remote server before we do anything else. if !names.IsValidUser(serverDetails.Username) { return errors.Errorf("%q is not a valid username", serverDetails.Username) } userTag = names.NewUserTag(serverDetails.Username) if !userTag.IsLocal() { // Remote users do not have their passwords stored in Juju // so we never attempt to change them. c.KeepPassword = true } info.Tag = userTag } if serverDetails.Password != "" { info.Password = serverDetails.Password } if serverDetails.Password == "" || serverDetails.Username == "" { info.UseMacaroons = true } if c == nil { panic("nil c") } if c.loginAPIOpen == nil { panic("no loginAPIOpen") } apiState, err := c.loginAPIOpen(&info, api.DefaultDialOpts()) if err != nil { return errors.Trace(err) } defer apiState.Close() // If we get to here, the credentials supplied were sufficient to connect // to the Juju Controller and login. Now we cache the details. controllerInfo, err := c.cacheConnectionInfo(serverDetails, apiState) if err != nil { return errors.Trace(err) } ctx.Infof("cached connection details as controller %q", c.Name) // If we get to here, we have been able to connect to the API server, and // also have been able to write the cached information. Now we can change // the user's password to a new randomly generated strong password, and // update the cached information knowing that the likelihood of failure is // minimal. if !c.KeepPassword { if err := c.updatePassword(ctx, apiState, userTag, controllerInfo); err != nil { return errors.Trace(err) } } return errors.Trace(envcmd.SetCurrentController(ctx, c.Name)) }