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  }