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