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