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