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