Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

overlord/snapshotstate/backend: introducing the snapshot backend #5066

Merged
merged 5 commits into from
May 22, 2018

Conversation

chipaca
Copy link
Contributor

@chipaca chipaca commented Apr 18, 2018

These are the low-level snapshot operations, that will get used by
snapshotstate to implement snapshot operations.

These are the low-level snapshot operations, that will get used by
snapshotstate to implement snapshot operations.
@codecov-io
Copy link

codecov-io commented Apr 18, 2018

Codecov Report

Merging #5066 into master will decrease coverage by 0.32%.
The diff coverage is 50.49%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #5066      +/-   ##
==========================================
- Coverage   79.06%   78.74%   -0.33%     
==========================================
  Files         488      493       +5     
  Lines       36448    36854     +406     
==========================================
+ Hits        28818    29021     +203     
- Misses       5346     5488     +142     
- Partials     2284     2345      +61
Impacted Files Coverage Δ
overlord/snapshotstate/backend/sizer.go 100% <100%> (ø)
dirs/dirs.go 97.72% <100%> (+0.02%) ⬆️
overlord/snapshotstate/backend/restorestate.go 16.12% <16.12%> (ø)
overlord/snapshotstate/backend/helpers.go 38.35% <38.35%> (ø)
overlord/snapshotstate/backend/reader.go 43.18% <43.18%> (ø)
overlord/snapshotstate/backend/backend.go 74.78% <74.78%> (ø)
overlord/hookstate/hookmgr.go 74.87% <0%> (-1.03%) ⬇️
... and 2 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 0e68d91...e43d1af. Read the comment docs.

Copy link
Contributor

@bboozzoo bboozzoo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First pass

dirs/dirs.go Outdated
@@ -259,4 +260,5 @@ func SetRootDir(rootdir string) {
SystemFontconfigCacheDir = filepath.Join(rootdir, "/var/cache/fontconfig")

FreezerCgroupDir = filepath.Join(rootdir, "/sys/fs/cgroup/freezer/")
SnapshotDir = filepath.Join(rootdir, "/var/spool/snapd/snapshots")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/SnapshotDir/SnapshotsDir/ sounds more fitting

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed

"github.com/snapcore/snapd/osutil"
"github.com/snapcore/snapd/snap"
"github.com/snapcore/snapd/strutil"
"regexp"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you group regex with other stdlib imports?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NO.
NO

.
.
.

(yes)


var (
dirOpen = os.Open
dirNames = (*os.File).Readdirnames
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will make the call site slightly non obvious

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I was aware of the tension between that and readability.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do have a convention where we try to write those names close to their underlying packages and method/function names, precisely so that it's more readable.

The second one would be over the top with fileReaddirnames indeed and still unclear given that it's a method, so dirNames sounds good there, but the first one reads properly with osOpen.

Alternatively, given that osOpen is itself already mocked, it can return a different type, possibly an interface that has all the necessary methods.

Another alternative is to provide a different directory location with the desired data instead of mocking all methods. That might be better in some cases because it ends up testing the real code, including the backing logic behavior (readdirnames itself, for instance), instead of faking its behavior.

var (
dirOpen = os.Open
dirNames = (*os.File).Readdirnames
open = Open
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit on on the fence with whether these should have their full type written down here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, having both Open and open on the same package is indeed somewhat unclear. Perhaps backendOpen? This at least would make it slightly less tempting to use one or the other interchangeably without understanding why.

return nil, -1, err
}

fi, err := f.Stat()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Error not checked

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good catch, ouch

return nil, -1, err
}

for _, f := range arch.File {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This f shadows the other f, made be go back and forth a bit.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

return nil, err
}
defer func() {
if e != nil {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not an issue but a bit of paranoia, I'd try if e != nill && f != nil { ... }

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a'ight

// for the system path, and we won't be creating the
// user's home (as we skip restore in that case).
// Also no chown happens for root/root.
if err := osutil.MkdirAllChown(parent, 0755, uid, gid); err != nil {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will try to create $HOME/snap/<name>, shouldn't we check whether $HOME exists first?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as the comment says, we explicitly check for and skip the restore in the case the home does not exist.
(see the os.Stat(usr.HomeDir) above)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missed that, thanks!

OTOH, should we check if SnapDataDir exists when restoring non-user data?

Copy link
Contributor Author

@chipaca chipaca Apr 19, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we check if the parent of si.DataDir() (that is, <SnapDataDir>/<snap>) exists and create it (as root:root) if it doesn't. I think this is reasonable; SnapDataDir not existing might indicate a brand new snapd (with the snapshot being 'imported' from elsewhere), which is probably fine.

Of course, <SnapDataDir>/<snap> might not exist because <snap> was removed, but you just asked to restore its data so that's probably ok, at least in backend when dealing with snapshots and not snapshot sets -- whether the frontend should exclude non-installed snaps from being restored unless explicitly requested is fair (and open) question.

@chipaca chipaca force-pushed the snapshot-backend branch 2 times, most recently from 7014843 to 0832d82 Compare April 18, 2018 23:56
return backend.Filename(skel)
}

func prepareSave(task *state.Task) (*snapshotState, *snap.Info, map[string]interface{}, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider using named return values, the same goes for prepareRestore

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this bit was pushed to this pr in error. I'm fixing it with a forced push. Please don't hate me!

return err
}
_, err = backend.Save(tomb.Context(nil), snapshot.SetID, cur, cfg, snapshot.Users)
return err
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW, looks like we do not need any extra cleanup here. The only 'artifact' created by backend.Save is a zip file which will go away on errors.

return err
}

trash, err := reader.RestoreLeavingTrash(tomb.Context(nil), snapshot.Users, task.Logf)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAICT this call also does not leave anything behind if errors occur inside.

Copy link
Contributor

@bboozzoo bboozzoo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually it'd be nice to review this with the corresponding changes in handlers to see the big picture.

// Filename of the given client.Snapshot in this backend.
func Filename(sh *client.Snapshot) string {
// this _needs_ the snap name and version to be valid
return filepath.Join(dirs.SnapshotsDir, fmt.Sprintf("%d_%s_%s.zip", sh.SetID, sh.Snap, sh.Version))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should revision be encoded in filename too?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it sounds reasonable given it's the only thing that defines uniqueness, and it wouldn't hurt since it's just a few chars. We do have the revision inside, but the same would be true for the other fields.

return nil, err
}
users[0] = root
seen := make(map[uint32]struct{}, len(ds)+1)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could be really fancy and use osutil/sys.UserID here

continue
}
seen[st.Uid] = yes
usr, err := user.LookupId(strconv.FormatUint(uint64(st.Uid), 10))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and osutil/sys.UserID.String() string (after adding it)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll leave these for a separate PR

Copy link
Contributor

@niemeyer niemeyer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be the final review. Let me know if you want to discuss any of those please.

dirs/dirs.go Outdated
@@ -259,4 +260,5 @@ func SetRootDir(rootdir string) {
SystemFontconfigCacheDir = filepath.Join(rootdir, "/var/cache/fontconfig")

FreezerCgroupDir = filepath.Join(rootdir, "/sys/fs/cgroup/freezer/")
SnapshotsDir = filepath.Join(rootdir, "/var/spool/snapd/snapshots")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not clear that this is the proper place for this. Spool is generally associated with things which are there just for a short while before going away into another system, such as email, print jobs, and similar.

From the Filesystem Hierarchy Standard:

/var/spool contains data which is awaiting some kind of later processing. Data in /var/spool represents work to be done in the future (by a program, user, or administrator); often data is deleted after it has been processed. [46]

It doesn't seem quite right for snapshots.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/var/lib/snapd/snapshots would be fine


var (
dirOpen = os.Open
dirNames = (*os.File).Readdirnames
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do have a convention where we try to write those names close to their underlying packages and method/function names, precisely so that it's more readable.

The second one would be over the top with fileReaddirnames indeed and still unclear given that it's a method, so dirNames sounds good there, but the first one reads properly with osOpen.

Alternatively, given that osOpen is itself already mocked, it can return a different type, possibly an interface that has all the necessary methods.

Another alternative is to provide a different directory location with the desired data instead of mocking all methods. That might be better in some cases because it ends up testing the real code, including the backing logic behavior (readdirnames itself, for instance), instead of faking its behavior.

var (
dirOpen = os.Open
dirNames = (*os.File).Readdirnames
open = Open
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, having both Open and open on the same package is indeed somewhat unclear. Perhaps backendOpen? This at least would make it slightly less tempting to use one or the other interchangeably without understanding why.

if openError == nil {
// if openError was nil the snapshot was opened and needs closing
if closeError := rsh.Close(); err == nil {
err = closeError
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's probably okay to leave it like this, since we don't really expect the close error to ever be an issue since we're not writing, but FWIW there's something asymmetric here about open errors and close errors. An open error is ignored and passed onto f via broken, but a close error is transformed into a big deal and stops the iteration. The Close error seems like the least interesting one when we're not really touching the file here. But again, this will probably never fail, so perhaps okay.

}
}

if err == io.EOF {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably be checked inside the loop, right after dirNames. It would be strange to observe this error elsewhere still.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The wording on Readdirnames makes me think that it might at some point return a non-empty list and a non-nil error: it's documented that if names is empty, then the error is non-nil. It does not say that a non-nil error implies names is empty.

The implementation confirms this suspicion.

I'll add a comment explaining this.

// check that you're not trying to use trash info twice; data loss imminent.
func (b *Trash) check() {
if b.Done {
panic("attempting to use a snapshot.Trash twice")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't feel worth crashing the daemon for that. Logging an internal error and preventing the actual data loss seems reasonable.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've removed the method, and replaced it at the call site with appropriate logging

b.check()
for _, dir := range b.Moved {
if err := os.RemoveAll(dir); err != nil {
logger.Noticef("cannot remove directory tree rooted at %q: %v", dir, err)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/c/C/, for the logs

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done for all of backend. Hope to propose a pr that does this everywhere sometime.

Config *json.RawMessage `json:"config,omitempty"`
}

// check that you're not trying to use trash info twice; data loss imminent.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation is phrased unconventionally and also a bit confusing in the sense that it's okay to use the information twice, otherwise we'd not have json fields above. There's something else that cannot be done twice, but it's not mentioned here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dropping the function made this point moot \o/

for _, dir := range b.Created {
logger.Debugf("removing %q", dir)
if err := os.RemoveAll(dir); err != nil {
logger.Noticef("while undoing changes because of a previous error: cannot remove %q: %v", dir, err)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Log messages start with a capital letter. Same above and below.

orig := trash2orig(dir)
if orig == "" {
// dir is not a trash?!?
logger.Debugf("skipping restore of %q which seems to not be a trash", dir)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"not be a trash" :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I said, I'd originally called it a backup, but you told me to change it without telling me what to change it to (I agree backup might be ambiguous in this context). Now you're saying I chose the wrong thing, but still not suggesting what is the right thing (except for the struct, for which you're suggesting RestoreState), . Suggestions still welcome.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

during the call we agreed that, in the end, restore state is the right name for this.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, here it goes:

"skipping restore out of %s: unrecognized name"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Skipping restore of %q: unrecognised filename."

@chipaca chipaca added the Simple 😃 A small PR which can be reviewed quickly label Apr 24, 2018
@zyga zyga added Complex and removed Simple 😃 A small PR which can be reviewed quickly labels Apr 24, 2018
Mostly small changes and improvements to the API:
* the directory is now /var/lib/snapd/snapshots
* `backend.Trash` is now `backend.RestoreState`.
* `backend.Iter` is now told to stop early with `backend.Stop` instead
  of `io.EOF`; the passed-in function returning `io.EOF` is an error.
* Snapshot filenames now include the revision
* When creating or restoring a user's data, tar is now run as the user.
* Errors about hashes now include a small hash prefix (abc1234…)
@niemeyer niemeyer merged commit 48cbfb9 into canonical:master May 22, 2018
@chipaca chipaca deleted the snapshot-backend branch June 18, 2018 10:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants