github.com/ubuntu-core/snappy@v0.0.0-20210827154228-9e584df982bb/daemon/api_general.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2015-2020 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  	"bytes"
    24  	"encoding/json"
    25  	"net/http"
    26  	"os/exec"
    27  	"sort"
    28  	"time"
    29  
    30  	"github.com/snapcore/snapd/arch"
    31  	"github.com/snapcore/snapd/client"
    32  	"github.com/snapcore/snapd/dirs"
    33  	"github.com/snapcore/snapd/interfaces"
    34  	"github.com/snapcore/snapd/logger"
    35  	"github.com/snapcore/snapd/osutil"
    36  	"github.com/snapcore/snapd/overlord/auth"
    37  	"github.com/snapcore/snapd/overlord/devicestate"
    38  	"github.com/snapcore/snapd/overlord/state"
    39  	"github.com/snapcore/snapd/release"
    40  	"github.com/snapcore/snapd/sandbox"
    41  	"github.com/snapcore/snapd/snap"
    42  )
    43  
    44  var (
    45  	// see daemon.go:canAccess for details how the access is controlled
    46  	rootCmd = &Command{
    47  		Path:       "/",
    48  		GET:        tbd,
    49  		ReadAccess: openAccess{},
    50  	}
    51  
    52  	sysInfoCmd = &Command{
    53  		Path:       "/v2/system-info",
    54  		GET:        sysInfo,
    55  		ReadAccess: openAccess{},
    56  	}
    57  
    58  	stateChangeCmd = &Command{
    59  		Path:        "/v2/changes/{id}",
    60  		GET:         getChange,
    61  		POST:        abortChange,
    62  		ReadAccess:  openAccess{},
    63  		WriteAccess: authenticatedAccess{Polkit: polkitActionManage},
    64  	}
    65  
    66  	stateChangesCmd = &Command{
    67  		Path:       "/v2/changes",
    68  		GET:        getChanges,
    69  		ReadAccess: openAccess{},
    70  	}
    71  
    72  	warningsCmd = &Command{
    73  		Path:        "/v2/warnings",
    74  		GET:         getWarnings,
    75  		POST:        ackWarnings,
    76  		ReadAccess:  openAccess{},
    77  		WriteAccess: authenticatedAccess{Polkit: polkitActionManage},
    78  	}
    79  )
    80  
    81  var (
    82  	buildID     = "unknown"
    83  	systemdVirt = ""
    84  )
    85  
    86  func init() {
    87  	// cache the build-id on startup to ensure that changes in
    88  	// the underlying binary do not affect us
    89  	if bid, err := osutil.MyBuildID(); err == nil {
    90  		buildID = bid
    91  	}
    92  	// cache systemd-detect-virt output as it's unlikely to change :-)
    93  	if buf, err := exec.Command("systemd-detect-virt").CombinedOutput(); err == nil {
    94  		systemdVirt = string(bytes.TrimSpace(buf))
    95  	}
    96  }
    97  
    98  func tbd(c *Command, r *http.Request, user *auth.UserState) Response {
    99  	return SyncResponse([]string{"TBD"})
   100  }
   101  
   102  func sysInfo(c *Command, r *http.Request, user *auth.UserState) Response {
   103  	st := c.d.overlord.State()
   104  	snapMgr := c.d.overlord.SnapManager()
   105  	deviceMgr := c.d.overlord.DeviceManager()
   106  	st.Lock()
   107  	defer st.Unlock()
   108  	nextRefresh := snapMgr.NextRefresh()
   109  	lastRefresh, _ := snapMgr.LastRefresh()
   110  	refreshHold, _ := snapMgr.EffectiveRefreshHold()
   111  	refreshScheduleStr, legacySchedule, err := snapMgr.RefreshSchedule()
   112  	if err != nil {
   113  		return InternalError("cannot get refresh schedule: %s", err)
   114  	}
   115  	users, err := auth.Users(st)
   116  	if err != nil && err != state.ErrNoState {
   117  		return InternalError("cannot get user auth data: %s", err)
   118  	}
   119  
   120  	refreshInfo := client.RefreshInfo{
   121  		Last: formatRefreshTime(lastRefresh),
   122  		Hold: formatRefreshTime(refreshHold),
   123  		Next: formatRefreshTime(nextRefresh),
   124  	}
   125  	if !legacySchedule {
   126  		refreshInfo.Timer = refreshScheduleStr
   127  	} else {
   128  		refreshInfo.Schedule = refreshScheduleStr
   129  	}
   130  
   131  	m := map[string]interface{}{
   132  		"series":         release.Series,
   133  		"version":        c.d.Version,
   134  		"build-id":       buildID,
   135  		"os-release":     release.ReleaseInfo,
   136  		"on-classic":     release.OnClassic,
   137  		"managed":        len(users) > 0,
   138  		"kernel-version": osutil.KernelVersion(),
   139  		"locations": map[string]interface{}{
   140  			"snap-mount-dir": dirs.SnapMountDir,
   141  			"snap-bin-dir":   dirs.SnapBinariesDir,
   142  		},
   143  		"refresh":      refreshInfo,
   144  		"architecture": arch.DpkgArchitecture(),
   145  		"system-mode":  deviceMgr.SystemMode(devicestate.SysAny),
   146  	}
   147  	if systemdVirt != "" {
   148  		m["virtualization"] = systemdVirt
   149  	}
   150  
   151  	// NOTE: Right now we don't have a good way to differentiate if we
   152  	// only have partial confinement (ala AppArmor disabled and Seccomp
   153  	// enabled) or no confinement at all. Once we have a better system
   154  	// in place how we can dynamically retrieve these information from
   155  	// snapd we will use this here.
   156  	if sandbox.ForceDevMode() {
   157  		m["confinement"] = "partial"
   158  	} else {
   159  		m["confinement"] = "strict"
   160  	}
   161  
   162  	// Convey richer information about features of available security backends.
   163  	if features := sandboxFeatures(c.d.overlord.InterfaceManager().Repository().Backends()); features != nil {
   164  		m["sandbox-features"] = features
   165  	}
   166  
   167  	return SyncResponse(m)
   168  }
   169  
   170  func formatRefreshTime(t time.Time) string {
   171  	if t.IsZero() {
   172  		return ""
   173  	}
   174  	return t.Truncate(time.Minute).Format(time.RFC3339)
   175  }
   176  
   177  func sandboxFeatures(backends []interfaces.SecurityBackend) map[string][]string {
   178  	result := make(map[string][]string, len(backends)+1)
   179  	for _, backend := range backends {
   180  		features := backend.SandboxFeatures()
   181  		if len(features) > 0 {
   182  			sort.Strings(features)
   183  			result[string(backend.Name())] = features
   184  		}
   185  	}
   186  
   187  	// Add information about supported confinement types as a fake backend
   188  	features := make([]string, 1, 3)
   189  	features[0] = "devmode"
   190  	if !sandbox.ForceDevMode() {
   191  		features = append(features, "strict")
   192  	}
   193  	if dirs.SupportsClassicConfinement() {
   194  		features = append(features, "classic")
   195  	}
   196  	sort.Strings(features)
   197  	result["confinement-options"] = features
   198  
   199  	return result
   200  }
   201  
   202  func getChange(c *Command, r *http.Request, user *auth.UserState) Response {
   203  	chID := muxVars(r)["id"]
   204  	state := c.d.overlord.State()
   205  	state.Lock()
   206  	defer state.Unlock()
   207  	chg := state.Change(chID)
   208  	if chg == nil {
   209  		return NotFound("cannot find change with id %q", chID)
   210  	}
   211  
   212  	return SyncResponse(change2changeInfo(chg))
   213  }
   214  
   215  func getChanges(c *Command, r *http.Request, user *auth.UserState) Response {
   216  	query := r.URL.Query()
   217  	qselect := query.Get("select")
   218  	if qselect == "" {
   219  		qselect = "in-progress"
   220  	}
   221  	var filter func(*state.Change) bool
   222  	switch qselect {
   223  	case "all":
   224  		filter = func(*state.Change) bool { return true }
   225  	case "in-progress":
   226  		filter = func(chg *state.Change) bool { return !chg.Status().Ready() }
   227  	case "ready":
   228  		filter = func(chg *state.Change) bool { return chg.Status().Ready() }
   229  	default:
   230  		return BadRequest("select should be one of: all,in-progress,ready")
   231  	}
   232  
   233  	if wantedName := query.Get("for"); wantedName != "" {
   234  		outerFilter := filter
   235  		filter = func(chg *state.Change) bool {
   236  			if !outerFilter(chg) {
   237  				return false
   238  			}
   239  
   240  			var snapNames []string
   241  			if err := chg.Get("snap-names", &snapNames); err != nil {
   242  				logger.Noticef("Cannot get snap-name for change %v", chg.ID())
   243  				return false
   244  			}
   245  
   246  			for _, name := range snapNames {
   247  				// due to
   248  				// https://bugs.launchpad.net/snapd/+bug/1880560
   249  				// the snap-names in service-control changes
   250  				// could have included <snap>.<app>
   251  				snapName, _ := snap.SplitSnapApp(name)
   252  				if snapName == wantedName {
   253  					return true
   254  				}
   255  			}
   256  			return false
   257  		}
   258  	}
   259  
   260  	state := c.d.overlord.State()
   261  	state.Lock()
   262  	defer state.Unlock()
   263  	chgs := state.Changes()
   264  	chgInfos := make([]*changeInfo, 0, len(chgs))
   265  	for _, chg := range chgs {
   266  		if !filter(chg) {
   267  			continue
   268  		}
   269  		chgInfos = append(chgInfos, change2changeInfo(chg))
   270  	}
   271  	return SyncResponse(chgInfos)
   272  }
   273  
   274  func abortChange(c *Command, r *http.Request, user *auth.UserState) Response {
   275  	chID := muxVars(r)["id"]
   276  	state := c.d.overlord.State()
   277  	state.Lock()
   278  	defer state.Unlock()
   279  	chg := state.Change(chID)
   280  	if chg == nil {
   281  		return NotFound("cannot find change with id %q", chID)
   282  	}
   283  
   284  	var reqData struct {
   285  		Action string `json:"action"`
   286  	}
   287  
   288  	decoder := json.NewDecoder(r.Body)
   289  	if err := decoder.Decode(&reqData); err != nil {
   290  		return BadRequest("cannot decode data from request body: %v", err)
   291  	}
   292  
   293  	if reqData.Action != "abort" {
   294  		return BadRequest("change action %q is unsupported", reqData.Action)
   295  	}
   296  
   297  	if chg.Status().Ready() {
   298  		return BadRequest("cannot abort change %s with nothing pending", chID)
   299  	}
   300  
   301  	// flag the change
   302  	chg.Abort()
   303  
   304  	// actually ask to proceed with the abort
   305  	ensureStateSoon(state)
   306  
   307  	return SyncResponse(change2changeInfo(chg))
   308  }
   309  
   310  type changeInfo struct {
   311  	ID      string      `json:"id"`
   312  	Kind    string      `json:"kind"`
   313  	Summary string      `json:"summary"`
   314  	Status  string      `json:"status"`
   315  	Tasks   []*taskInfo `json:"tasks,omitempty"`
   316  	Ready   bool        `json:"ready"`
   317  	Err     string      `json:"err,omitempty"`
   318  
   319  	SpawnTime time.Time  `json:"spawn-time,omitempty"`
   320  	ReadyTime *time.Time `json:"ready-time,omitempty"`
   321  
   322  	Data map[string]*json.RawMessage `json:"data,omitempty"`
   323  }
   324  
   325  type taskInfo struct {
   326  	ID       string           `json:"id"`
   327  	Kind     string           `json:"kind"`
   328  	Summary  string           `json:"summary"`
   329  	Status   string           `json:"status"`
   330  	Log      []string         `json:"log,omitempty"`
   331  	Progress taskInfoProgress `json:"progress"`
   332  
   333  	SpawnTime time.Time  `json:"spawn-time,omitempty"`
   334  	ReadyTime *time.Time `json:"ready-time,omitempty"`
   335  }
   336  
   337  type taskInfoProgress struct {
   338  	Label string `json:"label"`
   339  	Done  int    `json:"done"`
   340  	Total int    `json:"total"`
   341  }
   342  
   343  func change2changeInfo(chg *state.Change) *changeInfo {
   344  	status := chg.Status()
   345  	chgInfo := &changeInfo{
   346  		ID:      chg.ID(),
   347  		Kind:    chg.Kind(),
   348  		Summary: chg.Summary(),
   349  		Status:  status.String(),
   350  		Ready:   status.Ready(),
   351  
   352  		SpawnTime: chg.SpawnTime(),
   353  	}
   354  	readyTime := chg.ReadyTime()
   355  	if !readyTime.IsZero() {
   356  		chgInfo.ReadyTime = &readyTime
   357  	}
   358  	if err := chg.Err(); err != nil {
   359  		chgInfo.Err = err.Error()
   360  	}
   361  
   362  	tasks := chg.Tasks()
   363  	taskInfos := make([]*taskInfo, len(tasks))
   364  	for j, t := range tasks {
   365  		label, done, total := t.Progress()
   366  
   367  		taskInfo := &taskInfo{
   368  			ID:      t.ID(),
   369  			Kind:    t.Kind(),
   370  			Summary: t.Summary(),
   371  			Status:  t.Status().String(),
   372  			Log:     t.Log(),
   373  			Progress: taskInfoProgress{
   374  				Label: label,
   375  				Done:  done,
   376  				Total: total,
   377  			},
   378  			SpawnTime: t.SpawnTime(),
   379  		}
   380  		readyTime := t.ReadyTime()
   381  		if !readyTime.IsZero() {
   382  			taskInfo.ReadyTime = &readyTime
   383  		}
   384  		taskInfos[j] = taskInfo
   385  	}
   386  	chgInfo.Tasks = taskInfos
   387  
   388  	var data map[string]*json.RawMessage
   389  	if chg.Get("api-data", &data) == nil {
   390  		chgInfo.Data = data
   391  	}
   392  
   393  	return chgInfo
   394  }
   395  
   396  var (
   397  	stateOkayWarnings    = (*state.State).OkayWarnings
   398  	stateAllWarnings     = (*state.State).AllWarnings
   399  	statePendingWarnings = (*state.State).PendingWarnings
   400  )
   401  
   402  func getWarnings(c *Command, r *http.Request, _ *auth.UserState) Response {
   403  	query := r.URL.Query()
   404  	var all bool
   405  	sel := query.Get("select")
   406  	switch sel {
   407  	case "all":
   408  		all = true
   409  	case "pending", "":
   410  		all = false
   411  	default:
   412  		return BadRequest("invalid select parameter: %q", sel)
   413  	}
   414  
   415  	st := c.d.overlord.State()
   416  	st.Lock()
   417  	defer st.Unlock()
   418  
   419  	var ws []*state.Warning
   420  	if all {
   421  		ws = stateAllWarnings(st)
   422  	} else {
   423  		ws, _ = statePendingWarnings(st)
   424  	}
   425  	if len(ws) == 0 {
   426  		// no need to confuse the issue
   427  		return SyncResponse([]state.Warning{})
   428  	}
   429  
   430  	return SyncResponse(ws)
   431  }
   432  
   433  func ackWarnings(c *Command, r *http.Request, _ *auth.UserState) Response {
   434  	defer r.Body.Close()
   435  	var op struct {
   436  		Action    string    `json:"action"`
   437  		Timestamp time.Time `json:"timestamp"`
   438  	}
   439  	decoder := json.NewDecoder(r.Body)
   440  	if err := decoder.Decode(&op); err != nil {
   441  		return BadRequest("cannot decode request body into warnings operation: %v", err)
   442  	}
   443  	if op.Action != "okay" {
   444  		return BadRequest("unknown warning action %q", op.Action)
   445  	}
   446  	st := c.d.overlord.State()
   447  	st.Lock()
   448  	defer st.Unlock()
   449  	n := stateOkayWarnings(st, op.Timestamp)
   450  
   451  	return SyncResponse(n)
   452  }