github.com/david-imola/snapd@v0.0.0-20210611180407-2de8ddeece6d/overlord/snapshotstate/snapshotmgr.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 "os" 27 "time" 28 29 "gopkg.in/tomb.v2" 30 31 "github.com/snapcore/snapd/client" 32 "github.com/snapcore/snapd/logger" 33 "github.com/snapcore/snapd/overlord/configstate/config" 34 "github.com/snapcore/snapd/overlord/snapshotstate/backend" 35 "github.com/snapcore/snapd/overlord/snapstate" 36 "github.com/snapcore/snapd/overlord/state" 37 "github.com/snapcore/snapd/snap" 38 ) 39 40 var ( 41 osRemove = os.Remove 42 snapstateCurrentInfo = snapstate.CurrentInfo 43 configGetSnapConfig = config.GetSnapConfig 44 configSetSnapConfig = config.SetSnapConfig 45 backendOpen = backend.Open 46 backendSave = backend.Save 47 backendImport = backend.Import 48 backendRestore = (*backend.Reader).Restore // TODO: look into using an interface instead 49 backendCheck = (*backend.Reader).Check 50 backendRevert = (*backend.RestoreState).Revert // ditto 51 backendCleanup = (*backend.RestoreState).Cleanup 52 53 backendCleanupAbandondedImports = backend.CleanupAbandondedImports 54 55 autoExpirationInterval = time.Hour * 24 // interval between forgetExpiredSnapshots runs as part of Ensure() 56 ) 57 58 // SnapshotManager takes snapshots of active snaps 59 type SnapshotManager struct { 60 state *state.State 61 62 lastForgetExpiredSnapshotTime time.Time 63 } 64 65 // Manager returns a new SnapshotManager 66 func Manager(st *state.State, runner *state.TaskRunner) *SnapshotManager { 67 delayedCrossMgrInit() 68 69 runner.AddHandler("save-snapshot", doSave, doForget) 70 runner.AddHandler("forget-snapshot", doForget, nil) 71 runner.AddHandler("check-snapshot", doCheck, nil) 72 runner.AddHandler("restore-snapshot", doRestore, undoRestore) 73 runner.AddCleanup("restore-snapshot", cleanupRestore) 74 75 manager := &SnapshotManager{ 76 state: st, 77 } 78 snapstate.AddAffectedSnapsByAttr("snapshot-setup", manager.affectedSnaps) 79 80 return manager 81 } 82 83 // Ensure is part of the overlord.StateManager interface. 84 func (mgr *SnapshotManager) Ensure() error { 85 // process expired snapshots once a day. 86 if time.Now().After(mgr.lastForgetExpiredSnapshotTime.Add(autoExpirationInterval)) { 87 return mgr.forgetExpiredSnapshots() 88 } 89 90 return nil 91 } 92 93 func (mgr *SnapshotManager) StartUp() error { 94 if _, err := backendCleanupAbandondedImports(); err != nil { 95 logger.Noticef("cannot cleanup incomplete imports: %v", err) 96 } 97 return nil 98 } 99 100 func (mgr *SnapshotManager) forgetExpiredSnapshots() error { 101 mgr.state.Lock() 102 defer mgr.state.Unlock() 103 104 sets, err := expiredSnapshotSets(mgr.state, time.Now()) 105 if err != nil { 106 return fmt.Errorf("internal error: cannot determine expired snapshots: %v", err) 107 } 108 109 if len(sets) == 0 { 110 return nil 111 } 112 113 err = backendIter(context.TODO(), func(r *backend.Reader) error { 114 // forget needs to conflict with check and restore 115 if err := checkSnapshotConflict(mgr.state, r.SetID, "export-snapshot", 116 "check-snapshot", "restore-snapshot"); err != nil { 117 // there is a conflict, do nothing and we will retry this set on next Ensure(). 118 return nil 119 } 120 if sets[r.SetID] { 121 delete(sets, r.SetID) 122 // remove from state first: in case removeSnapshotState succeeds but osRemove fails we will never attempt 123 // to automatically remove this snapshot again and will leave it on the disk (so the user can still try to remove it manually); 124 // this is better than the other way around where a failing osRemove would be retried forever because snapshot would never 125 // leave the state. 126 if err := removeSnapshotState(mgr.state, r.SetID); err != nil { 127 return fmt.Errorf("internal error: cannot remove state of snapshot set %d: %v", r.SetID, err) 128 } 129 if err := osRemove(r.Name()); err != nil { 130 return fmt.Errorf("cannot remove snapshot file %q: %v", r.Name(), err) 131 } 132 } 133 return nil 134 }) 135 136 if err != nil { 137 return fmt.Errorf("cannot process expired snapshots: %v", err) 138 } 139 140 // only reset time if there are no sets left because of conflicts 141 if len(sets) == 0 { 142 mgr.lastForgetExpiredSnapshotTime = time.Now() 143 } 144 145 return nil 146 } 147 148 func (SnapshotManager) affectedSnaps(t *state.Task) ([]string, error) { 149 if k := t.Kind(); k == "check-snapshot" || k == "forget-snapshot" { 150 // check and forget don't affect snaps 151 // (this could also be written k != save && k != restore, but it's safer this way around) 152 return nil, nil 153 } 154 var snapshot snapshotSetup 155 if err := t.Get("snapshot-setup", &snapshot); err != nil { 156 return nil, taskGetErrMsg(t, err, "snapshot") 157 } 158 159 return []string{snapshot.Snap}, nil 160 } 161 162 type snapshotSetup struct { 163 SetID uint64 `json:"set-id"` 164 Snap string `json:"snap"` 165 Users []string `json:"users,omitempty"` 166 Filename string `json:"filename,omitempty"` 167 Current snap.Revision `json:"current"` 168 Auto bool `json:"auto,omitempty"` 169 } 170 171 func filename(setID uint64, si *snap.Info) string { 172 skel := &client.Snapshot{ 173 SetID: setID, 174 Snap: si.InstanceName(), 175 Revision: si.Revision, 176 Version: si.Version, 177 } 178 return backend.Filename(skel) 179 } 180 181 // prepareSave does all the steps of doSave that require the state lock; 182 // it has no real significance beyond making the lock handling simpler 183 func prepareSave(task *state.Task) (snapshot *snapshotSetup, cur *snap.Info, cfg map[string]interface{}, err error) { 184 st := task.State() 185 st.Lock() 186 defer st.Unlock() 187 188 if err := task.Get("snapshot-setup", &snapshot); err != nil { 189 return nil, nil, nil, taskGetErrMsg(task, err, "snapshot") 190 } 191 cur, err = snapstateCurrentInfo(st, snapshot.Snap) 192 if err != nil { 193 return nil, nil, nil, err 194 } 195 // updating snapshot-setup with the filename, for use in undo 196 snapshot.Filename = filename(snapshot.SetID, cur) 197 task.Set("snapshot-setup", &snapshot) 198 199 cfg, err = unmarshalSnapConfig(st, snapshot.Snap) 200 if err != nil { 201 return nil, nil, nil, err 202 } 203 204 // this should be done last because of it modifies the state and the caller needs to undo this if other operation fails. 205 if snapshot.Auto { 206 expiration, err := AutomaticSnapshotExpiration(st) 207 if err != nil { 208 return nil, nil, nil, err 209 } 210 if err := saveExpiration(st, snapshot.SetID, time.Now().Add(expiration)); err != nil { 211 return nil, nil, nil, err 212 } 213 } 214 215 return snapshot, cur, cfg, nil 216 } 217 218 func doSave(task *state.Task, tomb *tomb.Tomb) error { 219 snapshot, cur, cfg, err := prepareSave(task) 220 if err != nil { 221 return err 222 } 223 _, err = backendSave(tomb.Context(nil), snapshot.SetID, cur, cfg, snapshot.Users) 224 if err != nil { 225 st := task.State() 226 st.Lock() 227 defer st.Unlock() 228 removeSnapshotState(st, snapshot.SetID) 229 } 230 return err 231 } 232 233 // prepareRestore does the steps of doRestore that require the state lock 234 // before the backend Restore call. 235 func prepareRestore(task *state.Task) (snapshot *snapshotSetup, oldCfg map[string]interface{}, reader *backend.Reader, err error) { 236 st := task.State() 237 238 st.Lock() 239 defer st.Unlock() 240 241 if err := task.Get("snapshot-setup", &snapshot); err != nil { 242 return nil, nil, nil, taskGetErrMsg(task, err, "snapshot") 243 } 244 245 oldCfg, err = unmarshalSnapConfig(st, snapshot.Snap) 246 if err != nil { 247 return nil, nil, nil, err 248 } 249 reader, err = backendOpen(snapshot.Filename, backend.ExtractFnameSetID) 250 if err != nil { 251 return nil, nil, nil, fmt.Errorf("cannot open snapshot: %v", err) 252 } 253 // note given the Open succeeded, caller needs to close it when done 254 255 return snapshot, oldCfg, reader, nil 256 } 257 258 // marshalSnapConfig encodes cfg to JSON and returns raw JSON message, unless 259 // cfg is nil - in this case nil is returned. 260 func marshalSnapConfig(cfg map[string]interface{}) (*json.RawMessage, error) { 261 if cfg == nil { 262 // do not marshal nil - this would result in "null" raw message which 263 // we want to avoid. 264 return nil, nil 265 } 266 buf, err := json.Marshal(cfg) 267 if err != nil { 268 return nil, err 269 } 270 raw := (*json.RawMessage)(&buf) 271 return raw, err 272 } 273 274 func unmarshalSnapConfig(st *state.State, snapName string) (map[string]interface{}, error) { 275 rawCfg, err := configGetSnapConfig(st, snapName) 276 if err != nil { 277 return nil, fmt.Errorf("internal error: cannot obtain current snap config: %v", err) 278 } 279 var cfg map[string]interface{} 280 if rawCfg != nil { 281 if err := json.Unmarshal(*rawCfg, &cfg); err != nil { 282 return nil, fmt.Errorf("internal error: cannot decode current snap config: %v", err) 283 } 284 } 285 return cfg, nil 286 } 287 288 func doRestore(task *state.Task, tomb *tomb.Tomb) error { 289 snapshot, oldCfg, reader, err := prepareRestore(task) 290 if err != nil { 291 return err 292 } 293 defer reader.Close() 294 295 st := task.State() 296 logf := func(format string, args ...interface{}) { 297 st.Lock() 298 defer st.Unlock() 299 task.Logf(format, args...) 300 } 301 302 restoreState, err := backendRestore(reader, tomb.Context(nil), snapshot.Current, snapshot.Users, logf) 303 if err != nil { 304 return err 305 } 306 307 raw, err := marshalSnapConfig(reader.Conf) 308 if err != nil { 309 backendRevert(restoreState) 310 return fmt.Errorf("cannot marshal saved config: %v", err) 311 } 312 313 st.Lock() 314 defer st.Unlock() 315 316 if err := configSetSnapConfig(st, snapshot.Snap, raw); err != nil { 317 backendRevert(restoreState) 318 return fmt.Errorf("cannot set snap config: %v", err) 319 } 320 321 restoreState.Config = oldCfg 322 task.Set("restore-state", restoreState) 323 324 return nil 325 } 326 327 func undoRestore(task *state.Task, _ *tomb.Tomb) error { 328 var restoreState backend.RestoreState 329 var snapshot snapshotSetup 330 331 st := task.State() 332 st.Lock() 333 defer st.Unlock() 334 335 if err := task.Get("restore-state", &restoreState); err != nil { 336 return taskGetErrMsg(task, err, "snapshot restore") 337 } 338 if err := task.Get("snapshot-setup", &snapshot); err != nil { 339 return taskGetErrMsg(task, err, "snapshot") 340 } 341 342 raw, err := marshalSnapConfig(restoreState.Config) 343 if err != nil { 344 return fmt.Errorf("cannot marshal saved config: %v", err) 345 } 346 347 if err := configSetSnapConfig(st, snapshot.Snap, raw); err != nil { 348 return fmt.Errorf("cannot restore saved config: %v", err) 349 } 350 351 backendRevert(&restoreState) 352 353 return nil 354 } 355 356 func cleanupRestore(task *state.Task, _ *tomb.Tomb) error { 357 var restoreState backend.RestoreState 358 359 st := task.State() 360 st.Lock() 361 status := task.Status() 362 err := task.Get("restore-state", &restoreState) 363 st.Unlock() 364 365 if status != state.DoneStatus { 366 // only need to clean up restores that worked 367 return nil 368 } 369 370 if err != nil { 371 // this is bad: we somehow lost the information to restore things 372 // but if we return the error we'll just get called again :-( 373 // TODO: use warnings :-) 374 logger.Noticef("%v", taskGetErrMsg(task, err, "snapshot restore")) 375 return nil 376 } 377 378 backendCleanup(&restoreState) 379 380 return nil 381 } 382 383 func doCheck(task *state.Task, tomb *tomb.Tomb) error { 384 var snapshot snapshotSetup 385 386 st := task.State() 387 st.Lock() 388 err := task.Get("snapshot-setup", &snapshot) 389 st.Unlock() 390 if err != nil { 391 return taskGetErrMsg(task, err, "snapshot") 392 } 393 394 reader, err := backendOpen(snapshot.Filename, backend.ExtractFnameSetID) 395 if err != nil { 396 return fmt.Errorf("cannot open snapshot: %v", err) 397 } 398 defer reader.Close() 399 400 return backendCheck(reader, tomb.Context(nil), snapshot.Users) 401 } 402 403 func doForget(task *state.Task, _ *tomb.Tomb) error { 404 // note this is also undoSave 405 st := task.State() 406 st.Lock() 407 defer st.Unlock() 408 409 var snapshot snapshotSetup 410 err := task.Get("snapshot-setup", &snapshot) 411 412 if err != nil { 413 return taskGetErrMsg(task, err, "snapshot") 414 } 415 416 if snapshot.Filename == "" { 417 return fmt.Errorf("internal error: task %s (%s) snapshot info is missing the filename", task.ID(), task.Kind()) 418 } 419 420 // in case it's an automatic snapshot, remove the set also from the state (automatic snapshots have just one snap per set). 421 if err := removeSnapshotState(st, snapshot.SetID); err != nil { 422 return fmt.Errorf("internal error: cannot remove state of snapshot set %d: %v", snapshot.SetID, err) 423 } 424 425 return osRemove(snapshot.Filename) 426 } 427 428 func delayedCrossMgrInit() { 429 // hook automatic snapshots into snapstate logic 430 snapstate.AutomaticSnapshot = AutomaticSnapshot 431 snapstate.AutomaticSnapshotExpiration = AutomaticSnapshotExpiration 432 snapstate.EstimateSnapshotSize = EstimateSnapshotSize 433 } 434 435 func MockBackendSave(f func(context.Context, uint64, *snap.Info, map[string]interface{}, []string) (*client.Snapshot, error)) (restore func()) { 436 old := backendSave 437 backendSave = f 438 return func() { 439 backendSave = old 440 } 441 }