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