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