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