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