github.com/kubiko/snapd@v0.0.0-20201013125620-d4f3094d9ddf/cmd/snap/cmd_snapshot.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2018 Canonical Ltd
     5   *
     6   * This program is free software: you can redistribute it and/or modify
     7   * it under the terms of the GNU General Public License version 3 as
     8   * published by the Free Software Foundation.
     9   *
    10   * This program is distributed in the hope that it will be useful,
    11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13   * GNU General Public License for more details.
    14   *
    15   * You should have received a copy of the GNU General Public License
    16   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17   *
    18   */
    19  
    20  package main
    21  
    22  import (
    23  	"fmt"
    24  	"io"
    25  	"os"
    26  	"strconv"
    27  	"strings"
    28  
    29  	"github.com/jessevdk/go-flags"
    30  
    31  	"github.com/snapcore/snapd/i18n"
    32  	"github.com/snapcore/snapd/strutil"
    33  	"github.com/snapcore/snapd/strutil/quantity"
    34  )
    35  
    36  func fmtSize(size int64) string {
    37  	return quantity.FormatAmount(uint64(size), -1) + "B"
    38  }
    39  
    40  var (
    41  	shortSavedHelp          = i18n.G("List currently stored snapshots")
    42  	shortSaveHelp           = i18n.G("Save a snapshot of the current data")
    43  	shortForgetHelp         = i18n.G("Delete a snapshot")
    44  	shortCheckHelp          = i18n.G("Check a snapshot")
    45  	shortRestoreHelp        = i18n.G("Restore a snapshot")
    46  	shortExportSnapshotHelp = i18n.G("Export a snapshot")
    47  	shortImportSnapshotHelp = i18n.G("Import a snapshot")
    48  )
    49  
    50  var longSavedHelp = i18n.G(`
    51  The saved command displays a list of snapshots that have been created
    52  previously with the 'save' command.
    53  `)
    54  var longSaveHelp = i18n.G(`
    55  The save command creates a snapshot of the current user, system and
    56  configuration data for the given snaps.
    57  
    58  By default, this command saves the data of all snaps for all users.
    59  Alternatively, you can specify the data of which snaps to save, or
    60  for which users, or a combination of these.
    61  
    62  If a snap is included in a save operation, excluding its system and
    63  configuration data from the snapshot is not currently possible. This
    64  restriction may be lifted in the future.
    65  `)
    66  var longForgetHelp = i18n.G(`
    67  The forget command deletes a snapshot. This operation can not be
    68  undone.
    69  
    70  A snapshot contains archives for the user, system and configuration
    71  data of each snap included in the snapshot.
    72  
    73  By default, this command forgets all the data in a snapshot.
    74  Alternatively, you can specify the data of which snaps to forget.
    75  `)
    76  var longCheckHelp = i18n.G(`
    77  The check-snapshot command verifies the user, system and configuration
    78  data of the snaps included in the specified snapshot.
    79  
    80  The check operation runs the same data integrity verification that is
    81  performed when a snapshot is restored.
    82  
    83  By default, this command checks all the data in a snapshot.
    84  Alternatively, you can specify the data of which snaps to check, or
    85  for which users, or a combination of these.
    86  
    87  If a snap is included in a check-snapshot operation, excluding its
    88  system and configuration data from the check is not currently
    89  possible. This restriction may be lifted in the future.
    90  `)
    91  var longRestoreHelp = i18n.G(`
    92  The restore command replaces the current user, system and
    93  configuration data of included snaps, with the corresponding data from
    94  the specified snapshot.
    95  
    96  By default, this command restores all the data in a snapshot.
    97  Alternatively, you can specify the data of which snaps to restore, or
    98  for which users, or a combination of these.
    99  
   100  If a snap is included in a restore operation, excluding its system and
   101  configuration data from the restore is not currently possible. This
   102  restriction may be lifted in the future.
   103  `)
   104  
   105  var longExportSnapshotHelp = i18n.G(`
   106  Export a snapshot to the given filename.
   107  `)
   108  
   109  var longImportSnapshotHelp = i18n.G(`
   110  Import an exported snapshot set to the system. The snapshot is imported
   111  with a new snapshot ID and can be restored using the restore command.
   112  `)
   113  
   114  type savedCmd struct {
   115  	clientMixin
   116  	durationMixin
   117  	ID         snapshotID `long:"id"`
   118  	Positional struct {
   119  		Snaps []installedSnapName `positional-arg-name:"<snap>"`
   120  	} `positional-args:"yes"`
   121  }
   122  
   123  func (x *savedCmd) Execute([]string) error {
   124  	var setID uint64
   125  	var err error
   126  	if x.ID != "" {
   127  		setID, err = x.ID.ToUint()
   128  		if err != nil {
   129  			return err
   130  		}
   131  	}
   132  	snaps := installedSnapNames(x.Positional.Snaps)
   133  	list, err := x.client.SnapshotSets(setID, snaps)
   134  	if err != nil {
   135  		return err
   136  	}
   137  	if len(list) == 0 {
   138  		fmt.Fprintln(Stdout, i18n.G("No snapshots found."))
   139  		return nil
   140  	}
   141  
   142  	w := tabWriter()
   143  	defer w.Flush()
   144  
   145  	fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t%s\n",
   146  		// TRANSLATORS: 'Set' as in group or bag of things
   147  		i18n.G("Set"),
   148  		"Snap",
   149  		// TRANSLATORS: 'Age' as in how old something is
   150  		i18n.G("Age"),
   151  		i18n.G("Version"),
   152  		// TRANSLATORS: 'Rev' is an abbreviation of 'Revision'
   153  		i18n.G("Rev"),
   154  		i18n.G("Size"),
   155  		// TRANSLATORS: 'Notes' as in 'Comments'
   156  		i18n.G("Notes"))
   157  	for _, sg := range list {
   158  		for _, sh := range sg.Snapshots {
   159  			notes := []string{}
   160  			if sh.Auto {
   161  				notes = append(notes, "auto")
   162  			}
   163  			if sh.Broken != "" {
   164  				notes = append(notes, "broken: "+sh.Broken)
   165  			}
   166  			note := "-"
   167  			if len(notes) > 0 {
   168  				note = strings.Join(notes, ", ")
   169  			}
   170  			size := fmtSize(sh.Size)
   171  			age := x.fmtDuration(sh.Time)
   172  			fmt.Fprintf(w, "%d\t%s\t%s\t%s\t%s\t%s\t%s\n", sg.ID, sh.Snap, age, sh.Version, sh.Revision, size, note)
   173  		}
   174  	}
   175  	return nil
   176  }
   177  
   178  type saveCmd struct {
   179  	waitMixin
   180  	durationMixin
   181  	Users      string `long:"users"`
   182  	Positional struct {
   183  		Snaps []installedSnapName `positional-arg-name:"<snap>"`
   184  	} `positional-args:"yes"`
   185  }
   186  
   187  func (x *saveCmd) Execute([]string) error {
   188  	snaps := installedSnapNames(x.Positional.Snaps)
   189  	users := strutil.CommaSeparatedList(x.Users)
   190  	setID, changeID, err := x.client.SnapshotMany(snaps, users)
   191  	if err != nil {
   192  		return err
   193  	}
   194  	if _, err := x.wait(changeID); err != nil {
   195  		if err == noWait {
   196  			return nil
   197  		}
   198  		return err
   199  	}
   200  
   201  	y := &savedCmd{
   202  		clientMixin:   x.clientMixin,
   203  		durationMixin: x.durationMixin,
   204  		ID:            snapshotID(strconv.FormatUint(setID, 10)),
   205  	}
   206  	return y.Execute(nil)
   207  }
   208  
   209  type forgetCmd struct {
   210  	waitMixin
   211  	Positional struct {
   212  		ID    snapshotID          `positional-arg-name:"<id>"`
   213  		Snaps []installedSnapName `positional-arg-name:"<snap>"`
   214  	} `positional-args:"yes" required:"yes"`
   215  }
   216  
   217  func (x *forgetCmd) Execute([]string) error {
   218  	setID, err := x.Positional.ID.ToUint()
   219  	if err != nil {
   220  		return err
   221  	}
   222  	snaps := installedSnapNames(x.Positional.Snaps)
   223  	changeID, err := x.client.ForgetSnapshots(setID, snaps)
   224  	if err != nil {
   225  		return err
   226  	}
   227  	_, err = x.wait(changeID)
   228  	if err == noWait {
   229  		return nil
   230  	}
   231  	if err != nil {
   232  		return err
   233  	}
   234  
   235  	if len(snaps) > 0 {
   236  		// TRANSLATORS: the %s is a comma-separated list of quoted snap names
   237  		fmt.Fprintf(Stdout, i18n.NG("Snapshot #%s of snap %s forgotten.\n", "Snapshot #%s of snaps %s forgotten.\n", len(snaps)), x.Positional.ID, strutil.Quoted(snaps))
   238  	} else {
   239  		fmt.Fprintf(Stdout, i18n.G("Snapshot #%s forgotten.\n"), x.Positional.ID)
   240  	}
   241  	return nil
   242  }
   243  
   244  type checkSnapshotCmd struct {
   245  	waitMixin
   246  	Users      string `long:"users"`
   247  	Positional struct {
   248  		ID    snapshotID          `positional-arg-name:"<id>"`
   249  		Snaps []installedSnapName `positional-arg-name:"<snap>"`
   250  	} `positional-args:"yes" required:"yes"`
   251  }
   252  
   253  func (x *checkSnapshotCmd) Execute([]string) error {
   254  	setID, err := x.Positional.ID.ToUint()
   255  	if err != nil {
   256  		return err
   257  	}
   258  	snaps := installedSnapNames(x.Positional.Snaps)
   259  	users := strutil.CommaSeparatedList(x.Users)
   260  	changeID, err := x.client.CheckSnapshots(setID, snaps, users)
   261  	if err != nil {
   262  		return err
   263  	}
   264  	_, err = x.wait(changeID)
   265  	if err == noWait {
   266  		return nil
   267  	}
   268  	if err != nil {
   269  		return err
   270  	}
   271  
   272  	// TODO: also mention the home archives that were actually checked
   273  	if len(snaps) > 0 {
   274  		// TRANSLATORS: the %s is a comma-separated list of quoted snap names
   275  		fmt.Fprintf(Stdout, i18n.G("Snapshot #%s of snaps %s verified successfully.\n"),
   276  			x.Positional.ID, strutil.Quoted(snaps))
   277  	} else {
   278  		fmt.Fprintf(Stdout, i18n.G("Snapshot #%s verified successfully.\n"), x.Positional.ID)
   279  	}
   280  	return nil
   281  }
   282  
   283  type restoreCmd struct {
   284  	waitMixin
   285  	Users      string `long:"users"`
   286  	Positional struct {
   287  		ID    snapshotID          `positional-arg-name:"<id>"`
   288  		Snaps []installedSnapName `positional-arg-name:"<snap>"`
   289  	} `positional-args:"yes" required:"yes"`
   290  }
   291  
   292  func (x *restoreCmd) Execute([]string) error {
   293  	setID, err := x.Positional.ID.ToUint()
   294  	if err != nil {
   295  		return err
   296  	}
   297  	snaps := installedSnapNames(x.Positional.Snaps)
   298  	users := strutil.CommaSeparatedList(x.Users)
   299  	changeID, err := x.client.RestoreSnapshots(setID, snaps, users)
   300  	if err != nil {
   301  		return err
   302  	}
   303  	_, err = x.wait(changeID)
   304  	if err == noWait {
   305  		return nil
   306  	}
   307  	if err != nil {
   308  		return err
   309  	}
   310  
   311  	// TODO: also mention the home archives that were actually restored
   312  	if len(snaps) > 0 {
   313  		// TRANSLATORS: the %s is a comma-separated list of quoted snap names
   314  		fmt.Fprintf(Stdout, i18n.G("Restored snapshot #%s of snaps %s.\n"),
   315  			x.Positional.ID, strutil.Quoted(snaps))
   316  	} else {
   317  		fmt.Fprintf(Stdout, i18n.G("Restored snapshot #%s.\n"), x.Positional.ID)
   318  	}
   319  	return nil
   320  }
   321  
   322  func init() {
   323  	addCommand("saved",
   324  		shortSavedHelp,
   325  		longSavedHelp,
   326  		func() flags.Commander {
   327  			return &savedCmd{}
   328  		},
   329  		durationDescs.also(map[string]string{
   330  			// TRANSLATORS: This should not start with a lowercase letter.
   331  			"id": i18n.G("Show only a specific snapshot."),
   332  		}),
   333  		nil)
   334  
   335  	addCommand("save",
   336  		shortSaveHelp,
   337  		longSaveHelp,
   338  		func() flags.Commander {
   339  			return &saveCmd{}
   340  		}, durationDescs.also(waitDescs).also(map[string]string{
   341  			// TRANSLATORS: This should not start with a lowercase letter.
   342  			"users": i18n.G("Snapshot data of only specific users (comma-separated) (default: all users)"),
   343  		}), nil)
   344  
   345  	addCommand("restore",
   346  		shortRestoreHelp,
   347  		longRestoreHelp,
   348  		func() flags.Commander {
   349  			return &restoreCmd{}
   350  		}, waitDescs.also(map[string]string{
   351  			// TRANSLATORS: This should not start with a lowercase letter.
   352  			"users": i18n.G("Restore data of only specific users (comma-separated) (default: all users)"),
   353  		}), []argDesc{
   354  			{
   355  				name: "<id>",
   356  				// TRANSLATORS: This should not start with a lowercase letter.
   357  				desc: i18n.G("Set id of snapshot to restore (see 'snap help saved')"),
   358  			}, {
   359  				name: "<snap>",
   360  				// TRANSLATORS: This should not start with a lowercase letter.
   361  				desc: i18n.G("The snap for which data will be restored"),
   362  			},
   363  		})
   364  
   365  	addCommand("forget",
   366  		shortForgetHelp,
   367  		longForgetHelp,
   368  		func() flags.Commander {
   369  			return &forgetCmd{}
   370  		}, waitDescs, []argDesc{
   371  			{
   372  				name: "<id>",
   373  				// TRANSLATORS: This should not start with a lowercase letter.
   374  				desc: i18n.G("Set id of snapshot to delete (see 'snap help saved')"),
   375  			}, {
   376  				name: "<snap>",
   377  				// TRANSLATORS: This should not start with a lowercase letter.
   378  				desc: i18n.G("The snap for which data will be deleted"),
   379  			},
   380  		})
   381  
   382  	addCommand("check-snapshot",
   383  		shortCheckHelp,
   384  		longCheckHelp,
   385  		func() flags.Commander {
   386  			return &checkSnapshotCmd{}
   387  		}, waitDescs.also(map[string]string{
   388  			// TRANSLATORS: This should not start with a lowercase letter.
   389  			"users": i18n.G("Check data of only specific users (comma-separated) (default: all users)"),
   390  		}), []argDesc{
   391  			{
   392  				name: "<id>",
   393  				// TRANSLATORS: This should not start with a lowercase letter.
   394  				desc: i18n.G("Set id of snapshot to verify (see 'snap help saved')"),
   395  			}, {
   396  				name: "<snap>",
   397  				// TRANSLATORS: This should not start with a lowercase letter.
   398  				desc: i18n.G("The snap for which data will be verified"),
   399  			},
   400  		})
   401  
   402  	cmd := addCommand("export-snapshot",
   403  		shortExportSnapshotHelp,
   404  		longExportSnapshotHelp,
   405  		func() flags.Commander {
   406  			return &exportSnapshotCmd{}
   407  		}, nil, []argDesc{
   408  			{
   409  				name: "<id>",
   410  				// TRANSLATORS: This should not start with a lowercase letter.
   411  				desc: i18n.G("Set id of snapshot to export"),
   412  			},
   413  			{
   414  				// TRANSLATORS: This should retain < ... >. The file name is the name of an exported snapshot.
   415  				name: i18n.G("<filename>"),
   416  				// TRANSLATORS: This should not start with a lowercase letter.
   417  				desc: i18n.G("The filename of the export"),
   418  			},
   419  		})
   420  	// XXX: this command is hidden because import/export is not complete
   421  	cmd.hidden = true
   422  
   423  	cmd = addCommand("import-snapshot",
   424  		shortImportSnapshotHelp,
   425  		longImportSnapshotHelp,
   426  		func() flags.Commander {
   427  			return &importSnapshotCmd{}
   428  		}, nil, []argDesc{
   429  			{
   430  				name: "<filename>",
   431  				// TRANSLATORS: This should not start with a lowercase letter.
   432  				desc: i18n.G("Name of the snapshot export file to use"),
   433  			},
   434  		})
   435  	// XXX: this command is hidden because import/export is not complete
   436  	cmd.hidden = true
   437  }
   438  
   439  type exportSnapshotCmd struct {
   440  	clientMixin
   441  	Positional struct {
   442  		ID       snapshotID `positional-arg-name:"<id>"`
   443  		Filename string     `long:"filename"`
   444  	} `positional-args:"yes" required:"yes"`
   445  }
   446  
   447  func (x *exportSnapshotCmd) Execute([]string) (err error) {
   448  	setID, err := x.Positional.ID.ToUint()
   449  	if err != nil {
   450  		return err
   451  	}
   452  
   453  	r, expectedSize, err := x.client.SnapshotExport(setID)
   454  	if err != nil {
   455  		return err
   456  	}
   457  
   458  	filename := x.Positional.Filename
   459  	f, err := os.Create(filename + ".part")
   460  	if err != nil {
   461  		return err
   462  	}
   463  	defer f.Close()
   464  	defer func() {
   465  		if err != nil {
   466  			os.Remove(filename + ".part")
   467  		}
   468  	}()
   469  
   470  	// Pre-allocate the disk space for the snapshot, if the file system supports this.
   471  	if err := maybeReserveDiskSpace(f, expectedSize); err != nil {
   472  		return fmt.Errorf(i18n.G("cannot reserve disk space for snapshot: %v"), err)
   473  	}
   474  
   475  	n, err := io.Copy(f, r)
   476  	if err != nil {
   477  		return err
   478  	}
   479  	if n != expectedSize {
   480  		return fmt.Errorf(i18n.G("unexpected size, got: %v but wanted %v"), n, expectedSize)
   481  	}
   482  
   483  	if err := os.Rename(filename+".part", filename); err != nil {
   484  		return err
   485  	}
   486  
   487  	// TRANSLATORS: the first argument is the identifier of the snapshot, the second one is the file name.
   488  	fmt.Fprintf(Stdout, i18n.G("Exported snapshot #%s into %q\n"), x.Positional.ID, x.Positional.Filename)
   489  
   490  	return nil
   491  }
   492  
   493  type importSnapshotCmd struct {
   494  	clientMixin
   495  	durationMixin
   496  	Positional struct {
   497  		Filename string `long:"filename"`
   498  	} `positional-args:"yes" required:"yes"`
   499  }
   500  
   501  func (x *importSnapshotCmd) Execute([]string) error {
   502  	filename := x.Positional.Filename
   503  	f, err := os.Open(filename)
   504  	if err != nil {
   505  		return fmt.Errorf("error accessing file: %v", err)
   506  	}
   507  	defer f.Close()
   508  	st, err := f.Stat()
   509  	if err != nil {
   510  		return fmt.Errorf("cannot stat file: %v", err)
   511  	}
   512  
   513  	importSet, err := x.client.SnapshotImport(f, st.Size())
   514  	if err != nil {
   515  		return err
   516  	}
   517  
   518  	fmt.Fprintf(Stdout, i18n.G("Imported snapshot as #%d\n"), importSet.ID)
   519  	// Now display the details about this snapshot, re-use the
   520  	// "snap saved" command for this which displays details about
   521  	// the snapshot.
   522  	y := &savedCmd{
   523  		clientMixin:   x.clientMixin,
   524  		durationMixin: x.durationMixin,
   525  		ID:            snapshotID(strconv.FormatUint(importSet.ID, 10)),
   526  	}
   527  	return y.Execute(nil)
   528  }