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  }