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