-
Notifications
You must be signed in to change notification settings - Fork 582
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
Conversation
ad07cbf
to
59603ad
Compare
These are the low-level snapshot operations, that will get used by snapshotstate to implement snapshot operations.
59603ad
to
81ae315
Compare
Codecov Report
@@ 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
Continue to review full report at Codecov.
|
There was a problem hiding this 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") |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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" |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
|
||
var ( | ||
dirOpen = os.Open | ||
dirNames = (*os.File).Readdirnames |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Error not checked
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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 { ... }
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 remove
d, 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.
7014843
to
0832d82
Compare
return backend.Filename(skel) | ||
} | ||
|
||
func prepareSave(task *state.Task) (*snapshotState, *snap.Info, map[string]interface{}, error) { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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.
0832d82
to
d400538
Compare
There was a problem hiding this 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)) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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)) |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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
There was a problem hiding this 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") |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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") |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"not be a trash" :)
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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"
There was a problem hiding this comment.
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."
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…)
These are the low-level snapshot operations, that will get used by
snapshotstate to implement snapshot operations.