github.com/david-imola/snapd@v0.0.0-20210611180407-2de8ddeece6d/gadget/update.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2019-2020 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 gadget
    21  
    22  import (
    23  	"errors"
    24  	"fmt"
    25  	"strings"
    26  
    27  	"github.com/snapcore/snapd/gadget/quantity"
    28  	"github.com/snapcore/snapd/kernel"
    29  	"github.com/snapcore/snapd/logger"
    30  )
    31  
    32  var (
    33  	ErrNoUpdate = errors.New("nothing to update")
    34  )
    35  
    36  var (
    37  	// default positioning constraints that match ubuntu-image
    38  	DefaultConstraints = LayoutConstraints{
    39  		NonMBRStartOffset: 1 * quantity.OffsetMiB,
    40  	}
    41  )
    42  
    43  // GadgetData holds references to a gadget revision metadata and its data directory.
    44  type GadgetData struct {
    45  	// Info is the gadget metadata
    46  	Info *Info
    47  	// XXX: should be GadgetRootDir
    48  	// RootDir is the root directory of gadget snap data
    49  	RootDir string
    50  
    51  	// KernelRootDir is the root directory of kernel snap data
    52  	KernelRootDir string
    53  }
    54  
    55  // UpdatePolicyFunc is a callback that evaluates the provided pair of
    56  // (potentially not yet resolved) structures and returns true when the
    57  // pair should be part of an update. It may also return a filter
    58  // function for the resolved content when not all of the content
    59  // should be applied as part of the update (e.g. when updating assets
    60  // from the kernel snap).
    61  type UpdatePolicyFunc func(from, to *LaidOutStructure) (bool, ResolvedContentFilterFunc)
    62  
    63  // ResolvedContentFilterFunc is a callback that evaluates the given
    64  // ResolvedContent and returns true if it should be applied as part of
    65  // an update. This is relevant for e.g. asset updates that come from
    66  // the kernel snap.
    67  type ResolvedContentFilterFunc func(*ResolvedContent) bool
    68  
    69  // ContentChange carries paths to files containing the content data being
    70  // modified by the operation.
    71  type ContentChange struct {
    72  	// Before is a path to a file containing the original data before the
    73  	// operation takes place (or took place in case of ContentRollback).
    74  	Before string
    75  	// After is a path to a file location of the data applied by the operation.
    76  	After string
    77  }
    78  
    79  type ContentOperation int
    80  type ContentChangeAction int
    81  
    82  const (
    83  	ContentWrite ContentOperation = iota
    84  	ContentUpdate
    85  	ContentRollback
    86  
    87  	ChangeAbort ContentChangeAction = iota
    88  	ChangeApply
    89  	ChangeIgnore
    90  )
    91  
    92  // ContentObserver allows for observing operations on the content of the gadget
    93  // structures.
    94  type ContentObserver interface {
    95  	// Observe is called to observe an pending or completed action, related
    96  	// to content being written, updated or being rolled back. In each of
    97  	// the scenarios, the target path is relative under the root.
    98  	//
    99  	// For a file write or update, the source path points to the content
   100  	// that will be written. When called during rollback, observe call
   101  	// happens after the original file has been restored (or removed if the
   102  	// file was added during the update), the source path is empty.
   103  	//
   104  	// Returning ChangeApply indicates that the observer agrees for a given
   105  	// change to be applied. When called with a ContentUpdate or
   106  	// ContentWrite operation, returning ChangeIgnore indicates that the
   107  	// change shall be ignored. ChangeAbort is expected to be returned along
   108  	// with a non-nil error.
   109  	Observe(op ContentOperation, sourceStruct *LaidOutStructure,
   110  		targetRootDir, relativeTargetPath string, dataChange *ContentChange) (ContentChangeAction, error)
   111  }
   112  
   113  // ContentUpdateObserver allows for observing update (and potentially a
   114  // rollback) of the gadget structure content.
   115  type ContentUpdateObserver interface {
   116  	ContentObserver
   117  	// BeforeWrite is called when the backups of content that will get
   118  	// modified during the update are complete and update is ready to be
   119  	// applied.
   120  	BeforeWrite() error
   121  	// Canceled is called when the update has been canceled, or if changes
   122  	// were written and the update has been reverted.
   123  	Canceled() error
   124  }
   125  
   126  // Update applies the gadget update given the gadget information and data from
   127  // old and new revisions. It errors out when the update is not possible or
   128  // illegal, or a failure occurs at any of the steps. When there is no update, a
   129  // special error ErrNoUpdate is returned.
   130  //
   131  // Only structures selected by the update policy are part of the update. When
   132  // the policy is nil, a default one is used. The default policy selects
   133  // structures in an opt-in manner, only tructures with a higher value of Edition
   134  // field in the new gadget definition are part of the update.
   135  //
   136  // Data that would be modified during the update is first backed up inside the
   137  // rollback directory. Should the apply step fail, the modified data is
   138  // recovered.
   139  //
   140  //
   141  // The rules for gadget/kernel updates with "$kernel:refs":
   142  //
   143  // 1. When installing a kernel with assets that have "update: true"
   144  //    there *must* be a matching entry in gadget.yaml. If not we risk
   145  //    bricking the system because the kernel tells us that it *needs*
   146  //    this file to boot but without gadget.yaml we would not put it
   147  //    anywhere.
   148  // 2. When installing a gadget with "$kernel:ref" content it is okay
   149  //    if this content cannot get resolved as long as there is no
   150  //    "edition" jump. This means adding new "$kernel:ref" without
   151  //    "edition" updates is always possible.
   152  //
   153  // To add a new "$kernel:ref" to gadget/kernel:
   154  // a. Update gadget and gadget.yaml and add "$kernel:ref" but do not
   155  //    update edition (if edition update is needed, use epoch)
   156  // b. Update kernel and kernel.yaml with new assets.
   157  // c. snapd will refresh gadget (see rule 2) but refuse to take the
   158  //    new kernel (rule 1)
   159  // d. After step (c) is completed the kernel refresh will now also
   160  //    work (no more violation of rule 1)
   161  func Update(old, new GadgetData, rollbackDirPath string, updatePolicy UpdatePolicyFunc, observer ContentUpdateObserver) error {
   162  	// TODO: support multi-volume gadgets. But for now we simply
   163  	//       do not do any gadget updates on those. We cannot error
   164  	//       here because this would break refreshes of gadgets even
   165  	//       when they don't require any updates.
   166  	if len(new.Info.Volumes) != 1 || len(old.Info.Volumes) != 1 {
   167  		logger.Noticef("WARNING: gadget assests cannot be updated yet when multiple volumes are used")
   168  		return nil
   169  	}
   170  
   171  	oldVol, newVol, err := resolveVolume(old.Info, new.Info)
   172  	if err != nil {
   173  		return err
   174  	}
   175  
   176  	if oldVol.Schema == "" || newVol.Schema == "" {
   177  		return fmt.Errorf("internal error: unset volume schemas: old: %q new: %q", oldVol.Schema, newVol.Schema)
   178  	}
   179  
   180  	// layout old partially, without going deep into the layout of structure
   181  	// content
   182  	pOld, err := LayoutVolumePartially(oldVol, DefaultConstraints)
   183  	if err != nil {
   184  		return fmt.Errorf("cannot lay out the old volume: %v", err)
   185  	}
   186  
   187  	// Layout new volume, delay resolving of filesystem content
   188  	constraints := DefaultConstraints
   189  	constraints.SkipResolveContent = true
   190  	pNew, err := LayoutVolume(new.RootDir, new.KernelRootDir, newVol, constraints)
   191  	if err != nil {
   192  		return fmt.Errorf("cannot lay out the new volume: %v", err)
   193  	}
   194  
   195  	if err := canUpdateVolume(pOld, pNew); err != nil {
   196  		return fmt.Errorf("cannot apply update to volume: %v", err)
   197  	}
   198  
   199  	if updatePolicy == nil {
   200  		updatePolicy = defaultPolicy
   201  	}
   202  
   203  	// ensure all required kernel assets are found in the gadget
   204  	kernelInfo, err := kernel.ReadInfo(new.KernelRootDir)
   205  	if err != nil {
   206  		return err
   207  	}
   208  	if err := gadgetVolumeConsumesOneKernelUpdateAsset(pNew.Volume, kernelInfo); err != nil {
   209  		return err
   210  	}
   211  
   212  	// now we know which structure is which, find which ones need an update
   213  	updates, err := resolveUpdate(pOld, pNew, updatePolicy, new.RootDir, new.KernelRootDir, kernelInfo)
   214  	if err != nil {
   215  		return err
   216  	}
   217  	if len(updates) == 0 {
   218  		// nothing to update
   219  		return ErrNoUpdate
   220  	}
   221  
   222  	// can update old layout to new layout
   223  	for _, update := range updates {
   224  		if err := canUpdateStructure(update.from, update.to, pNew.Schema); err != nil {
   225  			return fmt.Errorf("cannot update volume structure %v: %v", update.to, err)
   226  		}
   227  	}
   228  
   229  	return applyUpdates(new, updates, rollbackDirPath, observer)
   230  }
   231  
   232  func resolveVolume(old *Info, new *Info) (oldVol, newVol *Volume, err error) {
   233  	// support only one volume
   234  	if len(new.Volumes) != 1 || len(old.Volumes) != 1 {
   235  		return nil, nil, errors.New("cannot update with more than one volume")
   236  	}
   237  
   238  	var name string
   239  	for n := range old.Volumes {
   240  		name = n
   241  		break
   242  	}
   243  	oldV := old.Volumes[name]
   244  
   245  	newV, ok := new.Volumes[name]
   246  	if !ok {
   247  		return nil, nil, fmt.Errorf("cannot find entry for volume %q in updated gadget info", name)
   248  	}
   249  
   250  	return oldV, newV, nil
   251  }
   252  
   253  func isSameOffset(one *quantity.Offset, two *quantity.Offset) bool {
   254  	if one == nil && two == nil {
   255  		return true
   256  	}
   257  	if one != nil && two != nil {
   258  		return *one == *two
   259  	}
   260  	return false
   261  }
   262  
   263  func isSameRelativeOffset(one *RelativeOffset, two *RelativeOffset) bool {
   264  	if one == nil && two == nil {
   265  		return true
   266  	}
   267  	if one != nil && two != nil {
   268  		return *one == *two
   269  	}
   270  	return false
   271  }
   272  
   273  func isLegacyMBRTransition(from *LaidOutStructure, to *LaidOutStructure) bool {
   274  	// legacy MBR could have been specified by setting type: mbr, with no
   275  	// role
   276  	return from.Type == schemaMBR && to.Role == schemaMBR
   277  }
   278  
   279  func canUpdateStructure(from *LaidOutStructure, to *LaidOutStructure, schema string) error {
   280  	if schema == schemaGPT && from.Name != to.Name {
   281  		// partition names are only effective when GPT is used
   282  		return fmt.Errorf("cannot change structure name from %q to %q", from.Name, to.Name)
   283  	}
   284  	if from.Size != to.Size {
   285  		return fmt.Errorf("cannot change structure size from %v to %v", from.Size, to.Size)
   286  	}
   287  	if !isSameOffset(from.Offset, to.Offset) {
   288  		return fmt.Errorf("cannot change structure offset from %v to %v", from.Offset, to.Offset)
   289  	}
   290  	if from.StartOffset != to.StartOffset {
   291  		return fmt.Errorf("cannot change structure start offset from %v to %v", from.StartOffset, to.StartOffset)
   292  	}
   293  	// TODO: should this limitation be lifted?
   294  	if !isSameRelativeOffset(from.OffsetWrite, to.OffsetWrite) {
   295  		return fmt.Errorf("cannot change structure offset-write from %v to %v", from.OffsetWrite, to.OffsetWrite)
   296  	}
   297  	if from.Role != to.Role {
   298  		return fmt.Errorf("cannot change structure role from %q to %q", from.Role, to.Role)
   299  	}
   300  	if from.Type != to.Type {
   301  		if !isLegacyMBRTransition(from, to) {
   302  			return fmt.Errorf("cannot change structure type from %q to %q", from.Type, to.Type)
   303  		}
   304  	}
   305  	if from.ID != to.ID {
   306  		return fmt.Errorf("cannot change structure ID from %q to %q", from.ID, to.ID)
   307  	}
   308  	if to.HasFilesystem() {
   309  		if !from.HasFilesystem() {
   310  			return fmt.Errorf("cannot change a bare structure to filesystem one")
   311  		}
   312  		if from.Filesystem != to.Filesystem {
   313  			return fmt.Errorf("cannot change filesystem from %q to %q",
   314  				from.Filesystem, to.Filesystem)
   315  		}
   316  		if from.Label != to.Label {
   317  			return fmt.Errorf("cannot change filesystem label from %q to %q",
   318  				from.Label, to.Label)
   319  		}
   320  	} else {
   321  		if from.HasFilesystem() {
   322  			return fmt.Errorf("cannot change a filesystem structure to a bare one")
   323  		}
   324  	}
   325  
   326  	return nil
   327  }
   328  
   329  func canUpdateVolume(from *PartiallyLaidOutVolume, to *LaidOutVolume) error {
   330  	if from.ID != to.ID {
   331  		return fmt.Errorf("cannot change volume ID from %q to %q", from.ID, to.ID)
   332  	}
   333  	if from.Schema != to.Schema {
   334  		return fmt.Errorf("cannot change volume schema from %q to %q", from.Schema, to.Schema)
   335  	}
   336  	if len(from.LaidOutStructure) != len(to.LaidOutStructure) {
   337  		return fmt.Errorf("cannot change the number of structures within volume from %v to %v", len(from.LaidOutStructure), len(to.LaidOutStructure))
   338  	}
   339  	return nil
   340  }
   341  
   342  type updatePair struct {
   343  	from *LaidOutStructure
   344  	to   *LaidOutStructure
   345  }
   346  
   347  func defaultPolicy(from, to *LaidOutStructure) (bool, ResolvedContentFilterFunc) {
   348  	return to.Update.Edition > from.Update.Edition, nil
   349  }
   350  
   351  // RemodelUpdatePolicy implements the update policy of a remodel scenario. The
   352  // policy selects all non-MBR structures for the update.
   353  func RemodelUpdatePolicy(from, to *LaidOutStructure) (bool, ResolvedContentFilterFunc) {
   354  	if from.Role == schemaMBR {
   355  		return false, nil
   356  	}
   357  	return true, nil
   358  }
   359  
   360  // KernelUpdatePolicy implements the update policy for kernel asset updates.
   361  //
   362  // This is called when there is a kernel->kernel refresh for kernels that
   363  // contain bootloader assets. In this case all bootloader assets that are
   364  // marked as "update: true" in the kernel.yaml need updating.
   365  //
   366  // But any non-kernel assets need to be ignored, they will be handled by
   367  // the regular gadget->gadget update mechanism and policy.
   368  func KernelUpdatePolicy(from, to *LaidOutStructure) (bool, ResolvedContentFilterFunc) {
   369  	// The policy function has to work on unresolved content, the
   370  	// returned filter will make sure that after resolving only the
   371  	// relevant $kernel:refs are updated.
   372  	for _, ct := range to.Content {
   373  		if strings.HasPrefix(ct.UnresolvedSource, "$kernel:") {
   374  			return true, func(rn *ResolvedContent) bool {
   375  				return rn.KernelUpdate
   376  			}
   377  		}
   378  	}
   379  
   380  	return false, nil
   381  }
   382  
   383  func resolveUpdate(oldVol *PartiallyLaidOutVolume, newVol *LaidOutVolume, policy UpdatePolicyFunc, newGadgetRootDir, newKernelRootDir string, kernelInfo *kernel.Info) (updates []updatePair, err error) {
   384  	if len(oldVol.LaidOutStructure) != len(newVol.LaidOutStructure) {
   385  		return nil, errors.New("internal error: the number of structures in new and old volume definitions is different")
   386  	}
   387  	for j, oldStruct := range oldVol.LaidOutStructure {
   388  		newStruct := newVol.LaidOutStructure[j]
   389  		// update only when the policy says so; boot assets
   390  		// are assumed to be backwards compatible, once
   391  		// deployed they are not rolled back or replaced unless
   392  		// told by the new policy
   393  		if update, filter := policy(&oldStruct, &newStruct); update {
   394  			// Ensure content is resolved and filtered. Filtering
   395  			// is required for e.g. KernelUpdatePolicy, see above.
   396  			resolvedContent, err := resolveVolumeContent(newGadgetRootDir, newKernelRootDir, kernelInfo, &newStruct, filter)
   397  			if err != nil {
   398  				return nil, err
   399  			}
   400  			// Nothing to do after filtering
   401  			if filter != nil && len(resolvedContent) == 0 && len(newStruct.LaidOutContent) == 0 {
   402  				continue
   403  			}
   404  			newVol.LaidOutStructure[j].ResolvedContent = resolvedContent
   405  
   406  			// and add to updates
   407  			updates = append(updates, updatePair{
   408  				from: &oldVol.LaidOutStructure[j],
   409  				to:   &newVol.LaidOutStructure[j],
   410  			})
   411  		}
   412  	}
   413  	return updates, nil
   414  }
   415  
   416  type Updater interface {
   417  	// Update applies the update or errors out on failures. When no actual
   418  	// update was applied because the new content is identical a special
   419  	// ErrNoUpdate is returned.
   420  	Update() error
   421  	// Backup prepares a backup copy of data that will be modified by
   422  	// Update()
   423  	Backup() error
   424  	// Rollback restores data modified by update
   425  	Rollback() error
   426  }
   427  
   428  func applyUpdates(new GadgetData, updates []updatePair, rollbackDir string, observer ContentUpdateObserver) error {
   429  	updaters := make([]Updater, len(updates))
   430  
   431  	for i, one := range updates {
   432  		up, err := updaterForStructure(one.to, new.RootDir, rollbackDir, observer)
   433  		if err != nil {
   434  			return fmt.Errorf("cannot prepare update for volume structure %v: %v", one.to, err)
   435  		}
   436  		updaters[i] = up
   437  	}
   438  
   439  	var backupErr error
   440  	for i, one := range updaters {
   441  		if err := one.Backup(); err != nil {
   442  			backupErr = fmt.Errorf("cannot backup volume structure %v: %v", updates[i].to, err)
   443  			break
   444  		}
   445  	}
   446  	if backupErr != nil {
   447  		if observer != nil {
   448  			if err := observer.Canceled(); err != nil {
   449  				logger.Noticef("cannot observe canceled prepare update: %v", err)
   450  			}
   451  		}
   452  		return backupErr
   453  	}
   454  	if observer != nil {
   455  		if err := observer.BeforeWrite(); err != nil {
   456  			return fmt.Errorf("cannot observe prepared update: %v", err)
   457  		}
   458  	}
   459  
   460  	var updateErr error
   461  	var updateLastAttempted int
   462  	var skipped int
   463  	for i, one := range updaters {
   464  		updateLastAttempted = i
   465  		if err := one.Update(); err != nil {
   466  			if err == ErrNoUpdate {
   467  				skipped++
   468  				continue
   469  			}
   470  			updateErr = fmt.Errorf("cannot update volume structure %v: %v", updates[i].to, err)
   471  			break
   472  		}
   473  	}
   474  	if skipped == len(updaters) {
   475  		// all updates were a noop
   476  		return ErrNoUpdate
   477  	}
   478  
   479  	if updateErr == nil {
   480  		// all good, updates applied successfully
   481  		return nil
   482  	}
   483  
   484  	logger.Noticef("cannot update gadget: %v", updateErr)
   485  	// not so good, rollback ones that got applied
   486  	for i := 0; i <= updateLastAttempted; i++ {
   487  		one := updaters[i]
   488  		if err := one.Rollback(); err != nil {
   489  			// TODO: log errors to oplog
   490  			logger.Noticef("cannot rollback volume structure %v update: %v", updates[i].to, err)
   491  		}
   492  	}
   493  
   494  	if observer != nil {
   495  		if err := observer.Canceled(); err != nil {
   496  			logger.Noticef("cannot observe canceled update: %v", err)
   497  		}
   498  	}
   499  
   500  	return updateErr
   501  }
   502  
   503  var updaterForStructure = updaterForStructureImpl
   504  
   505  func updaterForStructureImpl(ps *LaidOutStructure, newRootDir, rollbackDir string, observer ContentUpdateObserver) (Updater, error) {
   506  	var updater Updater
   507  	var err error
   508  	if !ps.HasFilesystem() {
   509  		updater, err = newRawStructureUpdater(newRootDir, ps, rollbackDir, findDeviceForStructureWithFallback)
   510  	} else {
   511  		updater, err = newMountedFilesystemUpdater(ps, rollbackDir, findMountPointForStructure, observer)
   512  	}
   513  	return updater, err
   514  }
   515  
   516  // MockUpdaterForStructure replace internal call with a mocked one, for use in tests only
   517  func MockUpdaterForStructure(mock func(ps *LaidOutStructure, rootDir, rollbackDir string, observer ContentUpdateObserver) (Updater, error)) (restore func()) {
   518  	old := updaterForStructure
   519  	updaterForStructure = mock
   520  	return func() {
   521  		updaterForStructure = old
   522  	}
   523  }