github.com/meulengracht/snapd@v0.0.0-20210719210640-8bde69bcc84e/overlord/snapshotstate/snapshotstate.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2018 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 snapshotstate 21 22 import ( 23 "context" 24 "encoding/json" 25 "fmt" 26 "io" 27 "sort" 28 "time" 29 30 "github.com/snapcore/snapd/client" 31 "github.com/snapcore/snapd/logger" 32 "github.com/snapcore/snapd/overlord/configstate/config" 33 "github.com/snapcore/snapd/overlord/snapshotstate/backend" 34 "github.com/snapcore/snapd/overlord/snapstate" 35 "github.com/snapcore/snapd/overlord/state" 36 "github.com/snapcore/snapd/release" 37 "github.com/snapcore/snapd/snap" 38 "github.com/snapcore/snapd/strutil" 39 ) 40 41 var ( 42 snapstateAll = snapstate.All 43 snapstateCheckChangeConflictMany = snapstate.CheckChangeConflictMany 44 backendIter = backend.Iter 45 backendEstimateSnapshotSize = backend.EstimateSnapshotSize 46 backendList = backend.List 47 backendNewSnapshotExport = backend.NewSnapshotExport 48 49 // Default expiration time for automatic snapshots, if not set by the user 50 defaultAutomaticSnapshotExpiration = time.Hour * 24 * 31 51 ) 52 53 type snapshotState struct { 54 ExpiryTime time.Time `json:"expiry-time"` 55 } 56 57 func newSnapshotSetID(st *state.State) (uint64, error) { 58 var lastDiskSetID, lastStateSetID uint64 59 60 // get last set id from state 61 err := st.Get("last-snapshot-set-id", &lastStateSetID) 62 if err != nil && err != state.ErrNoState { 63 return 0, err 64 } 65 66 // get highest set id from the snapshots/ directory 67 lastDiskSetID, err = backend.LastSnapshotSetID() 68 if err != nil { 69 return 0, fmt.Errorf("cannot determine last snapshot set id: %v", err) 70 } 71 72 // take the larger of the two numbers and store it back in the state. 73 // the value in state acts as an allocation of IDs for scheduled snapshots, 74 // they allocate set id early before any file gets created, so we cannot 75 // rely on disk only. 76 lastSetID := lastDiskSetID 77 if lastStateSetID > lastSetID { 78 lastSetID = lastStateSetID 79 } 80 lastSetID++ 81 st.Set("last-snapshot-set-id", lastSetID) 82 83 return lastSetID, nil 84 } 85 86 func allActiveSnapNames(st *state.State) ([]string, error) { 87 all, err := snapstateAll(st) 88 if err != nil { 89 return nil, err 90 } 91 names := make([]string, 0, len(all)) 92 for name, snapst := range all { 93 if snapst.Active { 94 names = append(names, name) 95 } 96 } 97 98 sort.Strings(names) 99 100 return names, nil 101 } 102 103 func EstimateSnapshotSize(st *state.State, instanceName string, users []string) (uint64, error) { 104 cur, err := snapstateCurrentInfo(st, instanceName) 105 if err != nil { 106 return 0, err 107 } 108 rawCfg, err := configGetSnapConfig(st, instanceName) 109 if err != nil { 110 return 0, err 111 } 112 sz, err := backendEstimateSnapshotSize(cur, users) 113 if err != nil { 114 return 0, err 115 } 116 if rawCfg != nil { 117 sz += uint64(len([]byte(*rawCfg))) 118 } 119 return sz, nil 120 } 121 122 func AutomaticSnapshotExpiration(st *state.State) (time.Duration, error) { 123 var expirationStr string 124 tr := config.NewTransaction(st) 125 err := tr.Get("core", "snapshots.automatic.retention", &expirationStr) 126 if err != nil && !config.IsNoOption(err) { 127 return 0, err 128 } 129 if err == nil { 130 if expirationStr == "no" { 131 return 0, nil 132 } 133 dur, err := time.ParseDuration(expirationStr) 134 if err == nil { 135 return dur, nil 136 } 137 logger.Noticef("snapshots.automatic.retention cannot be parsed: %v", err) 138 } 139 // TODO: automatic snapshots are currently disable by default 140 // on Ubuntu Core devices 141 if !release.OnClassic { 142 return 0, nil 143 } 144 return defaultAutomaticSnapshotExpiration, nil 145 } 146 147 // saveExpiration saves expiration date of the given snapshot set, in the state. 148 // The state needs to be locked by the caller. 149 func saveExpiration(st *state.State, setID uint64, expiryTime time.Time) error { 150 var snapshots map[uint64]*json.RawMessage 151 err := st.Get("snapshots", &snapshots) 152 if err != nil && err != state.ErrNoState { 153 return err 154 } 155 if snapshots == nil { 156 snapshots = make(map[uint64]*json.RawMessage) 157 } 158 data, err := json.Marshal(&snapshotState{ 159 ExpiryTime: expiryTime, 160 }) 161 if err != nil { 162 return err 163 } 164 raw := json.RawMessage(data) 165 snapshots[setID] = &raw 166 st.Set("snapshots", snapshots) 167 return nil 168 } 169 170 // removeSnapshotState removes given set IDs from the state. 171 func removeSnapshotState(st *state.State, setIDs ...uint64) error { 172 var snapshots map[uint64]*json.RawMessage 173 err := st.Get("snapshots", &snapshots) 174 if err != nil { 175 if err == state.ErrNoState { 176 return nil 177 } 178 return err 179 } 180 181 for _, setID := range setIDs { 182 delete(snapshots, setID) 183 } 184 185 st.Set("snapshots", snapshots) 186 return nil 187 } 188 189 // expiredSnapshotSets returns expired snapshot sets from the state whose expiry-time is before the given cutoffTime. 190 // The state needs to be locked by the caller. 191 func expiredSnapshotSets(st *state.State, cutoffTime time.Time) (map[uint64]bool, error) { 192 var snapshots map[uint64]*snapshotState 193 err := st.Get("snapshots", &snapshots) 194 if err != nil { 195 if err != state.ErrNoState { 196 return nil, err 197 } 198 return nil, nil 199 } 200 201 expired := make(map[uint64]bool) 202 for setID, snapshotSet := range snapshots { 203 if snapshotSet.ExpiryTime.Before(cutoffTime) { 204 expired[setID] = true 205 } 206 } 207 208 return expired, nil 209 } 210 211 // snapshotSnapSummaries are used internally to get useful data from a 212 // snapshot set when deciding whether to check/forget/restore it. 213 type snapshotSnapSummaries []*snapshotSnapSummary 214 215 func (summaries snapshotSnapSummaries) snapNames() []string { 216 names := make([]string, len(summaries)) 217 for i, summary := range summaries { 218 names[i] = summary.snap 219 } 220 return names 221 } 222 223 type snapshotSnapSummary struct { 224 snap string 225 snapID string 226 filename string 227 epoch snap.Epoch 228 } 229 230 // snapSummariesInSnapshotSet goes looking for the requested snaps in the 231 // given snap set, and returns summaries of the matching snaps in the set. 232 func snapSummariesInSnapshotSet(setID uint64, requested []string) (summaries snapshotSnapSummaries, err error) { 233 sort.Strings(requested) 234 found := false 235 err = backendIter(context.TODO(), func(r *backend.Reader) error { 236 if r.SetID == setID { 237 found = true 238 if len(requested) == 0 || strutil.SortedListContains(requested, r.Snap) { 239 summaries = append(summaries, &snapshotSnapSummary{ 240 filename: r.Name(), 241 snap: r.Snap, 242 snapID: r.SnapID, 243 epoch: r.Epoch, 244 }) 245 } 246 } 247 248 return nil 249 }) 250 if err != nil { 251 return nil, err 252 } 253 if !found { 254 return nil, client.ErrSnapshotSetNotFound 255 } 256 if len(summaries) == 0 { 257 return nil, client.ErrSnapshotSnapsNotFound 258 } 259 260 return summaries, nil 261 } 262 263 func taskGetErrMsg(task *state.Task, err error, what string) error { 264 if err == state.ErrNoState { 265 return fmt.Errorf("internal error: task %s (%s) is missing %s information", task.ID(), task.Kind(), what) 266 } 267 return fmt.Errorf("internal error: retrieving %s information from task %s (%s): %v", what, task.ID(), task.Kind(), err) 268 } 269 270 // checkSnapshotConflict checks whether there's an in-progress task for snapshots with the given set id. 271 func checkSnapshotConflict(st *state.State, setID uint64, conflictingKinds ...string) error { 272 if val := st.Cached("snapshot-ops"); val != nil { 273 snapshotOps, _ := val.(map[uint64]string) 274 if op, ok := snapshotOps[setID]; ok { 275 for _, conflicting := range conflictingKinds { 276 if op == conflicting { 277 return fmt.Errorf("cannot operate on snapshot set #%d while operation %s is in progress", setID, op) 278 } 279 } 280 } 281 } 282 for _, task := range st.Tasks() { 283 if task.Change().Status().Ready() { 284 continue 285 } 286 if !strutil.ListContains(conflictingKinds, task.Kind()) { 287 continue 288 } 289 290 var snapshot snapshotSetup 291 if err := task.Get("snapshot-setup", &snapshot); err != nil { 292 return taskGetErrMsg(task, err, "snapshot") 293 } 294 295 if snapshot.SetID == setID { 296 return fmt.Errorf("cannot operate on snapshot set #%d while change %q is in progress", setID, task.Change().ID()) 297 } 298 } 299 300 return nil 301 } 302 303 // List valid snapshots. 304 // Note that the state must be locked by the caller. 305 func List(ctx context.Context, st *state.State, setID uint64, snapNames []string) ([]client.SnapshotSet, error) { 306 sets, err := backendList(ctx, setID, snapNames) 307 if err != nil { 308 return nil, err 309 } 310 311 var snapshots map[uint64]*snapshotState 312 if err := st.Get("snapshots", &snapshots); err != nil && err != state.ErrNoState { 313 return nil, err 314 } 315 316 // decorate all snapshots with "auto" flag if we have expiry time set for them. 317 for _, sset := range sets { 318 // at the moment we only keep records with expiry time so checking non-zero 319 // expiry-time is not strictly necessary, but it makes it future-proof in case 320 // we add more attributes to these entries. 321 if snapshotState, ok := snapshots[sset.ID]; ok && !snapshotState.ExpiryTime.IsZero() { 322 for _, snapshot := range sset.Snapshots { 323 snapshot.Auto = true 324 } 325 } 326 } 327 328 return sets, nil 329 } 330 331 // Import a given snapshot ID from an exported snapshot 332 func Import(ctx context.Context, st *state.State, r io.Reader) (setID uint64, snapNames []string, err error) { 333 st.Lock() 334 setID, err = newSnapshotSetID(st) 335 // note, this is a new set id which is not exposed yet, no need to mark it 336 // for conflicts via snapshotOp. Also, since we're keeping state lock while 337 // checking conflicts below, there is no need to for setSnapshotOpInProgress. 338 st.Unlock() 339 if err != nil { 340 return 0, nil, err 341 } 342 343 snapNames, err = backendImport(ctx, setID, r, nil) 344 if err != nil { 345 if dupErr, ok := err.(backend.DuplicatedSnapshotImportError); ok { 346 st.Lock() 347 defer st.Unlock() 348 349 if err := checkSnapshotConflict(st, dupErr.SetID, "forget-snapshot"); err != nil { 350 // we found an existing snapshot but it's being forgotten, so 351 // retry the import without checking for existing snapshot. 352 flags := &backend.ImportFlags{NoDuplicatedImportCheck: true} 353 st.Unlock() 354 snapNames, err = backendImport(ctx, setID, r, flags) 355 st.Lock() 356 return setID, snapNames, err 357 } 358 359 // trying to import identical snapshot; instead return set ID of 360 // the existing one and reset its expiry time. 361 // XXX: at the moment expiry-time is the only attribute so we can 362 // just remove the record. If we ever add more attributes this needs 363 // to reset expiry-time only. 364 if err := removeSnapshotState(st, dupErr.SetID); err != nil { 365 return 0, nil, err 366 } 367 return dupErr.SetID, dupErr.SnapNames, nil 368 } 369 return 0, nil, err 370 } 371 return setID, snapNames, nil 372 } 373 374 // Save creates a taskset for taking snapshots of snaps' data. 375 // Note that the state must be locked by the caller. 376 func Save(st *state.State, instanceNames []string, users []string) (setID uint64, snapsSaved []string, ts *state.TaskSet, err error) { 377 if len(instanceNames) == 0 { 378 instanceNames, err = allActiveSnapNames(st) 379 if err != nil { 380 return 0, nil, nil, err 381 } 382 } 383 384 // Make sure we do not snapshot if anything like install/remove/refresh is in progress 385 if err := snapstateCheckChangeConflictMany(st, instanceNames, ""); err != nil { 386 return 0, nil, nil, err 387 } 388 389 setID, err = newSnapshotSetID(st) 390 if err != nil { 391 return 0, nil, nil, err 392 } 393 394 ts = state.NewTaskSet() 395 396 for _, name := range instanceNames { 397 desc := fmt.Sprintf("Save data of snap %q in snapshot set #%d", name, setID) 398 task := st.NewTask("save-snapshot", desc) 399 snapshot := snapshotSetup{ 400 SetID: setID, 401 Snap: name, 402 Users: users, 403 } 404 task.Set("snapshot-setup", &snapshot) 405 // Here, note that a snapshot set behaves as a unit: it either 406 // succeeds, or fails, as a whole; we don't use lanes, to have 407 // some snaps' snapshot succeed and not others in a single set. 408 // In practice: either the snapshot will be automatic and only 409 // for one snap (already in a lane via refresh), or it will be 410 // done by hand and the user can remove failing snaps (or find 411 // the cause of the failure). A snapshot failure can happen if 412 // a user has dropped files they can't read in their directory, 413 // for example. 414 // Also note we aren't promising this behaviour; we can change 415 // it if we find it to be wrong. 416 ts.AddTask(task) 417 } 418 419 return setID, instanceNames, ts, nil 420 } 421 422 func AutomaticSnapshot(st *state.State, snapName string) (ts *state.TaskSet, err error) { 423 expiration, err := AutomaticSnapshotExpiration(st) 424 if err != nil { 425 return nil, err 426 } 427 if expiration == 0 { 428 return nil, snapstate.ErrNothingToDo 429 } 430 setID, err := newSnapshotSetID(st) 431 if err != nil { 432 return nil, err 433 } 434 435 ts = state.NewTaskSet() 436 desc := fmt.Sprintf("Save data of snap %q in automatic snapshot set #%d", snapName, setID) 437 task := st.NewTask("save-snapshot", desc) 438 snapshot := snapshotSetup{ 439 SetID: setID, 440 Snap: snapName, 441 Auto: true, 442 } 443 task.Set("snapshot-setup", &snapshot) 444 ts.AddTask(task) 445 446 return ts, nil 447 } 448 449 // Restore creates a taskset for restoring a snapshot's data. 450 // Note that the state must be locked by the caller. 451 func Restore(st *state.State, setID uint64, snapNames []string, users []string) (snapsFound []string, ts *state.TaskSet, err error) { 452 summaries, err := snapSummariesInSnapshotSet(setID, snapNames) 453 if err != nil { 454 return nil, nil, err 455 } 456 all, err := snapstateAll(st) 457 if err != nil { 458 return nil, nil, err 459 } 460 461 snapsFound = summaries.snapNames() 462 463 if err := snapstateCheckChangeConflictMany(st, snapsFound, ""); err != nil { 464 return nil, nil, err 465 } 466 467 // restore needs to conflict with forget of itself 468 if err := checkSnapshotConflict(st, setID, "forget-snapshot"); err != nil { 469 return nil, nil, err 470 } 471 472 ts = state.NewTaskSet() 473 474 for _, summary := range summaries { 475 var current snap.Revision 476 if snapst, ok := all[summary.snap]; ok { 477 info, err := snapst.CurrentInfo() 478 if err != nil { 479 // how? 480 return nil, nil, fmt.Errorf("unexpected error while reading snap info: %v", err) 481 } 482 if !info.Epoch.CanRead(summary.epoch) { 483 const tpl = "cannot restore snapshot for %q: current snap (epoch %s) cannot read snapshot data (epoch %s)" 484 return nil, nil, fmt.Errorf(tpl, summary.snap, &info.Epoch, &summary.epoch) 485 } 486 if summary.snapID != "" && info.SnapID != "" && info.SnapID != summary.snapID { 487 const tpl = "cannot restore snapshot for %q: current snap (ID %.7s…) does not match snapshot (ID %.7s…)" 488 return nil, nil, fmt.Errorf(tpl, summary.snap, info.SnapID, summary.snapID) 489 } 490 current = snapst.Current 491 } 492 493 desc := fmt.Sprintf("Restore data of snap %q from snapshot set #%d", summary.snap, setID) 494 task := st.NewTask("restore-snapshot", desc) 495 snapshot := snapshotSetup{ 496 SetID: setID, 497 Snap: summary.snap, 498 Users: users, 499 Filename: summary.filename, 500 Current: current, 501 } 502 task.Set("snapshot-setup", &snapshot) 503 // see the note about snapshots not using lanes, above. 504 ts.AddTask(task) 505 } 506 507 return snapsFound, ts, nil 508 } 509 510 // Check creates a taskset for checking a snapshot's data. 511 // Note that the state must be locked by the caller. 512 func Check(st *state.State, setID uint64, snapNames []string, users []string) (snapsFound []string, ts *state.TaskSet, err error) { 513 // check needs to conflict with forget of itself 514 if err := checkSnapshotConflict(st, setID, "forget-snapshot"); err != nil { 515 return nil, nil, err 516 } 517 518 summaries, err := snapSummariesInSnapshotSet(setID, snapNames) 519 if err != nil { 520 return nil, nil, err 521 } 522 523 ts = state.NewTaskSet() 524 525 for _, summary := range summaries { 526 desc := fmt.Sprintf("Check data of snap %q in snapshot set #%d", summary.snap, setID) 527 task := st.NewTask("check-snapshot", desc) 528 snapshot := snapshotSetup{ 529 SetID: setID, 530 Snap: summary.snap, 531 Users: users, 532 Filename: summary.filename, 533 } 534 task.Set("snapshot-setup", &snapshot) 535 ts.AddTask(task) 536 } 537 538 return summaries.snapNames(), ts, nil 539 } 540 541 // Forget creates a taskset for deletinig a snapshot. 542 // Note that the state must be locked by the caller. 543 func Forget(st *state.State, setID uint64, snapNames []string) (snapsFound []string, ts *state.TaskSet, err error) { 544 // forget needs to conflict with check, restore, import and export. 545 if err := checkSnapshotConflict(st, setID, "export-snapshot", 546 "check-snapshot", "restore-snapshot"); err != nil { 547 return nil, nil, err 548 } 549 550 summaries, err := snapSummariesInSnapshotSet(setID, snapNames) 551 if err != nil { 552 return nil, nil, err 553 } 554 555 ts = state.NewTaskSet() 556 for _, summary := range summaries { 557 desc := fmt.Sprintf("Drop data of snap %q from snapshot set #%d", summary.snap, setID) 558 task := st.NewTask("forget-snapshot", desc) 559 snapshot := snapshotSetup{ 560 SetID: setID, 561 Snap: summary.snap, 562 Filename: summary.filename, 563 } 564 task.Set("snapshot-setup", &snapshot) 565 ts.AddTask(task) 566 } 567 568 return summaries.snapNames(), ts, nil 569 } 570 571 // setSnapshotOpInProgress marks the given set ID as being a subject of 572 // snapshot op inside state cache. The state must be locked by the caller. 573 func setSnapshotOpInProgress(st *state.State, setID uint64, op string) { 574 var snapshotOps map[uint64]string 575 if val := st.Cached("snapshot-ops"); val != nil { 576 snapshotOps, _ = val.(map[uint64]string) 577 } else { 578 snapshotOps = make(map[uint64]string) 579 } 580 snapshotOps[setID] = op 581 st.Cache("snapshot-ops", snapshotOps) 582 } 583 584 // UnsetSnapshotOpInProgress un-sets the given set ID as being a 585 // subject of a snapshot op. It returns the last operation (or empty string 586 // if no op was marked active). 587 // The state must be locked by the caller. 588 func UnsetSnapshotOpInProgress(st *state.State, setID uint64) string { 589 var op string 590 if val := st.Cached("snapshot-ops"); val != nil { 591 var snapshotOps map[uint64]string 592 snapshotOps, _ = val.(map[uint64]string) 593 op = snapshotOps[setID] 594 delete(snapshotOps, setID) 595 st.Cache("snapshot-ops", snapshotOps) 596 } 597 return op 598 } 599 600 // Export exports a given snapshot ID 601 // Note that the state must be locked by the caller. 602 func Export(ctx context.Context, st *state.State, setID uint64) (se *backend.SnapshotExport, err error) { 603 if err := checkSnapshotConflict(st, setID, "forget-snapshot"); err != nil { 604 return nil, err 605 } 606 607 setSnapshotOpInProgress(st, setID, "export-snapshot") 608 se, err = backendNewSnapshotExport(ctx, setID) 609 if err != nil { 610 UnsetSnapshotOpInProgress(st, setID) 611 } 612 return se, err 613 } 614 615 // SnapshotExport provides a snapshot export that can be streamed out 616 type SnapshotExport = backend.SnapshotExport