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