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