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