Permalink
Browse files

cmd/snpa-update-ns: add execWritableMimic

This patch adds a function that executes a plan for a writable mimic and
constructs one and returns an "undo plan" that contains simplified view
of the changes that are suitable for undo.

Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
  • Loading branch information...
1 parent e370d84 commit 9b4507bdbfd22f858808924fa45c633209d290c1 @zyga committed Nov 28, 2017
Showing with 214 additions and 0 deletions.
  1. +1 −0 cmd/snap-update-ns/export_test.go
  2. +87 −0 cmd/snap-update-ns/utils.go
  3. +126 −0 cmd/snap-update-ns/utils_test.go
@@ -39,6 +39,7 @@ var (
// utils
EnsureMountPoint = ensureMountPoint
PlanWritableMimic = planWritableMimic
+ ExecWritableMimic = execWritableMimic
SecureMkdirAll = secureMkdirAll
SecureMkfileAll = secureMkfileAll
SplitIntoSegments = splitIntoSegments
@@ -362,6 +362,93 @@ func planWritableMimic(dir string) ([]*Change, error) {
return changes, nil
}
+// execWritableMimic executes the plan for a writable mimic.
+// The result is a transformed mount namespace and a set of fake mount changes
+// that only exist in order to undo the plan.
+//
+// Certain assumptions are made about the plan, it must closely resemble that
+// created by planWritableMimic, in particular the sequence must look like this:
+//
+// - bind a directory aside into safekeeping location
+// - cover the original with tmpfs
+// - bind mount something from safekeeping location to an empty file or
+// directory in the tmpfs; this step can repeat any number of times
+// - unbind the safekeeping location
+//
+// Apart from merely executing the plan a fake plan is returned for undo. The
+// undo plan skips the following elements as compared to the original plan:
+//
+// - the initial bind mount that constructs the safekeeping directory is gone
+// - the final unmount that removes the safekeeping directory
+// - the source of each of the bind mounts that re-populate tmpfs.
+//
+// In the event of a failure the undo plan is executed and an error is
+// returned. If the undo plan fails the function panics as it cannot fix the
+// system from an inconsistent state.
+func execWritableMimic(plan []*Change) ([]*Change, error) {
+ undoChanges := make([]*Change, 0, len(plan)-2)
+ for i, change := range plan {
+ if _, err := changePerform(change); err != nil {
+ recoveryUndoChanges := make([]*Change, 0, len(undoChanges)+1)
+ if i > 0 {
+ // The undo plan doesn't contain the entry for the initial bind
+ // mount of the safe keeping directory but we have already
+ // performed it. For this recovery phase we need to insert that
+ // in front of the undo plan manually.
+ recoveryUndoChanges = append(recoveryUndoChanges, plan[0])
+ }
+ recoveryUndoChanges = append(recoveryUndoChanges, undoChanges...)
+
+ // Drat, we failed! Let's undo everything according to our own undo
+ // plan, by following it in reverse order.
+ for j := len(recoveryUndoChanges) - 1; j >= 0; j-- {
+ recoveryUndoChange := recoveryUndoChanges[j]
+ // All the changes mount something, we need to reverse that.
+ // The "undo plan" is "a plan that can be undone" not "the plan
+ // for how to undo" so we need to flip the actions.
+ recoveryUndoChange.Action = Unmount
+ if _, err2 := changePerform(recoveryUndoChange); err2 != nil {
+ // Drat, we failed when trying to recover from an error.
+ // We cannot do anything at this stage.
+ panic(fmt.Errorf("cannot undo change %q while recovering from earlier error %v: %v", recoveryUndoChange, err, err2))
+ }
+ }
+ return nil, err
+ }
+ if i == 0 || i == len(plan)-1 {
+ // Don't represent the initial and final changes in the undo plan.
+ // The initial change is the safe-keeping bind mount, the final
+ // change is the safe-keeping unmount.
+ continue
+ }
+ if kind, _ := change.Entry.OptStr("x-snapd.kind"); kind == "symlink" {
+ // Don't represent symlinks in the undo plan. They are removed when
+ // the tmpfs is unmounted.
+ continue
+
+ }
+ // Store an undo change for the change we just performed.
+ undoChange := &Change{
+ Action: Mount,
+ Entry: mount.Entry{Dir: change.Entry.Dir, Name: change.Entry.Name, Type: change.Entry.Type, Options: change.Entry.Options},
+ }
+ // Because of the use of a temporary bind mount (aka the safe-keeping
+ // directory) we cannot represent bind mounts fully (the temporary bind
+ // mount is unmounted as the last stage of this process). For that
+ // reason let's hide the original location and overwrite it so to
+ // appear as if the directory was a bind mount over itself. This is not
+ // fully true (it is a bind mount from the old self to the new empty
+ // directory or file in the same path, with the tmpfs in place already)
+ // but this is closer to the truth and more in line with the idea that
+ // this is just a plan for undoing the operation.
+ if undoChange.Entry.OptBool("bind") {
+ undoChange.Entry.Name = undoChange.Entry.Dir
+ }
+ undoChanges = append(undoChanges, undoChange)
+ }
+ return undoChanges, nil
+}
+
func ensureMountPoint(path string, mode os.FileMode, uid int, gid int) error {
// If the mount point is not present then create a directory in its
// place. This is very naive, doesn't handle read-only file systems
@@ -21,6 +21,7 @@ package main_test
import (
"bytes"
+ "fmt"
"os"
"path/filepath"
"syscall"
@@ -285,6 +286,131 @@ func (s *utilsSuite) TestPlanWritableMimicErrors(c *C) {
c.Assert(changes, HasLen, 0)
}
+func (s *utilsSuite) TestExecWirableMimicSuccess(c *C) {
+ // This plan is the same as in the test above. This is what comes out of planWritableMimic.
+ plan := []*update.Change{
+ {Entry: mount.Entry{Name: "/foo", Dir: "/tmp/.snap/foo", Options: []string{"bind"}}, Action: update.Mount},
+ {Entry: mount.Entry{Name: "tmpfs", Dir: "/foo", Type: "tmpfs"}, Action: update.Mount},
+ {Entry: mount.Entry{Name: "/tmp/.snap/foo/file", Dir: "/foo/file", Options: []string{"bind", "x-snapd.kind=file"}}, Action: update.Mount},
+ {Entry: mount.Entry{Name: "/tmp/.snap/foo/dir", Dir: "/foo/dir", Options: []string{"bind"}}, Action: update.Mount},
+ {Entry: mount.Entry{Name: "/tmp/.snap/foo/symlink", Dir: "/foo/symlink", Options: []string{"bind", "x-snapd.kind=symlink", "x-snapd.symlink=target"}}, Action: update.Mount},
+ {Entry: mount.Entry{Name: "none", Dir: "/tmp/.snap/foo"}, Action: update.Unmount},
+ }
+
+ // Mock the act of performing changes, each of the change we perform is coming from the plan.
+ restore := update.MockChangePerform(func(chg *update.Change) ([]*update.Change, error) {
+ c.Assert(plan, testutil.DeepContains, chg)
+ return nil, nil
+ })
+ defer restore()
+
+ // The executed plan leaves us with a simplified view of the plan that is suitable for undo.
+ undoPlan, err := update.ExecWritableMimic(plan)
+ c.Assert(err, IsNil)
+ c.Assert(undoPlan, DeepEquals, []*update.Change{
+ {Entry: mount.Entry{Name: "tmpfs", Dir: "/foo", Type: "tmpfs"}, Action: update.Mount},
+ {Entry: mount.Entry{Name: "/foo/file", Dir: "/foo/file", Options: []string{"bind", "x-snapd.kind=file"}}, Action: update.Mount},
+ {Entry: mount.Entry{Name: "/foo/dir", Dir: "/foo/dir", Options: []string{"bind"}}, Action: update.Mount},
+ })
+}
+
+func (s *utilsSuite) TestExecWirableMimicErrorWithRecovery(c *C) {
+ // This plan is the same as in the test above. This is what comes out of planWritableMimic.
+ plan := []*update.Change{
+ {Entry: mount.Entry{Name: "/foo", Dir: "/tmp/.snap/foo", Options: []string{"bind"}}, Action: update.Mount},
+ {Entry: mount.Entry{Name: "tmpfs", Dir: "/foo", Type: "tmpfs"}, Action: update.Mount},
+ {Entry: mount.Entry{Name: "/tmp/.snap/foo/file", Dir: "/foo/file", Options: []string{"bind", "x-snapd.kind=file"}}, Action: update.Mount},
+ {Entry: mount.Entry{Name: "/tmp/.snap/foo/dir", Dir: "/foo/dir", Options: []string{"bind"}}, Action: update.Mount},
+ {Entry: mount.Entry{Name: "/tmp/.snap/foo/symlink", Dir: "/foo/symlink", Options: []string{"bind", "x-snapd.kind=symlink", "x-snapd.symlink=target"}}, Action: update.Mount},
+ {Entry: mount.Entry{Name: "none", Dir: "/tmp/.snap/foo"}, Action: update.Unmount},
+ }
+
+ // Mock the act of performing changes. Before we inject a failure we ensure
+ // that each of the change we perform is coming from the plan. For the
+ // purpose of the test the change that bind mounts the "dir" over itself
+ // will fail and will trigger an recovery path. The changes performed in
+ // the recovery path are recorded.
+ var recoveryPlan []*update.Change
+ recovery := false
+ restore := update.MockChangePerform(func(chg *update.Change) ([]*update.Change, error) {
+ if !recovery {
+ c.Assert(plan, testutil.DeepContains, chg)
+ if chg.Entry.Name == "/tmp/.snap/foo/dir" {
+ recovery = true // switch to recovery mode
+ return nil, errTesting
+ }
+ } else {
+ recoveryPlan = append(recoveryPlan, chg)
+ }
+ return nil, nil
+ })
+ defer restore()
+
+ // The executed plan fails, leaving us with the error and an empty undo plan.
+ undoPlan, err := update.ExecWritableMimic(plan)
+ c.Assert(err, Equals, errTesting)
+ c.Assert(undoPlan, HasLen, 0)
+ // The changes we managed to perform were undone correctly.
+ c.Assert(recoveryPlan, DeepEquals, []*update.Change{
+ {Entry: mount.Entry{Name: "/foo/file", Dir: "/foo/file", Options: []string{"bind", "x-snapd.kind=file"}}, Action: update.Unmount},
+ {Entry: mount.Entry{Name: "tmpfs", Dir: "/foo", Type: "tmpfs"}, Action: update.Unmount},
+ {Entry: mount.Entry{Name: "/foo", Dir: "/tmp/.snap/foo", Options: []string{"bind"}}, Action: update.Unmount},
+ })
+}
+
+func (s *utilsSuite) TestExecWirableMimicErrorNothingDone(c *C) {
+ // This plan is the same as in the test above. This is what comes out of planWritableMimic.
+ plan := []*update.Change{
+ {Entry: mount.Entry{Name: "/foo", Dir: "/tmp/.snap/foo", Options: []string{"bind"}}, Action: update.Mount},
+ {Entry: mount.Entry{Name: "tmpfs", Dir: "/foo", Type: "tmpfs"}, Action: update.Mount},
+ {Entry: mount.Entry{Name: "/tmp/.snap/foo/file", Dir: "/foo/file", Options: []string{"bind", "x-snapd.kind=file"}}, Action: update.Mount},
+ {Entry: mount.Entry{Name: "/tmp/.snap/foo/dir", Dir: "/foo/dir", Options: []string{"bind"}}, Action: update.Mount},
+ {Entry: mount.Entry{Name: "/tmp/.snap/foo/symlink", Dir: "/foo/symlink", Options: []string{"bind", "x-snapd.kind=symlink", "x-snapd.symlink=target"}}, Action: update.Mount},
+ {Entry: mount.Entry{Name: "none", Dir: "/tmp/.snap/foo"}, Action: update.Unmount},
+ }
+
+ // Mock the act of performing changes and just fail on any request.
+ restore := update.MockChangePerform(func(chg *update.Change) ([]*update.Change, error) {
+ return nil, errTesting
+ })
+ defer restore()
+
+ // The executed plan fails, the recovery didn't fail (it's empty) so we just return that error.
+ undoPlan, err := update.ExecWritableMimic(plan)
+ c.Assert(err, Equals, errTesting)
+ c.Assert(undoPlan, HasLen, 0)
+}
+
+func (s *utilsSuite) TestExecWirableMimicErrorCannotUndo(c *C) {
+ // This plan is the same as in the test above. This is what comes out of planWritableMimic.
+ plan := []*update.Change{
+ {Entry: mount.Entry{Name: "/foo", Dir: "/tmp/.snap/foo", Options: []string{"bind"}}, Action: update.Mount},
+ {Entry: mount.Entry{Name: "tmpfs", Dir: "/foo", Type: "tmpfs"}, Action: update.Mount},
+ {Entry: mount.Entry{Name: "/tmp/.snap/foo/file", Dir: "/foo/file", Options: []string{"bind", "x-snapd.kind=file"}}, Action: update.Mount},
+ {Entry: mount.Entry{Name: "/tmp/.snap/foo/dir", Dir: "/foo/dir", Options: []string{"bind"}}, Action: update.Mount},
+ {Entry: mount.Entry{Name: "/tmp/.snap/foo/symlink", Dir: "/foo/symlink", Options: []string{"bind", "x-snapd.kind=symlink", "x-snapd.symlink=target"}}, Action: update.Mount},
+ {Entry: mount.Entry{Name: "none", Dir: "/tmp/.snap/foo"}, Action: update.Unmount},
+ }
+
+ // Mock the act of performing changes. After performing the first change
+ // correctly we will fail forever (this includes the recovery path) so the
+ // execute function ends up in a situation where it cannot perform the
+ // recovery path and will have to panic.
+ i := -1
+ restore := update.MockChangePerform(func(chg *update.Change) ([]*update.Change, error) {
+ i += 1
+ if i > 0 {
+ return nil, fmt.Errorf("failure-%d", i)
+ }
+ return nil, nil
+ })
+ defer restore()
+
+ // The executed plan fails, the recovery didn't fail (it's empty) so we just return that error.
+ c.Assert(func() { update.ExecWritableMimic(plan) }, PanicMatches,
+ `cannot undo change ".*" while recovering from earlier error failure-1: failure-2`)
+}
+
// realSystemSuite is not isolated / mocked from the system.
type realSystemSuite struct{}

0 comments on commit 9b4507b

Please sign in to comment.