Skip to content

Commit

Permalink
Check available disk space before creating an automatic snapshot on s…
Browse files Browse the repository at this point in the history
…nap remove.
  • Loading branch information
stolowski committed Aug 3, 2020
1 parent 3d6dc42 commit b76ce57
Show file tree
Hide file tree
Showing 5 changed files with 86 additions and 0 deletions.
5 changes: 5 additions & 0 deletions dirs/dirs.go
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,11 @@ func isInsideBaseSnap() (bool, error) {
return err == nil, err
}

// SnapdStateDir returns the path to /var/lib/snapd dir under rootdir.
func SnapdStateDir(rootdir string) string {
return filepath.Join(rootdir, snappyDir)
}

// SnapBlobDirUnder returns the path to the snap blob dir under rootdir.
func SnapBlobDirUnder(rootdir string) string {
return filepath.Join(rootdir, snappyDir, "snaps")
Expand Down
6 changes: 6 additions & 0 deletions overlord/snapstate/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,12 @@ func MockOsutilEnsureUserGroup(mock func(name string, id uint32, extraUsers bool
return func() { osutilEnsureUserGroup = old }
}

func MockOsutilCheckFreeSpace(mock func(path string, minSize uint64) error) (restore func()) {
old := osutilCheckFreeSpace
osutilCheckFreeSpace = mock
return func() { osutilCheckFreeSpace = old }
}

var (
CoreInfoInternal = coreInfo
CheckSnap = checkSnap
Expand Down
3 changes: 3 additions & 0 deletions overlord/snapstate/handlers_prereq_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ func (s *prereqSuite) SetUpTest(c *C) {
s.state.Set("seeded", true)
s.state.Set("refresh-privacy-key", "privacy-key")
s.AddCleanup(snapstatetest.MockDeviceModel(DefaultModel()))

restoreCheckFreeSpace := snapstate.MockOsutilCheckFreeSpace(func(string, uint64) error { return nil })
s.AddCleanup(restoreCheckFreeSpace)
}

func (s *prereqSuite) TestDoPrereqNothingToDo(c *C) {
Expand Down
18 changes: 18 additions & 0 deletions overlord/snapstate/snapstate.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import (
"github.com/snapcore/snapd/i18n"
"github.com/snapcore/snapd/interfaces"
"github.com/snapcore/snapd/logger"
"github.com/snapcore/snapd/osutil"
"github.com/snapcore/snapd/overlord/auth"
"github.com/snapcore/snapd/overlord/configstate/config"
"github.com/snapcore/snapd/overlord/ifacestate/ifacerepo"
Expand Down Expand Up @@ -70,6 +71,8 @@ const (

var ErrNothingToDo = errors.New("nothing to do")

var osutilCheckFreeSpace = osutil.CheckFreeSpace

func isParallelInstallable(snapsup *SnapSetup) error {
if snapsup.InstanceKey == "" {
return nil
Expand All @@ -80,6 +83,11 @@ func isParallelInstallable(snapsup *SnapSetup) error {
return fmt.Errorf("cannot install snap of type %v as %q", snapsup.Type, snapsup.InstanceName())
}

func requiredSpaceWithMargin(minSize uint64) uint64 {
// 10% extra + 1Mb
return minSize + uint64(0.1 * float64(minSize)) + 1024*1024
}

func optedIntoSnapdSnap(st *state.State) (bool, error) {
tr := config.NewTransaction(st)
experimentalAllowSnapd, err := features.Flag(tr, features.SnapdSnap)
Expand Down Expand Up @@ -1909,6 +1917,16 @@ func Remove(st *state.State, name string, revision snap.Revision, flags *RemoveF
if tp, _ := snapst.Type(); tp == snap.TypeApp && removeAll {
ts, err := AutomaticSnapshot(st, name)
if err == nil {
if sz, err := EstimateSnapshotSize(st, name); err == nil {
requiredSpace := requiredSpaceWithMargin(uint64(sz))
if err := osutilCheckFreeSpace(dirs.SnapdStateDir(dirs.GlobalRootDir), requiredSpace); err != nil {
if _, ok := err.(*osutil.NotEnoughDiskSpaceError); ok {
return nil, fmt.Errorf("cannot create automatic snapshot when removing last revision of the snap: %v", err)
}
return nil, err
}
} // XXX: should we fail if estimation fails?

addNext(ts)
} else {
if err != ErrNothingToDo {
Expand Down
54 changes: 54 additions & 0 deletions overlord/snapstate/snapstate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import (
"gopkg.in/tomb.v2"

"github.com/snapcore/snapd/asserts"
"github.com/snapcore/snapd/osutil"
"github.com/snapcore/snapd/bootloader"
"github.com/snapcore/snapd/bootloader/bootloadertest"
"github.com/snapcore/snapd/dirs"
Expand Down Expand Up @@ -100,6 +101,9 @@ func (s *snapmgrTestSuite) SetUpTest(c *C) {

s.BaseTest.AddCleanup(snap.MockSanitizePlugsSlots(func(snapInfo *snap.Info) {}))

restoreCheckFreeSpace := snapstate.MockOsutilCheckFreeSpace(func(string, uint64) error { return nil })
s.AddCleanup(restoreCheckFreeSpace)

s.fakeBackend = &fakeSnappyBackend{}
s.fakeBackend.emptyContainer = emptyContainer(c)
s.fakeStore = &fakeStore{
Expand Down Expand Up @@ -161,6 +165,10 @@ func (s *snapmgrTestSuite) SetUpTest(c *C) {
return nil, nil, nil
}))

oldEstimateSnapshotSize := snapstate.EstimateSnapshotSize
snapstate.EstimateSnapshotSize = func(st *state.State, instanceName string) (uint64, error) {
return 1, nil
}
oldAutomaticSnapshot := snapstate.AutomaticSnapshot
snapstate.AutomaticSnapshot = func(st *state.State, instanceName string) (ts *state.TaskSet, err error) {
task := st.NewTask("save-snapshot", "...")
Expand All @@ -171,6 +179,7 @@ func (s *snapmgrTestSuite) SetUpTest(c *C) {
oldAutomaticSnapshotExpiration := snapstate.AutomaticSnapshotExpiration
snapstate.AutomaticSnapshotExpiration = func(st *state.State) (time.Duration, error) { return 1, nil }
s.BaseTest.AddCleanup(func() {
snapstate.EstimateSnapshotSize = oldEstimateSnapshotSize
snapstate.AutomaticSnapshot = oldAutomaticSnapshot
snapstate.AutomaticSnapshotExpiration = oldAutomaticSnapshotExpiration
})
Expand Down Expand Up @@ -1065,6 +1074,51 @@ func (s *snapmgrTestSuite) TestRemoveConflict(c *C) {
c.Assert(err, ErrorMatches, `snap "some-snap" has "remove" change in progress`)
}

func (s *snapmgrTestSuite) TestRemoveDiskSpaceForSnapshotError(c *C) {
s.state.Lock()
defer s.state.Unlock()

restore := snapstate.MockOsutilCheckFreeSpace(func(string, uint64) error { return &osutil.NotEnoughDiskSpaceError{} })
defer restore()

snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
Active: true,
Sequence: []*snap.SideInfo{{RealName: "some-snap", Revision: snap.R(11)}},
Current: snap.R(11),
SnapType: "app",
})

_, err := snapstate.Remove(s.state, "some-snap", snap.R(0), nil)
c.Assert(err, ErrorMatches, `cannot create automatic snapshot when removing last revision of the snap: insufficient space.*`)
}


func (s *snapmgrTestSuite) TestRemoveDiskSpaceForSnapshotNotCheckedWhenSnapshotsDisabled(c *C) {
s.state.Lock()
defer s.state.Unlock()

restore := snapstate.MockOsutilCheckFreeSpace(func(string, uint64) error { return &osutil.NotEnoughDiskSpaceError{} })
defer restore()

var automaticSnapshotCalled bool
snapstate.AutomaticSnapshot = func(st *state.State, instanceName string) (ts *state.TaskSet, err error) {
automaticSnapshotCalled = true
// ErrNothingToDo is returned if automatic snapshots are disabled
return nil, snapstate.ErrNothingToDo
}

snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
Active: true,
Sequence: []*snap.SideInfo{{RealName: "some-snap", Revision: snap.R(11)}},
Current: snap.R(11),
SnapType: "app",
})

_, err := snapstate.Remove(s.state, "some-snap", snap.R(0), nil)
c.Assert(err, IsNil)
c.Assert(automaticSnapshotCalled, Equals, true)
}

func (s *snapmgrTestSuite) TestDisableSnapDisabledServicesSaved(c *C) {
s.state.Lock()
defer s.state.Unlock()
Expand Down

0 comments on commit b76ce57

Please sign in to comment.