gopkg.in/ubuntu-core/snappy.v0@v0.0.0-20210902073436-25a8614f10a6/overlord/snapshotstate/snapshotmgr.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  	"os"
    27  	"time"
    28  
    29  	"gopkg.in/tomb.v2"
    30  
    31  	"github.com/snapcore/snapd/client"
    32  	"github.com/snapcore/snapd/logger"
    33  	"github.com/snapcore/snapd/overlord/configstate/config"
    34  	"github.com/snapcore/snapd/overlord/snapshotstate/backend"
    35  	"github.com/snapcore/snapd/overlord/snapstate"
    36  	"github.com/snapcore/snapd/overlord/state"
    37  	"github.com/snapcore/snapd/snap"
    38  )
    39  
    40  var (
    41  	osRemove             = os.Remove
    42  	snapstateCurrentInfo = snapstate.CurrentInfo
    43  	configGetSnapConfig  = config.GetSnapConfig
    44  	configSetSnapConfig  = config.SetSnapConfig
    45  	backendOpen          = backend.Open
    46  	backendSave          = backend.Save
    47  	backendImport        = backend.Import
    48  	backendRestore       = (*backend.Reader).Restore // TODO: look into using an interface instead
    49  	backendCheck         = (*backend.Reader).Check
    50  	backendRevert        = (*backend.RestoreState).Revert // ditto
    51  	backendCleanup       = (*backend.RestoreState).Cleanup
    52  
    53  	backendCleanupAbandondedImports = backend.CleanupAbandondedImports
    54  
    55  	autoExpirationInterval = time.Hour * 24 // interval between forgetExpiredSnapshots runs as part of Ensure()
    56  )
    57  
    58  // SnapshotManager takes snapshots of active snaps
    59  type SnapshotManager struct {
    60  	state *state.State
    61  
    62  	lastForgetExpiredSnapshotTime time.Time
    63  }
    64  
    65  // Manager returns a new SnapshotManager
    66  func Manager(st *state.State, runner *state.TaskRunner) *SnapshotManager {
    67  	delayedCrossMgrInit()
    68  
    69  	runner.AddHandler("save-snapshot", doSave, doForget)
    70  	runner.AddHandler("forget-snapshot", doForget, nil)
    71  	runner.AddHandler("check-snapshot", doCheck, nil)
    72  	runner.AddHandler("restore-snapshot", doRestore, undoRestore)
    73  	runner.AddCleanup("restore-snapshot", cleanupRestore)
    74  
    75  	manager := &SnapshotManager{
    76  		state: st,
    77  	}
    78  	snapstate.AddAffectedSnapsByAttr("snapshot-setup", manager.affectedSnaps)
    79  
    80  	return manager
    81  }
    82  
    83  // Ensure is part of the overlord.StateManager interface.
    84  func (mgr *SnapshotManager) Ensure() error {
    85  	// process expired snapshots once a day.
    86  	if time.Now().After(mgr.lastForgetExpiredSnapshotTime.Add(autoExpirationInterval)) {
    87  		return mgr.forgetExpiredSnapshots()
    88  	}
    89  
    90  	return nil
    91  }
    92  
    93  func (mgr *SnapshotManager) StartUp() error {
    94  	if _, err := backendCleanupAbandondedImports(); err != nil {
    95  		logger.Noticef("cannot cleanup incomplete imports: %v", err)
    96  	}
    97  	return nil
    98  }
    99  
   100  func (mgr *SnapshotManager) forgetExpiredSnapshots() error {
   101  	mgr.state.Lock()
   102  	defer mgr.state.Unlock()
   103  
   104  	sets, err := expiredSnapshotSets(mgr.state, time.Now())
   105  	if err != nil {
   106  		return fmt.Errorf("internal error: cannot determine expired snapshots: %v", err)
   107  	}
   108  
   109  	if len(sets) == 0 {
   110  		return nil
   111  	}
   112  
   113  	err = backendIter(context.TODO(), func(r *backend.Reader) error {
   114  		// forget needs to conflict with check and restore
   115  		if err := checkSnapshotConflict(mgr.state, r.SetID, "export-snapshot",
   116  			"check-snapshot", "restore-snapshot"); err != nil {
   117  			// there is a conflict, do nothing and we will retry this set on next Ensure().
   118  			return nil
   119  		}
   120  		if sets[r.SetID] {
   121  			delete(sets, r.SetID)
   122  			// remove from state first: in case removeSnapshotState succeeds but osRemove fails we will never attempt
   123  			// to automatically remove this snapshot again and will leave it on the disk (so the user can still try to remove it manually);
   124  			// this is better than the other way around where a failing osRemove would be retried forever because snapshot would never
   125  			// leave the state.
   126  			if err := removeSnapshotState(mgr.state, r.SetID); err != nil {
   127  				return fmt.Errorf("internal error: cannot remove state of snapshot set %d: %v", r.SetID, err)
   128  			}
   129  			if err := osRemove(r.Name()); err != nil {
   130  				return fmt.Errorf("cannot remove snapshot file %q: %v", r.Name(), err)
   131  			}
   132  		}
   133  		return nil
   134  	})
   135  
   136  	if err != nil {
   137  		return fmt.Errorf("cannot process expired snapshots: %v", err)
   138  	}
   139  
   140  	// only reset time if there are no sets left because of conflicts
   141  	if len(sets) == 0 {
   142  		mgr.lastForgetExpiredSnapshotTime = time.Now()
   143  	}
   144  
   145  	return nil
   146  }
   147  
   148  func (SnapshotManager) affectedSnaps(t *state.Task) ([]string, error) {
   149  	if k := t.Kind(); k == "check-snapshot" || k == "forget-snapshot" {
   150  		// check and forget don't affect snaps
   151  		// (this could also be written k != save && k != restore, but it's safer this way around)
   152  		return nil, nil
   153  	}
   154  	var snapshot snapshotSetup
   155  	if err := t.Get("snapshot-setup", &snapshot); err != nil {
   156  		return nil, taskGetErrMsg(t, err, "snapshot")
   157  	}
   158  
   159  	return []string{snapshot.Snap}, nil
   160  }
   161  
   162  type snapshotSetup struct {
   163  	SetID    uint64        `json:"set-id"`
   164  	Snap     string        `json:"snap"`
   165  	Users    []string      `json:"users,omitempty"`
   166  	Filename string        `json:"filename,omitempty"`
   167  	Current  snap.Revision `json:"current"`
   168  	Auto     bool          `json:"auto,omitempty"`
   169  }
   170  
   171  func filename(setID uint64, si *snap.Info) string {
   172  	skel := &client.Snapshot{
   173  		SetID:    setID,
   174  		Snap:     si.InstanceName(),
   175  		Revision: si.Revision,
   176  		Version:  si.Version,
   177  	}
   178  	return backend.Filename(skel)
   179  }
   180  
   181  // prepareSave does all the steps of doSave that require the state lock;
   182  // it has no real significance beyond making the lock handling simpler
   183  func prepareSave(task *state.Task) (snapshot *snapshotSetup, cur *snap.Info, cfg map[string]interface{}, err error) {
   184  	st := task.State()
   185  	st.Lock()
   186  	defer st.Unlock()
   187  
   188  	if err := task.Get("snapshot-setup", &snapshot); err != nil {
   189  		return nil, nil, nil, taskGetErrMsg(task, err, "snapshot")
   190  	}
   191  	cur, err = snapstateCurrentInfo(st, snapshot.Snap)
   192  	if err != nil {
   193  		return nil, nil, nil, err
   194  	}
   195  	// updating snapshot-setup with the filename, for use in undo
   196  	snapshot.Filename = filename(snapshot.SetID, cur)
   197  	task.Set("snapshot-setup", &snapshot)
   198  
   199  	cfg, err = unmarshalSnapConfig(st, snapshot.Snap)
   200  	if err != nil {
   201  		return nil, nil, nil, err
   202  	}
   203  
   204  	// this should be done last because of it modifies the state and the caller needs to undo this if other operation fails.
   205  	if snapshot.Auto {
   206  		expiration, err := AutomaticSnapshotExpiration(st)
   207  		if err != nil {
   208  			return nil, nil, nil, err
   209  		}
   210  		if err := saveExpiration(st, snapshot.SetID, time.Now().Add(expiration)); err != nil {
   211  			return nil, nil, nil, err
   212  		}
   213  	}
   214  
   215  	return snapshot, cur, cfg, nil
   216  }
   217  
   218  func doSave(task *state.Task, tomb *tomb.Tomb) error {
   219  	snapshot, cur, cfg, err := prepareSave(task)
   220  	if err != nil {
   221  		return err
   222  	}
   223  	_, err = backendSave(tomb.Context(nil), snapshot.SetID, cur, cfg, snapshot.Users)
   224  	if err != nil {
   225  		st := task.State()
   226  		st.Lock()
   227  		defer st.Unlock()
   228  		removeSnapshotState(st, snapshot.SetID)
   229  	}
   230  	return err
   231  }
   232  
   233  // prepareRestore does the steps of doRestore that require the state lock
   234  // before the backend Restore call.
   235  func prepareRestore(task *state.Task) (snapshot *snapshotSetup, oldCfg map[string]interface{}, reader *backend.Reader, err error) {
   236  	st := task.State()
   237  
   238  	st.Lock()
   239  	defer st.Unlock()
   240  
   241  	if err := task.Get("snapshot-setup", &snapshot); err != nil {
   242  		return nil, nil, nil, taskGetErrMsg(task, err, "snapshot")
   243  	}
   244  
   245  	oldCfg, err = unmarshalSnapConfig(st, snapshot.Snap)
   246  	if err != nil {
   247  		return nil, nil, nil, err
   248  	}
   249  	reader, err = backendOpen(snapshot.Filename, backend.ExtractFnameSetID)
   250  	if err != nil {
   251  		return nil, nil, nil, fmt.Errorf("cannot open snapshot: %v", err)
   252  	}
   253  	// note given the Open succeeded, caller needs to close it when done
   254  
   255  	return snapshot, oldCfg, reader, nil
   256  }
   257  
   258  // marshalSnapConfig encodes cfg to JSON and returns raw JSON message, unless
   259  // cfg is nil - in this case nil is returned.
   260  func marshalSnapConfig(cfg map[string]interface{}) (*json.RawMessage, error) {
   261  	if cfg == nil {
   262  		// do not marshal nil - this would result in "null" raw message which
   263  		// we want to avoid.
   264  		return nil, nil
   265  	}
   266  	buf, err := json.Marshal(cfg)
   267  	if err != nil {
   268  		return nil, err
   269  	}
   270  	raw := (*json.RawMessage)(&buf)
   271  	return raw, err
   272  }
   273  
   274  func unmarshalSnapConfig(st *state.State, snapName string) (map[string]interface{}, error) {
   275  	rawCfg, err := configGetSnapConfig(st, snapName)
   276  	if err != nil {
   277  		return nil, fmt.Errorf("internal error: cannot obtain current snap config: %v", err)
   278  	}
   279  	var cfg map[string]interface{}
   280  	if rawCfg != nil {
   281  		if err := json.Unmarshal(*rawCfg, &cfg); err != nil {
   282  			return nil, fmt.Errorf("internal error: cannot decode current snap config: %v", err)
   283  		}
   284  	}
   285  	return cfg, nil
   286  }
   287  
   288  func doRestore(task *state.Task, tomb *tomb.Tomb) error {
   289  	snapshot, oldCfg, reader, err := prepareRestore(task)
   290  	if err != nil {
   291  		return err
   292  	}
   293  	defer reader.Close()
   294  
   295  	st := task.State()
   296  	logf := func(format string, args ...interface{}) {
   297  		st.Lock()
   298  		defer st.Unlock()
   299  		task.Logf(format, args...)
   300  	}
   301  
   302  	restoreState, err := backendRestore(reader, tomb.Context(nil), snapshot.Current, snapshot.Users, logf)
   303  	if err != nil {
   304  		return err
   305  	}
   306  
   307  	raw, err := marshalSnapConfig(reader.Conf)
   308  	if err != nil {
   309  		backendRevert(restoreState)
   310  		return fmt.Errorf("cannot marshal saved config: %v", err)
   311  	}
   312  
   313  	st.Lock()
   314  	defer st.Unlock()
   315  
   316  	if err := configSetSnapConfig(st, snapshot.Snap, raw); err != nil {
   317  		backendRevert(restoreState)
   318  		return fmt.Errorf("cannot set snap config: %v", err)
   319  	}
   320  
   321  	restoreState.Config = oldCfg
   322  	task.Set("restore-state", restoreState)
   323  
   324  	return nil
   325  }
   326  
   327  func undoRestore(task *state.Task, _ *tomb.Tomb) error {
   328  	var restoreState backend.RestoreState
   329  	var snapshot snapshotSetup
   330  
   331  	st := task.State()
   332  	st.Lock()
   333  	defer st.Unlock()
   334  
   335  	if err := task.Get("restore-state", &restoreState); err != nil {
   336  		return taskGetErrMsg(task, err, "snapshot restore")
   337  	}
   338  	if err := task.Get("snapshot-setup", &snapshot); err != nil {
   339  		return taskGetErrMsg(task, err, "snapshot")
   340  	}
   341  
   342  	raw, err := marshalSnapConfig(restoreState.Config)
   343  	if err != nil {
   344  		return fmt.Errorf("cannot marshal saved config: %v", err)
   345  	}
   346  
   347  	if err := configSetSnapConfig(st, snapshot.Snap, raw); err != nil {
   348  		return fmt.Errorf("cannot restore saved config: %v", err)
   349  	}
   350  
   351  	backendRevert(&restoreState)
   352  
   353  	return nil
   354  }
   355  
   356  func cleanupRestore(task *state.Task, _ *tomb.Tomb) error {
   357  	var restoreState backend.RestoreState
   358  
   359  	st := task.State()
   360  	st.Lock()
   361  	status := task.Status()
   362  	err := task.Get("restore-state", &restoreState)
   363  	st.Unlock()
   364  
   365  	if status != state.DoneStatus {
   366  		// only need to clean up restores that worked
   367  		return nil
   368  	}
   369  
   370  	if err != nil {
   371  		// this is bad: we somehow lost the information to restore things
   372  		// but if we return the error we'll just get called again :-(
   373  		// TODO: use warnings :-)
   374  		logger.Noticef("%v", taskGetErrMsg(task, err, "snapshot restore"))
   375  		return nil
   376  	}
   377  
   378  	backendCleanup(&restoreState)
   379  
   380  	return nil
   381  }
   382  
   383  func doCheck(task *state.Task, tomb *tomb.Tomb) error {
   384  	var snapshot snapshotSetup
   385  
   386  	st := task.State()
   387  	st.Lock()
   388  	err := task.Get("snapshot-setup", &snapshot)
   389  	st.Unlock()
   390  	if err != nil {
   391  		return taskGetErrMsg(task, err, "snapshot")
   392  	}
   393  
   394  	reader, err := backendOpen(snapshot.Filename, backend.ExtractFnameSetID)
   395  	if err != nil {
   396  		return fmt.Errorf("cannot open snapshot: %v", err)
   397  	}
   398  	defer reader.Close()
   399  
   400  	return backendCheck(reader, tomb.Context(nil), snapshot.Users)
   401  }
   402  
   403  func doForget(task *state.Task, _ *tomb.Tomb) error {
   404  	// note this is also undoSave
   405  	st := task.State()
   406  	st.Lock()
   407  	defer st.Unlock()
   408  
   409  	var snapshot snapshotSetup
   410  	err := task.Get("snapshot-setup", &snapshot)
   411  
   412  	if err != nil {
   413  		return taskGetErrMsg(task, err, "snapshot")
   414  	}
   415  
   416  	if snapshot.Filename == "" {
   417  		return fmt.Errorf("internal error: task %s (%s) snapshot info is missing the filename", task.ID(), task.Kind())
   418  	}
   419  
   420  	// in case it's an automatic snapshot, remove the set also from the state (automatic snapshots have just one snap per set).
   421  	if err := removeSnapshotState(st, snapshot.SetID); err != nil {
   422  		return fmt.Errorf("internal error: cannot remove state of snapshot set %d: %v", snapshot.SetID, err)
   423  	}
   424  
   425  	return osRemove(snapshot.Filename)
   426  }
   427  
   428  func delayedCrossMgrInit() {
   429  	// hook automatic snapshots into snapstate logic
   430  	snapstate.AutomaticSnapshot = AutomaticSnapshot
   431  	snapstate.AutomaticSnapshotExpiration = AutomaticSnapshotExpiration
   432  	snapstate.EstimateSnapshotSize = EstimateSnapshotSize
   433  }
   434  
   435  func MockBackendSave(f func(context.Context, uint64, *snap.Info, map[string]interface{}, []string) (*client.Snapshot, error)) (restore func()) {
   436  	old := backendSave
   437  	backendSave = f
   438  	return func() {
   439  		backendSave = old
   440  	}
   441  }