many: add `snap revert` #1275

Merged
merged 87 commits into from Jun 30, 2016

Conversation

Projects
None yet
7 participants
Collaborator

mvo5 commented Jun 6, 2016

This branch adds a snap revert command that allows going back to the previous version of a given snap.

mvo5 added some commits May 19, 2016

Member

elopio commented Jun 6, 2016

retest this please

overlord/snapstate/snapstate.go
+
+ // RollbackMode means the given snap got rolled back
+ RollbackMode
+
@pedronis

pedronis Jun 6, 2016

Contributor

not sure we should call Mode something like this also because it seems it's SnapSetup only flag, so it should follow my suggestion in the comment below in the code (if it's really a mode like trymode then the previous definition should go away and we should grab the value of firstInterimUsableFlagValue here instead of the one following)

overlord/snapstate/snapstate.go
@@ -302,8 +306,68 @@ func Remove(s *state.State, name string) (*state.TaskSet, error) {
// Rollback returns a set of tasks for rolling back a snap.
// Note that the state must be locked by the caller.
-func Rollback(s *state.State, snap, ver string) (*state.TaskSet, error) {
- return nil, fmt.Errorf("rollback not implemented")
+func Rollback(s *state.State, name, ver string) (*state.TaskSet, error) {
@pedronis

pedronis Jun 6, 2016

Contributor

bit confused about what should happen when ones rollback again? should we stop that here?

@mvo5

mvo5 Jun 7, 2016

Collaborator

@pedronis Yeah, that is a really good question. So far we only keep a single previous revision so you get only one rollback. Once we support more then of course RollbackR in the snapst is no longer good and it needs to become a list or a map. I can do this now so that we are ready?

@pedronis

pedronis Jun 7, 2016

Contributor

I don't have a strong opinion either way, but if keep it to just one rollback recorded, we should error here if a 2nd is tried. Also do we need to reset RollbackR somewhere?

@mvo5

mvo5 Jun 7, 2016

Collaborator

Yes, it is reset in https://github.com/snapcore/snapd/pull/1275/files#diff-82a44be6f243493a55d2ee2fe6f0addfR660. I added code that will prevent two rollbacks now.

@pedronis

pedronis Jun 7, 2016

Contributor

thanks

overlord/snapstate/snapstate.go
- // 0x40000000 >> iota
+ // Flags for SnapSetup:
+ // RollbackOp means the given snap got rolled back
+ RollbackOp = 0x40000000 >> iota
@pedronis

pedronis Jun 7, 2016

Contributor

could you double check, this is correct? if I read the lang ref correctly you need a new const block to get the right effect

@mvo5

mvo5 Jun 7, 2016

Collaborator

Ups, sorry. You are correct (of course). Fixing.

mvo5 added some commits Jun 7, 2016

overlord/snapstate/snapmgr.go
@@ -120,6 +125,14 @@ func (snapst *SnapState) Current() *snap.SideInfo {
return snapst.Sequence[n-1]
}
+func (snapst *SnapState) Previous() *snap.SideInfo {
@zyga

zyga Jun 7, 2016

Contributor

Nice, that's the meat of the problem :-)

overlord/snapstate/snapmgr.go
@@ -677,6 +698,9 @@ func (m *SnapManager) doLinkSnap(t *state.Task, _ *tomb.Tomb) error {
Set(st, ss.Name, snapst)
// Make sure if state commits and snapst is mutated we won't be rerun
t.SetStatus(state.DoneStatus)
+ // FIXME: this triggers a stale in the followup tasks in the
+ // taskrunner code, i.e. tasks after link-snap will
+ // only be run after the next EnsureBefore()
@pedronis

pedronis Jun 7, 2016

Contributor

this was fixed now

overlord/snapstate/snapmgr.go
@@ -107,6 +111,7 @@ type SnapState struct {
Active bool `json:"active,omitempty"`
Channel string `json:"channel,omitempty"`
Flags SnapStateFlags `json:"flags,omitempty"`
+ RollbackR snap.Revision `json:"rollback,omitempty"`
@pedronis

pedronis Jun 8, 2016

Contributor

if we think we'll need to support more than one rollback later, maybe this should start as []snap.Revision from day 1? to avoid the UnmarshalJSON juggling, though it wouldn't be too bad either

@mvo5

mvo5 Jun 9, 2016

Collaborator

Good point! fixed!

mvo5 added some commits Jun 9, 2016

Contributor

pedronis commented Jun 9, 2016

this is missing snapmgr_test.go tests and would be good if we started adding tests in the style of the ones in link_snap_test.go tests for link-snap handlers for any new handlers we introduce (I still plan to work through the preexisting handlers and add tests/fixes before RTM)

mvo5 added some commits Jun 10, 2016

Collaborator

mvo5 commented Jun 10, 2016

@pedronis thanks again! tests added :)

overlord/snapstate/snapstate.go
+// SnapSetup specific flags
+const (
+ // RollbackOp means the given snap got rolled back
+ RollbackOp = 0x40000000 >> iota
@pedronis

pedronis Jun 10, 2016

Contributor

same comment as I mentioned in first boot, given that this is only for SnapSetup flags it can be typed as SnapSetupFlags

overlord/snapstate/snapstate.go
+
+const (
+ // RollbackOp means the given snap got rolled back
+ RollbackOp = 0x40000000 >> iota
@pedronis

pedronis Jun 10, 2016

Contributor

sorry, probably was confusing but I also meant:

RollbackOp SnapSetupFlags = 0x40000000 >> iota

so it cannot be mixed easily with SnapStateFlags

@mvo5

mvo5 Jun 10, 2016

Collaborator

Oh, thank you!

Collaborator

mvo5 commented Jun 13, 2016

retest this please

mvo5 added some commits Jun 13, 2016

Member

chipaca commented Jun 15, 2016

LGTM FWIW

cmd/snap/cmd_snap_op.go
+ } `positional-args:"yes"`
+}
+
+var shortRollbackHelp = i18n.G("Rollback the given snap to the previous version")
@niemeyer

niemeyer Jun 17, 2016

Contributor

s/version/state/ seems a bit more accurate.

cmd/snap/cmd_snap_op.go
+
+var shortRollbackHelp = i18n.G("Rollback the given snap to the previous version")
+var longRollbackHelp = i18n.G(`
+The rollback command will rollback the given snap to the previous version.
@niemeyer

niemeyer Jun 17, 2016

Contributor
The rollback command will revert the given snap to its state before the latest refresh.
This will reactivate the previous snap revision, and will use the original data that was
associated with that revision, discarding any data changes that were done by the
latest revision. As an exception, data which the snap explicitly chooses to share
across revisions is not touched by the rollback process.
@niemeyer

niemeyer Jun 17, 2016

Contributor

(minor touches)

+ common.SnappySuite
+}
+
+func (s *rollbackAppSuite) TestInstallUpdateRollback(c *check.C) {
@niemeyer

niemeyer Jun 17, 2016

Contributor

This needs to be moved to spread, but let's not block on it. I see it depends on the fake store, which will need a bit of churn for us to establish the pattern. So a follow up seems more appropriate.

Contributor

niemeyer commented Jun 17, 2016

@mvo5 When you have a spare moment I'd like to have an interactive conversation with you to see whether we might take a slightly different approach for this work which would be both more flexible and require less state tracking.

Before going into the details of that, though can we replace the "rollback" terminology by "revert", as a shorter and nicer companion to "refresh"?

Then, here are the details:

  • SnapSetup gets Revert snap.Revision.
  • SnapState renames Current to CurrentSideInfo
  • SnapState gets new Current snap.Revision (this name is better as it reflects the current link)
  • CurrentSideInfo changes to look for Current in Sequence.
  • SnapState.RollbackR becomes unnecessary and is replaced by Sequence[currentIndex:]
  • RefreshCandidate.RollbackR renames to .Block for clarity

With all of that in place, I believe the whole process would work fine with arbitrary revisions, so I suggest accepting this as the initial form of revert:

snap revert <snap> [<revision>]

If the revision is not provided, it becomes the previous one in the sequence. If the current revision is the oldest one (again, sequence not numeric order) revert errors saying "error: revision %d is the oldest one for snap %q".

Note that the provided revision can be both older or newer than the current revision. As long as it's part of the currently installed sequence, we accept it.

What do you think?

Contributor

pedronis commented Jun 17, 2016

what happens in this model to the data of the reverted revisions, we just
keep it?

what happens on a new install? we put the it just after Current in the
sequence? and the reverted ones are after that?

On Fri, Jun 17, 2016 at 9:14 PM, Gustavo Niemeyer notifications@github.com
wrote:

@mvo5 https://github.com/mvo5 When you have a spare moment I'd like to
have an interactive conversation with you to see whether we might take a
slightly different approach for this work which would be both more flexible
and require less state tracking.

Before going into the details of that, though can we replace the
"rollback" terminology by "revert", as a shorter and nicer companion to
"refresh"?

Then, here are the details:

  • SnapSetup gets Revert snap.Revision.
  • SnapState renames Current to CurrentSideInfo
  • SnapState gets new Current snap.Revision (this name is better as it
    reflects the current link)
  • CurrentSideInfo changes to look for Current in Sequence.
  • SnapState.RollbackR becomes unnecessary and is replaced by
    Sequence[currentIndex:]
  • RefreshCandidate.RollbackR renames to .Block for clarity

With all of that in place, I believe the whole process would work fine
with arbitrary revisions, so I suggest accepting this as the initial form
of revert:

snap revert []

If the revision is not provided, it becomes the previous one in the
sequence. If the current revision is the oldest one (again, sequence not
numeric order) revert errors saying "error: revision %d is the oldest one
for snap %q".

Note that the provided revision can be both older or newer than the
current revision. As long as it's part of the currently installed sequence,
we accept it.

What do you think?


You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
#1275 (comment), or mute
the thread
https://github.com/notifications/unsubscribe/AA7jjcnLu5kwtYgMXlabBUUt3GcBtfY5ks5qMvIJgaJpZM4IuwjJ
.

Contributor

niemeyer commented Jun 17, 2016

Yes, I think we'd keep the data of the revert revisions until something else happens.

The new install is a good question, though. The only sane behavior I can think of is killing everything after the current one (effectively removing those revisions), and then appending as usual. This would make it look from the user perspective as if those revisions were never there, still preserving the correct sequence which was observed by the client as coming from the server.

A practical example to become more clear:

  • snap install foo - [>32]
  • snap refresh foo - [32, >35]
  • snap refresh foo - [32, 35, >47]
  • snap revert foo - [32, >35, 47]
  • snap refresh foo - [32, 35, >48]
  • snap revert foo - [32, >35, 48]
  • snap revert foo 48 - [32, 35, >48]

Sounds sane? Any other ideas?

Contributor

niemeyer commented Jun 17, 2016

There's an open edge there which we need to handle appropriately: how do we get back to the current revision without losing the latest changes that are sitting on the older revision that we reverted to.

I think the answer here is that we need to distinguish an automatic global refresh from an explicit one. That is, this should work assuming 48 is still the latest revision in the store:

  • snap revert foo - [32, >35, 48]
  • snap refresh foo - [32, 35, >48]

This is not a revert, though. In other words, in this case the data is again copied out from revision 35 exactly as it would be if 48 was in the store and not locally.

Contributor

niemeyer commented Jun 17, 2016

Another idea: we should probably support a few options to offer some flexibility without requiring explicit revisions to be stated:

  • --older - Previous in the sequence (default behavior)
  • --oldest - First in the sequence
  • --newer - Next in the sequence
  • --newest - Last in the sequence

In fact, this is so much nicer than explicit revisions. We probably don't even need explicit revisions with that?

Contributor

pedronis commented Jun 18, 2016

ok, with the idea of wiping the disabled (because reverted from) revisions when we install/something happens it seems reasonable, I think we need to have some prerequisite before though:

  • because we want to fiddle with SnapState we probably want migrations (that's something as I said I'm going to look into beginning next week) though I can see ways to not need them but the code would be definitely cleaner with them
  • because here again we hit the I want to delete a bunch of revisions on install etc which is unclear whether failing that should undo or not the install itself, which is a a variation on the theme of GC of unwanted/old revision on install we should think again about that problem in general, GC on install and how to partition chains of undos (I thought a bit about that problem, we can talk next week)

mvo5 added some commits Jun 29, 2016

Collaborator

mvo5 commented Jun 29, 2016

Thanks for the review! I addressed the points raised, please let me know if there is anything else I can improve.

@mvo5 mvo5 referenced this pull request in mvo5/snappy Jun 29, 2016

Closed

snapstate: drop revisions after "current" on refresh #12

overlord/snapstate/snapmgr.go
+ for _, si := range snapst.Sequence {
+ if si.Revision == ss.Revision {
+ snapst.Candidate = si
+ }
}
@niemeyer

niemeyer Jun 29, 2016

Contributor

We need some error checking logic here I think. Something along the lines of:

if snapst.Candidate == nil {
        return fmt.Errorf("cannot prepare snap %q with unknown revision %s", ss.Name, ss.Revision)
}
Contributor

niemeyer commented Jun 29, 2016

Just one simple error checking and LGTM!

overlord/snapstate/snapmgr.go
@@ -772,6 +813,7 @@ func (m *SnapManager) undoLinkSnap(t *state.Task, _ *tomb.Tomb) error {
snapst.Current = oldCurrent
snapst.Active = false
snapst.Channel = oldChannel
+ snapst.Current = oldCurrent
@pedronis

pedronis Jun 29, 2016

Contributor

this seems a repetition, of 772/813

overlord/snapstate/snapmgr_test.go
+ c.Assert(ts, IsNil)
+}
+
+func (s *snapmgrTestSuite) TestBlockunThrough(c *C) {
@pedronis

pedronis Jun 29, 2016

Contributor

this test name seems wrong

overlord/snapstate/snapmgr_test.go
@@ -1284,7 +1484,7 @@ func (s *snapmgrQuerySuite) TestInfo(c *C) {
c.Check(info.Description(), Equals, "Lots of text")
}
-func (s *snapmgrQuerySuite) TestCurrent(c *C) {
+func (s *snapmgrQuerySuite) TestCurrentSideInfo(c *C) {
@pedronis

pedronis Jun 29, 2016

Contributor

this tests CurrentInfo, it should be TestCurrentInfo

overlord/snapstate/snapstate.go
@@ -52,9 +52,6 @@ const (
// for use in the interim time while we have the backward compatible
// support
firstInterimUsableFlagValue
- // if we need flags for just SnapSetup it may be easier
- // to start a new sequence from the other end with:
- // 0x40000000 >> iota
)
@pedronis

pedronis Jun 29, 2016

Contributor

these comments need to be put back given that we aren't using this yet

- return err
+ for _, si := range snapst.Sequence {
+ if si.Revision == ss.Revision {
+ snapst.Candidate = si
@pedronis

pedronis Jun 29, 2016

Contributor

we can break here

+ for _, si := range snapst.Sequence[currentIndex:] {
+ out = append(out, si.Revision)
+ }
+ return out
@pedronis

pedronis Jun 29, 2016

Contributor

this can be out = append(out, snapst.Sequence[currentIndex:]...) I think

@mvo5

mvo5 Jun 29, 2016

Collaborator

These are different types, out is a []snap.Revision but snapst.Sequence is a []*snap.SideInfo.

@pedronis

pedronis Jun 29, 2016

Contributor

sorry, late in the day it seems

Contributor

pedronis commented Jun 29, 2016

I made some small comments, but I'm bit confused with some tests, I need to play with the branch locally tomorrow

overlord/managers_test.go
+ c.Assert(chg.Status(), Equals, state.DoneStatus, Commentf("revert-snap change failed with: %v", chg.Err()))
+
+ // ensure we are back to x1 after the revert
+ c.Assert(osutil.FileExists(filepath.Join(dirs.SnapBlobDir, "foo_x1.snap")), Equals, true)
@pedronis

pedronis Jun 29, 2016

Contributor

I don't understand this test, is checking only things that are true independently of what is active (mount units, snap blob), which is ok, but it should check also something that changes like wrappers (I think some other tests here do that),

atm I can comment out the revert invocation and it passes anyway

@mvo5

mvo5 Jun 30, 2016

Collaborator

Thanks, indeed, this test was useless, sorry for overlooking that. Fixed now.

Contributor

pedronis commented Jun 29, 2016

it seems we have a problem, and are missing some tests or missing testing some details:

this line in undoLinkSnap:

snapst.Candidate = snapst.Sequence[len(snapst.Sequence)-1]

isn't correct if the candidate was already in the sequence

Contributor

pedronis commented Jun 29, 2016

sorry, same for the other line there:

snapst.Sequence = snapst.Sequence[:len(snapst.Sequence)-1]
+ common.SnappySuite
+}
+
+func (s *revertAppSuite) TestInstallUpdateRevert(c *check.C) {
@pedronis

pedronis Jun 29, 2016

Contributor

I imagine this will need to be ported to spread sometime soon

@niemeyer

niemeyer Jun 30, 2016

Contributor

Yeah, @fgimenez can give us a hand with that so we don't block this month-long branch any further.

@fgimenez

fgimenez Jun 30, 2016

Contributor

Yep, I'll take care of this one with the remaining fake store tests.

@@ -66,49 +66,53 @@ func doInstall(s *state.State, curActive bool, ss *SnapSetup) (*state.TaskSet, e
ss.Channel = "stable"
}
- var prepare *state.Task
- if ss.SnapPath != "" {
+ var prepare, prev *state.Task
@pedronis

pedronis Jun 29, 2016

Contributor

given the changes to doInstall maybe we need a test in the style of TestInstallTasks like TestRevertTasks, to make sure the chaining is right

@mvo5

mvo5 Jun 30, 2016

Collaborator

Yes, I think that makes perfect sense, added now.

Contributor

pedronis commented Jun 29, 2016

finished my pass, mostly some issues with tests, proposing some more testing of tasks/chaining in doInstall and the undoLinkSnap issue

+
+ if !snapst.Active {
+ return nil, fmt.Errorf("cannot revert inactive snaps")
+ }
@pedronis

pedronis Jun 29, 2016

Contributor

not an issue atm but we should check if we are not trying to revert to snapst.Current and either do nothing? or error?

Collaborator

mvo5 commented Jun 29, 2016

undoLinkSnap needs a test for the case when the candidate is already in the sequence (and one where its appended if we don't have that already). we need to store the old sequence length so that we know if we added something or not.

mvo5 added some commits Jun 30, 2016

overlord/managers_test.go
+ var newSnapst snapstate.SnapState
+ snapstate.Get(st, snapName, &newSnapst)
+ c.Assert(newSnapst.CurrentSideInfo().Revision.Unset(), Equals, false)
+ c.Assert(snapst.CurrentSideInfo(), Not(DeepEquals), newSnapst.CurrentSideInfo())
@pedronis

pedronis Jun 30, 2016

Contributor

this seems a bit too internal details

mvo5 added some commits Jun 30, 2016

overlord/snapstate/snapmgr.go
m.backend.Candidate(snapst.Candidate)
- snapst.Sequence = append(snapst.Sequence, snapst.Candidate)
+ hadCandidateInSeq := false
@pedronis

pedronis Jun 30, 2016

Contributor

this seems reversed, it true by default (and false if we had to change seq to add the cand there)

overlord/snapstate/snapmgr.go
@@ -775,11 +824,17 @@ func (m *SnapManager) undoLinkSnap(t *state.Task, _ *tomb.Tomb) error {
if err != nil {
return err
}
+ var hadCandidateInSeq bool
+ err = t.Get("had-candidate-in-seq", &hadCandidateInSeq)
+ if err != nil {
@pedronis

pedronis Jun 30, 2016

Contributor

I suppose we can also ignore ErrNoState for backward compat

overlord/snapstate/snapmgr.go
- snapst.Candidate = snapst.Sequence[len(snapst.Sequence)-1]
- snapst.Sequence = snapst.Sequence[:len(snapst.Sequence)-1]
+ snapst.Candidate = snapst.Sequence[snapst.findIndex(snapst.Current)]
+ if hadCandidateInSeq {
@pedronis

pedronis Jun 30, 2016

Contributor

this would become !hadCandidateInSeq

overlord/snapstate/snapmgr.go
- snapst.Sequence = snapst.Sequence[:len(snapst.Sequence)-1]
+ snapst.Candidate = snapst.Sequence[snapst.findIndex(snapst.Current)]
+ if hadCandidateInSeq {
+ snapst.Sequence = snapst.Sequence[:len(snapst.Sequence)-1]
@pedronis

pedronis Jun 30, 2016

Contributor

I think Gustavo suggestion was to remove it by revision and not assume it was at the end

overlord/snapstate/snapmgr.go
@@ -780,7 +780,7 @@ func (m *SnapManager) doLinkSnap(t *state.Task, _ *tomb.Tomb) error {
t.Set("old-trymode", oldTryMode)
t.Set("old-channel", oldChannel)
t.Set("old-current", oldCurrent)
- t.Set("had-candidate-in-seq", hadCandidateInSeq)
+ t.Set("added-candidate", addedCandidate)
@pedronis

pedronis Jun 30, 2016

Contributor

bit confused here, this is the same a snapst.Current and snapst.Candidate.Current so it's already available in undo

Contributor

pedronis commented Jun 30, 2016

undo code looks right now, needs some test in link_snap_test.go though ?

Contributor

pedronis commented Jun 30, 2016

I'm updating the title but description needs updating too

@pedronis pedronis changed the title from many: add `snap rollback` to many: add `snap revert` Jun 30, 2016

Contributor

pedronis commented Jun 30, 2016

apart description and missing test LGTM

overlord/snapstate/snapmgr.go
@@ -161,6 +162,43 @@ func (snapst *SnapState) CurrentSideInfo() *snap.SideInfo {
panic("cannot find snapst.Current in the snapst.Sequence")
}
+func (snapst *SnapState) PreviousSideInfo() *snap.SideInfo {
@pedronis

pedronis Jun 30, 2016

Contributor

should this be private? or have a comment?

should maybe start with a check for HasCurrent as CurrentSideInfo does

mvo5 added some commits Jun 30, 2016

overlord/snapstate/snapmgr.go
+ if !hadCandidate {
+ i := snapst.findIndex(snapst.Current)
+ if i < 0 {
+ return fmt.Errorf("internal error: cannot find revision %d in %v for undoing the added revision", snapst.Candidate.Revision, snapst.Sequence)
@niemeyer

niemeyer Jun 30, 2016

Contributor

This is unreachable. If i < 0, Sequence was indexed with a negative number just above, and so we had a panic.

The error checking is good, though. Let's just move this up and put the index on a var:

currentIndex := snapst.findIndex(snapst.Current)
if currentIndex < 0 { ... }
snapst.Candidate = ...
if !hadCandidate { ... }
Contributor

niemeyer commented Jun 30, 2016

Just that one trivial detail, and LGTM

@@ -247,3 +247,95 @@ func (s *linkSnapSuite) TestDoLinkSnapSuccessCoreRestarts(c *C) {
c.Check(t.Log()[0], Matches, `.*INFO snap "core" at revision 33 made available to the system.`)
c.Check(t.Log()[1], Matches, `.*INFO Restarting snapd\.\.\.`)
}
+
+func (s *linkSnapSuite) TestDoUndoLinkSnapRevertNotHadIndex(c *C) {
@pedronis

pedronis Jun 30, 2016

Contributor

...HadCandidate?

+ c.Check(t.Status(), Equals, state.UndoneStatus)
+}
+
+func (s *linkSnapSuite) TestDoUndoLinkSnapRevertHadIndex(c *C) {
@pedronis

pedronis Jun 30, 2016

Contributor

...HadCandidate?

+ var prepare, prev *state.Task
+ // if we have a revision here we know we need to go back to that
+ // revision
+ if ss.SnapPath != "" || !ss.Revision.Unset() {
prepare = s.NewTask("prepare-snap", fmt.Sprintf(i18n.G("Prepare snap %q"), ss.SnapPath))
@pedronis

pedronis Jun 30, 2016

Contributor

wondering if "Prepare snap" is a good message for the revert case

@niemeyer

niemeyer Jun 30, 2016

Contributor

I think it's fine.. preparing to put it live etc..

@niemeyer niemeyer merged commit 3f61d47 into snapcore:master Jun 30, 2016

2 of 3 checks passed

autopkgtest
Details
Integration tests Success
Details
continuous-integration/travis-ci/pr The Travis CI build passed
Details
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment