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 }