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 }