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