func (c *modelsCommand) getModelInfo(userModels []base.UserModel) ([]params.ModelInfo, error) { client, err := c.getModelManagerAPI() if err != nil { return nil, errors.Trace(err) } defer client.Close() tags := make([]names.ModelTag, len(userModels)) for i, m := range userModels { tags[i] = names.NewModelTag(m.UUID) } results, err := client.ModelInfo(tags) if err != nil { return nil, errors.Trace(err) } info := make([]params.ModelInfo, len(tags)) for i, result := range results { if result.Error != nil { if params.IsCodeUnauthorized(result.Error) { // If we get this, then the model was removed // between the initial listing and the call // to query its details. continue } return nil, errors.Annotatef( result.Error, "getting model %s (%q) info", userModels[i].UUID, userModels[i].Name, ) } info[i] = *result.Result } return info, nil }
// RestoreError makes a best effort at converting the given error // back into an error originally converted by ServerError(). If the // error could not be converted then false is returned. func RestoreError(err error) (error, bool) { err = errors.Cause(err) if apiErr, ok := err.(*params.Error); !ok { return err, false } else if apiErr == nil { return nil, true } if params.ErrCode(err) == "" { return err, false } msg := err.Error() if singleton, ok := singletonError(err); ok { return singleton, true } // TODO(ericsnow) Support the other error types handled by ServerError(). switch { case params.IsCodeUnauthorized(err): return errors.NewUnauthorized(nil, msg), true case params.IsCodeNotFound(err): // TODO(ericsnow) UnknownModelError should be handled here too. // ...by parsing msg? return errors.NewNotFound(nil, msg), true case params.IsCodeAlreadyExists(err): return errors.NewAlreadyExists(nil, msg), true case params.IsCodeNotAssigned(err): return errors.NewNotAssigned(nil, msg), true case params.IsCodeHasAssignedUnits(err): // TODO(ericsnow) Handle state.HasAssignedUnitsError here. // ...by parsing msg? return err, false case params.IsCodeNoAddressSet(err): // TODO(ericsnow) Handle isNoAddressSetError here. // ...by parsing msg? return err, false case params.IsCodeNotProvisioned(err): return errors.NewNotProvisioned(nil, msg), true case params.IsCodeUpgradeInProgress(err): // TODO(ericsnow) Handle state.UpgradeInProgressError here. // ...by parsing msg? return err, false case params.IsCodeMachineHasAttachedStorage(err): // TODO(ericsnow) Handle state.HasAttachmentsError here. // ...by parsing msg? return err, false case params.IsCodeNotSupported(err): return errors.NewNotSupported(nil, msg), true case params.IsBadRequest(err): return errors.NewBadRequest(nil, msg), true case params.IsMethodNotAllowed(err): return errors.NewMethodNotAllowed(nil, msg), true case params.ErrCode(err) == params.CodeDischargeRequired: // TODO(ericsnow) Handle DischargeRequiredError here. return err, false default: return err, false } }
// addCharmViaAPI calls the appropriate client API calls to add the // given charm URL to state. For non-public charm URLs, this function also // handles the macaroon authorization process using the given csClient. // The resulting charm URL of the added charm is displayed on stdout. func addCharmViaAPI(client *api.Client, ctx *cmd.Context, curl *charm.URL, repo charmrepo.Interface, csclient *csClient) (*charm.URL, error) { switch curl.Schema { case "local": ch, err := repo.Get(curl) if err != nil { return nil, err } stateCurl, err := client.AddLocalCharm(curl, ch) if err != nil { return nil, err } curl = stateCurl case "cs": if err := client.AddCharm(curl); err != nil { if !params.IsCodeUnauthorized(err) { return nil, errors.Mask(err) } m, err := csclient.authorize(curl) if err != nil { return nil, errors.Mask(err) } if err := client.AddCharmWithAuthorization(curl, m); err != nil { return nil, errors.Mask(err) } } default: return nil, fmt.Errorf("unsupported charm URL schema: %q", curl.Schema) } ctx.Infof("Added charm %q to the environment.", curl) return curl, nil }
// Run implements Command.Run. func (c *addCommand) Run(ctx *cmd.Context) error { return c.RunWithAPI(ctx, func(api SpaceAPI, ctx *cmd.Context) error { // Prepare a nicer message and proper arguments to use in case // there are not CIDRs given. var subnetIds []string msgSuffix := "no subnets" if !c.CIDRs.IsEmpty() { subnetIds = c.CIDRs.SortedValues() msgSuffix = fmt.Sprintf("subnets %s", strings.Join(subnetIds, ", ")) } // Add the new space. // TODO(dimitern): Accept --public|--private and pass it here. err := api.AddSpace(c.Name, subnetIds, true) if err != nil { if errors.IsNotSupported(err) { ctx.Infof("cannot add space %q: %v", c.Name, err) } if params.IsCodeUnauthorized(err) { common.PermissionsMessage(ctx.Stderr, "add a space") } return errors.Annotatef(err, "cannot add space %q", c.Name) } ctx.Infof("added space %q with %s", c.Name, msgSuffix) return nil }) }
// addCharmFromURL calls the appropriate client API calls to add the // given charm URL to state. For non-public charm URLs, this function also // handles the macaroon authorization process using the given csClient. // The resulting charm URL of the added charm is displayed on stdout. func addCharmFromURL(client *api.Client, curl *charm.URL, repo charmrepo.Interface, csclient *csClient) (*charm.URL, error) { switch curl.Schema { case "local": ch, err := repo.Get(curl) if err != nil { return nil, err } stateCurl, err := client.AddLocalCharm(curl, ch) if err != nil { return nil, err } curl = stateCurl case "cs": if err := client.AddCharm(curl); err != nil { if !params.IsCodeUnauthorized(err) { return nil, errors.Trace(err) } m, err := csclient.authorize(curl) if err != nil { return nil, maybeTermsAgreementError(err) } if err := client.AddCharmWithAuthorization(curl, m); err != nil { return nil, errors.Trace(err) } } default: return nil, fmt.Errorf("unsupported charm URL schema: %q", curl.Schema) } return curl, nil }
// Run implements Command.Run. func (c *addCommand) Run(ctx *cmd.Context) error { return c.RunWithAPI(ctx, func(api SubnetAPI, ctx *cmd.Context) error { if c.CIDR.Id() != "" && c.RawCIDR != c.CIDR.Id() { ctx.Infof( "WARNING: using CIDR %q instead of the incorrectly specified %q.", c.CIDR.Id(), c.RawCIDR, ) } // Add the existing subnet. err := api.AddSubnet(c.CIDR, network.Id(c.ProviderId), c.Space, c.Zones) // TODO(dimitern): Change this once the API returns a concrete error. if err != nil && strings.Contains(err.Error(), "multiple subnets with") { // Special case: multiple subnets with the same CIDR exist ctx.Infof("ERROR: %v.", err) return nil } else if err != nil { if params.IsCodeUnauthorized(err) { common.PermissionsMessage(ctx.Stderr, "add a subnet") } return errors.Annotatef(err, "cannot add subnet") } if c.ProviderId != "" { ctx.Infof("added subnet with ProviderId %q in space %q", c.ProviderId, c.Space.Id()) } else { ctx.Infof("added subnet with CIDR %q in space %q", c.CIDR.Id(), c.Space.Id()) } return nil }) }
// OpenAPIState opens the API using the given information. The agent's // password is changed if the fallback password was used to connect to // the API. func OpenAPIState(agentConfig agent.Config, a Agent) (_ *api.State, _ *apiagent.Entity, outErr error) { info := agentConfig.APIInfo() st, usedOldPassword, err := openAPIStateUsingInfo(info, a, agentConfig.OldPassword()) if err != nil { return nil, nil, err } defer func() { if outErr != nil && st != nil { st.Close() } }() entity, err := st.Agent().Entity(a.Tag()) if err == nil && entity.Life() == params.Dead { logger.Errorf("agent terminating - entity %q is dead", a.Tag()) return nil, nil, worker.ErrTerminateAgent } if params.IsCodeUnauthorized(err) { logger.Errorf("agent terminating due to error returned during entity lookup: %v", err) return nil, nil, worker.ErrTerminateAgent } if err != nil { return nil, nil, err } if !usedOldPassword { // Call set password with the current password. If we've recently // become a state server, this will fix up our credentials in mongo. if err := entity.SetPassword(info.Password); err != nil { return nil, nil, errors.Annotate(err, "can't reset agent password") } } else { // We succeeded in connecting with the fallback // password, so we need to create a new password // for the future. newPassword, err := utils.RandomPassword() if err != nil { return nil, nil, err } err = setAgentPassword(newPassword, info.Password, a, entity) if err != nil { return nil, nil, err } // Reconnect to the API with the new password. st.Close() info.Password = newPassword st, err = apiOpen(info, api.DialOpts{}) if err != nil { return nil, nil, err } } return st, entity, err }
// 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 (c *addRelationCommand) Run(ctx *cmd.Context) error { client, err := c.newAPIFunc() if err != nil { return err } defer client.Close() _, err = client.AddRelation(c.Endpoints...) if params.IsCodeUnauthorized(err) { common.PermissionsMessage(ctx.Stderr, "add a relation") } return block.ProcessBlockedError(err, block.BlockChange) }
// Run implements Command.Run. func (c *addCommand) Run(ctx *cmd.Context) (err error) { api, err := c.newAPIFunc() if err != nil { return err } defer api.Close() storages := c.createStorageAddParams() results, err := api.AddToUnit(storages) if err != nil { if params.IsCodeUnauthorized(err) { common.PermissionsMessage(ctx.Stderr, "add storage") } return err } var added []string var failures []string // If there was a unit-related error, then all storages will get the same error. // We want to collapse these - no need to repeat the same things ad nauseam. collapsedFailures := set.NewStrings() for i, one := range results { us := storages[i] if one.Error != nil { failures = append(failures, fmt.Sprintf(fail, us.StorageName, one.Error)) collapsedFailures.Add(one.Error.Error()) continue } added = append(added, fmt.Sprintf(success, us.StorageName)) } if len(added) > 0 { fmt.Fprintln(ctx.Stdout, strings.Join(added, newline)) } if len(failures) == len(storages) { // If we managed to collapse, then display these instead of the whole list. if len(collapsedFailures) < len(failures) { for _, one := range collapsedFailures.SortedValues() { fmt.Fprintln(ctx.Stderr, one) } return cmd.ErrSilent } } if len(failures) > 0 { fmt.Fprintln(ctx.Stderr, strings.Join(failures, newline)) return cmd.ErrSilent } return nil }
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 }
func opClientResolved(c *gc.C, st api.Connection, _ *state.State) (func(), error) { err := st.Client().Resolved("wordpress/1", false) // There are several scenarios in which this test is called, one is // that the user is not authorized. In that case we want to exit now, // letting the error percolate out so the caller knows that the // permission error was correctly generated. if err != nil && params.IsCodeUnauthorized(err) { return func() {}, err } // Otherwise, the user was authorized, but we expect an error anyway // because the unit is not in an error state when we tried to resolve // the error. Therefore, since it is complaining it means that the // call to Resolved worked, so we're happy. c.Assert(err, gc.NotNil) c.Assert(err.Error(), gc.Equals, `unit "wordpress/1" is not in an error state`) return func() {}, nil }
// addCharmFromURL calls the appropriate client API calls to add the // given charm URL to state. For non-public charm URLs, this function also // handles the macaroon authorization process using the given csClient. // The resulting charm URL of the added charm is displayed on stdout. func addCharmFromURL(client *api.Client, curl *charm.URL, channel csparams.Channel, csClient *csclient.Client) (*charm.URL, *macaroon.Macaroon, error) { var csMac *macaroon.Macaroon if err := client.AddCharm(curl, channel); err != nil { if !params.IsCodeUnauthorized(err) { return nil, nil, errors.Trace(err) } m, err := authorizeCharmStoreEntity(csClient, curl) if err != nil { return nil, nil, maybeTermsAgreementError(err) } if err := client.AddCharmWithAuthorization(curl, channel, m); err != nil { return nil, nil, errors.Trace(err) } csMac = m } return curl, csMac, nil }
// addCharmFromURL calls the appropriate client API calls to add the // given charm URL to state. For non-public charm URLs, this function also // handles the macaroon authorization process using the given csClient. // The resulting charm URL of the added charm is displayed on stdout. // // The repo holds the charm repository associated with with the URL // by resolveCharmStoreEntityURL. func addCharmFromURL(client *api.Client, curl *charm.URL, channel csparams.Channel, repo charmrepo.Interface) (*charm.URL, *macaroon.Macaroon, error) { var csMac *macaroon.Macaroon switch curl.Schema { case "local": ch, err := repo.Get(curl) if err != nil { return nil, nil, err } stateCurl, err := client.AddLocalCharm(curl, ch) if err != nil { return nil, nil, err } curl = stateCurl case "cs": repo, ok := repo.(*charmrepo.CharmStore) if !ok { return nil, nil, errors.Errorf("(cannot happen) cs-schema URL with unexpected repo type %T", repo) } csClient := repo.Client() if err := client.AddCharm(curl, channel); err != nil { if !params.IsCodeUnauthorized(err) { return nil, nil, errors.Trace(err) } m, err := authorizeCharmStoreEntity(csClient, curl) if err != nil { return nil, nil, maybeTermsAgreementError(err) } if err := client.AddCharmWithAuthorization(curl, channel, m); err != nil { return nil, nil, errors.Trace(err) } csMac = m } default: return nil, nil, fmt.Errorf("unsupported charm URL schema: %q", curl.Schema) } return curl, csMac, nil }
// ScaryConnect logs into the API using the supplied agent's credentials, // like OnlyConnect; and then: // // * returns ErrConnectImpossible if the agent entity is dead or // unauthorized for all known passwords; // * if the agent's config does not specify a model, tries to record the // model we just connected to; // * replaces insecure credentials with freshly (locally) generated ones // (and returns ErrPasswordChanged, expecting to be reinvoked); // * unconditionally resets the remote-state password to its current value // (for what seems like a bad reason). // // This is clearly a mess but at least now it's a documented and localized // mess; it should be used only when making the primary API connection for // a machine or unit agent running in its own process. func ScaryConnect(a agent.Agent, apiOpen api.OpenFunc) (_ api.Connection, err error) { agentConfig := a.CurrentConfig() info, ok := agentConfig.APIInfo() if !ok { return nil, errors.New("API info not available") } oldPassword := agentConfig.OldPassword() defer func() { cause := errors.Cause(err) switch { case cause == apiagent.ErrDenied: case cause == errAgentEntityDead: case params.IsCodeUnauthorized(cause): case params.IsCodeNotProvisioned(cause): default: return } err = ErrConnectImpossible }() // Start connection... conn, usedOldPassword, err := connectFallback(apiOpen, info, oldPassword) if err != nil { return nil, errors.Trace(err) } // ...and make sure we close it if anything goes wrong. defer func() { if err != nil { if err := conn.Close(); err != nil { logger.Errorf("while closing API connection: %v", err) } } }() // Update the agent config if necessary; this should just read the // conn's properties, rather than making api calls, so we don't // need to think about facades yet. maybeSetAgentModelTag(a, conn) // newConnFacade is patched out in export_test, because exhaustion. // proper config/params struct would be better. facade, err := newConnFacade(conn) if err != nil { return nil, errors.Trace(err) } // First of all, see if we're dead or removed, which will render // any further work pointless. entity := agentConfig.Tag() life, err := facade.Life(entity) if err != nil { return nil, errors.Trace(err) } switch life { case apiagent.Alive, apiagent.Dying: case apiagent.Dead: return nil, errAgentEntityDead default: return nil, errors.Errorf("unknown life value %q", life) } // If we need to change the password, it's far cleaner to // exit with ErrChangedPassword and depend on the framework // for expeditious retry than it is to mess around with those // responsibilities in here. if usedOldPassword { logger.Debugf("changing password...") err := changePassword(oldPassword, a, facade) if err != nil { return nil, errors.Trace(err) } logger.Debugf("password changed") return nil, ErrChangedPassword } // If we *didn't* need to change the password, we apparently need // to reset our password to its current value anyway. Reportedly, // a machine agent promoted to controller status might have bad // auth data in mongodb, and this "fixes" it... but this is scary, // wrong, coincidental duct tape. The RTTD is to make controller- // promotion work correctly in the first place. // // Still, can't fix everything at once. if err := facade.SetPassword(entity, info.Password); err != nil { return nil, errors.Annotate(err, "can't reset agent password") } return conn, nil }
// Run implements Command.Run. func (c *addCommand) Run(ctx *cmd.Context) error { api := c.api if api == nil { var err error api, err = c.NewUserManagerAPIClient() if err != nil { return errors.Trace(err) } defer api.Close() } // Add a user without a password. This will generate a temporary // secret key, which we'll print out for the user to supply to // "juju register". _, secretKey, err := api.AddUser(c.User, c.DisplayName, "") if err != nil { if params.IsCodeUnauthorized(err) { common.PermissionsMessage(ctx.Stderr, "add a user") } return block.ProcessBlockedError(err, block.BlockChange) } displayName := c.User if c.DisplayName != "" { displayName = fmt.Sprintf("%s (%s)", c.DisplayName, c.User) } // Generate the base64-encoded string for the user to pass to // "juju register". We marshal the information using ASN.1 // to keep the size down, since we need to encode binary data. controllerDetails, err := c.ClientStore().ControllerByName(c.ControllerName()) if err != nil { return errors.Trace(err) } registrationInfo := jujuclient.RegistrationInfo{ User: c.User, Addrs: controllerDetails.APIEndpoints, SecretKey: secretKey, ControllerName: c.ControllerName(), } registrationData, err := asn1.Marshal(registrationInfo) if err != nil { return errors.Trace(err) } // Use URLEncoding so we don't get + or / in the string, // and pad with zero bytes so we don't get =; this all // makes it easier to copy & paste in a terminal. // // The embedded ASN.1 data is length-encoded, so the // padding will not complicate decoding. remainder := len(registrationData) % 3 for remainder > 0 { registrationData = append(registrationData, 0) remainder-- } base64RegistrationData := base64.URLEncoding.EncodeToString( registrationData, ) fmt.Fprintf(ctx.Stdout, "User %q added\n", displayName) fmt.Fprintf(ctx.Stdout, "Please send this command to %v:\n", c.User) fmt.Fprintf(ctx.Stdout, " juju register %s\n", base64RegistrationData, ) fmt.Fprintf(ctx.Stdout, ` %q has not been granted access to any models. You can use "juju grant" to grant access. `, displayName) return nil }
func (c *addCommand) Run(ctx *cmd.Context) error { var err error c.Constraints, err = common.ParseConstraints(ctx, c.ConstraintsStr) if err != nil { return err } client, err := c.getClientAPI() if err != nil { return errors.Trace(err) } defer client.Close() var machineManager MachineManagerAPI if len(c.Disks) > 0 { machineManager, err = c.getMachineManagerAPI() if err != nil { return errors.Trace(err) } defer machineManager.Close() if machineManager.BestAPIVersion() < 1 { return errors.New("cannot add machines with disks: not supported by the API server") } } logger.Infof("load config") modelConfigClient, err := c.getModelConfigAPI() if err != nil { return errors.Trace(err) } defer modelConfigClient.Close() configAttrs, err := modelConfigClient.ModelGet() if err != nil { if params.IsCodeUnauthorized(err) { common.PermissionsMessage(ctx.Stderr, "add a machine to this model") } return errors.Trace(err) } config, err := config.New(config.NoDefaults, configAttrs) if err != nil { return errors.Trace(err) } if c.Placement != nil && c.Placement.Scope == "ssh" { logger.Infof("manual provisioning") authKeys, err := common.ReadAuthorizedKeys(ctx, "") if err != nil { return errors.Annotate(err, "reading authorized-keys") } args := manual.ProvisionMachineArgs{ Host: c.Placement.Directive, Client: client, Stdin: ctx.Stdin, Stdout: ctx.Stdout, Stderr: ctx.Stderr, AuthorizedKeys: authKeys, UpdateBehavior: ¶ms.UpdateBehavior{ config.EnableOSRefreshUpdate(), config.EnableOSUpgrade(), }, } machineId, err := manualProvisioner(args) if err == nil { ctx.Infof("created machine %v", machineId) } return err } logger.Infof("model provisioning") if c.Placement != nil && c.Placement.Scope == "model-uuid" { uuid, ok := client.ModelUUID() if !ok { return errors.New("API connection is controller-only (should never happen)") } c.Placement.Scope = uuid } if c.Placement != nil && c.Placement.Scope == instance.MachineScope { // It does not make sense to add-machine <id>. return errors.Errorf("machine-id cannot be specified when adding machines") } jobs := []multiwatcher.MachineJob{multiwatcher.JobHostUnits} machineParams := params.AddMachineParams{ Placement: c.Placement, Series: c.Series, Constraints: c.Constraints, Jobs: jobs, Disks: c.Disks, } machines := make([]params.AddMachineParams, c.NumMachines) for i := 0; i < c.NumMachines; i++ { machines[i] = machineParams } var results []params.AddMachinesResult // If storage is specified, we attempt to use a new API on the service facade. if len(c.Disks) > 0 { results, err = machineManager.AddMachines(machines) } else { results, err = client.AddMachines(machines) } if params.IsCodeOperationBlocked(err) { return block.ProcessBlockedError(err, block.BlockChange) } if err != nil { return errors.Trace(err) } errs := []error{} for _, machineInfo := range results { if machineInfo.Error != nil { errs = append(errs, machineInfo.Error) continue } machineId := machineInfo.Machine if names.IsContainerMachine(machineId) { ctx.Infof("created container %v", machineId) } else { ctx.Infof("created machine %v", machineId) } } if len(errs) == 1 { fmt.Fprint(ctx.Stderr, "failed to create 1 machine\n") return errs[0] } if len(errs) > 1 { fmt.Fprintf(ctx.Stderr, "failed to create %d machines\n", len(errs)) returnErr := []string{} for _, e := range errs { returnErr = append(returnErr, e.Error()) } return errors.New(strings.Join(returnErr, ", ")) } return nil }
func (c *addModelCommand) Run(ctx *cmd.Context) error { api, err := c.newAPIRoot() if err != nil { return errors.Annotate(err, "opening API connection") } defer api.Close() store := c.ClientStore() controllerName := c.ControllerName() accountDetails, err := store.AccountDetails(controllerName) if err != nil { return errors.Trace(err) } modelOwner := accountDetails.User if c.Owner != "" { if !names.IsValidUser(c.Owner) { return errors.Errorf("%q is not a valid user name", c.Owner) } modelOwner = names.NewUserTag(c.Owner).Id() } forUserSuffix := fmt.Sprintf(" for user '%s'", names.NewUserTag(modelOwner).Name()) attrs, err := c.getConfigValues(ctx) if err != nil { return errors.Trace(err) } cloudClient := c.newCloudAPI(api) var cloudTag names.CloudTag var cloud jujucloud.Cloud var cloudRegion string if c.CloudRegion != "" { cloudTag, cloud, cloudRegion, err = c.getCloudRegion(cloudClient) if err != nil { return errors.Trace(err) } } // If the user has specified a credential, then we will upload it if // it doesn't already exist in the controller, and it exists locally. var credentialTag names.CloudCredentialTag if c.CredentialName != "" { var err error if c.CloudRegion == "" { if cloudTag, cloud, err = defaultCloud(cloudClient); err != nil { return errors.Trace(err) } } credentialTag, err = c.maybeUploadCredential(ctx, cloudClient, cloudTag, cloudRegion, cloud, modelOwner) if err != nil { return errors.Trace(err) } } addModelClient := c.newAddModelAPI(api) model, err := addModelClient.CreateModel(c.Name, modelOwner, cloudTag.Id(), cloudRegion, credentialTag, attrs) if err != nil { if params.IsCodeUnauthorized(err) { common.PermissionsMessage(ctx.Stderr, "add a model") } return errors.Trace(err) } messageFormat := "Added '%s' model" messageArgs := []interface{}{c.Name} if modelOwner == accountDetails.User { controllerName := c.ControllerName() if err := store.UpdateModel(controllerName, c.Name, jujuclient.ModelDetails{ model.UUID, }); err != nil { return errors.Trace(err) } if err := store.SetCurrentModel(controllerName, c.Name); err != nil { return errors.Trace(err) } } if c.CloudRegion != "" || model.CloudRegion != "" { // The user explicitly requested a cloud/region, // or the cloud supports multiple regions. Whichever // the case, tell the user which cloud/region the // model was deployed to. cloudRegion := model.Cloud if model.CloudRegion != "" { cloudRegion += "/" + model.CloudRegion } messageFormat += " on %s" messageArgs = append(messageArgs, cloudRegion) } if model.CloudCredential != "" { tag := names.NewCloudCredentialTag(model.CloudCredential) credentialName := tag.Name() if tag.Owner().Id() != modelOwner { credentialName = fmt.Sprintf("%s/%s", tag.Owner().Id(), credentialName) } messageFormat += " with credential '%s'" messageArgs = append(messageArgs, credentialName) } messageFormat += forUserSuffix // "Added '<model>' model [on <cloud>/<region>] [with credential '<credential>'] for user '<user namePart>'" ctx.Infof(messageFormat, messageArgs...) if _, ok := attrs[config.AuthorizedKeysKey]; !ok { // It is not an error to have no authorized-keys when adding a // model, though this should never happen since we generate // juju-specific SSH keys. ctx.Infof(` No SSH authorized-keys were found. You must use "juju add-ssh-key" before "juju ssh", "juju scp", or "juju debug-hooks" will work.`) } return nil }
// openAPIState opens the API using the given information, and // returns the opened state and the api entity with // the given tag. The given changeConfig function is // called if the password changes to set the password. func openAPIState(agentConfig agent.Config, a Agent) (_ *api.State, _ *apiagent.Entity, resultErr 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. info := agentConfig.APIInfo() 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 = agentConfig.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) { return nil, nil, worker.ErrTerminateAgent } if params.IsCodeUnauthorized(err) { return nil, nil, worker.ErrTerminateAgent } return nil, nil, err } defer func() { if resultErr != nil && st != nil { st.Close() } }() entity, err := st.Agent().Entity(a.Tag()) if err == nil && entity.Life() == params.Dead { return nil, nil, worker.ErrTerminateAgent } if err != nil { if params.IsCodeUnauthorized(err) { return nil, nil, worker.ErrTerminateAgent } return nil, nil, err } if usedOldPassword { // We succeeded in connecting with the fallback // password, so we need to create a new password // for the future. newPassword, err := utils.RandomPassword() if err != nil { return nil, nil, err } // Change the configuration *before* setting the entity // password, so that we avoid the possibility that // we might successfully change the entity's // password but fail to write the configuration, // thus locking us out completely. if err := a.ChangeConfig(func(c agent.ConfigSetter) error { c.SetPassword(newPassword) c.SetOldPassword(info.Password) return nil }); err != nil { return nil, nil, err } if err := entity.SetPassword(newPassword); err != nil { return nil, nil, err } st.Close() info.Password = newPassword st, err = apiOpen(info, api.DialOpts{}) if err != nil { return nil, nil, err } } return st, entity, nil }
// OpenAPIState opens the API using the given information. The agent's // password is changed if the fallback password was used to connect to // the API. func OpenAPIState(a agent.Agent) (_ api.Connection, err error) { agentConfig := a.CurrentConfig() info, ok := agentConfig.APIInfo() if !ok { return nil, errors.New("API info not available") } st, usedOldPassword, err := openAPIStateUsingInfo(info, agentConfig.OldPassword()) if err != nil { return nil, err } defer func() { // NOTE(fwereade): we may close and overwrite st below, // so we need to double-check what we need to do here. if err != nil && st != nil { if err := st.Close(); err != nil { logger.Errorf("while closing API connection: %v", err) } } }() tag := agentConfig.Tag() entity, err := st.Agent().Entity(tag) if params.IsCodeUnauthorized(err) { logger.Errorf("agent terminating due to error returned during entity lookup: %v", err) return nil, worker.ErrTerminateAgent } else if err != nil { return nil, err } if entity.Life() == params.Dead { // The entity is Dead, so the password cannot (and should not) be updated. return st, nil } if !usedOldPassword { // Call set password with the current password. If we've recently // become a controller, this will fix up our credentials in mongo. if err := entity.SetPassword(info.Password); err != nil { return nil, errors.Annotate(err, "can't reset agent password") } } else { // We succeeded in connecting with the fallback // password, so we need to create a new password // for the future. logger.Debugf("replacing insecure password") newPassword, err := utils.RandomPassword() if err != nil { return nil, err } err = setAgentPassword(newPassword, info.Password, a, entity) if err != nil { return nil, err } // Reconnect to the API with the new password. if err := st.Close(); err != nil { logger.Errorf("while closing API connection with old password: %v", err) } info.Password = newPassword // NOTE(fwereade): this is where we rebind st. If you accidentally make // it a local variable you will break this func in a subtle and currently- // untested way. st, err = apiOpen(info, api.DialOpts{}) if err != nil { return nil, err } } return st, nil }