Permalink
| // Copyright 2013 Canonical Ltd. | |
| // Licensed under the AGPLv3, see LICENCE file for details. | |
| package state | |
| import ( | |
| "fmt" | |
| "strings" | |
| "github.com/juju/errors" | |
| jujutxn "github.com/juju/txn" | |
| "github.com/juju/utils/featureflag" | |
| "github.com/juju/version" | |
| "gopkg.in/juju/names.v2" | |
| "gopkg.in/mgo.v2" | |
| "gopkg.in/mgo.v2/bson" | |
| "gopkg.in/mgo.v2/txn" | |
| jujucloud "github.com/juju/juju/cloud" | |
| "github.com/juju/juju/constraints" | |
| "github.com/juju/juju/environs/config" | |
| "github.com/juju/juju/feature" | |
| "github.com/juju/juju/permission" | |
| "github.com/juju/juju/status" | |
| "github.com/juju/juju/storage" | |
| ) | |
| // modelGlobalKey is the key for the model, its | |
| // settings and constraints. | |
| const modelGlobalKey = "e" | |
| // modelKey will create the kei for a given model using the modelGlobalKey. | |
| func modelKey(modelUUID string) string { | |
| return fmt.Sprintf("%s#%s", modelGlobalKey, modelUUID) | |
| } | |
| // ModelType signals the type of a model - IAAS or CAAS | |
| type ModelType string | |
| const ( | |
| modelTypeNone = ModelType("") | |
| ModelTypeIAAS = ModelType("iaas") | |
| ModelTypeCAAS = ModelType("caas") | |
| ) | |
| // ParseModelType turns a valid model type string into a ModelType | |
| // constant. | |
| func ParseModelType(raw string) (ModelType, error) { | |
| for _, typ := range []ModelType{ModelTypeIAAS, ModelTypeCAAS} { | |
| if raw == string(typ) { | |
| return typ, nil | |
| } | |
| } | |
| return "", errors.NotValidf("model type %v", raw) | |
| } | |
| // MigrationMode specifies where the Model is with respect to migration. | |
| type MigrationMode string | |
| const ( | |
| // MigrationModeNone is the default mode for a model and reflects | |
| // that it isn't involved with a model migration. | |
| MigrationModeNone = MigrationMode("") | |
| // MigrationModeExporting reflects a model that is in the process of being | |
| // exported from one controller to another. | |
| MigrationModeExporting = MigrationMode("exporting") | |
| // MigrationModeImporting reflects a model that is being imported into a | |
| // controller, but is not yet fully active. | |
| MigrationModeImporting = MigrationMode("importing") | |
| ) | |
| // Model represents the state of a model. | |
| type Model struct { | |
| st *State | |
| doc modelDoc | |
| } | |
| // modelDoc represents the internal state of the model in MongoDB. | |
| type modelDoc struct { | |
| UUID string `bson:"_id"` | |
| Name string `bson:"name"` | |
| Type ModelType `bson:"type"` | |
| Life Life `bson:"life"` | |
| Owner string `bson:"owner"` | |
| ControllerUUID string `bson:"controller-uuid"` | |
| MigrationMode MigrationMode `bson:"migration-mode"` | |
| // EnvironVersion is the version of the Environ. As providers | |
| // evolve, cloud resource representations may change; the environ | |
| // version tracks the current version of that. | |
| EnvironVersion int `bson:"environ-version"` | |
| // Cloud is the name of the cloud to which the model is deployed. | |
| Cloud string `bson:"cloud"` | |
| // CloudRegion is the name of the cloud region to which the model is | |
| // deployed. This will be empty for clouds that do not support regions. | |
| CloudRegion string `bson:"cloud-region,omitempty"` | |
| // CloudCredential is the ID of the cloud credential that is used | |
| // for managing cloud resources for this model. This will be empty | |
| // for clouds that do not require credentials. | |
| CloudCredential string `bson:"cloud-credential,omitempty"` | |
| // LatestAvailableTools is a string representing the newest version | |
| // found while checking streams for new versions. | |
| LatestAvailableTools string `bson:"available-tools,omitempty"` | |
| // SLA is the current support level of the model. | |
| SLA slaDoc `bson:"sla"` | |
| // MeterStatus is the current meter status of the model. | |
| MeterStatus modelMeterStatusdoc `bson:"meter-status"` | |
| } | |
| // slaLevel enumerates the support levels available to a model. | |
| type slaLevel string | |
| const ( | |
| slaNone = slaLevel("") | |
| SLAUnsupported = slaLevel("unsupported") | |
| SLAEssential = slaLevel("essential") | |
| SLAStandard = slaLevel("standard") | |
| SLAAdvanced = slaLevel("advanced") | |
| ) | |
| // String implements fmt.Stringer returning the string representation of an | |
| // SLALevel. | |
| func (l slaLevel) String() string { | |
| if l == slaNone { | |
| l = SLAUnsupported | |
| } | |
| return string(l) | |
| } | |
| // newSLALevel returns a new SLA level from a string representation. | |
| func newSLALevel(level string) (slaLevel, error) { | |
| l := slaLevel(level) | |
| if l == slaNone { | |
| l = SLAUnsupported | |
| } | |
| switch l { | |
| case SLAUnsupported, SLAEssential, SLAStandard, SLAAdvanced: | |
| return l, nil | |
| } | |
| return l, errors.NotValidf("SLA level %q", level) | |
| } | |
| // slaDoc represents the state of the SLA on the model. | |
| type slaDoc struct { | |
| // Level is the current support level set on the model. | |
| Level slaLevel `bson:"level"` | |
| // Owner is the SLA owner of the model. | |
| Owner string `bson:"owner,omitempty"` | |
| // Credentials authenticates the support level setting. | |
| Credentials []byte `bson:"credentials"` | |
| } | |
| type modelMeterStatusdoc struct { | |
| Code string `bson:"code"` | |
| Info string `bson:"info"` | |
| } | |
| // modelEntityRefsDoc records references to the top-level entities in the | |
| // model. This is also used to determine if a model can be destroyed. | |
| // Consequently, any changes, especially additions of entities, here, would | |
| // need to be reflected, at least, in Model.checkModelEntityRefsEmpty(...) as | |
| // well as Model.destroyOps(...) | |
| type modelEntityRefsDoc struct { | |
| UUID string `bson:"_id"` | |
| // Machines contains the names of the top-level machines in the model. | |
| Machines []string `bson:"machines"` | |
| // Applicatons contains the names of the applications in the model. | |
| Applications []string `bson:"applications"` | |
| // Volumes contains the IDs of the volumes in the model. | |
| Volumes []string `bson:"volumes"` | |
| // Filesystems contains the IDs of the filesystems in the model. | |
| Filesystems []string `bson:"filesystems"` | |
| } | |
| // Model returns the model entity. | |
| func (st *State) Model() (*Model, error) { | |
| model := &Model{ | |
| st: st, | |
| } | |
| if err := model.refresh(st.modelTag.Id()); err != nil { | |
| return nil, errors.Trace(err) | |
| } | |
| return model, nil | |
| } | |
| // AllModelUUIDs returns the UUIDs for all models in the controller. | |
| // Results are sorted by (name, owner). | |
| func (st *State) AllModelUUIDs() ([]string, error) { | |
| models, closer := st.db().GetCollection(modelsC) | |
| defer closer() | |
| var docs []bson.M | |
| err := models.Find(nil).Sort("name", "owner").Select(bson.M{"_id": 1}).All(&docs) | |
| if err != nil { | |
| return nil, err | |
| } | |
| out := make([]string, len(docs)) | |
| for i, doc := range docs { | |
| out[i] = doc["_id"].(string) | |
| } | |
| return out, nil | |
| } | |
| // ModelExists returns true if a model with the supplied UUID exists. | |
| func (st *State) ModelExists(uuid string) (bool, error) { | |
| models, closer := st.db().GetCollection(modelsC) | |
| defer closer() | |
| count, err := models.FindId(uuid).Count() | |
| if err != nil { | |
| return false, errors.Annotate(err, "querying model") | |
| } | |
| return count > 0, nil | |
| } | |
| // ModelArgs is a params struct for creating a new model. | |
| type ModelArgs struct { | |
| // Type specifies the general type of the model (IAAS or CAAS). | |
| Type ModelType | |
| // CloudName is the name of the cloud to which the model is deployed. | |
| CloudName string | |
| // CloudRegion is the name of the cloud region to which the model is | |
| // deployed. This will be empty for clouds that do not support regions. | |
| CloudRegion string | |
| // CloudCredential is the tag of the cloud credential that will be | |
| // used for managing cloud resources for this model. This will be | |
| // empty for clouds that do not require credentials. | |
| CloudCredential names.CloudCredentialTag | |
| // Config is the model config. | |
| Config *config.Config | |
| // Constraints contains the initial constraints for the model. | |
| Constraints constraints.Value | |
| // StorageProviderRegistry is used to determine and store the | |
| // details of the default storage pools. | |
| StorageProviderRegistry storage.ProviderRegistry | |
| // Owner is the user that owns the model. | |
| Owner names.UserTag | |
| // MigrationMode is the initial migration mode of the model. | |
| MigrationMode MigrationMode | |
| // EnvironVersion is the initial version of the Environ for the model. | |
| EnvironVersion int | |
| } | |
| // Validate validates the ModelArgs. | |
| func (m ModelArgs) Validate() error { | |
| if m.Type == modelTypeNone { | |
| return errors.NotValidf("empty Type") | |
| } | |
| if m.Type == ModelTypeCAAS && !featureflag.Enabled(feature.CAAS) { | |
| return errors.NotSupportedf("model type") | |
| } | |
| if m.Config == nil { | |
| return errors.NotValidf("nil Config") | |
| } | |
| if !names.IsValidCloud(m.CloudName) { | |
| return errors.NotValidf("Cloud Name %q", m.CloudName) | |
| } | |
| if m.Type == ModelTypeCAAS && m.CloudRegion != "" { | |
| return errors.NotSupportedf("CAAS model with CloudRegion") | |
| } | |
| if m.Owner == (names.UserTag{}) { | |
| return errors.NotValidf("empty Owner") | |
| } | |
| if m.StorageProviderRegistry == nil && m.Type == ModelTypeIAAS { | |
| return errors.NotValidf("nil StorageProviderRegistry") | |
| } | |
| if m.StorageProviderRegistry != nil && m.Type == ModelTypeCAAS { | |
| return errors.NotValidf("CAAS model with StorageProviderRegistry") | |
| } | |
| switch m.MigrationMode { | |
| case MigrationModeNone, MigrationModeImporting: | |
| default: | |
| return errors.NotValidf("initial migration mode %q", m.MigrationMode) | |
| } | |
| return nil | |
| } | |
| // NewModel creates a new model with its own UUID and | |
| // prepares it for use. Model and State instances for the new | |
| // model are returned. | |
| // | |
| // The controller model's UUID is attached to the new | |
| // model's document. Having the server UUIDs stored with each | |
| // model document means that we have a way to represent external | |
| // models, perhaps for future use around cross model | |
| // relations. | |
| func (st *State) NewModel(args ModelArgs) (_ *Model, _ *State, err error) { | |
| if err := args.Validate(); err != nil { | |
| return nil, nil, errors.Trace(err) | |
| } | |
| controllerInfo, err := st.ControllerInfo() | |
| if err != nil { | |
| return nil, nil, errors.Trace(err) | |
| } | |
| // For IAAS Models, the model cloud must be the same as the controller cloud. | |
| if args.Type == ModelTypeIAAS && controllerInfo.CloudName != args.CloudName { | |
| return nil, nil, errors.NewNotValid( | |
| nil, fmt.Sprintf("controller cloud %s does not match model cloud %s", controllerInfo.CloudName, args.CloudName)) | |
| } | |
| // Ensure that the cloud region is valid, or if one is not specified, | |
| // that the cloud does not support regions. | |
| controllerCloud, err := st.Cloud(args.CloudName) | |
| if err != nil { | |
| return nil, nil, errors.Trace(err) | |
| } | |
| var prereqOps []txn.Op | |
| if args.Type == ModelTypeIAAS { | |
| assertCloudRegionOp, err := validateCloudRegion(controllerCloud, args.CloudRegion) | |
| if err != nil { | |
| return nil, nil, errors.Trace(err) | |
| } | |
| prereqOps = append(prereqOps, assertCloudRegionOp) | |
| } | |
| // Ensure that the cloud credential is valid, or if one is not | |
| // specified, that the cloud supports the "empty" authentication | |
| // type. | |
| owner := args.Owner | |
| cloudCredentials, err := st.CloudCredentials(owner, args.CloudName) | |
| if err != nil { | |
| return nil, nil, errors.Trace(err) | |
| } | |
| assertCloudCredentialOp, err := validateCloudCredential( | |
| controllerCloud, cloudCredentials, args.CloudCredential, | |
| ) | |
| if err != nil { | |
| return nil, nil, errors.Trace(err) | |
| } | |
| prereqOps = append(prereqOps, assertCloudCredentialOp) | |
| if owner.IsLocal() { | |
| if _, err := st.User(owner); err != nil { | |
| return nil, nil, errors.Annotate(err, "cannot create model") | |
| } | |
| } | |
| uuid := args.Config.UUID() | |
| session := st.session.Copy() | |
| newSt, err := newState( | |
| names.NewModelTag(uuid), | |
| controllerInfo.ModelTag, | |
| session, | |
| st.newPolicy, | |
| st.clock(), | |
| st.runTransactionObserver, | |
| ) | |
| if err != nil { | |
| return nil, nil, errors.Annotate(err, "could not create state for new model") | |
| } | |
| defer func() { | |
| if err != nil { | |
| newSt.Close() | |
| } | |
| }() | |
| newSt.controllerModelTag = st.controllerModelTag | |
| modelOps, modelStatusDoc, err := newSt.modelSetupOps(st.controllerTag.Id(), args, nil) | |
| if err != nil { | |
| return nil, nil, errors.Annotate(err, "failed to create new model") | |
| } | |
| ops := append(prereqOps, modelOps...) | |
| err = newSt.db().RunTransaction(ops) | |
| if err == txn.ErrAborted { | |
| // We have a unique key restriction on the "owner" and "name" fields, | |
| // which will cause the insert to fail if there is another record with | |
| // the same "owner" and "name" in the collection. If the txn is | |
| // aborted, check if it is due to the unique key restriction. | |
| name := args.Config.Name() | |
| models, closer := st.db().GetCollection(modelsC) | |
| defer closer() | |
| envCount, countErr := models.Find(bson.D{ | |
| {"owner", owner.Id()}, | |
| {"name", name}}, | |
| ).Count() | |
| if countErr != nil { | |
| err = errors.Trace(countErr) | |
| } else if envCount > 0 { | |
| err = errors.AlreadyExistsf("model %q for %s", name, owner.Id()) | |
| } else { | |
| err = errors.New("model already exists") | |
| } | |
| } | |
| if err != nil { | |
| return nil, nil, errors.Trace(err) | |
| } | |
| err = newSt.start(st.controllerTag, nil) | |
| if err != nil { | |
| return nil, nil, errors.Annotate(err, "could not start state for new model") | |
| } | |
| newModel, err := newSt.Model() | |
| if err != nil { | |
| return nil, nil, errors.Trace(err) | |
| } | |
| if args.MigrationMode != MigrationModeImporting { | |
| probablyUpdateStatusHistory(newSt.db(), modelGlobalKey, modelStatusDoc) | |
| } | |
| _, err = newSt.SetUserAccess(newModel.Owner(), newModel.ModelTag(), permission.AdminAccess) | |
| if err != nil { | |
| return nil, nil, errors.Annotate(err, "granting admin permission to the owner") | |
| } | |
| if err := InitDbLogs(session, uuid); err != nil { | |
| return nil, nil, errors.Annotate(err, "initialising model logs collection") | |
| } | |
| return newModel, newSt, nil | |
| } | |
| // validateCloudRegion validates the given region name against the | |
| // provided Cloud definition, and returns a txn.Op to include in a | |
| // transaction to assert the same. | |
| func validateCloudRegion(cloud jujucloud.Cloud, regionName string) (txn.Op, error) { | |
| // Ensure that the cloud region is valid, or if one is not specified, | |
| // that the cloud does not support regions. | |
| assertCloudRegionOp := txn.Op{ | |
| C: cloudsC, | |
| Id: cloud.Name, | |
| } | |
| if regionName != "" { | |
| region, err := jujucloud.RegionByName(cloud.Regions, regionName) | |
| if err != nil { | |
| return txn.Op{}, errors.Trace(err) | |
| } | |
| assertCloudRegionOp.Assert = bson.D{ | |
| {"regions." + region.Name, bson.D{{"$exists", true}}}, | |
| } | |
| } else { | |
| if len(cloud.Regions) > 0 { | |
| return txn.Op{}, errors.NotValidf("missing CloudRegion") | |
| } | |
| assertCloudRegionOp.Assert = bson.D{ | |
| {"regions", bson.D{{"$exists", false}}}, | |
| } | |
| } | |
| return assertCloudRegionOp, nil | |
| } | |
| // validateCloudCredential validates the given cloud credential | |
| // name against the provided cloud definition and credentials, | |
| // and returns a txn.Op to include in a transaction to assert the | |
| // same. A user is supplied, for which access to the credential | |
| // will be asserted. | |
| func validateCloudCredential( | |
| cloud jujucloud.Cloud, | |
| cloudCredentials map[string]jujucloud.Credential, | |
| cloudCredential names.CloudCredentialTag, | |
| ) (txn.Op, error) { | |
| if cloudCredential != (names.CloudCredentialTag{}) { | |
| if cloudCredential.Cloud().Id() != cloud.Name { | |
| return txn.Op{}, errors.NotValidf("credential %q", cloudCredential.Id()) | |
| } | |
| var found bool | |
| for tag := range cloudCredentials { | |
| if tag == cloudCredential.Id() { | |
| found = true | |
| break | |
| } | |
| } | |
| if !found { | |
| return txn.Op{}, errors.NotFoundf("credential %q", cloudCredential.Id()) | |
| } | |
| // NOTE(axw) if we add ACLs for credentials, | |
| // we'll need to check access here. The map | |
| // we check above contains only the credentials | |
| // that the model owner has access to. | |
| return txn.Op{ | |
| C: cloudCredentialsC, | |
| Id: cloudCredentialDocID(cloudCredential), | |
| Assert: txn.DocExists, | |
| }, nil | |
| } | |
| var hasEmptyAuth bool | |
| for _, authType := range cloud.AuthTypes { | |
| if authType != jujucloud.EmptyAuthType { | |
| continue | |
| } | |
| hasEmptyAuth = true | |
| break | |
| } | |
| if !hasEmptyAuth { | |
| return txn.Op{}, errors.NotValidf("missing CloudCredential") | |
| } | |
| return txn.Op{ | |
| C: cloudsC, | |
| Id: cloud.Name, | |
| Assert: bson.D{{"auth-types", string(jujucloud.EmptyAuthType)}}, | |
| }, nil | |
| } | |
| // Tag returns a name identifying the model. | |
| // The returned name will be different from other Tag values returned | |
| // by any other entities from the same state. | |
| func (m *Model) Tag() names.Tag { | |
| return m.ModelTag() | |
| } | |
| // ModelTag is the concrete model tag for this model. | |
| func (m *Model) ModelTag() names.ModelTag { | |
| return names.NewModelTag(m.doc.UUID) | |
| } | |
| // ControllerTag is the tag for the controller that the model is | |
| // running within. | |
| func (m *Model) ControllerTag() names.ControllerTag { | |
| return names.NewControllerTag(m.doc.ControllerUUID) | |
| } | |
| // UUID returns the universally unique identifier of the model. | |
| func (m *Model) UUID() string { | |
| return m.doc.UUID | |
| } | |
| // ControllerUUID returns the universally unique identifier of the controller | |
| // in which the model is running. | |
| func (m *Model) ControllerUUID() string { | |
| return m.doc.ControllerUUID | |
| } | |
| // Name returns the human friendly name of the model. | |
| func (m *Model) Name() string { | |
| return m.doc.Name | |
| } | |
| // Type returns the human friendly name of the model. | |
| func (m *Model) Type() ModelType { | |
| return m.doc.Type | |
| } | |
| // Cloud returns the name of the cloud to which the model is deployed. | |
| func (m *Model) Cloud() string { | |
| return m.doc.Cloud | |
| } | |
| // CloudRegion returns the name of the cloud region to which the model is deployed. | |
| func (m *Model) CloudRegion() string { | |
| return m.doc.CloudRegion | |
| } | |
| // CloudCredential returns the tag of the cloud credential used for managing the | |
| // model's cloud resources, and a boolean indicating whether a credential is set. | |
| func (m *Model) CloudCredential() (names.CloudCredentialTag, bool) { | |
| if names.IsValidCloudCredential(m.doc.CloudCredential) { | |
| return names.NewCloudCredentialTag(m.doc.CloudCredential), true | |
| } | |
| return names.CloudCredentialTag{}, false | |
| } | |
| // MigrationMode returns whether the model is active or being migrated. | |
| func (m *Model) MigrationMode() MigrationMode { | |
| return m.doc.MigrationMode | |
| } | |
| // SetMigrationMode updates the migration mode of the model. | |
| func (m *Model) SetMigrationMode(mode MigrationMode) error { | |
| ops := []txn.Op{{ | |
| C: modelsC, | |
| Id: m.doc.UUID, | |
| Assert: txn.DocExists, | |
| Update: bson.D{{"$set", bson.D{{"migration-mode", mode}}}}, | |
| }} | |
| if err := m.st.db().RunTransaction(ops); err != nil { | |
| return errors.Trace(err) | |
| } | |
| return m.Refresh() | |
| } | |
| // Life returns whether the model is Alive, Dying or Dead. | |
| func (m *Model) Life() Life { | |
| return m.doc.Life | |
| } | |
| // Owner returns tag representing the owner of the model. | |
| // The owner is the user that created the model. | |
| func (m *Model) Owner() names.UserTag { | |
| return names.NewUserTag(m.doc.Owner) | |
| } | |
| // Status returns the status of the model. | |
| func (m *Model) Status() (status.StatusInfo, error) { | |
| status, err := getStatus(m.st.db(), m.globalKey(), "model") | |
| if err != nil { | |
| return status, err | |
| } | |
| return status, nil | |
| } | |
| // localID returns the local id value by stripping off the model uuid prefix | |
| // if it is there. | |
| func (m *Model) localID(ID string) string { | |
| modelUUID, localID, ok := splitDocID(ID) | |
| if !ok || modelUUID != m.doc.UUID { | |
| return ID | |
| } | |
| return localID | |
| } | |
| // SetStatus sets the status of the model. | |
| func (m *Model) SetStatus(sInfo status.StatusInfo) error { | |
| if !status.ValidModelStatus(sInfo.Status) { | |
| return errors.Errorf("cannot set invalid status %q", sInfo.Status) | |
| } | |
| return setStatus(m.st.db(), setStatusParams{ | |
| badge: "model", | |
| globalKey: m.globalKey(), | |
| status: sInfo.Status, | |
| message: sInfo.Message, | |
| rawData: sInfo.Data, | |
| updated: timeOrNow(sInfo.Since, m.st.clock()), | |
| }) | |
| } | |
| // StatusHistory returns a slice of at most filter.Size StatusInfo items | |
| // or items as old as filter.Date or items newer than now - filter.Delta time | |
| // representing past statuses for this application. | |
| func (m *Model) StatusHistory(filter status.StatusHistoryFilter) ([]status.StatusInfo, error) { | |
| args := &statusHistoryArgs{ | |
| db: m.st.db(), | |
| globalKey: m.globalKey(), | |
| filter: filter, | |
| } | |
| return statusHistory(args) | |
| } | |
| // Config returns the config for the model. | |
| func (m *Model) Config() (*config.Config, error) { | |
| return getModelConfig(m.st.db()) | |
| } | |
| // UpdateLatestToolsVersion looks up for the latest available version of | |
| // juju tools and updates environementDoc with it. | |
| func (m *Model) UpdateLatestToolsVersion(ver version.Number) error { | |
| v := ver.String() | |
| // TODO(perrito666): I need to assert here that there isn't a newer | |
| // version in place. | |
| ops := []txn.Op{{ | |
| C: modelsC, | |
| Id: m.doc.UUID, | |
| Update: bson.D{{"$set", bson.D{{"available-tools", v}}}}, | |
| }} | |
| err := m.st.db().RunTransaction(ops) | |
| if err != nil { | |
| return errors.Trace(err) | |
| } | |
| return m.Refresh() | |
| } | |
| // LatestToolsVersion returns the newest version found in the last | |
| // check in the streams. | |
| // Bear in mind that the check was performed filtering only | |
| // new patches for the current major.minor. (major.minor.patch) | |
| func (m *Model) LatestToolsVersion() version.Number { | |
| ver := m.doc.LatestAvailableTools | |
| if ver == "" { | |
| return version.Zero | |
| } | |
| v, err := version.Parse(ver) | |
| if err != nil { | |
| // This is being stored from a valid version but | |
| // in case this data would beacame corrupt It is not | |
| // worth to fail because of it. | |
| return version.Zero | |
| } | |
| return v | |
| } | |
| // SLALevel returns the SLA level as a string. | |
| func (m *Model) SLALevel() string { | |
| return m.doc.SLA.Level.String() | |
| } | |
| // SLAOwner returns the SLA owner as a string. Note that this may differ from | |
| // the model owner. | |
| func (m *Model) SLAOwner() string { | |
| return m.doc.SLA.Owner | |
| } | |
| // SLACredential returns the SLA credential. | |
| func (m *Model) SLACredential() []byte { | |
| return m.doc.SLA.Credentials | |
| } | |
| // SetSLA sets the SLA on the model. | |
| func (m *Model) SetSLA(level, owner string, credentials []byte) error { | |
| l, err := newSLALevel(level) | |
| if err != nil { | |
| return errors.Trace(err) | |
| } | |
| ops := []txn.Op{{ | |
| C: modelsC, | |
| Id: m.doc.UUID, | |
| Update: bson.D{{"$set", bson.D{{"sla", slaDoc{ | |
| Level: l, | |
| Owner: owner, | |
| Credentials: credentials, | |
| }}}}}, | |
| }} | |
| err = m.st.db().RunTransaction(ops) | |
| if err != nil { | |
| return errors.Trace(err) | |
| } | |
| return m.Refresh() | |
| } | |
| // SetMeterStatus sets the current meter status for this model. | |
| func (m *Model) SetMeterStatus(status, info string) error { | |
| if _, err := isValidMeterStatusCode(status); err != nil { | |
| return errors.Trace(err) | |
| } | |
| ops := []txn.Op{{ | |
| C: modelsC, | |
| Id: m.doc.UUID, | |
| Update: bson.D{{"$set", bson.D{{"meter-status", modelMeterStatusdoc{ | |
| Code: status, | |
| Info: info, | |
| }}}}}, | |
| }} | |
| err := m.st.db().RunTransaction(ops) | |
| if err != nil { | |
| return errors.Trace(err) | |
| } | |
| return m.Refresh() | |
| } | |
| // MeterStatus returns the current meter status for this model. | |
| func (m *Model) MeterStatus() MeterStatus { | |
| ms := m.doc.MeterStatus | |
| return MeterStatus{ | |
| Code: MeterStatusFromString(ms.Code), | |
| Info: ms.Info, | |
| } | |
| } | |
| // EnvironVersion is the version of the model's environ -- the related | |
| // cloud provider resources. The environ version is used by the controller | |
| // to identify environ/provider upgrade steps to run for a model's environ | |
| // after the controller is upgraded, or the model is migrated to another | |
| // controller. | |
| func (m *Model) EnvironVersion() int { | |
| return m.doc.EnvironVersion | |
| } | |
| // SetEnvironVersion sets the model's current environ version. The value | |
| // must be monotonically increasing. | |
| func (m *Model) SetEnvironVersion(v int) error { | |
| mOrig := m | |
| mCopy := *m | |
| m = &mCopy // copy so we can refresh without affecting the original m | |
| buildTxn := func(attempt int) ([]txn.Op, error) { | |
| if attempt > 0 { | |
| if err := m.Refresh(); err != nil { | |
| return nil, errors.Trace(err) | |
| } | |
| } | |
| if v < m.doc.EnvironVersion { | |
| return nil, errors.Errorf( | |
| "cannot set environ version to %v, which is less than the current version %v", | |
| v, m.doc.EnvironVersion, | |
| ) | |
| } | |
| if v == m.doc.EnvironVersion { | |
| return nil, jujutxn.ErrNoOperations | |
| } | |
| return []txn.Op{{ | |
| C: modelsC, | |
| Id: m.doc.UUID, | |
| Assert: bson.D{{"environ-version", m.doc.EnvironVersion}}, | |
| Update: bson.D{{"$set", bson.D{{"environ-version", v}}}}, | |
| }}, nil | |
| } | |
| if err := m.st.db().Run(buildTxn); err != nil { | |
| return errors.Trace(err) | |
| } | |
| mOrig.doc.EnvironVersion = v | |
| return nil | |
| } | |
| // globalKey returns the global database key for the model. | |
| func (m *Model) globalKey() string { | |
| return modelGlobalKey | |
| } | |
| func (m *Model) Refresh() error { | |
| return m.refresh(m.UUID()) | |
| } | |
| func (m *Model) refresh(uuid string) error { | |
| models, closer := m.st.db().GetCollection(modelsC) | |
| defer closer() | |
| err := models.FindId(uuid).One(&m.doc) | |
| if err == mgo.ErrNotFound { | |
| return errors.NotFoundf("model %q", uuid) | |
| } | |
| return err | |
| } | |
| // AllUnits returns all units for a model, for all applications. | |
| func (m *Model) AllUnits() ([]*Unit, error) { | |
| coll, closer := m.st.db().GetCollection(unitsC) | |
| defer closer() | |
| docs := []unitDoc{} | |
| err := coll.Find(nil).All(&docs) | |
| if err != nil { | |
| return nil, errors.Annotate(err, "cannot get all units for model") | |
| } | |
| var units []*Unit | |
| for i := range docs { | |
| units = append(units, newUnit(m.st, &docs[i])) | |
| } | |
| return units, nil | |
| } | |
| // Users returns a slice of all users for this model. | |
| func (m *Model) Users() ([]permission.UserAccess, error) { | |
| coll, closer := m.st.db().GetCollection(modelUsersC) | |
| defer closer() | |
| var userDocs []userAccessDoc | |
| err := coll.Find(nil).All(&userDocs) | |
| if err != nil { | |
| return nil, errors.Trace(err) | |
| } | |
| var modelUsers []permission.UserAccess | |
| for _, doc := range userDocs { | |
| // check if the User belonging to this model user has | |
| // been deleted, in this case we should not return it. | |
| userTag := names.NewUserTag(doc.UserName) | |
| if userTag.IsLocal() { | |
| _, err := m.st.User(userTag) | |
| if err != nil { | |
| if _, ok := err.(DeletedUserError); !ok { | |
| // We ignore deleted users for now. So if it is not a | |
| // DeletedUserError we return the error. | |
| return nil, errors.Trace(err) | |
| } | |
| continue | |
| } | |
| } | |
| mu, err := NewModelUserAccess(m.st, doc) | |
| if err != nil { | |
| return nil, errors.Trace(err) | |
| } | |
| modelUsers = append(modelUsers, mu) | |
| } | |
| return modelUsers, nil | |
| } | |
| func (m *Model) isControllerModel() bool { | |
| return m.st.controllerModelTag.Id() == m.doc.UUID | |
| } | |
| // DestroyModelParams contains parameters for destroy a model. | |
| type DestroyModelParams struct { | |
| // DestroyHostedModels controls whether or not hosted models | |
| // are destroyed also. This only applies to the controller | |
| // model. | |
| // | |
| // If this is false when destroying the controller model, | |
| // there must be no hosted models, or an error satisfying | |
| // IsHasHostedModelsError will be returned. | |
| // | |
| // TODO(axw) this should be moved to the Controller type. | |
| DestroyHostedModels bool | |
| // DestroyStorage controls whether or not storage in the | |
| // model (and hosted models, if DestroyHostedModels is true) | |
| // should be destroyed. | |
| // | |
| // This is ternary: nil, false, or true. If nil and | |
| // there is persistent storage in the model (or hosted | |
| // models), an error satisfying IsHasPersistentStorageError | |
| // will be returned. | |
| DestroyStorage *bool | |
| } | |
| func (m *Model) uniqueIndexID() string { | |
| return userModelNameIndex(m.doc.Owner, m.doc.Name) | |
| } | |
| // Destroy sets the models's lifecycle to Dying, preventing | |
| // addition of services or machines to state. If called on | |
| // an empty hosted model, the lifecycle will be advanced | |
| // straight to Dead. | |
| func (m *Model) Destroy(args DestroyModelParams) (err error) { | |
| defer errors.DeferredAnnotatef(&err, "failed to destroy model") | |
| buildTxn := func(attempt int) ([]txn.Op, error) { | |
| // On the first attempt, we assume memory state is recent | |
| // enough to try using... | |
| if attempt != 0 { | |
| // ...but on subsequent attempts, we read fresh environ | |
| // state from the DB. Note that we do *not* refresh the | |
| // original `m` itself, as detailed in doc/hacking-state.txt | |
| if attempt == 1 { | |
| mCopy := *m | |
| m = &mCopy | |
| } | |
| if err := m.Refresh(); err != nil { | |
| return nil, errors.Trace(err) | |
| } | |
| } | |
| ops, err := m.destroyOps(args, false, false) | |
| if err == errModelNotAlive { | |
| return nil, jujutxn.ErrNoOperations | |
| } else if err != nil { | |
| return nil, errors.Trace(err) | |
| } | |
| return ops, nil | |
| } | |
| return m.st.db().Run(buildTxn) | |
| } | |
| // errModelNotAlive is a signal emitted from destroyOps to indicate | |
| // that model destruction is already underway. | |
| var errModelNotAlive = errors.New("model is no longer alive") | |
| type hasHostedModelsError int | |
| func (e hasHostedModelsError) Error() string { | |
| s := "" | |
| if e != 1 { | |
| s = "s" | |
| } | |
| return fmt.Sprintf("hosting %d other model"+s, e) | |
| } | |
| // IsHasHostedModelsError reports whether or not the given error | |
| // was caused by an attempt to destroy the controller model while | |
| // it contained non-empty hosted models, without specifying that | |
| // they should also be destroyed. | |
| func IsHasHostedModelsError(err error) bool { | |
| _, ok := errors.Cause(err).(hasHostedModelsError) | |
| return ok | |
| } | |
| type hasPersistentStorageError struct{} | |
| func (hasPersistentStorageError) Error() string { | |
| return "model contains persistent storage" | |
| } | |
| // IsHasPersistentStorageError reports whether or not the given | |
| // error was caused by an attempt to destroy a model while it | |
| // contained persistent storage, without specifying how the | |
| // storage should be removed (destroyed or released). | |
| func IsHasPersistentStorageError(err error) bool { | |
| _, ok := errors.Cause(err).(hasPersistentStorageError) | |
| return ok | |
| } | |
| type modelNotEmptyError struct { | |
| machines int | |
| applications int | |
| volumes int | |
| filesystems int | |
| } | |
| // Error is part of the error interface. | |
| func (e modelNotEmptyError) Error() string { | |
| msg := "model not empty, found " | |
| plural := func(n int, thing string) string { | |
| s := fmt.Sprintf("%d %s", n, thing) | |
| if n != 1 { | |
| s += "s" | |
| } | |
| return s | |
| } | |
| var contains []string | |
| if n := e.machines; n > 0 { | |
| contains = append(contains, plural(n, "machine")) | |
| } | |
| if n := e.applications; n > 0 { | |
| contains = append(contains, plural(n, "application")) | |
| } | |
| if n := e.volumes; n > 0 { | |
| contains = append(contains, plural(n, "volume")) | |
| } | |
| if n := e.filesystems; n > 0 { | |
| contains = append(contains, plural(n, "filesystem")) | |
| } | |
| return msg + strings.Join(contains, ", ") | |
| } | |
| // IsModelNotEmptyError reports whether or not the given error was caused | |
| // due to an operation requiring a model to be empty, where the model is | |
| // non-empty. | |
| func IsModelNotEmptyError(err error) bool { | |
| _, ok := errors.Cause(err).(modelNotEmptyError) | |
| return ok | |
| } | |
| // destroyOps returns the txn operations necessary to begin model | |
| // destruction, or an error indicating why it can't. | |
| // | |
| // If ensureEmpty is true, then destroyOps will return an error | |
| // if the model is non-empty. | |
| // | |
| // If destroyingController is true, then destroyOps will progress | |
| // empty models to Dead, but otherwise will return only non-mutating | |
| // ops to assert the current state of the model, and will leave it | |
| // to the "models" cleanup to destroy the model. | |
| func (m *Model) destroyOps( | |
| args DestroyModelParams, | |
| ensureEmpty bool, | |
| destroyingController bool, | |
| ) ([]txn.Op, error) { | |
| if m.Life() != Alive { | |
| return nil, errModelNotAlive | |
| } | |
| // Check if the model is empty. If it is, we can advance the model's | |
| // lifecycle state directly to Dead. | |
| modelEntityRefs, err := m.getEntityRefs() | |
| if err != nil { | |
| return nil, errors.Annotate(err, "getting model entity refs") | |
| } | |
| isEmpty := true | |
| modelUUID := m.UUID() | |
| nextLife := Dying | |
| prereqOps, err := checkModelEntityRefsEmpty(modelEntityRefs) | |
| if err != nil { | |
| if ensureEmpty { | |
| return nil, errors.Trace(err) | |
| } | |
| isEmpty = false | |
| prereqOps = nil | |
| if args.DestroyStorage == nil { | |
| // The model is non-empty, and the user has not specified | |
| // whether storage should be destroyed or released. Make | |
| // sure there are no filesystems or volumes in the model. | |
| storageOps, err := checkModelEntityRefsNoPersistentStorage( | |
| m.st.db(), modelEntityRefs, | |
| ) | |
| if err != nil { | |
| return nil, errors.Trace(err) | |
| } | |
| prereqOps = storageOps | |
| } else if !*args.DestroyStorage { | |
| // The model is non-empty, and the user has specified that | |
| // storage should be released. Make sure the storage is | |
| // all releasable. | |
| im, err := m.IAASModel() | |
| if err != nil { | |
| return nil, errors.Trace(err) | |
| } | |
| storageOps, err := checkModelEntityRefsAllReleasableStorage( | |
| im, modelEntityRefs, | |
| ) | |
| if err != nil { | |
| return nil, err | |
| } | |
| prereqOps = storageOps | |
| } | |
| } else { | |
| if !m.isControllerModel() { | |
| // The model is empty, and is not the controller | |
| // model, so we can move it straight to Dead. | |
| nextLife = Dead | |
| } | |
| } | |
| if m.isControllerModel() && (!args.DestroyHostedModels || args.DestroyStorage == nil || !*args.DestroyStorage) { | |
| // This is the controller model, and we've not been instructed | |
| // to destroy hosted models, or we've not been instructed to | |
| // destroy storage. | |
| // | |
| // Check for any Dying or alive but non-empty models. If there | |
| // are any and we have not been instructed to destroy them, we | |
| // return an error indicating that there are hosted models. | |
| // We need access State instances for hosted models and it's | |
| // too hard to thread an external StatePool to here, so create | |
| // a fresh one. Creating new States is relatively slow but | |
| // this is ok because this is an infrequently used code path. | |
| pool := NewStatePool(m.st) | |
| defer pool.Close() | |
| modelUUIDs, err := m.st.AllModelUUIDs() | |
| if err != nil { | |
| return nil, errors.Trace(err) | |
| } | |
| var aliveEmpty, aliveNonEmpty, dying, dead int | |
| for _, modelUUID := range modelUUIDs { | |
| if modelUUID == m.UUID() { | |
| // Ignore the controller model. | |
| continue | |
| } | |
| st, release, err := pool.Get(modelUUID) | |
| if err != nil { | |
| // This model could have been removed. | |
| if errors.IsNotFound(err) { | |
| continue | |
| } | |
| return nil, errors.Trace(err) | |
| } | |
| defer release() | |
| model, err := st.Model() | |
| if err != nil { | |
| return nil, errors.Trace(err) | |
| } | |
| if model.Life() == Dead { | |
| // Dead hosted models don't affect | |
| // whether the controller can be | |
| // destroyed or not, but they are | |
| // still counted in the hosted models. | |
| dead++ | |
| continue | |
| } | |
| // See if the model is empty, and if it is, | |
| // get the ops required to ensure it can be | |
| // destroyed, but without effecting the | |
| // destruction. The destruction is carried | |
| // out by the cleanup. | |
| ops, err := model.destroyOps(args, !args.DestroyHostedModels, true) | |
| switch err { | |
| case errModelNotAlive: | |
| dying++ | |
| case nil: | |
| prereqOps = append(prereqOps, ops...) | |
| aliveEmpty++ | |
| default: | |
| if !IsModelNotEmptyError(err) { | |
| return nil, errors.Trace(err) | |
| } | |
| aliveNonEmpty++ | |
| } | |
| } | |
| if !args.DestroyHostedModels && (dying > 0 || aliveNonEmpty > 0) { | |
| // There are Dying, or Alive but non-empty models. | |
| // We cannot destroy the controller without first | |
| // destroying the models and waiting for them to | |
| // become Dead. | |
| return nil, errors.Trace( | |
| hasHostedModelsError(dying + aliveNonEmpty + aliveEmpty), | |
| ) | |
| } | |
| // Ensure that the number of active models has not changed | |
| // between the query and when the transaction is applied. | |
| // | |
| // Note that we assert that each empty model that we intend | |
| // move to Dead is still Alive, so we're protected from an | |
| // ABA style problem where an empty model is concurrently | |
| // removed, and replaced with a non-empty model. | |
| prereqOps = append(prereqOps, | |
| assertHostedModelsOp(aliveEmpty+aliveNonEmpty+dying+dead), | |
| ) | |
| } | |
| timeOfDying := m.st.nowToTheSecond() | |
| modelUpdateValues := bson.D{ | |
| {"life", nextLife}, | |
| {"time-of-dying", timeOfDying}, | |
| } | |
| var ops []txn.Op | |
| if nextLife == Dead { | |
| modelUpdateValues = append(modelUpdateValues, bson.DocElem{ | |
| "time-of-death", timeOfDying, | |
| }) | |
| ops = append(ops, txn.Op{ | |
| // Cleanup the owner:envName unique key. | |
| C: usermodelnameC, | |
| Id: m.uniqueIndexID(), | |
| Remove: true, | |
| }) | |
| } | |
| modelOp := txn.Op{ | |
| C: modelsC, | |
| Id: modelUUID, | |
| Assert: isAliveDoc, | |
| } | |
| if !destroyingController || nextLife == Dead { | |
| modelOp.Update = bson.D{{"$set", modelUpdateValues}} | |
| } | |
| ops = append(ops, modelOp) | |
| if destroyingController { | |
| // We're destroying the controller model, and being asked | |
| // to check the validity of destroying hosted models, | |
| // progressing empty models to Dead. We don't want to | |
| // include the cleanups below, as they assume we're | |
| // destroying the model. | |
| return append(prereqOps, ops...), nil | |
| } | |
| // Because txn operations execute in order, and may encounter | |
| // arbitrarily long delays, we need to make sure every op | |
| // causes a state change that's still consistent; so we make | |
| // sure the cleanup ops are the last thing that will execute. | |
| if m.isControllerModel() { | |
| ops = append(ops, newCleanupOp( | |
| cleanupModelsForDyingController, modelUUID, | |
| // pass through the DestroyModelArgs to the cleanup, | |
| // so the models can be destroyed according to the | |
| // same rules. | |
| args, | |
| )) | |
| } | |
| if !isEmpty { | |
| // We only need to destroy resources if the model is non-empty. | |
| // It wouldn't normally be harmful to enqueue the cleanups | |
| // otherwise, except for when we're destroying an empty | |
| // hosted model in the course of destroying the controller. In | |
| // that case we'll get errors if we try to enqueue hosted-model | |
| // cleanups, because the cleanups collection is non-global. | |
| ops = append(ops, | |
| newCleanupOp(cleanupApplicationsForDyingModel, modelUUID), | |
| ) | |
| if m.Type() == ModelTypeIAAS { | |
| ops = append(ops, newCleanupOp(cleanupMachinesForDyingModel, modelUUID)) | |
| if args.DestroyStorage != nil { | |
| // The user has specified that the storage should be destroyed | |
| // or released, which we can do in a cleanup. If the user did | |
| // not specify either, then we have already added prereq ops | |
| // to assert that there is no storage in the model. | |
| ops = append(ops, newCleanupOp( | |
| cleanupStorageForDyingModel, modelUUID, | |
| // pass through DestroyModelArgs.DestroyStorage to the | |
| // cleanup, so the storage can be destroyed/released | |
| // according to the parameters. | |
| *args.DestroyStorage, | |
| )) | |
| } | |
| } | |
| } | |
| return append(prereqOps, ops...), nil | |
| } | |
| // getEntityRefs reads the current model entity refs document for the model. | |
| func (m *Model) getEntityRefs() (*modelEntityRefsDoc, error) { | |
| modelEntityRefs, closer := m.st.db().GetCollection(modelEntityRefsC) | |
| defer closer() | |
| var doc modelEntityRefsDoc | |
| if err := modelEntityRefs.FindId(m.UUID()).One(&doc); err != nil { | |
| if err == mgo.ErrNotFound { | |
| return nil, errors.NotFoundf("entity references doc for model %s", m.UUID()) | |
| } | |
| return nil, errors.Annotatef(err, "getting entity references for model %s", m.UUID()) | |
| } | |
| return &doc, nil | |
| } | |
| // (TODO) externalreality: Temporary method to access state from model while | |
| // factoring Model concerns out from state. | |
| func (model *Model) State() *State { | |
| return model.st | |
| } | |
| // checkModelEntityRefsEmpty checks that the model is empty of any entities | |
| // that may require external resource cleanup. If the model is not empty, | |
| // then an error will be returned; otherwise txn.Ops are returned to assert | |
| // the continued emptiness. | |
| func checkModelEntityRefsEmpty(doc *modelEntityRefsDoc) ([]txn.Op, error) { | |
| // These errors could be potentially swallowed as we re-try to destroy model. | |
| // Let's, at least, log them for observation. | |
| err := modelNotEmptyError{ | |
| machines: len(doc.Machines), | |
| applications: len(doc.Applications), | |
| volumes: len(doc.Volumes), | |
| filesystems: len(doc.Filesystems), | |
| } | |
| if err != (modelNotEmptyError{}) { | |
| return nil, err | |
| } | |
| return []txn.Op{{ | |
| C: modelEntityRefsC, | |
| Id: doc.UUID, | |
| Assert: bson.D{ | |
| {"machines", bson.D{{"$size", 0}}}, | |
| {"applications", bson.D{{"$size", 0}}}, | |
| {"volumes", bson.D{{"$size", 0}}}, | |
| {"filesystems", bson.D{{"$size", 0}}}, | |
| }, | |
| }}, nil | |
| } | |
| // checkModelEntityRefsNoPersistentStorage checks that there is no | |
| // persistent storage in the model. If there is, then an error of | |
| // type hasPersistentStorageError is returned. If there is not, | |
| // txn.Ops are returned to assert the same. | |
| func checkModelEntityRefsNoPersistentStorage( | |
| db Database, doc *modelEntityRefsDoc, | |
| ) ([]txn.Op, error) { | |
| for _, volumeId := range doc.Volumes { | |
| volumeTag := names.NewVolumeTag(volumeId) | |
| detachable, err := isDetachableVolumeTag(db, volumeTag) | |
| if err != nil { | |
| return nil, errors.Trace(err) | |
| } | |
| if detachable { | |
| return nil, hasPersistentStorageError{} | |
| } | |
| } | |
| for _, filesystemId := range doc.Filesystems { | |
| filesystemTag := names.NewFilesystemTag(filesystemId) | |
| detachable, err := isDetachableFilesystemTag(db, filesystemTag) | |
| if err != nil { | |
| return nil, errors.Trace(err) | |
| } | |
| if detachable { | |
| return nil, hasPersistentStorageError{} | |
| } | |
| } | |
| return noNewStorageModelEntityRefs(doc), nil | |
| } | |
| // checkModelEntityRefsAllReleasableStorage checks that there all | |
| // persistent storage in the model is releasable. If it is, then | |
| // txn.Ops are returned to assert the same; if it is not, then an | |
| // error is returned. | |
| func checkModelEntityRefsAllReleasableStorage(im *IAASModel, doc *modelEntityRefsDoc) ([]txn.Op, error) { | |
| for _, volumeId := range doc.Volumes { | |
| volumeTag := names.NewVolumeTag(volumeId) | |
| volume, err := im.volumeByTag(volumeTag) | |
| if err != nil { | |
| return nil, errors.Trace(err) | |
| } | |
| if !volume.Detachable() { | |
| continue | |
| } | |
| if err := checkStoragePoolReleasable(im, volume.pool()); err != nil { | |
| return nil, errors.Annotatef(err, | |
| "cannot release %s", names.ReadableString(volumeTag), | |
| ) | |
| } | |
| } | |
| for _, filesystemId := range doc.Filesystems { | |
| filesystemTag := names.NewFilesystemTag(filesystemId) | |
| filesystem, err := im.filesystemByTag(filesystemTag) | |
| if err != nil { | |
| return nil, errors.Trace(err) | |
| } | |
| if !filesystem.Detachable() { | |
| continue | |
| } | |
| if err := checkStoragePoolReleasable(im, filesystem.pool()); err != nil { | |
| return nil, errors.Annotatef(err, | |
| "cannot release %s", names.ReadableString(filesystemTag), | |
| ) | |
| } | |
| } | |
| return noNewStorageModelEntityRefs(doc), nil | |
| } | |
| func noNewStorageModelEntityRefs(doc *modelEntityRefsDoc) []txn.Op { | |
| noNewVolumes := bson.DocElem{ | |
| "volumes", bson.D{{ | |
| "$not", bson.D{{ | |
| "$elemMatch", bson.D{{ | |
| "$nin", doc.Volumes, | |
| }}, | |
| }}, | |
| }}, | |
| // There are no volumes that are not in | |
| // the set of volumes we previously knew | |
| // about => the current set of volumes | |
| // is a subset of the previously known set. | |
| } | |
| noNewFilesystems := bson.DocElem{ | |
| "filesystems", bson.D{{ | |
| "$not", bson.D{{ | |
| "$elemMatch", bson.D{{ | |
| "$nin", doc.Filesystems, | |
| }}, | |
| }}, | |
| }}, | |
| } | |
| return []txn.Op{{ | |
| C: modelEntityRefsC, | |
| Id: doc.UUID, | |
| Assert: bson.D{ | |
| noNewVolumes, | |
| noNewFilesystems, | |
| }, | |
| }} | |
| } | |
| func addModelMachineRefOp(mb modelBackend, machineId string) txn.Op { | |
| return addModelEntityRefOp(mb, "machines", machineId) | |
| } | |
| func removeModelMachineRefOp(mb modelBackend, machineId string) txn.Op { | |
| return removeModelEntityRefOp(mb, "machines", machineId) | |
| } | |
| func addModelApplicationRefOp(mb modelBackend, applicationname string) txn.Op { | |
| return addModelEntityRefOp(mb, "applications", applicationname) | |
| } | |
| func removeModelApplicationRefOp(mb modelBackend, applicationname string) txn.Op { | |
| return removeModelEntityRefOp(mb, "applications", applicationname) | |
| } | |
| func addModelVolumeRefOp(mb modelBackend, volumeId string) txn.Op { | |
| return addModelEntityRefOp(mb, "volumes", volumeId) | |
| } | |
| func removeModelVolumeRefOp(mb modelBackend, volumeId string) txn.Op { | |
| return removeModelEntityRefOp(mb, "volumes", volumeId) | |
| } | |
| func addModelFilesystemRefOp(mb modelBackend, filesystemId string) txn.Op { | |
| return addModelEntityRefOp(mb, "filesystems", filesystemId) | |
| } | |
| func removeModelFilesystemRefOp(mb modelBackend, filesystemId string) txn.Op { | |
| return removeModelEntityRefOp(mb, "filesystems", filesystemId) | |
| } | |
| func addModelEntityRefOp(mb modelBackend, entityField, entityId string) txn.Op { | |
| return txn.Op{ | |
| C: modelEntityRefsC, | |
| Id: mb.modelUUID(), | |
| Assert: txn.DocExists, | |
| Update: bson.D{{"$addToSet", bson.D{{entityField, entityId}}}}, | |
| } | |
| } | |
| func removeModelEntityRefOp(mb modelBackend, entityField, entityId string) txn.Op { | |
| return txn.Op{ | |
| C: modelEntityRefsC, | |
| Id: mb.modelUUID(), | |
| Update: bson.D{{"$pull", bson.D{{entityField, entityId}}}}, | |
| } | |
| } | |
| // createModelOp returns the operation needed to create | |
| // an model document with the given name and UUID. | |
| func createModelOp( | |
| modelType ModelType, | |
| owner names.UserTag, | |
| name, uuid, controllerUUID, cloudName, cloudRegion string, | |
| cloudCredential names.CloudCredentialTag, | |
| migrationMode MigrationMode, | |
| environVersion int, | |
| ) txn.Op { | |
| doc := &modelDoc{ | |
| Type: modelType, | |
| UUID: uuid, | |
| Name: name, | |
| Life: Alive, | |
| Owner: owner.Id(), | |
| ControllerUUID: controllerUUID, | |
| MigrationMode: migrationMode, | |
| EnvironVersion: environVersion, | |
| Cloud: cloudName, | |
| CloudRegion: cloudRegion, | |
| CloudCredential: cloudCredential.Id(), | |
| } | |
| return txn.Op{ | |
| C: modelsC, | |
| Id: uuid, | |
| Assert: txn.DocMissing, | |
| Insert: doc, | |
| } | |
| } | |
| func createModelEntityRefsOp(uuid string) txn.Op { | |
| return txn.Op{ | |
| C: modelEntityRefsC, | |
| Id: uuid, | |
| Assert: txn.DocMissing, | |
| Insert: &modelEntityRefsDoc{UUID: uuid}, | |
| } | |
| } | |
| const hostedModelCountKey = "hostedModelCount" | |
| type hostedModelCountDoc struct { | |
| // RefCount is the number of models in the Juju system. | |
| // We do not count the system model. | |
| RefCount int `bson:"refcount"` | |
| } | |
| func assertHostedModelsOp(n int) txn.Op { | |
| return txn.Op{ | |
| C: controllersC, | |
| Id: hostedModelCountKey, | |
| Assert: bson.D{{"refcount", n}}, | |
| } | |
| } | |
| func incHostedModelCountOp() txn.Op { | |
| return HostedModelCountOp(1) | |
| } | |
| func decHostedModelCountOp() txn.Op { | |
| return HostedModelCountOp(-1) | |
| } | |
| func HostedModelCountOp(amount int) txn.Op { | |
| return txn.Op{ | |
| C: controllersC, | |
| Id: hostedModelCountKey, | |
| Assert: txn.DocExists, | |
| Update: bson.M{ | |
| "$inc": bson.M{"refcount": amount}, | |
| }, | |
| } | |
| } | |
| func hostedModelCount(st *State) (int, error) { | |
| var doc hostedModelCountDoc | |
| controllers, closer := st.db().GetCollection(controllersC) | |
| defer closer() | |
| if err := controllers.Find(bson.D{{"_id", hostedModelCountKey}}).One(&doc); err != nil { | |
| return 0, errors.Trace(err) | |
| } | |
| return doc.RefCount, nil | |
| } | |
| // createUniqueOwnerModelNameOp returns the operation needed to create | |
| // an usermodelnameC document with the given owner and model name. | |
| func createUniqueOwnerModelNameOp(owner names.UserTag, envName string) txn.Op { | |
| return txn.Op{ | |
| C: usermodelnameC, | |
| Id: userModelNameIndex(owner.Id(), envName), | |
| Assert: txn.DocMissing, | |
| Insert: bson.M{}, | |
| } | |
| } | |
| // assertAliveOp returns a txn.Op that asserts the model is alive. | |
| func (m *Model) assertActiveOp() txn.Op { | |
| return assertModelActiveOp(m.UUID()) | |
| } | |
| // assertModelActiveOp returns a txn.Op that asserts the given | |
| // model UUID refers to an Alive model. | |
| func assertModelActiveOp(modelUUID string) txn.Op { | |
| return txn.Op{ | |
| C: modelsC, | |
| Id: modelUUID, | |
| Assert: append(isAliveDoc, bson.DocElem{"migration-mode", MigrationModeNone}), | |
| } | |
| } | |
| func checkModelActive(st *State) error { | |
| model, err := st.Model() | |
| if (err == nil && model.Life() != Alive) || errors.IsNotFound(err) { | |
| return errors.Errorf("model %q is no longer alive", model.Name()) | |
| } else if err != nil { | |
| return errors.Annotate(err, "unable to read model") | |
| } else if mode := model.MigrationMode(); mode != MigrationModeNone { | |
| return errors.Errorf("model %q is being migrated", model.Name()) | |
| } | |
| return nil | |
| } |