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