github.com/chipaca/snappy@v0.0.0-20210104084008-1f06296fe8ad/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 := checkSnapshotTaskConflict(mgr.state, r.SetID, "check-snapshot", "restore-snapshot"); err != nil {
   116  			// there is a conflict, do nothing and we will retry this set on next Ensure().
   117  			return nil
   118  		}
   119  		if sets[r.SetID] {
   120  			delete(sets, r.SetID)
   121  			// remove from state first: in case removeSnapshotState succeeds but osRemove fails we will never attempt
   122  			// to automatically remove this snapshot again and will leave it on the disk (so the user can still try to remove it manually);
   123  			// this is better than the other way around where a failing osRemove would be retried forever because snapshot would never
   124  			// leave the state.
   125  			if err := removeSnapshotState(mgr.state, r.SetID); err != nil {
   126  				return fmt.Errorf("internal error: cannot remove state of snapshot set %d: %v", r.SetID, err)
   127  			}
   128  			if err := osRemove(r.Name()); err != nil {
   129  				return fmt.Errorf("cannot remove snapshot file %q: %v", r.Name(), err)
   130  			}
   131  		}
   132  		return nil
   133  	})
   134  
   135  	if err != nil {
   136  		return fmt.Errorf("cannot process expired snapshots: %v", err)
   137  	}
   138  
   139  	// only reset time if there are no sets left because of conflicts
   140  	if len(sets) == 0 {
   141  		mgr.lastForgetExpiredSnapshotTime = time.Now()
   142  	}
   143  
   144  	return nil
   145  }
   146  
   147  func (SnapshotManager) affectedSnaps(t *state.Task) ([]string, error) {
   148  	if k := t.Kind(); k == "check-snapshot" || k == "forget-snapshot" {
   149  		// check and forget don't affect snaps
   150  		// (this could also be written k != save && k != restore, but it's safer this way around)
   151  		return nil, nil
   152  	}
   153  	var snapshot snapshotSetup
   154  	if err := t.Get("snapshot-setup", &snapshot); err != nil {
   155  		return nil, taskGetErrMsg(t, err, "snapshot")
   156  	}
   157  
   158  	return []string{snapshot.Snap}, nil
   159  }
   160  
   161  type snapshotSetup struct {
   162  	SetID    uint64        `json:"set-id"`
   163  	Snap     string        `json:"snap"`
   164  	Users    []string      `json:"users,omitempty"`
   165  	Filename string        `json:"filename,omitempty"`
   166  	Current  snap.Revision `json:"current"`
   167  	Auto     bool          `json:"auto,omitempty"`
   168  }
   169  
   170  func filename(setID uint64, si *snap.Info) string {
   171  	skel := &client.Snapshot{
   172  		SetID:    setID,
   173  		Snap:     si.InstanceName(),
   174  		Revision: si.Revision,
   175  		Version:  si.Version,
   176  	}
   177  	return backend.Filename(skel)
   178  }
   179  
   180  // prepareSave does all the steps of doSave that require the state lock;
   181  // it has no real significance beyond making the lock handling simpler
   182  func prepareSave(task *state.Task) (snapshot *snapshotSetup, cur *snap.Info, cfg map[string]interface{}, err error) {
   183  	st := task.State()
   184  	st.Lock()
   185  	defer st.Unlock()
   186  
   187  	if err := task.Get("snapshot-setup", &snapshot); err != nil {
   188  		return nil, nil, nil, taskGetErrMsg(task, err, "snapshot")
   189  	}
   190  	cur, err = snapstateCurrentInfo(st, snapshot.Snap)
   191  	if err != nil {
   192  		return nil, nil, nil, err
   193  	}
   194  	// updating snapshot-setup with the filename, for use in undo
   195  	snapshot.Filename = filename(snapshot.SetID, cur)
   196  	task.Set("snapshot-setup", &snapshot)
   197  
   198  	rawCfg, err := configGetSnapConfig(st, snapshot.Snap)
   199  	if err != nil {
   200  		return nil, nil, nil, err
   201  	}
   202  	if rawCfg != nil {
   203  		if err := json.Unmarshal(*rawCfg, &cfg); err != nil {
   204  			return nil, nil, nil, err
   205  		}
   206  	}
   207  
   208  	// this should be done last because of it modifies the state and the caller needs to undo this if other operation fails.
   209  	if snapshot.Auto {
   210  		expiration, err := AutomaticSnapshotExpiration(st)
   211  		if err != nil {
   212  			return nil, nil, nil, err
   213  		}
   214  		if err := saveExpiration(st, snapshot.SetID, time.Now().Add(expiration)); err != nil {
   215  			return nil, nil, nil, err
   216  		}
   217  	}
   218  
   219  	return snapshot, cur, cfg, nil
   220  }
   221  
   222  func doSave(task *state.Task, tomb *tomb.Tomb) error {
   223  	snapshot, cur, cfg, err := prepareSave(task)
   224  	if err != nil {
   225  		return err
   226  	}
   227  	_, err = backendSave(tomb.Context(nil), snapshot.SetID, cur, cfg, snapshot.Users)
   228  	if err != nil {
   229  		st := task.State()
   230  		st.Lock()
   231  		defer st.Unlock()
   232  		removeSnapshotState(st, snapshot.SetID)
   233  	}
   234  	return err
   235  }
   236  
   237  // prepareRestore does the steps of doRestore that require the state lock
   238  // before the backend Restore call.
   239  func prepareRestore(task *state.Task) (snapshot *snapshotSetup, oldCfg map[string]interface{}, reader *backend.Reader, err error) {
   240  	st := task.State()
   241  
   242  	st.Lock()
   243  	defer st.Unlock()
   244  
   245  	if err := task.Get("snapshot-setup", &snapshot); err != nil {
   246  		return nil, nil, nil, taskGetErrMsg(task, err, "snapshot")
   247  	}
   248  
   249  	rawCfg, err := configGetSnapConfig(st, snapshot.Snap)
   250  	if err != nil {
   251  		return nil, nil, nil, fmt.Errorf("internal error: cannot obtain current snap config for snapshot restore: %v", err)
   252  	}
   253  
   254  	if rawCfg != nil {
   255  		if err := json.Unmarshal(*rawCfg, &oldCfg); err != nil {
   256  			return nil, nil, nil, fmt.Errorf("internal error: cannot decode current snap config: %v", err)
   257  		}
   258  	}
   259  
   260  	reader, err = backendOpen(snapshot.Filename, backend.ExtractFnameSetID)
   261  	if err != nil {
   262  		return nil, nil, nil, fmt.Errorf("cannot open snapshot: %v", err)
   263  	}
   264  	// note given the Open succeeded, caller needs to close it when done
   265  
   266  	return snapshot, oldCfg, reader, nil
   267  }
   268  
   269  func doRestore(task *state.Task, tomb *tomb.Tomb) error {
   270  	snapshot, oldCfg, reader, err := prepareRestore(task)
   271  	if err != nil {
   272  		return err
   273  	}
   274  	defer reader.Close()
   275  
   276  	st := task.State()
   277  	logf := func(format string, args ...interface{}) {
   278  		st.Lock()
   279  		defer st.Unlock()
   280  		task.Logf(format, args...)
   281  	}
   282  
   283  	restoreState, err := backendRestore(reader, tomb.Context(nil), snapshot.Current, snapshot.Users, logf)
   284  	if err != nil {
   285  		return err
   286  	}
   287  
   288  	buf, err := json.Marshal(reader.Conf)
   289  	if err != nil {
   290  		backendRevert(restoreState)
   291  		return fmt.Errorf("cannot marshal saved config: %v", err)
   292  	}
   293  
   294  	st.Lock()
   295  	defer st.Unlock()
   296  
   297  	if err := configSetSnapConfig(st, snapshot.Snap, (*json.RawMessage)(&buf)); err != nil {
   298  		backendRevert(restoreState)
   299  		return fmt.Errorf("cannot set snap config: %v", err)
   300  	}
   301  
   302  	restoreState.Config = oldCfg
   303  	task.Set("restore-state", restoreState)
   304  
   305  	return nil
   306  }
   307  
   308  func undoRestore(task *state.Task, _ *tomb.Tomb) error {
   309  	var restoreState backend.RestoreState
   310  	var snapshot snapshotSetup
   311  
   312  	st := task.State()
   313  	st.Lock()
   314  	defer st.Unlock()
   315  
   316  	if err := task.Get("restore-state", &restoreState); err != nil {
   317  		return taskGetErrMsg(task, err, "snapshot restore")
   318  	}
   319  	if err := task.Get("snapshot-setup", &snapshot); err != nil {
   320  		return taskGetErrMsg(task, err, "snapshot")
   321  	}
   322  
   323  	buf, err := json.Marshal(restoreState.Config)
   324  	if err != nil {
   325  		return fmt.Errorf("cannot marshal saved config: %v", err)
   326  	}
   327  
   328  	if err := configSetSnapConfig(st, snapshot.Snap, (*json.RawMessage)(&buf)); err != nil {
   329  		return fmt.Errorf("cannot restore saved config: %v", err)
   330  	}
   331  
   332  	backendRevert(&restoreState)
   333  
   334  	return nil
   335  }
   336  
   337  func cleanupRestore(task *state.Task, _ *tomb.Tomb) error {
   338  	var restoreState backend.RestoreState
   339  
   340  	st := task.State()
   341  	st.Lock()
   342  	status := task.Status()
   343  	err := task.Get("restore-state", &restoreState)
   344  	st.Unlock()
   345  
   346  	if status != state.DoneStatus {
   347  		// only need to clean up restores that worked
   348  		return nil
   349  	}
   350  
   351  	if err != nil {
   352  		// this is bad: we somehow lost the information to restore things
   353  		// but if we return the error we'll just get called again :-(
   354  		// TODO: use warnings :-)
   355  		logger.Noticef("%v", taskGetErrMsg(task, err, "snapshot restore"))
   356  		return nil
   357  	}
   358  
   359  	backendCleanup(&restoreState)
   360  
   361  	return nil
   362  }
   363  
   364  func doCheck(task *state.Task, tomb *tomb.Tomb) error {
   365  	var snapshot snapshotSetup
   366  
   367  	st := task.State()
   368  	st.Lock()
   369  	err := task.Get("snapshot-setup", &snapshot)
   370  	st.Unlock()
   371  	if err != nil {
   372  		return taskGetErrMsg(task, err, "snapshot")
   373  	}
   374  
   375  	reader, err := backendOpen(snapshot.Filename, backend.ExtractFnameSetID)
   376  	if err != nil {
   377  		return fmt.Errorf("cannot open snapshot: %v", err)
   378  	}
   379  	defer reader.Close()
   380  
   381  	return backendCheck(reader, tomb.Context(nil), snapshot.Users)
   382  }
   383  
   384  func doForget(task *state.Task, _ *tomb.Tomb) error {
   385  	// note this is also undoSave
   386  	st := task.State()
   387  	st.Lock()
   388  	defer st.Unlock()
   389  
   390  	var snapshot snapshotSetup
   391  	err := task.Get("snapshot-setup", &snapshot)
   392  
   393  	if err != nil {
   394  		return taskGetErrMsg(task, err, "snapshot")
   395  	}
   396  
   397  	if snapshot.Filename == "" {
   398  		return fmt.Errorf("internal error: task %s (%s) snapshot info is missing the filename", task.ID(), task.Kind())
   399  	}
   400  
   401  	// in case it's an automatic snapshot, remove the set also from the state (automatic snapshots have just one snap per set).
   402  	if err := removeSnapshotState(st, snapshot.SetID); err != nil {
   403  		return fmt.Errorf("internal error: cannot remove state of snapshot set %d: %v", snapshot.SetID, err)
   404  	}
   405  
   406  	return osRemove(snapshot.Filename)
   407  }
   408  
   409  func delayedCrossMgrInit() {
   410  	// hook automatic snapshots into snapstate logic
   411  	snapstate.AutomaticSnapshot = AutomaticSnapshot
   412  	snapstate.AutomaticSnapshotExpiration = AutomaticSnapshotExpiration
   413  	snapstate.EstimateSnapshotSize = EstimateSnapshotSize
   414  }
   415  
   416  func MockBackendSave(f func(context.Context, uint64, *snap.Info, map[string]interface{}, []string) (*client.Snapshot, error)) (restore func()) {
   417  	old := backendSave
   418  	backendSave = f
   419  	return func() {
   420  		backendSave = old
   421  	}
   422  }