gopkg.in/ubuntu-core/snappy.v0@v0.0.0-20210902073436-25a8614f10a6/overlord/devicestate/handlers_systems.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  /*
     3   * Copyright (C) 2021 Canonical Ltd
     4   *
     5   * This program is free software: you can redistribute it and/or modify
     6   * it under the terms of the GNU General Public License version 3 as
     7   * published by the Free Software Foundation.
     8   *
     9   * This program is distributed in the hope that it will be useful,
    10   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    11   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    12   * GNU General Public License for more details.
    13   *
    14   * You should have received a copy of the GNU General Public License
    15   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    16   *
    17   */
    18  
    19  package devicestate
    20  
    21  import (
    22  	"bufio"
    23  	"bytes"
    24  	"fmt"
    25  	"io/ioutil"
    26  	"os"
    27  	"path/filepath"
    28  	"strings"
    29  
    30  	"gopkg.in/tomb.v2"
    31  
    32  	"github.com/snapcore/snapd/asserts"
    33  	"github.com/snapcore/snapd/boot"
    34  	"github.com/snapcore/snapd/logger"
    35  	"github.com/snapcore/snapd/osutil"
    36  	"github.com/snapcore/snapd/overlord/assertstate"
    37  	"github.com/snapcore/snapd/overlord/snapstate"
    38  	"github.com/snapcore/snapd/overlord/state"
    39  	"github.com/snapcore/snapd/release"
    40  	"github.com/snapcore/snapd/snap"
    41  	"github.com/snapcore/snapd/snap/snapfile"
    42  	"github.com/snapcore/snapd/strutil"
    43  )
    44  
    45  func taskRecoverySystemSetup(t *state.Task) (*recoverySystemSetup, error) {
    46  	var setup recoverySystemSetup
    47  
    48  	err := t.Get("recovery-system-setup", &setup)
    49  	if err == nil {
    50  		return &setup, nil
    51  	}
    52  	if err != state.ErrNoState {
    53  		return nil, err
    54  	}
    55  	// find the task which holds the data
    56  	var id string
    57  	if err := t.Get("recovery-system-setup-task", &id); err != nil {
    58  		return nil, err
    59  	}
    60  	ts := t.State().Task(id)
    61  	if ts == nil {
    62  		return nil, fmt.Errorf("internal error: cannot find referenced task %v", id)
    63  	}
    64  	if err := ts.Get("recovery-system-setup", &setup); err != nil {
    65  		return nil, err
    66  	}
    67  	return &setup, nil
    68  }
    69  
    70  func setTaskRecoverySystemSetup(t *state.Task, setup *recoverySystemSetup) error {
    71  	if t.Has("recovery-system-setup") {
    72  		t.Set("recovery-system-setup", setup)
    73  		return nil
    74  	}
    75  	return fmt.Errorf("internal error: cannot indirectly set recovery-system-setup")
    76  }
    77  
    78  func logNewSystemSnapFile(logfile, fileName string) error {
    79  	if !strings.HasPrefix(filepath.Dir(fileName), boot.InitramfsUbuntuSeedDir+"/") {
    80  		return fmt.Errorf("internal error: unexpected recovery system snap location %q", fileName)
    81  	}
    82  	currentLog, err := ioutil.ReadFile(logfile)
    83  	if err != nil && !os.IsNotExist(err) {
    84  		return err
    85  	}
    86  	modifiedLog := bytes.NewBuffer(currentLog)
    87  	fmt.Fprintln(modifiedLog, fileName)
    88  	return osutil.AtomicWriteFile(logfile, modifiedLog.Bytes(), 0644, 0)
    89  }
    90  
    91  func purgeNewSystemSnapFiles(logfile string) error {
    92  	f, err := os.Open(logfile)
    93  	if err != nil {
    94  		if os.IsNotExist(err) {
    95  			return nil
    96  		}
    97  		return err
    98  	}
    99  	defer f.Close()
   100  	s := bufio.NewScanner(f)
   101  	for {
   102  		if !s.Scan() {
   103  			break
   104  		}
   105  		// one file per line
   106  		fileName := strings.TrimSpace(s.Text())
   107  		if fileName == "" {
   108  			continue
   109  		}
   110  		if !strings.HasPrefix(fileName, boot.InitramfsUbuntuSeedDir) {
   111  			logger.Noticef("while removing new seed snap %q: unexpected recovery system snap location", fileName)
   112  			continue
   113  		}
   114  		if err := os.Remove(fileName); err != nil && !os.IsNotExist(err) {
   115  			logger.Noticef("while removing new seed snap %q: %v", fileName, err)
   116  		}
   117  	}
   118  	return s.Err()
   119  }
   120  
   121  func (m *DeviceManager) doCreateRecoverySystem(t *state.Task, _ *tomb.Tomb) (err error) {
   122  	if release.OnClassic {
   123  		// TODO: this may need to be lifted in the future
   124  		return fmt.Errorf("cannot create recovery systems on a classic system")
   125  	}
   126  
   127  	st := t.State()
   128  	st.Lock()
   129  	defer st.Unlock()
   130  
   131  	remodelCtx, err := DeviceCtx(st, t, nil)
   132  	if err != nil {
   133  		return err
   134  	}
   135  	model := remodelCtx.Model()
   136  	isRemodel := remodelCtx.ForRemodeling()
   137  
   138  	setup, err := taskRecoverySystemSetup(t)
   139  	if err != nil {
   140  		return fmt.Errorf("internal error: cannot obtain recovery system setup information")
   141  	}
   142  	label := setup.Label
   143  	systemDirectory := setup.Directory
   144  
   145  	// get all infos
   146  	infoGetter := func(name string) (info *snap.Info, present bool, err error) {
   147  		// snap may be present in the system in which case info comes
   148  		// from snapstate
   149  		info, err = snapstate.CurrentInfo(st, name)
   150  		if err == nil {
   151  			hash, _, err := asserts.SnapFileSHA3_384(info.MountFile())
   152  			if err != nil {
   153  				return nil, true, fmt.Errorf("cannot compute SHA3 of snap file: %v", err)
   154  			}
   155  			info.Sha3_384 = hash
   156  			return info, true, nil
   157  		}
   158  		if _, ok := err.(*snap.NotInstalledError); !ok {
   159  			return nil, false, err
   160  		}
   161  		logger.Debugf("requested info for not yet installed snap %q", name)
   162  
   163  		if !isRemodel {
   164  			// when not in remodel, a recovery system can only be
   165  			// created from snaps that are already installed
   166  			return nil, false, nil
   167  		}
   168  
   169  		// in a remodel scenario, the snaps may need to be fetched, and
   170  		// thus we can pull the relevant information from the tasks
   171  		// carrying snap-setup
   172  
   173  		for _, tskID := range setup.SnapSetupTasks {
   174  			taskWithSnapSetup := st.Task(tskID)
   175  			snapsup, err := snapstate.TaskSnapSetup(taskWithSnapSetup)
   176  			if err != nil {
   177  				return nil, false, err
   178  			}
   179  			if snapsup.SnapName() != name {
   180  				continue
   181  			}
   182  			// by the time this task runs, the file has already been
   183  			// downloaded and validated
   184  			snapFile, err := snapfile.Open(snapsup.MountFile())
   185  			if err != nil {
   186  				return nil, false, err
   187  			}
   188  			info, err = snap.ReadInfoFromSnapFile(snapFile, snapsup.SideInfo)
   189  			if err != nil {
   190  				return nil, false, err
   191  			}
   192  
   193  			return info, true, nil
   194  		}
   195  		return nil, false, nil
   196  	}
   197  
   198  	observeSnapFileWrite := func(recoverySystemDir, where string) error {
   199  		if recoverySystemDir != systemDirectory {
   200  			return fmt.Errorf("internal error: unexpected recovery system path %q", recoverySystemDir)
   201  		}
   202  		// track all the files, both asserted shared snaps and private
   203  		// ones
   204  		return logNewSystemSnapFile(filepath.Join(recoverySystemDir, "snapd-new-file-log"), where)
   205  	}
   206  
   207  	var db asserts.RODatabase
   208  	if isRemodel {
   209  		// during remodel, the model assertion is not yet present in the
   210  		// assertstate database, hence we need to use a temporary one to
   211  		// which we explicitly add the new model assertion, as
   212  		// createSystemForModelFromValidatedSnaps expects all relevant
   213  		// assertions to be present in the passed db
   214  		tempDB := assertstate.TemporaryDB(st)
   215  		if err := tempDB.Add(model); err != nil {
   216  			return fmt.Errorf("cannot create a temporary database with model: %v", err)
   217  		}
   218  		db = tempDB
   219  	} else {
   220  		db = assertstate.DB(st)
   221  	}
   222  	defer func() {
   223  		if err == nil {
   224  			return
   225  		}
   226  		if err := purgeNewSystemSnapFiles(filepath.Join(systemDirectory, "snapd-new-file-log")); err != nil {
   227  			logger.Noticef("when removing seed files: %v", err)
   228  		}
   229  		// this is ok, as before the change with this task was created,
   230  		// we checked that the system directory did not exist; it may
   231  		// exist now if one of the post-create steps failed, or the the
   232  		// task is being re-run after a reboot and creating a system
   233  		// failed
   234  		if err := os.RemoveAll(systemDirectory); err != nil && !os.IsNotExist(err) {
   235  			logger.Noticef("when removing recovery system %q: %v", label, err)
   236  		}
   237  		if err := boot.DropRecoverySystem(remodelCtx, label); err != nil {
   238  			logger.Noticef("when dropping the recovery system %q: %v", label, err)
   239  		}
   240  		// we could have reentered the task after a reboot, but the
   241  		// state was set up sufficiently such that the system was
   242  		// actually tried and ended up in the tried systems list, which
   243  		// we should reset now
   244  		st.Set("tried-systems", nil)
   245  	}()
   246  	// 1. prepare recovery system from remodel snaps (or current snaps)
   247  	// TODO: this fails when there is a partially complete system seed which
   248  	// creation could have been interrupted by an unexpected reboot;
   249  	// consider clearing the recovery system directory and restarting from
   250  	// scratch
   251  	_, err = createSystemForModelFromValidatedSnaps(model, label, db, infoGetter, observeSnapFileWrite)
   252  	if err != nil {
   253  		return fmt.Errorf("cannot create a recovery system with label %q for %v: %v", label, model.Model(), err)
   254  	}
   255  	logger.Debugf("recovery system dir: %v", systemDirectory)
   256  
   257  	// 2. keep track of the system in task state
   258  	if err := setTaskRecoverySystemSetup(t, setup); err != nil {
   259  		return fmt.Errorf("cannot record recovery system setup state: %v", err)
   260  	}
   261  	// 3. set up boot variables for tracking the tried system state
   262  	if err := boot.SetTryRecoverySystem(remodelCtx, label); err != nil {
   263  		// rollback?
   264  		return fmt.Errorf("cannot attempt booting into recovery system %q: %v", label, err)
   265  	}
   266  	// 4. and set up the next boot that that system
   267  	if err := boot.SetRecoveryBootSystemAndMode(remodelCtx, label, "recover"); err != nil {
   268  		return fmt.Errorf("cannot set device to boot into candidate system %q: %v", label, err)
   269  	}
   270  
   271  	// this task is done, further processing happens in finalize
   272  	t.SetStatus(state.DoneStatus)
   273  
   274  	logger.Noticef("restarting into candidate system %q", label)
   275  	m.state.RequestRestart(state.RestartSystemNow)
   276  	return nil
   277  }
   278  
   279  func (m *DeviceManager) undoCreateRecoverySystem(t *state.Task, _ *tomb.Tomb) error {
   280  	if release.OnClassic {
   281  		// TODO: this may need to be lifted in the future
   282  		return fmt.Errorf("internal error: cannot create recovery systems on a classic system")
   283  	}
   284  
   285  	st := t.State()
   286  	st.Lock()
   287  	defer st.Unlock()
   288  
   289  	remodelCtx, err := DeviceCtx(st, t, nil)
   290  	if err != nil {
   291  		return err
   292  	}
   293  
   294  	setup, err := taskRecoverySystemSetup(t)
   295  	if err != nil {
   296  		return fmt.Errorf("internal error: cannot obtain recovery system setup information")
   297  	}
   298  	label := setup.Label
   299  
   300  	var undoErr error
   301  
   302  	if err := purgeNewSystemSnapFiles(filepath.Join(setup.Directory, "snapd-new-file-log")); err != nil {
   303  		t.Logf("when removing seed files: %v", err)
   304  	}
   305  	if err := os.RemoveAll(setup.Directory); err != nil && !os.IsNotExist(err) {
   306  		t.Logf("when removing recovery system %q: %v", label, err)
   307  		undoErr = err
   308  	} else {
   309  		t.Logf("removed recovery system directory %v", setup.Directory)
   310  	}
   311  
   312  	if err := boot.DropRecoverySystem(remodelCtx, label); err != nil {
   313  		return fmt.Errorf("cannot drop a current recovery system %q: %v", label, err)
   314  	}
   315  
   316  	return undoErr
   317  }
   318  
   319  func (m *DeviceManager) doFinalizeTriedRecoverySystem(t *state.Task, _ *tomb.Tomb) error {
   320  	if release.OnClassic {
   321  		// TODO: this may need to be lifted in the future
   322  		return fmt.Errorf("internal error: cannot finalize recovery systems on a classic system")
   323  	}
   324  
   325  	st := t.State()
   326  	st.Lock()
   327  	defer st.Unlock()
   328  
   329  	if ok, _ := st.Restarting(); ok {
   330  		// don't continue until we are in the restarted snapd
   331  		t.Logf("Waiting for system reboot...")
   332  		return &state.Retry{}
   333  	}
   334  
   335  	remodelCtx, err := DeviceCtx(st, t, nil)
   336  	if err != nil {
   337  		return err
   338  	}
   339  	isRemodel := remodelCtx.ForRemodeling()
   340  
   341  	var triedSystems []string
   342  	// after rebooting to the recovery system and back, the system got moved
   343  	// to the tried-systems list in the state
   344  	if err := st.Get("tried-systems", &triedSystems); err != nil {
   345  		return fmt.Errorf("cannot obtain tried recovery systems: %v", err)
   346  	}
   347  
   348  	setup, err := taskRecoverySystemSetup(t)
   349  	if err != nil {
   350  		return err
   351  	}
   352  	label := setup.Label
   353  
   354  	logger.Debugf("finalize recovery system with label %q", label)
   355  
   356  	if isRemodel {
   357  		// so far so good, a recovery system created during remodel was
   358  		// tested successfully
   359  		if !strutil.ListContains(triedSystems, label) {
   360  			// system failed, trigger undoing of everything we did so far
   361  			return fmt.Errorf("tried recovery system %q failed", label)
   362  		}
   363  
   364  		// XXX: candidate system is promoted to the list of good ones once we
   365  		// complete the whole remodel change
   366  		logger.Debugf("recovery system created during remodel will be promoted later")
   367  	} else {
   368  		if err := boot.PromoteTriedRecoverySystem(remodelCtx, label, triedSystems); err != nil {
   369  			return fmt.Errorf("cannot promote recovery system %q: %v", label, err)
   370  		}
   371  
   372  		// tried systems should be a one item list, we can clear it now
   373  		st.Set("tried-systems", nil)
   374  	}
   375  
   376  	// we are done
   377  	t.SetStatus(state.DoneStatus)
   378  
   379  	return nil
   380  }
   381  
   382  func (m *DeviceManager) undoFinalizeTriedRecoverySystem(t *state.Task, _ *tomb.Tomb) error {
   383  	st := t.State()
   384  	st.Lock()
   385  	defer st.Unlock()
   386  
   387  	remodelCtx, err := DeviceCtx(st, t, nil)
   388  	if err != nil {
   389  		return err
   390  	}
   391  
   392  	setup, err := taskRecoverySystemSetup(t)
   393  	if err != nil {
   394  		return err
   395  	}
   396  	label := setup.Label
   397  
   398  	if err := boot.DropRecoverySystem(remodelCtx, label); err != nil {
   399  		return fmt.Errorf("cannot drop a good recovery system %q: %v", label, err)
   400  	}
   401  
   402  	return nil
   403  }
   404  
   405  func (m *DeviceManager) cleanupRecoverySystem(t *state.Task, _ *tomb.Tomb) error {
   406  	st := t.State()
   407  	st.Lock()
   408  	defer st.Unlock()
   409  
   410  	setup, err := taskRecoverySystemSetup(t)
   411  	if err != nil {
   412  		return err
   413  	}
   414  	if os.Remove(filepath.Join(setup.Directory, "snapd-new-file-log")); err != nil && !os.IsNotExist(err) {
   415  		return err
   416  	}
   417  	return nil
   418  }