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