github.com/Lephar/snapd@v0.0.0-20210825215435-c7fba9cef4d2/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  	backendList                      = backend.List
    47  	backendNewSnapshotExport         = backend.NewSnapshotExport
    48  
    49  	// Default expiration time for automatic snapshots, if not set by the user
    50  	defaultAutomaticSnapshotExpiration = time.Hour * 24 * 31
    51  )
    52  
    53  type snapshotState struct {
    54  	ExpiryTime time.Time `json:"expiry-time"`
    55  }
    56  
    57  func newSnapshotSetID(st *state.State) (uint64, error) {
    58  	var lastDiskSetID, lastStateSetID uint64
    59  
    60  	// get last set id from state
    61  	err := st.Get("last-snapshot-set-id", &lastStateSetID)
    62  	if err != nil && err != state.ErrNoState {
    63  		return 0, err
    64  	}
    65  
    66  	// get highest set id from the snapshots/ directory
    67  	lastDiskSetID, err = backend.LastSnapshotSetID()
    68  	if err != nil {
    69  		return 0, fmt.Errorf("cannot determine last snapshot set id: %v", err)
    70  	}
    71  
    72  	// take the larger of the two numbers and store it back in the state.
    73  	// the value in state acts as an allocation of IDs for scheduled snapshots,
    74  	// they allocate set id early before any file gets created, so we cannot
    75  	// rely on disk only.
    76  	lastSetID := lastDiskSetID
    77  	if lastStateSetID > lastSetID {
    78  		lastSetID = lastStateSetID
    79  	}
    80  	lastSetID++
    81  	st.Set("last-snapshot-set-id", lastSetID)
    82  
    83  	return lastSetID, nil
    84  }
    85  
    86  func allActiveSnapNames(st *state.State) ([]string, error) {
    87  	all, err := snapstateAll(st)
    88  	if err != nil {
    89  		return nil, err
    90  	}
    91  	names := make([]string, 0, len(all))
    92  	for name, snapst := range all {
    93  		if snapst.Active {
    94  			names = append(names, name)
    95  		}
    96  	}
    97  
    98  	sort.Strings(names)
    99  
   100  	return names, nil
   101  }
   102  
   103  func EstimateSnapshotSize(st *state.State, instanceName string, users []string) (uint64, error) {
   104  	cur, err := snapstateCurrentInfo(st, instanceName)
   105  	if err != nil {
   106  		return 0, err
   107  	}
   108  	rawCfg, err := configGetSnapConfig(st, instanceName)
   109  	if err != nil {
   110  		return 0, err
   111  	}
   112  	sz, err := backendEstimateSnapshotSize(cur, users)
   113  	if err != nil {
   114  		return 0, err
   115  	}
   116  	if rawCfg != nil {
   117  		sz += uint64(len([]byte(*rawCfg)))
   118  	}
   119  	return sz, nil
   120  }
   121  
   122  func AutomaticSnapshotExpiration(st *state.State) (time.Duration, error) {
   123  	var expirationStr string
   124  	tr := config.NewTransaction(st)
   125  	err := tr.Get("core", "snapshots.automatic.retention", &expirationStr)
   126  	if err != nil && !config.IsNoOption(err) {
   127  		return 0, err
   128  	}
   129  	if err == nil {
   130  		if expirationStr == "no" {
   131  			return 0, nil
   132  		}
   133  		dur, err := time.ParseDuration(expirationStr)
   134  		if err == nil {
   135  			return dur, nil
   136  		}
   137  		logger.Noticef("snapshots.automatic.retention cannot be parsed: %v", err)
   138  	}
   139  	// TODO: automatic snapshots are currently disable by default
   140  	// on Ubuntu Core devices
   141  	if !release.OnClassic {
   142  		return 0, nil
   143  	}
   144  	return defaultAutomaticSnapshotExpiration, nil
   145  }
   146  
   147  // saveExpiration saves expiration date of the given snapshot set, in the state.
   148  // The state needs to be locked by the caller.
   149  func saveExpiration(st *state.State, setID uint64, expiryTime time.Time) error {
   150  	var snapshots map[uint64]*json.RawMessage
   151  	err := st.Get("snapshots", &snapshots)
   152  	if err != nil && err != state.ErrNoState {
   153  		return err
   154  	}
   155  	if snapshots == nil {
   156  		snapshots = make(map[uint64]*json.RawMessage)
   157  	}
   158  	data, err := json.Marshal(&snapshotState{
   159  		ExpiryTime: expiryTime,
   160  	})
   161  	if err != nil {
   162  		return err
   163  	}
   164  	raw := json.RawMessage(data)
   165  	snapshots[setID] = &raw
   166  	st.Set("snapshots", snapshots)
   167  	return nil
   168  }
   169  
   170  // removeSnapshotState removes given set IDs from the state.
   171  func removeSnapshotState(st *state.State, setIDs ...uint64) error {
   172  	var snapshots map[uint64]*json.RawMessage
   173  	err := st.Get("snapshots", &snapshots)
   174  	if err != nil {
   175  		if err == state.ErrNoState {
   176  			return nil
   177  		}
   178  		return err
   179  	}
   180  
   181  	for _, setID := range setIDs {
   182  		delete(snapshots, setID)
   183  	}
   184  
   185  	st.Set("snapshots", snapshots)
   186  	return nil
   187  }
   188  
   189  // expiredSnapshotSets returns expired snapshot sets from the state whose expiry-time is before the given cutoffTime.
   190  // The state needs to be locked by the caller.
   191  func expiredSnapshotSets(st *state.State, cutoffTime time.Time) (map[uint64]bool, error) {
   192  	var snapshots map[uint64]*snapshotState
   193  	err := st.Get("snapshots", &snapshots)
   194  	if err != nil {
   195  		if err != state.ErrNoState {
   196  			return nil, err
   197  		}
   198  		return nil, nil
   199  	}
   200  
   201  	expired := make(map[uint64]bool)
   202  	for setID, snapshotSet := range snapshots {
   203  		if snapshotSet.ExpiryTime.Before(cutoffTime) {
   204  			expired[setID] = true
   205  		}
   206  	}
   207  
   208  	return expired, nil
   209  }
   210  
   211  // snapshotSnapSummaries are used internally to get useful data from a
   212  // snapshot set when deciding whether to check/forget/restore it.
   213  type snapshotSnapSummaries []*snapshotSnapSummary
   214  
   215  func (summaries snapshotSnapSummaries) snapNames() []string {
   216  	names := make([]string, len(summaries))
   217  	for i, summary := range summaries {
   218  		names[i] = summary.snap
   219  	}
   220  	return names
   221  }
   222  
   223  type snapshotSnapSummary struct {
   224  	snap     string
   225  	snapID   string
   226  	filename string
   227  	epoch    snap.Epoch
   228  }
   229  
   230  // snapSummariesInSnapshotSet goes looking for the requested snaps in the
   231  // given snap set, and returns summaries of the matching snaps in the set.
   232  func snapSummariesInSnapshotSet(setID uint64, requested []string) (summaries snapshotSnapSummaries, err error) {
   233  	sort.Strings(requested)
   234  	found := false
   235  	err = backendIter(context.TODO(), func(r *backend.Reader) error {
   236  		if r.SetID == setID {
   237  			found = true
   238  			if len(requested) == 0 || strutil.SortedListContains(requested, r.Snap) {
   239  				summaries = append(summaries, &snapshotSnapSummary{
   240  					filename: r.Name(),
   241  					snap:     r.Snap,
   242  					snapID:   r.SnapID,
   243  					epoch:    r.Epoch,
   244  				})
   245  			}
   246  		}
   247  
   248  		return nil
   249  	})
   250  	if err != nil {
   251  		return nil, err
   252  	}
   253  	if !found {
   254  		return nil, client.ErrSnapshotSetNotFound
   255  	}
   256  	if len(summaries) == 0 {
   257  		return nil, client.ErrSnapshotSnapsNotFound
   258  	}
   259  
   260  	return summaries, nil
   261  }
   262  
   263  func taskGetErrMsg(task *state.Task, err error, what string) error {
   264  	if err == state.ErrNoState {
   265  		return fmt.Errorf("internal error: task %s (%s) is missing %s information", task.ID(), task.Kind(), what)
   266  	}
   267  	return fmt.Errorf("internal error: retrieving %s information from task %s (%s): %v", what, task.ID(), task.Kind(), err)
   268  }
   269  
   270  // checkSnapshotConflict checks whether there's an in-progress task for snapshots with the given set id.
   271  func checkSnapshotConflict(st *state.State, setID uint64, conflictingKinds ...string) error {
   272  	if val := st.Cached("snapshot-ops"); val != nil {
   273  		snapshotOps, _ := val.(map[uint64]string)
   274  		if op, ok := snapshotOps[setID]; ok {
   275  			for _, conflicting := range conflictingKinds {
   276  				if op == conflicting {
   277  					return fmt.Errorf("cannot operate on snapshot set #%d while operation %s is in progress", setID, op)
   278  				}
   279  			}
   280  		}
   281  	}
   282  	for _, task := range st.Tasks() {
   283  		if task.Change().Status().Ready() {
   284  			continue
   285  		}
   286  		if !strutil.ListContains(conflictingKinds, task.Kind()) {
   287  			continue
   288  		}
   289  
   290  		var snapshot snapshotSetup
   291  		if err := task.Get("snapshot-setup", &snapshot); err != nil {
   292  			return taskGetErrMsg(task, err, "snapshot")
   293  		}
   294  
   295  		if snapshot.SetID == setID {
   296  			return fmt.Errorf("cannot operate on snapshot set #%d while change %q is in progress", setID, task.Change().ID())
   297  		}
   298  	}
   299  
   300  	return nil
   301  }
   302  
   303  // List valid snapshots.
   304  // Note that the state must be locked by the caller.
   305  func List(ctx context.Context, st *state.State, setID uint64, snapNames []string) ([]client.SnapshotSet, error) {
   306  	sets, err := backendList(ctx, setID, snapNames)
   307  	if err != nil {
   308  		return nil, err
   309  	}
   310  
   311  	var snapshots map[uint64]*snapshotState
   312  	if err := st.Get("snapshots", &snapshots); err != nil && err != state.ErrNoState {
   313  		return nil, err
   314  	}
   315  
   316  	// decorate all snapshots with "auto" flag if we have expiry time set for them.
   317  	for _, sset := range sets {
   318  		// at the moment we only keep records with expiry time so checking non-zero
   319  		// expiry-time is not strictly necessary, but it makes it future-proof in case
   320  		// we add more attributes to these entries.
   321  		if snapshotState, ok := snapshots[sset.ID]; ok && !snapshotState.ExpiryTime.IsZero() {
   322  			for _, snapshot := range sset.Snapshots {
   323  				snapshot.Auto = true
   324  			}
   325  		}
   326  	}
   327  
   328  	return sets, nil
   329  }
   330  
   331  // Import a given snapshot ID from an exported snapshot
   332  func Import(ctx context.Context, st *state.State, r io.Reader) (setID uint64, snapNames []string, err error) {
   333  	st.Lock()
   334  	setID, err = newSnapshotSetID(st)
   335  	// note, this is a new set id which is not exposed yet, no need to mark it
   336  	// for conflicts via snapshotOp. Also, since we're keeping state lock while
   337  	// checking conflicts below, there is no need to for setSnapshotOpInProgress.
   338  	st.Unlock()
   339  	if err != nil {
   340  		return 0, nil, err
   341  	}
   342  
   343  	snapNames, err = backendImport(ctx, setID, r, nil)
   344  	if err != nil {
   345  		if dupErr, ok := err.(backend.DuplicatedSnapshotImportError); ok {
   346  			st.Lock()
   347  			defer st.Unlock()
   348  
   349  			if err := checkSnapshotConflict(st, dupErr.SetID, "forget-snapshot"); err != nil {
   350  				// we found an existing snapshot but it's being forgotten, so
   351  				// retry the import without checking for existing snapshot.
   352  				flags := &backend.ImportFlags{NoDuplicatedImportCheck: true}
   353  				st.Unlock()
   354  				snapNames, err = backendImport(ctx, setID, r, flags)
   355  				st.Lock()
   356  				return setID, snapNames, err
   357  			}
   358  
   359  			// trying to import identical snapshot; instead return set ID of
   360  			// the existing one and reset its expiry time.
   361  			// XXX: at the moment expiry-time is the only attribute so we can
   362  			// just remove the record. If we ever add more attributes this needs
   363  			// to reset expiry-time only.
   364  			if err := removeSnapshotState(st, dupErr.SetID); err != nil {
   365  				return 0, nil, err
   366  			}
   367  			return dupErr.SetID, dupErr.SnapNames, nil
   368  		}
   369  		return 0, nil, err
   370  	}
   371  	return setID, snapNames, nil
   372  }
   373  
   374  // Save creates a taskset for taking snapshots of snaps' data.
   375  // Note that the state must be locked by the caller.
   376  func Save(st *state.State, instanceNames []string, users []string) (setID uint64, snapsSaved []string, ts *state.TaskSet, err error) {
   377  	if len(instanceNames) == 0 {
   378  		instanceNames, err = allActiveSnapNames(st)
   379  		if err != nil {
   380  			return 0, nil, nil, err
   381  		}
   382  	}
   383  
   384  	// Make sure we do not snapshot if anything like install/remove/refresh is in progress
   385  	if err := snapstateCheckChangeConflictMany(st, instanceNames, ""); err != nil {
   386  		return 0, nil, nil, err
   387  	}
   388  
   389  	setID, err = newSnapshotSetID(st)
   390  	if err != nil {
   391  		return 0, nil, nil, err
   392  	}
   393  
   394  	ts = state.NewTaskSet()
   395  
   396  	for _, name := range instanceNames {
   397  		desc := fmt.Sprintf("Save data of snap %q in snapshot set #%d", name, setID)
   398  		task := st.NewTask("save-snapshot", desc)
   399  		snapshot := snapshotSetup{
   400  			SetID: setID,
   401  			Snap:  name,
   402  			Users: users,
   403  		}
   404  		task.Set("snapshot-setup", &snapshot)
   405  		// Here, note that a snapshot set behaves as a unit: it either
   406  		// succeeds, or fails, as a whole; we don't use lanes, to have
   407  		// some snaps' snapshot succeed and not others in a single set.
   408  		// In practice: either the snapshot will be automatic and only
   409  		// for one snap (already in a lane via refresh), or it will be
   410  		// done by hand and the user can remove failing snaps (or find
   411  		// the cause of the failure). A snapshot failure can happen if
   412  		// a user has dropped files they can't read in their directory,
   413  		// for example.
   414  		// Also note we aren't promising this behaviour; we can change
   415  		// it if we find it to be wrong.
   416  		ts.AddTask(task)
   417  	}
   418  
   419  	return setID, instanceNames, ts, nil
   420  }
   421  
   422  func AutomaticSnapshot(st *state.State, snapName string) (ts *state.TaskSet, err error) {
   423  	expiration, err := AutomaticSnapshotExpiration(st)
   424  	if err != nil {
   425  		return nil, err
   426  	}
   427  	if expiration == 0 {
   428  		return nil, snapstate.ErrNothingToDo
   429  	}
   430  	setID, err := newSnapshotSetID(st)
   431  	if err != nil {
   432  		return nil, err
   433  	}
   434  
   435  	ts = state.NewTaskSet()
   436  	desc := fmt.Sprintf("Save data of snap %q in automatic snapshot set #%d", snapName, setID)
   437  	task := st.NewTask("save-snapshot", desc)
   438  	snapshot := snapshotSetup{
   439  		SetID: setID,
   440  		Snap:  snapName,
   441  		Auto:  true,
   442  	}
   443  	task.Set("snapshot-setup", &snapshot)
   444  	ts.AddTask(task)
   445  
   446  	return ts, nil
   447  }
   448  
   449  // Restore creates a taskset for restoring a snapshot's data.
   450  // Note that the state must be locked by the caller.
   451  func Restore(st *state.State, setID uint64, snapNames []string, users []string) (snapsFound []string, ts *state.TaskSet, err error) {
   452  	summaries, err := snapSummariesInSnapshotSet(setID, snapNames)
   453  	if err != nil {
   454  		return nil, nil, err
   455  	}
   456  	all, err := snapstateAll(st)
   457  	if err != nil {
   458  		return nil, nil, err
   459  	}
   460  
   461  	snapsFound = summaries.snapNames()
   462  
   463  	if err := snapstateCheckChangeConflictMany(st, snapsFound, ""); err != nil {
   464  		return nil, nil, err
   465  	}
   466  
   467  	// restore needs to conflict with forget of itself
   468  	if err := checkSnapshotConflict(st, setID, "forget-snapshot"); err != nil {
   469  		return nil, nil, err
   470  	}
   471  
   472  	ts = state.NewTaskSet()
   473  
   474  	for _, summary := range summaries {
   475  		var current snap.Revision
   476  		if snapst, ok := all[summary.snap]; ok {
   477  			info, err := snapst.CurrentInfo()
   478  			if err != nil {
   479  				// how?
   480  				return nil, nil, fmt.Errorf("unexpected error while reading snap info: %v", err)
   481  			}
   482  			if !info.Epoch.CanRead(summary.epoch) {
   483  				const tpl = "cannot restore snapshot for %q: current snap (epoch %s) cannot read snapshot data (epoch %s)"
   484  				return nil, nil, fmt.Errorf(tpl, summary.snap, &info.Epoch, &summary.epoch)
   485  			}
   486  			if summary.snapID != "" && info.SnapID != "" && info.SnapID != summary.snapID {
   487  				const tpl = "cannot restore snapshot for %q: current snap (ID %.7s…) does not match snapshot (ID %.7s…)"
   488  				return nil, nil, fmt.Errorf(tpl, summary.snap, info.SnapID, summary.snapID)
   489  			}
   490  			current = snapst.Current
   491  		}
   492  
   493  		desc := fmt.Sprintf("Restore data of snap %q from snapshot set #%d", summary.snap, setID)
   494  		task := st.NewTask("restore-snapshot", desc)
   495  		snapshot := snapshotSetup{
   496  			SetID:    setID,
   497  			Snap:     summary.snap,
   498  			Users:    users,
   499  			Filename: summary.filename,
   500  			Current:  current,
   501  		}
   502  		task.Set("snapshot-setup", &snapshot)
   503  		// see the note about snapshots not using lanes, above.
   504  		ts.AddTask(task)
   505  	}
   506  
   507  	return snapsFound, ts, nil
   508  }
   509  
   510  // Check creates a taskset for checking a snapshot's data.
   511  // Note that the state must be locked by the caller.
   512  func Check(st *state.State, setID uint64, snapNames []string, users []string) (snapsFound []string, ts *state.TaskSet, err error) {
   513  	// check needs to conflict with forget of itself
   514  	if err := checkSnapshotConflict(st, setID, "forget-snapshot"); err != nil {
   515  		return nil, nil, err
   516  	}
   517  
   518  	summaries, err := snapSummariesInSnapshotSet(setID, snapNames)
   519  	if err != nil {
   520  		return nil, nil, err
   521  	}
   522  
   523  	ts = state.NewTaskSet()
   524  
   525  	for _, summary := range summaries {
   526  		desc := fmt.Sprintf("Check data of snap %q in snapshot set #%d", summary.snap, setID)
   527  		task := st.NewTask("check-snapshot", desc)
   528  		snapshot := snapshotSetup{
   529  			SetID:    setID,
   530  			Snap:     summary.snap,
   531  			Users:    users,
   532  			Filename: summary.filename,
   533  		}
   534  		task.Set("snapshot-setup", &snapshot)
   535  		ts.AddTask(task)
   536  	}
   537  
   538  	return summaries.snapNames(), ts, nil
   539  }
   540  
   541  // Forget creates a taskset for deletinig a snapshot.
   542  // Note that the state must be locked by the caller.
   543  func Forget(st *state.State, setID uint64, snapNames []string) (snapsFound []string, ts *state.TaskSet, err error) {
   544  	// forget needs to conflict with check, restore, import and export.
   545  	if err := checkSnapshotConflict(st, setID, "export-snapshot",
   546  		"check-snapshot", "restore-snapshot"); err != nil {
   547  		return nil, nil, err
   548  	}
   549  
   550  	summaries, err := snapSummariesInSnapshotSet(setID, snapNames)
   551  	if err != nil {
   552  		return nil, nil, err
   553  	}
   554  
   555  	ts = state.NewTaskSet()
   556  	for _, summary := range summaries {
   557  		desc := fmt.Sprintf("Drop data of snap %q from snapshot set #%d", summary.snap, setID)
   558  		task := st.NewTask("forget-snapshot", desc)
   559  		snapshot := snapshotSetup{
   560  			SetID:    setID,
   561  			Snap:     summary.snap,
   562  			Filename: summary.filename,
   563  		}
   564  		task.Set("snapshot-setup", &snapshot)
   565  		ts.AddTask(task)
   566  	}
   567  
   568  	return summaries.snapNames(), ts, nil
   569  }
   570  
   571  // setSnapshotOpInProgress marks the given set ID as being a subject of
   572  // snapshot op inside state cache. The state must be locked by the caller.
   573  func setSnapshotOpInProgress(st *state.State, setID uint64, op string) {
   574  	var snapshotOps map[uint64]string
   575  	if val := st.Cached("snapshot-ops"); val != nil {
   576  		snapshotOps, _ = val.(map[uint64]string)
   577  	} else {
   578  		snapshotOps = make(map[uint64]string)
   579  	}
   580  	snapshotOps[setID] = op
   581  	st.Cache("snapshot-ops", snapshotOps)
   582  }
   583  
   584  // UnsetSnapshotOpInProgress un-sets the given set ID as being a
   585  // subject of a snapshot op. It returns the last operation (or empty string
   586  // if no op was marked active).
   587  // The state must be locked by the caller.
   588  func UnsetSnapshotOpInProgress(st *state.State, setID uint64) string {
   589  	var op string
   590  	if val := st.Cached("snapshot-ops"); val != nil {
   591  		var snapshotOps map[uint64]string
   592  		snapshotOps, _ = val.(map[uint64]string)
   593  		op = snapshotOps[setID]
   594  		delete(snapshotOps, setID)
   595  		st.Cache("snapshot-ops", snapshotOps)
   596  	}
   597  	return op
   598  }
   599  
   600  // Export exports a given snapshot ID
   601  // Note that the state must be locked by the caller.
   602  func Export(ctx context.Context, st *state.State, setID uint64) (se *backend.SnapshotExport, err error) {
   603  	if err := checkSnapshotConflict(st, setID, "forget-snapshot"); err != nil {
   604  		return nil, err
   605  	}
   606  
   607  	setSnapshotOpInProgress(st, setID, "export-snapshot")
   608  	se, err = backendNewSnapshotExport(ctx, setID)
   609  	if err != nil {
   610  		UnsetSnapshotOpInProgress(st, setID)
   611  	}
   612  	return se, err
   613  }
   614  
   615  // SnapshotExport provides a snapshot export that can be streamed out
   616  type SnapshotExport = backend.SnapshotExport