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