github.com/kubiko/snapd@v0.0.0-20201013125620-d4f3094d9ddf/overlord/snapshotstate/snapshotstate.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 snapshotstate
    21  
    22  import (
    23  	"context"
    24  	"encoding/json"
    25  	"fmt"
    26  	"io"
    27  	"sort"
    28  	"time"
    29  
    30  	"github.com/snapcore/snapd/client"
    31  	"github.com/snapcore/snapd/logger"
    32  	"github.com/snapcore/snapd/overlord/configstate/config"
    33  	"github.com/snapcore/snapd/overlord/snapshotstate/backend"
    34  	"github.com/snapcore/snapd/overlord/snapstate"
    35  	"github.com/snapcore/snapd/overlord/state"
    36  	"github.com/snapcore/snapd/release"
    37  	"github.com/snapcore/snapd/snap"
    38  	"github.com/snapcore/snapd/strutil"
    39  )
    40  
    41  var (
    42  	snapstateAll                     = snapstate.All
    43  	snapstateCheckChangeConflictMany = snapstate.CheckChangeConflictMany
    44  	backendIter                      = backend.Iter
    45  	backendEstimateSnapshotSize      = backend.EstimateSnapshotSize
    46  
    47  	// Default expiration time for automatic snapshots, if not set by the user
    48  	defaultAutomaticSnapshotExpiration = time.Hour * 24 * 31
    49  )
    50  
    51  type snapshotState struct {
    52  	ExpiryTime time.Time `json:"expiry-time"`
    53  }
    54  
    55  func newSnapshotSetID(st *state.State) (uint64, error) {
    56  	var lastDiskSetID, lastStateSetID uint64
    57  
    58  	// get last set id from state
    59  	err := st.Get("last-snapshot-set-id", &lastStateSetID)
    60  	if err != nil && err != state.ErrNoState {
    61  		return 0, err
    62  	}
    63  
    64  	// get highest set id from the snapshots/ directory
    65  	lastDiskSetID, err = backend.LastSnapshotSetID()
    66  	if err != nil {
    67  		return 0, fmt.Errorf("cannot determine last snapshot set id: %v", err)
    68  	}
    69  
    70  	// take the larger of the two numbers and store it back in the state.
    71  	// the value in state acts as an allocation of IDs for scheduled snapshots,
    72  	// they allocate set id early before any file gets created, so we cannot
    73  	// rely on disk only.
    74  	lastSetID := lastDiskSetID
    75  	if lastStateSetID > lastSetID {
    76  		lastSetID = lastStateSetID
    77  	}
    78  	lastSetID++
    79  	st.Set("last-snapshot-set-id", lastSetID)
    80  
    81  	return lastSetID, nil
    82  }
    83  
    84  func allActiveSnapNames(st *state.State) ([]string, error) {
    85  	all, err := snapstateAll(st)
    86  	if err != nil {
    87  		return nil, err
    88  	}
    89  	names := make([]string, 0, len(all))
    90  	for name, snapst := range all {
    91  		if snapst.Active {
    92  			names = append(names, name)
    93  		}
    94  	}
    95  
    96  	sort.Strings(names)
    97  
    98  	return names, nil
    99  }
   100  
   101  func EstimateSnapshotSize(st *state.State, instanceName string, users []string) (uint64, error) {
   102  	cur, err := snapstateCurrentInfo(st, instanceName)
   103  	if err != nil {
   104  		return 0, err
   105  	}
   106  	rawCfg, err := configGetSnapConfig(st, instanceName)
   107  	if err != nil {
   108  		return 0, err
   109  	}
   110  	sz, err := backendEstimateSnapshotSize(cur, users)
   111  	if err != nil {
   112  		return 0, err
   113  	}
   114  	if rawCfg != nil {
   115  		sz += uint64(len([]byte(*rawCfg)))
   116  	}
   117  	return sz, nil
   118  }
   119  
   120  func AutomaticSnapshotExpiration(st *state.State) (time.Duration, error) {
   121  	var expirationStr string
   122  	tr := config.NewTransaction(st)
   123  	err := tr.Get("core", "snapshots.automatic.retention", &expirationStr)
   124  	if err != nil && !config.IsNoOption(err) {
   125  		return 0, err
   126  	}
   127  	if err == nil {
   128  		if expirationStr == "no" {
   129  			return 0, nil
   130  		}
   131  		dur, err := time.ParseDuration(expirationStr)
   132  		if err == nil {
   133  			return dur, nil
   134  		}
   135  		logger.Noticef("snapshots.automatic.retention cannot be parsed: %v", err)
   136  	}
   137  	// TODO: automatic snapshots are currently disable by default
   138  	// on Ubuntu Core devices
   139  	if !release.OnClassic {
   140  		return 0, nil
   141  	}
   142  	return defaultAutomaticSnapshotExpiration, nil
   143  }
   144  
   145  // saveExpiration saves expiration date of the given snapshot set, in the state.
   146  // The state needs to be locked by the caller.
   147  func saveExpiration(st *state.State, setID uint64, expiryTime time.Time) error {
   148  	var snapshots map[uint64]*json.RawMessage
   149  	err := st.Get("snapshots", &snapshots)
   150  	if err != nil && err != state.ErrNoState {
   151  		return err
   152  	}
   153  	if snapshots == nil {
   154  		snapshots = make(map[uint64]*json.RawMessage)
   155  	}
   156  	data, err := json.Marshal(&snapshotState{
   157  		ExpiryTime: expiryTime,
   158  	})
   159  	if err != nil {
   160  		return err
   161  	}
   162  	raw := json.RawMessage(data)
   163  	snapshots[setID] = &raw
   164  	st.Set("snapshots", snapshots)
   165  	return nil
   166  }
   167  
   168  // removeSnapshotState removes given set IDs from the state.
   169  func removeSnapshotState(st *state.State, setIDs ...uint64) error {
   170  	var snapshots map[uint64]*json.RawMessage
   171  	err := st.Get("snapshots", &snapshots)
   172  	if err != nil {
   173  		if err == state.ErrNoState {
   174  			return nil
   175  		}
   176  		return err
   177  	}
   178  
   179  	for _, setID := range setIDs {
   180  		delete(snapshots, setID)
   181  	}
   182  
   183  	st.Set("snapshots", snapshots)
   184  	return nil
   185  }
   186  
   187  // expiredSnapshotSets returns expired snapshot sets from the state whose expiry-time is before the given cutoffTime.
   188  // The state needs to be locked by the caller.
   189  func expiredSnapshotSets(st *state.State, cutoffTime time.Time) (map[uint64]bool, error) {
   190  	var snapshots map[uint64]*snapshotState
   191  	err := st.Get("snapshots", &snapshots)
   192  	if err != nil {
   193  		if err != state.ErrNoState {
   194  			return nil, err
   195  		}
   196  		return nil, nil
   197  	}
   198  
   199  	expired := make(map[uint64]bool)
   200  	for setID, snapshotSet := range snapshots {
   201  		if snapshotSet.ExpiryTime.Before(cutoffTime) {
   202  			expired[setID] = true
   203  		}
   204  	}
   205  
   206  	return expired, nil
   207  }
   208  
   209  // snapshotSnapSummaries are used internally to get useful data from a
   210  // snapshot set when deciding whether to check/forget/restore it.
   211  type snapshotSnapSummaries []*snapshotSnapSummary
   212  
   213  func (summaries snapshotSnapSummaries) snapNames() []string {
   214  	names := make([]string, len(summaries))
   215  	for i, summary := range summaries {
   216  		names[i] = summary.snap
   217  	}
   218  	return names
   219  }
   220  
   221  type snapshotSnapSummary struct {
   222  	snap     string
   223  	snapID   string
   224  	filename string
   225  	epoch    snap.Epoch
   226  }
   227  
   228  // snapSummariesInSnapshotSet goes looking for the requested snaps in the
   229  // given snap set, and returns summaries of the matching snaps in the set.
   230  func snapSummariesInSnapshotSet(setID uint64, requested []string) (summaries snapshotSnapSummaries, err error) {
   231  	sort.Strings(requested)
   232  	found := false
   233  	err = backendIter(context.TODO(), func(r *backend.Reader) error {
   234  		if r.SetID == setID {
   235  			found = true
   236  			if len(requested) == 0 || strutil.SortedListContains(requested, r.Snap) {
   237  				summaries = append(summaries, &snapshotSnapSummary{
   238  					filename: r.Name(),
   239  					snap:     r.Snap,
   240  					snapID:   r.SnapID,
   241  					epoch:    r.Epoch,
   242  				})
   243  			}
   244  		}
   245  
   246  		return nil
   247  	})
   248  	if err != nil {
   249  		return nil, err
   250  	}
   251  	if !found {
   252  		return nil, client.ErrSnapshotSetNotFound
   253  	}
   254  	if len(summaries) == 0 {
   255  		return nil, client.ErrSnapshotSnapsNotFound
   256  	}
   257  
   258  	return summaries, nil
   259  }
   260  
   261  func taskGetErrMsg(task *state.Task, err error, what string) error {
   262  	if err == state.ErrNoState {
   263  		return fmt.Errorf("internal error: task %s (%s) is missing %s information", task.ID(), task.Kind(), what)
   264  	}
   265  	return fmt.Errorf("internal error: retrieving %s information from task %s (%s): %v", what, task.ID(), task.Kind(), err)
   266  }
   267  
   268  // checkSnapshotTaskConflict checks whether there's an in-progress task for snapshots with the given set id.
   269  func checkSnapshotTaskConflict(st *state.State, setID uint64, conflictingKinds ...string) error {
   270  	for _, task := range st.Tasks() {
   271  		if task.Change().Status().Ready() {
   272  			continue
   273  		}
   274  		if !strutil.ListContains(conflictingKinds, task.Kind()) {
   275  			continue
   276  		}
   277  
   278  		var snapshot snapshotSetup
   279  		if err := task.Get("snapshot-setup", &snapshot); err != nil {
   280  			return taskGetErrMsg(task, err, "snapshot")
   281  		}
   282  
   283  		if snapshot.SetID == setID {
   284  			return fmt.Errorf("cannot operate on snapshot set #%d while change %q is in progress", setID, task.Change().ID())
   285  		}
   286  	}
   287  
   288  	return nil
   289  }
   290  
   291  // List valid snapshots.
   292  // Note that the state must be locked by the caller.
   293  var List = backend.List
   294  
   295  // Import a given snapshot ID from an exported snapshot
   296  func Import(ctx context.Context, st *state.State, r io.Reader, size int64) (setID uint64, snapNames []string, err error) {
   297  	return 0, nil, fmt.Errorf("snapshot import not implemented yet")
   298  }
   299  
   300  // Save creates a taskset for taking snapshots of snaps' data.
   301  // Note that the state must be locked by the caller.
   302  func Save(st *state.State, instanceNames []string, users []string) (setID uint64, snapsSaved []string, ts *state.TaskSet, err error) {
   303  	if len(instanceNames) == 0 {
   304  		instanceNames, err = allActiveSnapNames(st)
   305  		if err != nil {
   306  			return 0, nil, nil, err
   307  		}
   308  	}
   309  
   310  	// Make sure we do not snapshot if anything like install/remove/refresh is in progress
   311  	if err := snapstateCheckChangeConflictMany(st, instanceNames, ""); err != nil {
   312  		return 0, nil, nil, err
   313  	}
   314  
   315  	setID, err = newSnapshotSetID(st)
   316  	if err != nil {
   317  		return 0, nil, nil, err
   318  	}
   319  
   320  	ts = state.NewTaskSet()
   321  
   322  	for _, name := range instanceNames {
   323  		desc := fmt.Sprintf("Save data of snap %q in snapshot set #%d", name, setID)
   324  		task := st.NewTask("save-snapshot", desc)
   325  		snapshot := snapshotSetup{
   326  			SetID: setID,
   327  			Snap:  name,
   328  			Users: users,
   329  		}
   330  		task.Set("snapshot-setup", &snapshot)
   331  		// Here, note that a snapshot set behaves as a unit: it either
   332  		// succeeds, or fails, as a whole; we don't use lanes, to have
   333  		// some snaps' snapshot succeed and not others in a single set.
   334  		// In practice: either the snapshot will be automatic and only
   335  		// for one snap (already in a lane via refresh), or it will be
   336  		// done by hand and the user can remove failing snaps (or find
   337  		// the cause of the failure). A snapshot failure can happen if
   338  		// a user has dropped files they can't read in their directory,
   339  		// for example.
   340  		// Also note we aren't promising this behaviour; we can change
   341  		// it if we find it to be wrong.
   342  		ts.AddTask(task)
   343  	}
   344  
   345  	return setID, instanceNames, ts, nil
   346  }
   347  
   348  func AutomaticSnapshot(st *state.State, snapName string) (ts *state.TaskSet, err error) {
   349  	expiration, err := AutomaticSnapshotExpiration(st)
   350  	if err != nil {
   351  		return nil, err
   352  	}
   353  	if expiration == 0 {
   354  		return nil, snapstate.ErrNothingToDo
   355  	}
   356  	setID, err := newSnapshotSetID(st)
   357  	if err != nil {
   358  		return nil, err
   359  	}
   360  
   361  	ts = state.NewTaskSet()
   362  	desc := fmt.Sprintf("Save data of snap %q in automatic snapshot set #%d", snapName, setID)
   363  	task := st.NewTask("save-snapshot", desc)
   364  	snapshot := snapshotSetup{
   365  		SetID: setID,
   366  		Snap:  snapName,
   367  		Auto:  true,
   368  	}
   369  	task.Set("snapshot-setup", &snapshot)
   370  	ts.AddTask(task)
   371  
   372  	return ts, nil
   373  }
   374  
   375  // Restore creates a taskset for restoring a snapshot's data.
   376  // Note that the state must be locked by the caller.
   377  func Restore(st *state.State, setID uint64, snapNames []string, users []string) (snapsFound []string, ts *state.TaskSet, err error) {
   378  	summaries, err := snapSummariesInSnapshotSet(setID, snapNames)
   379  	if err != nil {
   380  		return nil, nil, err
   381  	}
   382  	all, err := snapstateAll(st)
   383  	if err != nil {
   384  		return nil, nil, err
   385  	}
   386  
   387  	snapsFound = summaries.snapNames()
   388  
   389  	if err := snapstateCheckChangeConflictMany(st, snapsFound, ""); err != nil {
   390  		return nil, nil, err
   391  	}
   392  
   393  	// restore needs to conflict with forget of itself
   394  	if err := checkSnapshotTaskConflict(st, setID, "forget-snapshot"); err != nil {
   395  		return nil, nil, err
   396  	}
   397  
   398  	ts = state.NewTaskSet()
   399  
   400  	for _, summary := range summaries {
   401  		var current snap.Revision
   402  		if snapst, ok := all[summary.snap]; ok {
   403  			info, err := snapst.CurrentInfo()
   404  			if err != nil {
   405  				// how?
   406  				return nil, nil, fmt.Errorf("unexpected error while reading snap info: %v", err)
   407  			}
   408  			if !info.Epoch.CanRead(summary.epoch) {
   409  				const tpl = "cannot restore snapshot for %q: current snap (epoch %s) cannot read snapshot data (epoch %s)"
   410  				return nil, nil, fmt.Errorf(tpl, summary.snap, &info.Epoch, &summary.epoch)
   411  			}
   412  			if summary.snapID != "" && info.SnapID != "" && info.SnapID != summary.snapID {
   413  				const tpl = "cannot restore snapshot for %q: current snap (ID %.7s…) does not match snapshot (ID %.7s…)"
   414  				return nil, nil, fmt.Errorf(tpl, summary.snap, info.SnapID, summary.snapID)
   415  			}
   416  			current = snapst.Current
   417  		}
   418  
   419  		desc := fmt.Sprintf("Restore data of snap %q from snapshot set #%d", summary.snap, setID)
   420  		task := st.NewTask("restore-snapshot", desc)
   421  		snapshot := snapshotSetup{
   422  			SetID:    setID,
   423  			Snap:     summary.snap,
   424  			Users:    users,
   425  			Filename: summary.filename,
   426  			Current:  current,
   427  		}
   428  		task.Set("snapshot-setup", &snapshot)
   429  		// see the note about snapshots not using lanes, above.
   430  		ts.AddTask(task)
   431  	}
   432  
   433  	return snapsFound, ts, nil
   434  }
   435  
   436  // Check creates a taskset for checking a snapshot's data.
   437  // Note that the state must be locked by the caller.
   438  func Check(st *state.State, setID uint64, snapNames []string, users []string) (snapsFound []string, ts *state.TaskSet, err error) {
   439  	// check needs to conflict with forget of itself
   440  	if err := checkSnapshotTaskConflict(st, setID, "forget-snapshot"); err != nil {
   441  		return nil, nil, err
   442  	}
   443  
   444  	summaries, err := snapSummariesInSnapshotSet(setID, snapNames)
   445  	if err != nil {
   446  		return nil, nil, err
   447  	}
   448  
   449  	ts = state.NewTaskSet()
   450  
   451  	for _, summary := range summaries {
   452  		desc := fmt.Sprintf("Check data of snap %q in snapshot set #%d", summary.snap, setID)
   453  		task := st.NewTask("check-snapshot", desc)
   454  		snapshot := snapshotSetup{
   455  			SetID:    setID,
   456  			Snap:     summary.snap,
   457  			Users:    users,
   458  			Filename: summary.filename,
   459  		}
   460  		task.Set("snapshot-setup", &snapshot)
   461  		ts.AddTask(task)
   462  	}
   463  
   464  	return summaries.snapNames(), ts, nil
   465  }
   466  
   467  // Forget creates a taskset for deletinig a snapshot.
   468  // Note that the state must be locked by the caller.
   469  func Forget(st *state.State, setID uint64, snapNames []string) (snapsFound []string, ts *state.TaskSet, err error) {
   470  	// forget needs to conflict with check and restore
   471  	if err := checkSnapshotTaskConflict(st, setID, "check-snapshot", "restore-snapshot"); err != nil {
   472  		return nil, nil, err
   473  	}
   474  
   475  	summaries, err := snapSummariesInSnapshotSet(setID, snapNames)
   476  	if err != nil {
   477  		return nil, nil, err
   478  	}
   479  
   480  	ts = state.NewTaskSet()
   481  	for _, summary := range summaries {
   482  		desc := fmt.Sprintf("Drop data of snap %q from snapshot set #%d", summary.snap, setID)
   483  		task := st.NewTask("forget-snapshot", desc)
   484  		snapshot := snapshotSetup{
   485  			SetID:    setID,
   486  			Snap:     summary.snap,
   487  			Filename: summary.filename,
   488  		}
   489  		task.Set("snapshot-setup", &snapshot)
   490  		ts.AddTask(task)
   491  	}
   492  
   493  	return summaries.snapNames(), ts, nil
   494  }
   495  
   496  // Export exports a given snapshot ID
   497  // Note that the state much be locked by the caller.
   498  var Export = backend.NewSnapshotExport
   499  
   500  // SnapshotExport provides a snapshot export that can be streamed out
   501  type SnapshotExport = backend.SnapshotExport