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

cmd/snap, tests: snapshots for all #5955

Merged
merged 8 commits into from Oct 26, 2018
4 changes: 4 additions & 0 deletions cmd/snap/cmd_help.go
Expand Up @@ -176,6 +176,10 @@ var helpCategories = []helpCategory{
Label: i18n.G("Permissions"),
Description: i18n.G("manage permissions"),
Commands: []string{"interfaces", "interface", "connect", "disconnect"},
}, {
Label: i18n.G("Snapshots"),
Description: i18n.G("archives of snap data"),
Commands: []string{"saved", "save", "check-snapshot", "restore", "forget"},
}, {
Label: i18n.G("Other"),
Description: i18n.G("miscellanea"),
Expand Down
335 changes: 335 additions & 0 deletions cmd/snap/cmd_snapshot.go
@@ -0,0 +1,335 @@
// -*- Mode: Go; indent-tabs-mode: t -*-

/*
* Copyright (C) 2018 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

package main

import (
"fmt"
"strings"

"github.com/jessevdk/go-flags"

"github.com/snapcore/snapd/i18n"
"github.com/snapcore/snapd/strutil"
"github.com/snapcore/snapd/strutil/quantity"
)

func fmtSize(size int64) string {
return quantity.FormatAmount(uint64(size), -1)
}

type savedCmd struct {
clientMixin
durationMixin
ID snapshotID `long:"id"`
Positional struct {
Snaps []installedSnapName `positional-arg-name:"<snap>"`
} `positional-args:"yes"`
}

var (
shortSavedHelp = i18n.G("List currently stored snapshots")
shortSaveHelp = i18n.G("Save a snapshot of the current data")
shortForgetHelp = i18n.G("Delete a snapshot")
shortCheckHelp = i18n.G("Check a snapshot")
shortRestoreHelp = i18n.G("Restore a snapshot")
)

var longSavedHelp = i18n.G(`
The saved command displays a list of snapshots that have been created
previously with the 'save' command.
`)
var longSaveHelp = i18n.G(`
The save command creates a snapshot of the current user, system and
configuration data for the given snaps.

By default, this command saves the data of all snaps for all users.
Alternatively, you can specify the data of which snaps to save, or
for which users, or a combination of these.

If a snap is included in a save operation, excluding its system and
configuration data from the snapshot is not currently possible. This
restriction may be lifted in the future.
`)
var longForgetHelp = i18n.G(`
The forget command deletes a snapshot. This operation can not be
undone.

A snapshot contains archives for the user, system and configuration
data of each snap included in the snapshot.

By default, this command forgets all the data in a snapshot.
Alternatively, you can specify the data of which snaps to forget.
`)
var longCheckHelp = i18n.G(`
The check-snapshot command verifies the user, system and configuration
data of the snaps included in the specified snapshot.

The check operation runs the same data integrity verification that is
performed when a snapshot is restored.

By default, this command checks all the data in a snapshot.
Alternatively, you can specify the data of which snaps to check, or
for which users, or a combination of these.

If a snap is included in a check-snapshot operation, excluding its
Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure it's necessary to say If a snap is included as, by implication of the subject, it already is. Maybe start with Excluding a snap's...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

my problem is that

Excluding a snap's system and configuration data from the snapshot is not currently possible.

can be read as that a snapshot will always include system and configuration data for all snaps, which isn't the case

Copy link
Contributor

Choose a reason for hiding this comment

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

It is not currently possible to exclude a specific snap's system and configuration data in a restore operation?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

doesn't that read like we're saying that if the snapshot is for foo, bar and baz snaps, you can't exclude system and config data from foo and not not exclude it from the others?

Copy link
Contributor

Choose a reason for hiding this comment

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

yeah, you're right. It's the use of included and excluded in the original text that confuses me, but I can't really think of anything better and it does work.

system and configuration data from the check is not currently
possible. This restriction may be lifted in the future.
`)
var longRestoreHelp = i18n.G(`
The restore command replaces the current user, system and
configuration data of included snaps, with the corresponding data from
the specified snapshot.

By default, this command restores all the data in a snapshot.
Alternatively, you can specify the data of which snaps to restore, or
for which users, or a combination of these.

If a snap is included in a restore operation, excluding its system and
Copy link
Contributor

Choose a reason for hiding this comment

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

As above...

configuration data from the restore is not currently possible. This
restriction may be lifted in the future.
`)

func (x *savedCmd) Execute([]string) error {
list, err := x.client.SnapshotSets(uint64(x.ID), installedSnapNames(x.Positional.Snaps))
if err != nil {
return err
}
if len(list) == 0 {
fmt.Fprintln(Stdout, i18n.G("No snapshots found."))
return nil
}
w := tabWriter()
defer w.Flush()

fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t%s\n",
// TRANSLATORS: 'Set' as in group or bag of things
chipaca marked this conversation as resolved.
Show resolved Hide resolved
i18n.G("Set"),
"Snap",
// TRANSLATORS: 'Age' as in how old something is
i18n.G("Age"),
chipaca marked this conversation as resolved.
Show resolved Hide resolved
i18n.G("Version"),
// TRANSLATORS: 'Rev' is an abbreviation of 'Revision'
i18n.G("Rev"),
i18n.G("Size"),
// TRANSLATORS: 'Notes' as in 'Comments'
i18n.G("Notes"))
for _, sg := range list {
for _, sh := range sg.Snapshots {
note := "-"
if sh.Broken != "" {
note = "broken: " + sh.Broken
}
size := quantity.FormatAmount(uint64(sh.Size), -1) + "B"
fmt.Fprintf(w, "%d\t%s\t%s\t%s\t%s\t%s\t%s\n", sg.ID, sh.Snap, x.fmtDuration(sh.Time), sh.Version, sh.Revision, size, note)
}
}
return nil
}

type saveCmd struct {
waitMixin
durationMixin
Users string `long:"users"`
Positional struct {
Snaps []installedSnapName `positional-arg-name:"<snap>"`
} `positional-args:"yes"`
}

func (x *saveCmd) Execute([]string) error {
var users []string
if len(x.Users) > 0 {
users = strings.Split(x.Users, ",")
}
setID, changeID, err := x.client.SnapshotMany(installedSnapNames(x.Positional.Snaps), users)
if err != nil {
return err
}
if _, err := x.wait(changeID); err != nil {
if err == noWait {
return nil
}
return err
}

y := &savedCmd{
clientMixin: x.clientMixin,
durationMixin: x.durationMixin,
ID: snapshotID(setID),
}
return y.Execute(nil)
}

type forgetCmd struct {
waitMixin
Positional struct {
ID snapshotID `positional-arg-name:"<id>"`
Snaps []installedSnapName `positional-arg-name:"<snap>"`
} `positional-args:"yes" required:"yes"`
}

func (x *forgetCmd) Execute([]string) error {
snaps := installedSnapNames(x.Positional.Snaps)
changeID, err := x.client.ForgetSnapshots(uint64(x.Positional.ID), snaps)
if err != nil {
return err
}
_, err = x.wait(changeID)
if err == noWait {
return nil
}
if err != nil {
return err
}

if len(snaps) > 0 {
// TRANSLATORS: the %s is a comma-separated list of quoted snap names
fmt.Fprintf(Stdout, i18n.NG("Snapshot #%d of snap %s forgotten.\n", "Snapshot #%d of snaps %s forgotten.\n", len(snaps)), x.Positional.ID, strutil.Quoted(snaps))
} else {
fmt.Fprintf(Stdout, i18n.G("Snapshot #%d forgotten.\n"), x.Positional.ID)
}
return nil
}

type checkSnapshotCmd struct {
waitMixin
Users string `long:"users"`
chipaca marked this conversation as resolved.
Show resolved Hide resolved
Positional struct {
ID snapshotID `positional-arg-name:"<id>"`
Snaps []installedSnapName `positional-arg-name:"<snap>"`
} `positional-args:"yes" required:"yes"`
}

func (x *checkSnapshotCmd) Execute([]string) error {
snaps := installedSnapNames(x.Positional.Snaps)
var users []string
if len(x.Users) > 0 {
users = strings.Split(x.Users, ",")
}
changeID, err := x.client.CheckSnapshots(uint64(x.Positional.ID), snaps, users)
if err != nil {
return err
}
_, err = x.wait(changeID)
if err == noWait {
return nil
}
if err != nil {
return err
}

// TODO: also mention the home archives that were actually checked
if len(snaps) > 0 {
// TRANSLATORS: the %s is a comma-separated list of quoted snap names
fmt.Fprintf(Stdout, i18n.G("Snapshot #%d of snaps %s verified successfully.\n"),
x.Positional.ID, strutil.Quoted(snaps))
} else {
fmt.Fprintf(Stdout, i18n.G("Snapshot #%d verified successfully.\n"), x.Positional.ID)
}
return nil
}

type restoreCmd struct {
waitMixin
Users string `long:"users"`
Positional struct {
ID snapshotID `positional-arg-name:"<id>"`
Snaps []installedSnapName `positional-arg-name:"<snap>"`
} `positional-args:"yes" required:"yes"`
}

func (x *restoreCmd) Execute([]string) error {
snaps := installedSnapNames(x.Positional.Snaps)
var users []string
Copy link
Collaborator

Choose a reason for hiding this comment

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

Perhaps a small helper for handling the users field across many commands:

func splitUsers(users string) []string {
   if len(users) > 0 {
     return strings.Split(users, ",")
   }
   return 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.

I'll move daemon.splitQS to strutil

Copy link
Contributor Author

Choose a reason for hiding this comment

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

(... in a followup)

if len(x.Users) > 0 {
users = strings.Split(x.Users, ",")
}
changeID, err := x.client.RestoreSnapshots(uint64(x.Positional.ID), snaps, users)
if err != nil {
return err
}
_, err = x.wait(changeID)
if err == noWait {
return nil
}
if err != nil {
return err
}

// TODO: also mention the home archives that were actually restored
if len(snaps) > 0 {
// TRANSLATORS: the %s is a comma-separated list of quoted snap names
fmt.Fprintf(Stdout, i18n.G("Restored snapshot #%d of snaps %s.\n"),
x.Positional.ID, strutil.Quoted(snaps))
} else {
fmt.Fprintf(Stdout, i18n.G("Restored snapshot #%d.\n"), x.Positional.ID)
}
return nil
}

func init() {
addCommand("saved",
bboozzoo marked this conversation as resolved.
Show resolved Hide resolved
shortSavedHelp,
longSavedHelp,
func() flags.Commander {
return &savedCmd{}
},
durationDescs.also(map[string]string{
// TRANSLATORS: This should not start with a lowercase letter.
"id": i18n.G("Show only a specific snapshot."),
}),
nil)

addCommand("save",
shortSaveHelp,
longSaveHelp,
func() flags.Commander {
return &saveCmd{}
}, durationDescs.also(waitDescs).also(map[string]string{
// TRANSLATORS: This should not start with a lowercase letter.
"users": i18n.G("Snapshot data of only specific users (comma-separated) (default: all users)"),
}), nil)

addCommand("restore",
shortRestoreHelp,
longRestoreHelp,
func() flags.Commander {
return &restoreCmd{}
}, waitDescs.also(map[string]string{
// TRANSLATORS: This should not start with a lowercase letter.
"users": i18n.G("Restore data of only specific users (comma-separated) (default: all users)"),
}), nil)

addCommand("forget",
shortForgetHelp,
longForgetHelp,
func() flags.Commander {
return &forgetCmd{}
}, waitDescs, nil)

addCommand("check-snapshot",
shortCheckHelp,
longCheckHelp,
func() flags.Commander {
return &checkSnapshotCmd{}
}, waitDescs.also(map[string]string{
// TRANSLATORS: This should not start with a lowercase letter.
"users": i18n.G("Check data of only specific users (comma-separated) (default: all users)"),
}), nil)
}
19 changes: 19 additions & 0 deletions cmd/snap/complete.go
Expand Up @@ -23,6 +23,7 @@ import (
"bufio"
"fmt"
"os"
"strconv"
"strings"

"github.com/jessevdk/go-flags"
Expand Down Expand Up @@ -473,3 +474,21 @@ func (s aliasOrSnap) Complete(match string) []flags.Completion {
}
return ret
}

type snapshotID uint64

func (snapshotID) Complete(match string) []flags.Completion {
shots, err := mkClient().SnapshotSets(0, nil)
if err != nil {
return nil
}
var ret []flags.Completion
for _, sg := range shots {
sid := strconv.FormatUint(sg.ID, 10)
if strings.HasPrefix(sid, match) {
ret = append(ret, flags.Completion{Item: sid})
}
}

return ret
}