github.com/tompreston/snapd@v0.0.0-20210817193607-954edfcb9611/daemon/api_snapshots.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 daemon 21 22 import ( 23 "context" 24 "encoding/json" 25 "fmt" 26 "io" 27 "net/http" 28 "strconv" 29 "strings" 30 31 "github.com/snapcore/snapd/client" 32 "github.com/snapcore/snapd/i18n" 33 "github.com/snapcore/snapd/overlord/auth" 34 "github.com/snapcore/snapd/overlord/snapshotstate" 35 "github.com/snapcore/snapd/overlord/state" 36 "github.com/snapcore/snapd/strutil" 37 ) 38 39 var snapshotCmd = &Command{ 40 // TODO: also support /v2/snapshots/<id> 41 Path: "/v2/snapshots", 42 GET: listSnapshots, 43 POST: changeSnapshots, 44 ReadAccess: openAccess{}, 45 WriteAccess: authenticatedAccess{Polkit: polkitActionManage}, 46 } 47 48 var snapshotExportCmd = &Command{ 49 Path: "/v2/snapshots/{id}/export", 50 GET: getSnapshotExport, 51 ReadAccess: authenticatedAccess{}, 52 } 53 54 var ( 55 snapshotList = snapshotstate.List 56 snapshotCheck = snapshotstate.Check 57 snapshotForget = snapshotstate.Forget 58 snapshotRestore = snapshotstate.Restore 59 snapshotSave = snapshotstate.Save 60 snapshotExport = snapshotstate.Export 61 snapshotImport = snapshotstate.Import 62 ) 63 64 func listSnapshots(c *Command, r *http.Request, user *auth.UserState) Response { 65 query := r.URL.Query() 66 var setID uint64 67 if sid := query.Get("set"); sid != "" { 68 var err error 69 setID, err = strconv.ParseUint(sid, 10, 64) 70 if err != nil { 71 return BadRequest("'set', if given, must be a positive base 10 number; got %q", sid) 72 } 73 } 74 75 st := c.d.overlord.State() 76 st.Lock() 77 defer st.Unlock() 78 sets, err := snapshotList(context.TODO(), st, setID, strutil.CommaSeparatedList(r.URL.Query().Get("snaps"))) 79 if err != nil { 80 return InternalError("%v", err) 81 } 82 return SyncResponse(sets) 83 } 84 85 // A snapshotAction is used to request an operation on a snapshot 86 // keep this in sync with client/snapshotAction... 87 type snapshotAction struct { 88 SetID uint64 `json:"set"` 89 Action string `json:"action"` 90 Snaps []string `json:"snaps,omitempty"` 91 Users []string `json:"users,omitempty"` 92 } 93 94 func (action snapshotAction) String() string { 95 // verb of snapshot #N [for snaps %q] [for users %q] 96 var snaps string 97 var users string 98 if len(action.Snaps) > 0 { 99 snaps = " for snaps " + strutil.Quoted(action.Snaps) 100 } 101 if len(action.Users) > 0 { 102 users = " for users " + strutil.Quoted(action.Users) 103 } 104 return fmt.Sprintf("%s of snapshot set #%d%s%s", strings.Title(action.Action), action.SetID, snaps, users) 105 } 106 107 func changeSnapshots(c *Command, r *http.Request, user *auth.UserState) Response { 108 contentType := r.Header.Get("Content-Type") 109 if contentType == client.SnapshotExportMediaType { 110 return doSnapshotImport(c, r, user) 111 } 112 113 var action snapshotAction 114 decoder := json.NewDecoder(r.Body) 115 if err := decoder.Decode(&action); err != nil { 116 return BadRequest("cannot decode request body into snapshot operation: %v", err) 117 } 118 if decoder.More() { 119 return BadRequest("extra content found after snapshot operation") 120 } 121 122 if action.SetID == 0 { 123 return BadRequest("snapshot operation requires snapshot set ID") 124 } 125 126 if action.Action == "" { 127 return BadRequest("snapshot operation requires action") 128 } 129 130 var affected []string 131 var ts *state.TaskSet 132 var err error 133 134 st := c.d.overlord.State() 135 st.Lock() 136 defer st.Unlock() 137 138 switch action.Action { 139 case "check": 140 affected, ts, err = snapshotCheck(st, action.SetID, action.Snaps, action.Users) 141 case "restore": 142 affected, ts, err = snapshotRestore(st, action.SetID, action.Snaps, action.Users) 143 case "forget": 144 if len(action.Users) != 0 { 145 return BadRequest(`snapshot "forget" operation cannot specify users`) 146 } 147 affected, ts, err = snapshotForget(st, action.SetID, action.Snaps) 148 default: 149 return BadRequest("unknown snapshot operation %q", action.Action) 150 } 151 152 switch err { 153 case nil: 154 // woo 155 case client.ErrSnapshotSetNotFound, client.ErrSnapshotSnapsNotFound: 156 return NotFound("%v", err) 157 default: 158 return InternalError("%v", err) 159 } 160 161 chg := newChange(st, action.Action+"-snapshot", action.String(), []*state.TaskSet{ts}, affected) 162 chg.Set("api-data", map[string]interface{}{"snap-names": affected}) 163 ensureStateSoon(st) 164 165 return AsyncResponse(nil, chg.ID()) 166 } 167 168 // getSnapshotExport streams an archive containing an export of existing snapshots. 169 // 170 // The snapshots are re-packaged into a single uncompressed tar archive and 171 // internally contain multiple zip files. 172 func getSnapshotExport(c *Command, r *http.Request, user *auth.UserState) Response { 173 st := c.d.overlord.State() 174 st.Lock() 175 defer st.Unlock() 176 177 vars := muxVars(r) 178 sid := vars["id"] 179 setID, err := strconv.ParseUint(sid, 10, 64) 180 if err != nil { 181 return BadRequest("'id' must be a positive base 10 number; got %q", sid) 182 } 183 184 export, err := snapshotExport(context.TODO(), st, setID) 185 if err != nil { 186 return BadRequest("cannot export %v: %v", setID, err) 187 } 188 // init (size calculation) can be slow so drop the lock 189 st.Unlock() 190 err = export.Init() 191 st.Lock() 192 if err != nil { 193 return BadRequest("cannot calculate size of exported snapshot %v: %v", setID, err) 194 } 195 196 return &snapshotExportResponse{SnapshotExport: export, setID: setID, st: st} 197 } 198 199 func doSnapshotImport(c *Command, r *http.Request, user *auth.UserState) Response { 200 defer r.Body.Close() 201 202 expectedSize, err := strconv.ParseInt(r.Header.Get("Content-Length"), 10, 64) 203 if err != nil { 204 return BadRequest("cannot parse Content-Length: %v", err) 205 } 206 // ensure we don't read more than we expect 207 limitedBodyReader := io.LimitReader(r.Body, expectedSize) 208 209 // XXX: check that we have enough space to import the compressed snapshots 210 st := c.d.overlord.State() 211 setID, snapNames, err := snapshotImport(context.TODO(), st, limitedBodyReader) 212 if err != nil { 213 return BadRequest(err.Error()) 214 } 215 216 result := map[string]interface{}{"set-id": setID, "snaps": snapNames} 217 return SyncResponse(result) 218 } 219 220 func snapshotMany(inst *snapInstruction, st *state.State) (*snapInstructionResult, error) { 221 setID, snapshotted, ts, err := snapshotSave(st, inst.Snaps, inst.Users) 222 if err != nil { 223 return nil, err 224 } 225 226 var msg string 227 if len(inst.Snaps) == 0 { 228 msg = i18n.G("Snapshot all snaps") 229 } else { 230 // TRANSLATORS: the %s is a comma-separated list of quoted snap names 231 msg = fmt.Sprintf(i18n.G("Snapshot snaps %s"), strutil.Quoted(inst.Snaps)) 232 } 233 234 return &snapInstructionResult{ 235 Summary: msg, 236 Affected: snapshotted, 237 Tasksets: []*state.TaskSet{ts}, 238 Result: map[string]interface{}{"set-id": setID}, 239 }, nil 240 }