github.com/rigado/snapd@v2.42.5-go-mod+incompatible/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  package gadget
    20  
    21  import (
    22  	"errors"
    23  	"fmt"
    24  
    25  	"github.com/snapcore/snapd/logger"
    26  )
    27  
    28  var (
    29  	ErrNoUpdate = errors.New("nothing to update")
    30  )
    31  
    32  var (
    33  	// default positioning constraints that match ubuntu-image
    34  	defaultConstraints = LayoutConstraints{
    35  		NonMBRStartOffset: 1 * SizeMiB,
    36  		SectorSize:        512,
    37  	}
    38  )
    39  
    40  // GadgetData holds references to a gadget revision metadata and its data directory.
    41  type GadgetData struct {
    42  	// Info is the gadget metadata
    43  	Info *Info
    44  	// RootDir is the root directory of gadget snap data
    45  	RootDir string
    46  }
    47  
    48  // Update applies the gadget update given the gadget information and data from
    49  // old and new revisions. It errors out when the update is not possible or
    50  // illegal, or a failure occurs at any of the steps. When there is no update, a
    51  // special error ErrNoUpdate is returned.
    52  //
    53  // Updates are opt-in, and are only applied to structures with a higher value of
    54  // Edition field in the new gadget definition.
    55  //
    56  // Data that would be modified during the update is first backed up inside the
    57  // rollback directory. Should the apply step fail, the modified data is
    58  // recovered.
    59  func Update(old, new GadgetData, rollbackDirPath string) error {
    60  	// TODO: support multi-volume gadgets. But for now we simply
    61  	//       do not do any gadget updates on those. We cannot error
    62  	//       here because this would break refreshes of gadgets even
    63  	//       when they don't require any updates.
    64  	if len(new.Info.Volumes) != 1 || len(old.Info.Volumes) != 1 {
    65  		logger.Noticef("WARNING: gadget assests cannot be updated yet when multiple volumes are used")
    66  		return nil
    67  	}
    68  
    69  	oldVol, newVol, err := resolveVolume(old.Info, new.Info)
    70  	if err != nil {
    71  		return err
    72  	}
    73  
    74  	// layout old partially, without going deep into the layout of structure
    75  	// content
    76  	pOld, err := LayoutVolumePartially(oldVol, defaultConstraints)
    77  	if err != nil {
    78  		return fmt.Errorf("cannot lay out the old volume: %v", err)
    79  	}
    80  
    81  	// layout new
    82  	pNew, err := LayoutVolume(new.RootDir, newVol, defaultConstraints)
    83  	if err != nil {
    84  		return fmt.Errorf("cannot lay out the new volume: %v", err)
    85  	}
    86  
    87  	if err := canUpdateVolume(pOld, pNew); err != nil {
    88  		return fmt.Errorf("cannot apply update to volume: %v", err)
    89  	}
    90  
    91  	// now we know which structure is which, find which ones need an update
    92  	updates, err := resolveUpdate(pOld, pNew)
    93  	if err != nil {
    94  		return err
    95  	}
    96  	if len(updates) == 0 {
    97  		// nothing to update
    98  		return ErrNoUpdate
    99  	}
   100  
   101  	// can update old layout to new layout
   102  	for _, update := range updates {
   103  		if err := canUpdateStructure(update.from, update.to); err != nil {
   104  			return fmt.Errorf("cannot update volume structure %v: %v", update.to, err)
   105  		}
   106  	}
   107  
   108  	return applyUpdates(new, updates, rollbackDirPath)
   109  }
   110  
   111  func resolveVolume(old *Info, new *Info) (oldVol, newVol *Volume, err error) {
   112  	// support only one volume
   113  	if len(new.Volumes) != 1 || len(old.Volumes) != 1 {
   114  		return nil, nil, errors.New("cannot update with more than one volume")
   115  	}
   116  
   117  	var name string
   118  	for n := range old.Volumes {
   119  		name = n
   120  		break
   121  	}
   122  	oldV := old.Volumes[name]
   123  
   124  	newV, ok := new.Volumes[name]
   125  	if !ok {
   126  		return nil, nil, fmt.Errorf("cannot find entry for volume %q in updated gadget info", name)
   127  	}
   128  
   129  	return &oldV, &newV, nil
   130  }
   131  
   132  func isSameOffset(one *Size, two *Size) bool {
   133  	if one == nil && two == nil {
   134  		return true
   135  	}
   136  	if one != nil && two != nil {
   137  		return *one == *two
   138  	}
   139  	return false
   140  }
   141  
   142  func isSameRelativeOffset(one *RelativeOffset, two *RelativeOffset) bool {
   143  	if one == nil && two == nil {
   144  		return true
   145  	}
   146  	if one != nil && two != nil {
   147  		return *one == *two
   148  	}
   149  	return false
   150  }
   151  
   152  func isLegacyMBRTransition(from *LaidOutStructure, to *LaidOutStructure) bool {
   153  	// legacy MBR could have been specified by setting type: mbr, with no
   154  	// role
   155  	return from.Type == MBR && to.EffectiveRole() == MBR
   156  }
   157  
   158  func canUpdateStructure(from *LaidOutStructure, to *LaidOutStructure) error {
   159  	if from.Size != to.Size {
   160  		return fmt.Errorf("cannot change structure size from %v to %v", from.Size, to.Size)
   161  	}
   162  	if !isSameOffset(from.Offset, to.Offset) {
   163  		return fmt.Errorf("cannot change structure offset from %v to %v", from.Offset, to.Offset)
   164  	}
   165  	if from.StartOffset != to.StartOffset {
   166  		return fmt.Errorf("cannot change structure start offset from %v to %v", from.StartOffset, to.StartOffset)
   167  	}
   168  	// TODO: should this limitation be lifted?
   169  	if !isSameRelativeOffset(from.OffsetWrite, to.OffsetWrite) {
   170  		return fmt.Errorf("cannot change structure offset-write from %v to %v", from.OffsetWrite, to.OffsetWrite)
   171  	}
   172  	if from.EffectiveRole() != to.EffectiveRole() {
   173  		return fmt.Errorf("cannot change structure role from %q to %q", from.EffectiveRole(), to.EffectiveRole())
   174  	}
   175  	if from.Type != to.Type {
   176  		if !isLegacyMBRTransition(from, to) {
   177  			return fmt.Errorf("cannot change structure type from %q to %q", from.Type, to.Type)
   178  		}
   179  	}
   180  	if from.ID != to.ID {
   181  		return fmt.Errorf("cannot change structure ID from %q to %q", from.ID, to.ID)
   182  	}
   183  	if !to.IsBare() {
   184  		if from.IsBare() {
   185  			return fmt.Errorf("cannot change a bare structure to filesystem one")
   186  		}
   187  		if from.Filesystem != to.Filesystem {
   188  			return fmt.Errorf("cannot change filesystem from %q to %q",
   189  				from.Filesystem, to.Filesystem)
   190  		}
   191  		if from.EffectiveFilesystemLabel() != to.EffectiveFilesystemLabel() {
   192  			return fmt.Errorf("cannot change filesystem label from %q to %q",
   193  				from.Label, to.Label)
   194  		}
   195  	} else {
   196  		if !from.IsBare() {
   197  			return fmt.Errorf("cannot change a filesystem structure to a bare one")
   198  		}
   199  	}
   200  
   201  	return nil
   202  }
   203  
   204  func canUpdateVolume(from *PartiallyLaidOutVolume, to *LaidOutVolume) error {
   205  	if from.ID != to.ID {
   206  		return fmt.Errorf("cannot change volume ID from %q to %q", from.ID, to.ID)
   207  	}
   208  	if from.EffectiveSchema() != to.EffectiveSchema() {
   209  		return fmt.Errorf("cannot change volume schema from %q to %q", from.EffectiveSchema(), to.EffectiveSchema())
   210  	}
   211  	if len(from.LaidOutStructure) != len(to.LaidOutStructure) {
   212  		return fmt.Errorf("cannot change the number of structures within volume from %v to %v", len(from.LaidOutStructure), len(to.LaidOutStructure))
   213  	}
   214  	return nil
   215  }
   216  
   217  type updatePair struct {
   218  	from *LaidOutStructure
   219  	to   *LaidOutStructure
   220  }
   221  
   222  func resolveUpdate(oldVol *PartiallyLaidOutVolume, newVol *LaidOutVolume) (updates []updatePair, err error) {
   223  	if len(oldVol.LaidOutStructure) != len(newVol.LaidOutStructure) {
   224  		return nil, errors.New("internal error: the number of structures in new and old volume definitions is different")
   225  	}
   226  	for j, oldStruct := range oldVol.LaidOutStructure {
   227  		newStruct := newVol.LaidOutStructure[j]
   228  		// update only when new edition is higher than the old one; boot
   229  		// assets are assumed to be backwards compatible, once deployed
   230  		// are not rolled back or replaced unless a higher edition is
   231  		// available
   232  		if newStruct.Update.Edition > oldStruct.Update.Edition {
   233  			updates = append(updates, updatePair{
   234  				from: &oldVol.LaidOutStructure[j],
   235  				to:   &newVol.LaidOutStructure[j],
   236  			})
   237  		}
   238  	}
   239  	return updates, nil
   240  }
   241  
   242  type Updater interface {
   243  	// Update applies the update or errors out on failures
   244  	Update() error
   245  	// Backup prepares a backup copy of data that will be modified by
   246  	// Update()
   247  	Backup() error
   248  	// Rollback restores data modified by update
   249  	Rollback() error
   250  }
   251  
   252  func applyUpdates(new GadgetData, updates []updatePair, rollbackDir string) error {
   253  	updaters := make([]Updater, len(updates))
   254  
   255  	for i, one := range updates {
   256  		up, err := updaterForStructure(one.to, new.RootDir, rollbackDir)
   257  		if err != nil {
   258  			return fmt.Errorf("cannot prepare update for volume structure %v: %v", one.to, err)
   259  		}
   260  		updaters[i] = up
   261  	}
   262  
   263  	for i, one := range updaters {
   264  		if err := one.Backup(); err != nil {
   265  			return fmt.Errorf("cannot backup volume structure %v: %v", updates[i].to, err)
   266  		}
   267  	}
   268  
   269  	var updateErr error
   270  	var updateLastAttempted int
   271  	for i, one := range updaters {
   272  		updateLastAttempted = i
   273  		if err := one.Update(); err != nil {
   274  			updateErr = fmt.Errorf("cannot update volume structure %v: %v", updates[i].to, err)
   275  			break
   276  		}
   277  	}
   278  
   279  	if updateErr == nil {
   280  		// all good, updates applied successfully
   281  		return nil
   282  	}
   283  
   284  	logger.Noticef("cannot update gadget: %v", updateErr)
   285  	// not so good, rollback ones that got applied
   286  	for i := 0; i <= updateLastAttempted; i++ {
   287  		one := updaters[i]
   288  		if err := one.Rollback(); err != nil {
   289  			// TODO: log errors to oplog
   290  			logger.Noticef("cannot rollback volume structure %v update: %v", updates[i].to, err)
   291  		}
   292  	}
   293  
   294  	return updateErr
   295  }
   296  
   297  var updaterForStructure = updaterForStructureImpl
   298  
   299  func updaterForStructureImpl(ps *LaidOutStructure, newRootDir, rollbackDir string) (Updater, error) {
   300  	var updater Updater
   301  	var err error
   302  	if ps.IsBare() {
   303  		updater, err = NewRawStructureUpdater(newRootDir, ps, rollbackDir, FindDeviceForStructureWithFallback)
   304  	} else {
   305  		updater, err = NewMountedFilesystemUpdater(newRootDir, ps, rollbackDir, FindMountPointForStructure)
   306  	}
   307  	return updater, err
   308  }
   309  
   310  // MockUpdaterForStructure replace internal call with a mocked one, for use in tests only
   311  func MockUpdaterForStructure(mock func(ps *LaidOutStructure, rootDir, rollbackDir string) (Updater, error)) (restore func()) {
   312  	old := updaterForStructure
   313  	updaterForStructure = mock
   314  	return func() {
   315  		updaterForStructure = old
   316  	}
   317  }