Skip to content

Commit

Permalink
storage: add "Provider.Releasable()" method
Browse files Browse the repository at this point in the history
Add the Provider.Releasable() method, which
reports whether the storage provider supports
releasing storage. This is used in state to
prevent marking storage as "releasing", when
releasing that storage is not possible.

Currently, the Azure and Oracle storage
providers are the only two that do not
support releasing storage. Support for
Oracle should be introduced soon; support
for Azure is dependent on upstream changes.
  • Loading branch information
axw committed Aug 30, 2017
1 parent 3aaad60 commit d7ab142
Show file tree
Hide file tree
Showing 21 changed files with 265 additions and 15 deletions.
21 changes: 12 additions & 9 deletions featuretests/storage_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,8 @@ modelscoped:
provider: modelscoped
modelscoped-block:
provider: modelscoped-block
modelscoped-unreleasable:
provider: modelscoped-unreleasable
rootfs:
provider: rootfs
static:
Expand All @@ -262,15 +264,16 @@ func (s *cmdStorageSuite) TestListPoolsTabular(c *gc.C) {
stdout, _, err := runPoolList(c)
c.Assert(err, jc.ErrorIsNil)
expected := `
Name Provider Attrs
block loop it=works
loop loop
machinescoped machinescoped
modelscoped modelscoped
modelscoped-block modelscoped-block
rootfs rootfs
static static
tmpfs tmpfs
Name Provider Attrs
block loop it=works
loop loop
machinescoped machinescoped
modelscoped modelscoped
modelscoped-block modelscoped-block
modelscoped-unreleasable modelscoped-unreleasable
rootfs rootfs
static static
tmpfs tmpfs
`[1:]
c.Assert(stdout, gc.Equals, expected)
Expand Down
10 changes: 10 additions & 0 deletions provider/azure/storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,16 @@ func (e *azureStorageProvider) Dynamic() bool {
return true
}

// Releasable is part of the Provider interface.
func (e *azureStorageProvider) Releasable() bool {
// NOTE(axw) Azure storage is currently tied to a model, and cannot
// be released or imported. To support releasing and importing, we'll
// need several things:
// - for the provider to use managed disks
// - for Azure to support moving managed disks between resource groups
return false
}

// DefaultPools is part of the Provider interface.
func (e *azureStorageProvider) DefaultPools() []*storage.Config {
return nil
Expand Down
5 changes: 5 additions & 0 deletions provider/ec2/ebs.go
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,11 @@ func (e *ebsProvider) Dynamic() bool {
return true
}

// Releasable is defined on the Provider interface.
func (*ebsProvider) Releasable() bool {
return true
}

// DefaultPools is defined on the Provider interface.
func (e *ebsProvider) DefaultPools() []*storage.Config {
ssdPool, _ := storage.NewConfig("ebs-ssd", EBS_ProviderType, map[string]interface{}{
Expand Down
4 changes: 4 additions & 0 deletions provider/gce/disks.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ func (g *storageProvider) Dynamic() bool {
return true
}

func (e *storageProvider) Releasable() bool {
return true
}

func (g *storageProvider) DefaultPools() []*storage.Config {
// TODO(perrito666) Add explicit pools.
return nil
Expand Down
5 changes: 5 additions & 0 deletions provider/lxd/storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,11 @@ func (e *lxdStorageProvider) Dynamic() bool {
return true
}

// Releasable is defined on the Provider interface.
func (*lxdStorageProvider) Releasable() bool {
return true
}

// DefaultPools is part of the Provider interface.
func (e *lxdStorageProvider) DefaultPools() []*storage.Config {
zfsPool, _ := storage.NewConfig("lxd-zfs", lxdStorageProviderType, map[string]interface{}{
Expand Down
5 changes: 5 additions & 0 deletions provider/maas/volumes.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,11 @@ func (maasStorageProvider) Dynamic() bool {
return false
}

// Releasable is defined on the Provider interface.
func (maasStorageProvider) Releasable() bool {
return false
}

// DefaultPools is defined on the Provider interface.
func (maasStorageProvider) DefaultPools() []*storage.Config {
return nil
Expand Down
5 changes: 5 additions & 0 deletions provider/openstack/cinder.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,11 @@ func (p *cinderProvider) Dynamic() bool {
return true
}

// Releasable is defined on the Provider interface.
func (*cinderProvider) Releasable() bool {
return true
}

// DefaultPools implements storage.Provider.
func (p *cinderProvider) DefaultPools() []*storage.Config {
return nil
Expand Down
6 changes: 6 additions & 0 deletions provider/oracle/storage_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ func (s storageProvider) Dynamic() bool {
return true
}

// Releasable is defined on the Provider interface.
func (s storageProvider) Releasable() bool {
// TODO(axw) support releasing Oracle storage volumes.
return false
}

// DefaultPools is defined on the storage.Provider interface.
func (s storageProvider) DefaultPools() []*storage.Config {
latencyPool, _ := storage.NewConfig("oracle-latency", oracleStorageProvideType, map[string]interface{}{
Expand Down
7 changes: 7 additions & 0 deletions state/filesystem.go
Original file line number Diff line number Diff line change
Expand Up @@ -520,6 +520,13 @@ func (f *filesystem) Detachable() bool {
return f.doc.MachineId == ""
}

func (f *filesystem) pool() string {
if f.doc.Info != nil {
return f.doc.Info.Pool
}
return f.doc.Params.Pool
}

// isDetachableFilesystemPool reports whether or not the given
// storage pool will create a filesystem that is not inherently
// bound to a machine, and therefore can be detached.
Expand Down
24 changes: 24 additions & 0 deletions state/filesystem_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -627,6 +627,30 @@ func (s *FilesystemStateSuite) TestReleaseStorageInstanceFilesystemReleasing(c *
c.Assert(filesystem.Releasing(), jc.IsTrue)
}

func (s *FilesystemStateSuite) TestReleaseStorageInstanceFilesystemUnreleasable(c *gc.C) {
_, u, storageTag := s.setupSingleStorage(c, "filesystem", "modelscoped-unreleasable")
err := s.State.AssignUnit(u, state.AssignCleanEmpty)
c.Assert(err, jc.ErrorIsNil)
filesystem := s.storageInstanceFilesystem(c, storageTag)
c.Assert(err, jc.ErrorIsNil)
c.Assert(filesystem.Releasing(), jc.IsFalse)
err = s.IAASModel.SetFilesystemInfo(filesystem.FilesystemTag(), state.FilesystemInfo{FilesystemId: "vol-123"})
c.Assert(err, jc.ErrorIsNil)

err = u.Destroy()
c.Assert(err, jc.ErrorIsNil)
err = s.IAASModel.ReleaseStorageInstance(storageTag, true)
c.Assert(err, gc.ErrorMatches,
`cannot release storage "data/0": storage provider "modelscoped-unreleasable" does not support releasing storage`)
err = s.IAASModel.DetachStorage(storageTag, u.UnitTag())
c.Assert(err, jc.ErrorIsNil)

// The filesystem should should be dying, and releasing.
filesystem = s.filesystem(c, filesystem.FilesystemTag())
c.Assert(filesystem.Life(), gc.Equals, state.Alive)
c.Assert(filesystem.Releasing(), jc.IsFalse)
}

func (s *FilesystemStateSuite) TestSetFilesystemAttachmentInfoFilesystemNotProvisioned(c *gc.C) {
_, filesystemAttachment, _, _ := s.addUnitWithFilesystemUnprovisioned(c, "rootfs", false)
err := s.IAASModel.SetFilesystemAttachmentInfo(
Expand Down
65 changes: 61 additions & 4 deletions state/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -1084,6 +1084,21 @@ func (m *Model) destroyOps(
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
}
Expand All @@ -1097,10 +1112,10 @@ func (m *Model) destroyOps(
}
}

if m.isControllerModel() && (!args.DestroyHostedModels || args.DestroyStorage == nil) {
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 how
// to remove storage.
// 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
Expand Down Expand Up @@ -1331,6 +1346,48 @@ func checkModelEntityRefsNoPersistentStorage(
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{{
Expand Down Expand Up @@ -1360,7 +1417,7 @@ func checkModelEntityRefsNoPersistentStorage(
noNewVolumes,
noNewFilesystems,
},
}}, nil
}}
}

func addModelMachineRefOp(mb modelBackend, machineId string) txn.Op {
Expand Down
26 changes: 26 additions & 0 deletions state/model_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -871,6 +871,32 @@ func (s *ModelSuite) testDestroyModelDestroyStorage(c *gc.C, destroyStorage bool
c.Assert(volume.Releasing(), gc.Equals, !destroyStorage)
}

func (s *ModelSuite) TestDestroyModelReleaseStorageUnreleasable(c *gc.C) {
m, err := s.State.Model()
c.Assert(err, jc.ErrorIsNil)

imodel, err := m.IAASModel()
c.Assert(err, jc.ErrorIsNil)

s.Factory.MakeUnit(c, &factory.UnitParams{
Application: s.Factory.MakeApplication(c, &factory.ApplicationParams{
Charm: s.AddTestingCharm(c, "storage-block"),
Storage: map[string]state.StorageConstraints{
"data": {Count: 1, Size: 1024, Pool: "modelscoped-unreleasable"},
},
}),
})

destroyStorage := false
err = imodel.Destroy(state.DestroyModelParams{DestroyStorage: &destroyStorage})
c.Assert(err, gc.ErrorMatches,
`failed to destroy model: cannot release volume 0: `+
`storage provider "modelscoped-unreleasable" does not support releasing storage`)
c.Assert(imodel.Refresh(), jc.ErrorIsNil)
c.Assert(imodel.Life(), gc.Equals, state.Alive)
assertDoesNotNeedCleanup(c, s.State)
}

func (s *ModelSuite) TestDestroyModelAddServiceConcurrently(c *gc.C) {
st := s.Factory.MakeModel(c, nil)
defer st.Close()
Expand Down
20 changes: 20 additions & 0 deletions state/storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,12 @@ func (im *IAASModel) destroyStorageInstanceOps(
ownerAssert = bson.DocElem{"owner", bson.D{{"$exists", false}}}
}

if releaseStorage {
if err := checkStoragePoolReleasable(im, s.Pool()); err != nil {
return nil, errors.Trace(err)
}
}

// There are still attachments: the storage instance will be removed
// when the last attachment is removed. We schedule a cleanup to destroy
// attachments.
Expand All @@ -398,6 +404,20 @@ func (im *IAASModel) destroyStorageInstanceOps(
return ops, nil
}

func checkStoragePoolReleasable(im *IAASModel, pool string) error {
providerType, provider, err := poolStorageProvider(im, pool)
if err != nil {
return errors.Trace(err)
}
if !provider.Releasable() {
return errors.Errorf(
"storage provider %q does not support releasing storage",
providerType,
)
}
return nil
}

// removeStorageInstanceOps removes the storage instance with the given
// tag from state, if the specified assertions hold true.
func removeStorageInstanceOps(si *storageInstance, assert bson.D) ([]txn.Op, error) {
Expand Down
15 changes: 13 additions & 2 deletions state/volume.go
Original file line number Diff line number Diff line change
Expand Up @@ -473,12 +473,23 @@ func isDetachableVolumeTag(db Database, tag names.VolumeTag) (bool, error) {
if err != nil {
return false, errors.Trace(err)
}
return doc.MachineId == "", nil
return detachableVolumeDoc(&doc), nil
}

// Detachable reports whether or not the volume is detachable.
func (v *volume) Detachable() bool {
return v.doc.MachineId == ""
return detachableVolumeDoc(&v.doc)
}

func (v *volume) pool() string {
if v.doc.Info != nil {
return v.doc.Info.Pool
}
return v.doc.Params.Pool
}

func detachableVolumeDoc(doc *volumeDoc) bool {
return doc.MachineId == ""
}

// isDetachableVolumePool reports whether or not the given storage
Expand Down
25 changes: 25 additions & 0 deletions state/volume_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -580,6 +580,31 @@ func (s *VolumeStateSuite) TestReleaseStorageInstanceVolumeReleasing(c *gc.C) {
c.Assert(volume.Releasing(), jc.IsTrue)
}

func (s *VolumeStateSuite) TestReleaseStorageInstanceVolumeUnreleasable(c *gc.C) {
_, u, storageTag := s.setupSingleStorage(c, "block", "modelscoped-unreleasable")
err := s.State.AssignUnit(u, state.AssignCleanEmpty)
c.Assert(err, jc.ErrorIsNil)
volume := s.storageInstanceVolume(c, storageTag)
c.Assert(err, jc.ErrorIsNil)
c.Assert(volume.Releasing(), jc.IsFalse)
err = s.IAASModel.SetVolumeInfo(volume.VolumeTag(), state.VolumeInfo{VolumeId: "vol-123"})
c.Assert(err, jc.ErrorIsNil)

err = u.Destroy()
c.Assert(err, jc.ErrorIsNil)
err = s.IAASModel.ReleaseStorageInstance(storageTag, true)
c.Assert(err, gc.ErrorMatches,
`cannot release storage "data/0": storage provider "modelscoped-unreleasable" does not support releasing storage`,
)
err = s.IAASModel.DetachStorage(storageTag, u.UnitTag())
c.Assert(err, jc.ErrorIsNil)

// The volume should should still be alive.
volume = s.volume(c, volume.VolumeTag())
c.Assert(volume.Life(), gc.Equals, state.Alive)
c.Assert(volume.Releasing(), jc.IsFalse)
}

func (s *VolumeStateSuite) TestSetVolumeAttachmentInfoVolumeNotProvisioned(c *gc.C) {
_, u, storageTag := s.setupSingleStorage(c, "block", "loop-pool")
err := s.State.AssignUnit(u, state.AssignCleanEmpty)
Expand Down
5 changes: 5 additions & 0 deletions storage/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,11 @@ type Provider interface {
// created at the time a machine is provisioned.
Dynamic() bool

// Releasable reports whether or not the storage provider is capable
// of releasing dynamic storage, with either ReleaseVolumes or
// ReleaseFilesystems.
Releasable() bool

// DefaultPools returns the default storage pools for this provider,
// to register in each new model.
DefaultPools() []*Config
Expand Down
Loading

0 comments on commit d7ab142

Please sign in to comment.