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