github.com/kubiko/snapd@v0.0.0-20201013125620-d4f3094d9ddf/gadget/update.go (about)

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