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