github.com/chipaca/snappy@v0.0.0-20210104084008-1f06296fe8ad/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 := checkSnapshotTaskConflict(mgr.state, r.SetID, "check-snapshot", "restore-snapshot"); err != nil { 116 // there is a conflict, do nothing and we will retry this set on next Ensure(). 117 return nil 118 } 119 if sets[r.SetID] { 120 delete(sets, r.SetID) 121 // remove from state first: in case removeSnapshotState succeeds but osRemove fails we will never attempt 122 // to automatically remove this snapshot again and will leave it on the disk (so the user can still try to remove it manually); 123 // this is better than the other way around where a failing osRemove would be retried forever because snapshot would never 124 // leave the state. 125 if err := removeSnapshotState(mgr.state, r.SetID); err != nil { 126 return fmt.Errorf("internal error: cannot remove state of snapshot set %d: %v", r.SetID, err) 127 } 128 if err := osRemove(r.Name()); err != nil { 129 return fmt.Errorf("cannot remove snapshot file %q: %v", r.Name(), err) 130 } 131 } 132 return nil 133 }) 134 135 if err != nil { 136 return fmt.Errorf("cannot process expired snapshots: %v", err) 137 } 138 139 // only reset time if there are no sets left because of conflicts 140 if len(sets) == 0 { 141 mgr.lastForgetExpiredSnapshotTime = time.Now() 142 } 143 144 return nil 145 } 146 147 func (SnapshotManager) affectedSnaps(t *state.Task) ([]string, error) { 148 if k := t.Kind(); k == "check-snapshot" || k == "forget-snapshot" { 149 // check and forget don't affect snaps 150 // (this could also be written k != save && k != restore, but it's safer this way around) 151 return nil, nil 152 } 153 var snapshot snapshotSetup 154 if err := t.Get("snapshot-setup", &snapshot); err != nil { 155 return nil, taskGetErrMsg(t, err, "snapshot") 156 } 157 158 return []string{snapshot.Snap}, nil 159 } 160 161 type snapshotSetup struct { 162 SetID uint64 `json:"set-id"` 163 Snap string `json:"snap"` 164 Users []string `json:"users,omitempty"` 165 Filename string `json:"filename,omitempty"` 166 Current snap.Revision `json:"current"` 167 Auto bool `json:"auto,omitempty"` 168 } 169 170 func filename(setID uint64, si *snap.Info) string { 171 skel := &client.Snapshot{ 172 SetID: setID, 173 Snap: si.InstanceName(), 174 Revision: si.Revision, 175 Version: si.Version, 176 } 177 return backend.Filename(skel) 178 } 179 180 // prepareSave does all the steps of doSave that require the state lock; 181 // it has no real significance beyond making the lock handling simpler 182 func prepareSave(task *state.Task) (snapshot *snapshotSetup, cur *snap.Info, cfg map[string]interface{}, err error) { 183 st := task.State() 184 st.Lock() 185 defer st.Unlock() 186 187 if err := task.Get("snapshot-setup", &snapshot); err != nil { 188 return nil, nil, nil, taskGetErrMsg(task, err, "snapshot") 189 } 190 cur, err = snapstateCurrentInfo(st, snapshot.Snap) 191 if err != nil { 192 return nil, nil, nil, err 193 } 194 // updating snapshot-setup with the filename, for use in undo 195 snapshot.Filename = filename(snapshot.SetID, cur) 196 task.Set("snapshot-setup", &snapshot) 197 198 rawCfg, err := configGetSnapConfig(st, snapshot.Snap) 199 if err != nil { 200 return nil, nil, nil, err 201 } 202 if rawCfg != nil { 203 if err := json.Unmarshal(*rawCfg, &cfg); err != nil { 204 return nil, nil, nil, err 205 } 206 } 207 208 // this should be done last because of it modifies the state and the caller needs to undo this if other operation fails. 209 if snapshot.Auto { 210 expiration, err := AutomaticSnapshotExpiration(st) 211 if err != nil { 212 return nil, nil, nil, err 213 } 214 if err := saveExpiration(st, snapshot.SetID, time.Now().Add(expiration)); err != nil { 215 return nil, nil, nil, err 216 } 217 } 218 219 return snapshot, cur, cfg, nil 220 } 221 222 func doSave(task *state.Task, tomb *tomb.Tomb) error { 223 snapshot, cur, cfg, err := prepareSave(task) 224 if err != nil { 225 return err 226 } 227 _, err = backendSave(tomb.Context(nil), snapshot.SetID, cur, cfg, snapshot.Users) 228 if err != nil { 229 st := task.State() 230 st.Lock() 231 defer st.Unlock() 232 removeSnapshotState(st, snapshot.SetID) 233 } 234 return err 235 } 236 237 // prepareRestore does the steps of doRestore that require the state lock 238 // before the backend Restore call. 239 func prepareRestore(task *state.Task) (snapshot *snapshotSetup, oldCfg map[string]interface{}, reader *backend.Reader, err error) { 240 st := task.State() 241 242 st.Lock() 243 defer st.Unlock() 244 245 if err := task.Get("snapshot-setup", &snapshot); err != nil { 246 return nil, nil, nil, taskGetErrMsg(task, err, "snapshot") 247 } 248 249 rawCfg, err := configGetSnapConfig(st, snapshot.Snap) 250 if err != nil { 251 return nil, nil, nil, fmt.Errorf("internal error: cannot obtain current snap config for snapshot restore: %v", err) 252 } 253 254 if rawCfg != nil { 255 if err := json.Unmarshal(*rawCfg, &oldCfg); err != nil { 256 return nil, nil, nil, fmt.Errorf("internal error: cannot decode current snap config: %v", err) 257 } 258 } 259 260 reader, err = backendOpen(snapshot.Filename, backend.ExtractFnameSetID) 261 if err != nil { 262 return nil, nil, nil, fmt.Errorf("cannot open snapshot: %v", err) 263 } 264 // note given the Open succeeded, caller needs to close it when done 265 266 return snapshot, oldCfg, reader, nil 267 } 268 269 func doRestore(task *state.Task, tomb *tomb.Tomb) error { 270 snapshot, oldCfg, reader, err := prepareRestore(task) 271 if err != nil { 272 return err 273 } 274 defer reader.Close() 275 276 st := task.State() 277 logf := func(format string, args ...interface{}) { 278 st.Lock() 279 defer st.Unlock() 280 task.Logf(format, args...) 281 } 282 283 restoreState, err := backendRestore(reader, tomb.Context(nil), snapshot.Current, snapshot.Users, logf) 284 if err != nil { 285 return err 286 } 287 288 buf, err := json.Marshal(reader.Conf) 289 if err != nil { 290 backendRevert(restoreState) 291 return fmt.Errorf("cannot marshal saved config: %v", err) 292 } 293 294 st.Lock() 295 defer st.Unlock() 296 297 if err := configSetSnapConfig(st, snapshot.Snap, (*json.RawMessage)(&buf)); err != nil { 298 backendRevert(restoreState) 299 return fmt.Errorf("cannot set snap config: %v", err) 300 } 301 302 restoreState.Config = oldCfg 303 task.Set("restore-state", restoreState) 304 305 return nil 306 } 307 308 func undoRestore(task *state.Task, _ *tomb.Tomb) error { 309 var restoreState backend.RestoreState 310 var snapshot snapshotSetup 311 312 st := task.State() 313 st.Lock() 314 defer st.Unlock() 315 316 if err := task.Get("restore-state", &restoreState); err != nil { 317 return taskGetErrMsg(task, err, "snapshot restore") 318 } 319 if err := task.Get("snapshot-setup", &snapshot); err != nil { 320 return taskGetErrMsg(task, err, "snapshot") 321 } 322 323 buf, err := json.Marshal(restoreState.Config) 324 if err != nil { 325 return fmt.Errorf("cannot marshal saved config: %v", err) 326 } 327 328 if err := configSetSnapConfig(st, snapshot.Snap, (*json.RawMessage)(&buf)); err != nil { 329 return fmt.Errorf("cannot restore saved config: %v", err) 330 } 331 332 backendRevert(&restoreState) 333 334 return nil 335 } 336 337 func cleanupRestore(task *state.Task, _ *tomb.Tomb) error { 338 var restoreState backend.RestoreState 339 340 st := task.State() 341 st.Lock() 342 status := task.Status() 343 err := task.Get("restore-state", &restoreState) 344 st.Unlock() 345 346 if status != state.DoneStatus { 347 // only need to clean up restores that worked 348 return nil 349 } 350 351 if err != nil { 352 // this is bad: we somehow lost the information to restore things 353 // but if we return the error we'll just get called again :-( 354 // TODO: use warnings :-) 355 logger.Noticef("%v", taskGetErrMsg(task, err, "snapshot restore")) 356 return nil 357 } 358 359 backendCleanup(&restoreState) 360 361 return nil 362 } 363 364 func doCheck(task *state.Task, tomb *tomb.Tomb) error { 365 var snapshot snapshotSetup 366 367 st := task.State() 368 st.Lock() 369 err := task.Get("snapshot-setup", &snapshot) 370 st.Unlock() 371 if err != nil { 372 return taskGetErrMsg(task, err, "snapshot") 373 } 374 375 reader, err := backendOpen(snapshot.Filename, backend.ExtractFnameSetID) 376 if err != nil { 377 return fmt.Errorf("cannot open snapshot: %v", err) 378 } 379 defer reader.Close() 380 381 return backendCheck(reader, tomb.Context(nil), snapshot.Users) 382 } 383 384 func doForget(task *state.Task, _ *tomb.Tomb) error { 385 // note this is also undoSave 386 st := task.State() 387 st.Lock() 388 defer st.Unlock() 389 390 var snapshot snapshotSetup 391 err := task.Get("snapshot-setup", &snapshot) 392 393 if err != nil { 394 return taskGetErrMsg(task, err, "snapshot") 395 } 396 397 if snapshot.Filename == "" { 398 return fmt.Errorf("internal error: task %s (%s) snapshot info is missing the filename", task.ID(), task.Kind()) 399 } 400 401 // in case it's an automatic snapshot, remove the set also from the state (automatic snapshots have just one snap per set). 402 if err := removeSnapshotState(st, snapshot.SetID); err != nil { 403 return fmt.Errorf("internal error: cannot remove state of snapshot set %d: %v", snapshot.SetID, err) 404 } 405 406 return osRemove(snapshot.Filename) 407 } 408 409 func delayedCrossMgrInit() { 410 // hook automatic snapshots into snapstate logic 411 snapstate.AutomaticSnapshot = AutomaticSnapshot 412 snapstate.AutomaticSnapshotExpiration = AutomaticSnapshotExpiration 413 snapstate.EstimateSnapshotSize = EstimateSnapshotSize 414 } 415 416 func MockBackendSave(f func(context.Context, uint64, *snap.Info, map[string]interface{}, []string) (*client.Snapshot, error)) (restore func()) { 417 old := backendSave 418 backendSave = f 419 return func() { 420 backendSave = old 421 } 422 }