github.com/kubiko/snapd@v0.0.0-20201013125620-d4f3094d9ddf/client/snapshot.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 client 21 22 import ( 23 "bytes" 24 "context" 25 "encoding/json" 26 "errors" 27 "fmt" 28 "io" 29 "net/url" 30 "strconv" 31 "strings" 32 "time" 33 34 "github.com/snapcore/snapd/snap" 35 ) 36 37 // SnapshotExportMediaType is the media type used to identify snapshot exports in the API. 38 const SnapshotExportMediaType = "application/x.snapd.snapshot" 39 40 var ( 41 ErrSnapshotSetNotFound = errors.New("no snapshot set with the given ID") 42 ErrSnapshotSnapsNotFound = errors.New("no snapshot for the requested snaps found in the set with the given ID") 43 ) 44 45 // A snapshotAction is used to request an operation on a snapshot. 46 type snapshotAction struct { 47 SetID uint64 `json:"set"` 48 Action string `json:"action"` 49 Snaps []string `json:"snaps,omitempty"` 50 Users []string `json:"users,omitempty"` 51 } 52 53 // A Snapshot is a collection of archives with a simple metadata json file 54 // (and hashsums of everything). 55 type Snapshot struct { 56 // SetID is the ID of the snapshot set (a snapshot set is the result of a "snap save" invocation) 57 SetID uint64 `json:"set"` 58 // the time this snapshot's data collection was started 59 Time time.Time `json:"time"` 60 61 // information about the snap this data is for 62 Snap string `json:"snap"` 63 Revision snap.Revision `json:"revision"` 64 SnapID string `json:"snap-id,omitempty"` 65 Epoch snap.Epoch `json:"epoch,omitempty"` 66 Summary string `json:"summary"` 67 Version string `json:"version"` 68 69 // the snap's configuration at snapshot time 70 Conf map[string]interface{} `json:"conf,omitempty"` 71 72 // the hash of the archives' data, keyed by archive path 73 // (either 'archive.tgz' for the system archive, or 74 // user/<username>.tgz for each user) 75 SHA3_384 map[string]string `json:"sha3-384"` 76 // the sum of the archive sizes 77 Size int64 `json:"size,omitempty"` 78 // if the snapshot failed to open this will be the reason why 79 Broken string `json:"broken,omitempty"` 80 81 // set if the snapshot was created automatically on snap removal 82 Auto bool `json:"auto,omitempty"` 83 } 84 85 // IsValid checks whether the snapshot is missing information that 86 // should be there for a snapshot that's just been opened. 87 func (sh *Snapshot) IsValid() bool { 88 return !(sh == nil || sh.SetID == 0 || sh.Snap == "" || sh.Revision.Unset() || len(sh.SHA3_384) == 0 || sh.Time.IsZero()) 89 } 90 91 // A SnapshotSet is a set of snapshots created by a single "snap save". 92 type SnapshotSet struct { 93 ID uint64 `json:"id"` 94 Snapshots []*Snapshot `json:"snapshots"` 95 } 96 97 // Time returns the earliest time in the set. 98 func (ss SnapshotSet) Time() time.Time { 99 if len(ss.Snapshots) == 0 { 100 return time.Time{} 101 } 102 mint := ss.Snapshots[0].Time 103 for _, sh := range ss.Snapshots { 104 if sh.Time.Before(mint) { 105 mint = sh.Time 106 } 107 } 108 return mint 109 } 110 111 // Size returns the sum of the set's sizes. 112 func (ss SnapshotSet) Size() int64 { 113 var sum int64 114 for _, sh := range ss.Snapshots { 115 sum += sh.Size 116 } 117 return sum 118 } 119 120 // SnapshotSets lists the snapshot sets in the system that belong to the 121 // given set (if non-zero) and are for the given snaps (if non-empty). 122 func (client *Client) SnapshotSets(setID uint64, snapNames []string) ([]SnapshotSet, error) { 123 q := make(url.Values) 124 if setID > 0 { 125 q.Add("set", strconv.FormatUint(setID, 10)) 126 } 127 if len(snapNames) > 0 { 128 q.Add("snaps", strings.Join(snapNames, ",")) 129 } 130 131 var snapshotSets []SnapshotSet 132 _, err := client.doSync("GET", "/v2/snapshots", q, nil, nil, &snapshotSets) 133 return snapshotSets, err 134 } 135 136 // ForgetSnapshots permanently removes the snapshot set, limited to the 137 // given snaps (if non-empty). 138 func (client *Client) ForgetSnapshots(setID uint64, snaps []string) (changeID string, err error) { 139 return client.snapshotAction(&snapshotAction{ 140 SetID: setID, 141 Action: "forget", 142 Snaps: snaps, 143 }) 144 } 145 146 // CheckSnapshots verifies the archive checksums in the given snapshot set. 147 // 148 // If snaps or users are non-empty, limit to checking only those 149 // archives of the snapshot. 150 func (client *Client) CheckSnapshots(setID uint64, snaps []string, users []string) (changeID string, err error) { 151 return client.snapshotAction(&snapshotAction{ 152 SetID: setID, 153 Action: "check", 154 Snaps: snaps, 155 Users: users, 156 }) 157 } 158 159 // RestoreSnapshots extracts the given snapshot set. 160 // 161 // If snaps or users are non-empty, limit to checking only those 162 // archives of the snapshot. 163 func (client *Client) RestoreSnapshots(setID uint64, snaps []string, users []string) (changeID string, err error) { 164 return client.snapshotAction(&snapshotAction{ 165 SetID: setID, 166 Action: "restore", 167 Snaps: snaps, 168 Users: users, 169 }) 170 } 171 172 func (client *Client) snapshotAction(action *snapshotAction) (changeID string, err error) { 173 data, err := json.Marshal(action) 174 if err != nil { 175 return "", fmt.Errorf("cannot marshal snapshot action: %v", err) 176 } 177 178 headers := map[string]string{ 179 "Content-Type": "application/json", 180 } 181 182 return client.doAsync("POST", "/v2/snapshots", nil, headers, bytes.NewBuffer(data)) 183 } 184 185 // SnapshotExport streams the requested snapshot set. 186 // 187 // The return value includes the length of the returned stream. 188 func (client *Client) SnapshotExport(setID uint64) (stream io.ReadCloser, contentLength int64, err error) { 189 rsp, err := client.raw(context.Background(), "GET", fmt.Sprintf("/v2/snapshots/%v/export", setID), nil, nil, nil) 190 if err != nil { 191 return nil, 0, err 192 } 193 if rsp.StatusCode != 200 { 194 defer rsp.Body.Close() 195 196 var r response 197 specificErr := r.err(client, rsp.StatusCode) 198 if err != nil { 199 return nil, 0, specificErr 200 } 201 return nil, 0, fmt.Errorf("unexpected status code: %v", rsp.Status) 202 } 203 contentType := rsp.Header.Get("Content-Type") 204 if contentType != SnapshotExportMediaType { 205 return nil, 0, fmt.Errorf("unexpected snapshot export content type %q", contentType) 206 } 207 208 return rsp.Body, rsp.ContentLength, nil 209 } 210 211 // SnapshotImportSet is a snapshot import created by a "snap import-snapshot". 212 type SnapshotImportSet struct { 213 ID uint64 `json:"set-id"` 214 Snaps []string `json:"snaps"` 215 } 216 217 // SnapshotImport imports an exported snapshot set. 218 func (client *Client) SnapshotImport(exportStream io.Reader, size int64) (SnapshotImportSet, error) { 219 headers := map[string]string{ 220 "Content-Type": SnapshotExportMediaType, 221 "Content-Length": strconv.FormatInt(size, 10), 222 } 223 224 var importSet SnapshotImportSet 225 if _, err := client.doSync("POST", "/v2/snapshots", nil, headers, exportStream, &importSet); err != nil { 226 return importSet, err 227 } 228 229 return importSet, nil 230 }