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