mirror of
https://github.com/token2/snapd.git
synced 2026-03-13 11:15:47 -07:00
* many: remove all `device-setup` fde-setup code When we initially worked on the inline-cryto-engine (ICE) code we had a design based on the fde-setup hook that would return a `device-setup` feature as a hint that ICE should be used. It turned out this design was impractical and we moved to a much better approach that got merged in https://github.com/snapcore/snapd/pull/12589 and relies on support for ICE in the kernel and cryptsetup. With that new approach all the code that was supporting the `device-setup` approach can be removed now. * daemon: consider again the fde-setup hook case * client: consider the ICE encryption type * osu/disks,kernel: remove device-unlock support as well --------- Co-authored-by: Samuele Pedroni <pedronis@lucediurna.net>
448 lines
13 KiB
Go
448 lines
13 KiB
Go
// -*- Mode: Go; indent-tabs-mode: t -*-
|
|
|
|
/*
|
|
* Copyright (C) 2015-2023 Canonical Ltd
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License version 3 as
|
|
* published by the Free Software Foundation.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*
|
|
*/
|
|
|
|
package daemon
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"sort"
|
|
"time"
|
|
|
|
"github.com/snapcore/snapd/asserts"
|
|
"github.com/snapcore/snapd/dirs"
|
|
"github.com/snapcore/snapd/gadget"
|
|
"github.com/snapcore/snapd/gadget/device"
|
|
"github.com/snapcore/snapd/osutil/disks"
|
|
"github.com/snapcore/snapd/overlord/assertstate"
|
|
"github.com/snapcore/snapd/overlord/auth"
|
|
"github.com/snapcore/snapd/overlord/devicestate"
|
|
"github.com/snapcore/snapd/overlord/snapstate"
|
|
"github.com/snapcore/snapd/overlord/state"
|
|
"github.com/snapcore/snapd/secboot"
|
|
"github.com/snapcore/snapd/timings"
|
|
)
|
|
|
|
var debugCmd = &Command{
|
|
Path: "/v2/debug",
|
|
GET: getDebug,
|
|
POST: postDebug,
|
|
ReadAccess: openAccess{},
|
|
WriteAccess: rootAccess{},
|
|
}
|
|
|
|
type debugAction struct {
|
|
Action string `json:"action"`
|
|
Message string `json:"message"`
|
|
Params struct {
|
|
ChgID string `json:"chg-id"`
|
|
|
|
RecoverySystemLabel string `json:"recovery-system-label"`
|
|
} `json:"params"`
|
|
Snaps []string `json:"snaps"`
|
|
}
|
|
|
|
type connectivityStatus struct {
|
|
Connectivity bool `json:"connectivity"`
|
|
Unreachable []string `json:"unreachable,omitempty"`
|
|
}
|
|
|
|
func getBaseDeclaration(st *state.State) Response {
|
|
bd, err := assertstate.BaseDeclaration(st)
|
|
if err != nil {
|
|
return InternalError("cannot get base declaration: %s", err)
|
|
}
|
|
return SyncResponse(map[string]interface{}{
|
|
"base-declaration": string(asserts.Encode(bd)),
|
|
})
|
|
|
|
}
|
|
|
|
func checkConnectivity(st *state.State) Response {
|
|
theStore := snapstate.Store(st, nil)
|
|
st.Unlock()
|
|
defer st.Lock()
|
|
checkResult, err := theStore.ConnectivityCheck()
|
|
if err != nil {
|
|
return InternalError("cannot run connectivity check: %v", err)
|
|
}
|
|
status := connectivityStatus{Connectivity: true}
|
|
for host, reachable := range checkResult {
|
|
if !reachable {
|
|
status.Connectivity = false
|
|
status.Unreachable = append(status.Unreachable, host)
|
|
}
|
|
}
|
|
sort.Strings(status.Unreachable)
|
|
|
|
return SyncResponse(status)
|
|
}
|
|
|
|
type changeTimings struct {
|
|
Status string `json:"status,omitempty"`
|
|
Kind string `json:"kind,omitempty"`
|
|
Summary string `json:"summary,omitempty"`
|
|
Lane int `json:"lane,omitempty"`
|
|
ReadyTime time.Time `json:"ready-time,omitempty"`
|
|
DoingTime time.Duration `json:"doing-time,omitempty"`
|
|
UndoingTime time.Duration `json:"undoing-time,omitempty"`
|
|
DoingTimings []*timings.TimingJSON `json:"doing-timings,omitempty"`
|
|
UndoingTimings []*timings.TimingJSON `json:"undoing-timings,omitempty"`
|
|
}
|
|
|
|
type debugTimings struct {
|
|
ChangeID string `json:"change-id"`
|
|
// total duration of the activity - present for ensure and startup timings only
|
|
TotalDuration time.Duration `json:"total-duration,omitempty"`
|
|
EnsureTimings []*timings.TimingJSON `json:"ensure-timings,omitempty"`
|
|
StartupTimings []*timings.TimingJSON `json:"startup-timings,omitempty"`
|
|
// ChangeTimings are indexed by task id
|
|
ChangeTimings map[string]*changeTimings `json:"change-timings,omitempty"`
|
|
}
|
|
|
|
// minLane determines the lowest lane number for the task
|
|
func minLane(t *state.Task) int {
|
|
lanes := t.Lanes()
|
|
minLane := lanes[0]
|
|
for _, l := range lanes[1:] {
|
|
if l < minLane {
|
|
minLane = l
|
|
}
|
|
}
|
|
return minLane
|
|
}
|
|
|
|
func collectChangeTimings(st *state.State, changeID string) (map[string]*changeTimings, error) {
|
|
chg := st.Change(changeID)
|
|
if chg == nil {
|
|
return nil, fmt.Errorf("cannot find change: %v", changeID)
|
|
}
|
|
|
|
// collect "timings" for tasks of given change
|
|
stateTimings, err := timings.Get(st, -1, func(tags map[string]string) bool { return tags["change-id"] == changeID })
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot get timings of change %s: %v", changeID, err)
|
|
}
|
|
|
|
doingTimingsByTask := make(map[string][]*timings.TimingJSON)
|
|
undoingTimingsByTask := make(map[string][]*timings.TimingJSON)
|
|
for _, tm := range stateTimings {
|
|
taskID := tm.Tags["task-id"]
|
|
if status, ok := tm.Tags["task-status"]; ok {
|
|
switch {
|
|
case status == state.DoingStatus.String():
|
|
doingTimingsByTask[taskID] = tm.NestedTimings
|
|
case status == state.UndoingStatus.String():
|
|
undoingTimingsByTask[taskID] = tm.NestedTimings
|
|
default:
|
|
return nil, fmt.Errorf("unexpected task status %q for timing of task %s", status, taskID)
|
|
}
|
|
}
|
|
}
|
|
|
|
m := map[string]*changeTimings{}
|
|
for _, t := range chg.Tasks() {
|
|
m[t.ID()] = &changeTimings{
|
|
Kind: t.Kind(),
|
|
Status: t.Status().String(),
|
|
Summary: t.Summary(),
|
|
Lane: minLane(t),
|
|
ReadyTime: t.ReadyTime(),
|
|
DoingTime: t.DoingTime(),
|
|
UndoingTime: t.UndoingTime(),
|
|
DoingTimings: doingTimingsByTask[t.ID()],
|
|
UndoingTimings: undoingTimingsByTask[t.ID()],
|
|
}
|
|
}
|
|
return m, nil
|
|
}
|
|
|
|
func collectEnsureTimings(st *state.State, ensureTag string, allEnsures bool) ([]*debugTimings, error) {
|
|
ensures, err := timings.Get(st, -1, func(tags map[string]string) bool {
|
|
return tags["ensure"] == ensureTag
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot get timings of ensure %s: %v", ensureTag, err)
|
|
}
|
|
if len(ensures) == 0 {
|
|
return nil, fmt.Errorf("cannot find ensure: %v", ensureTag)
|
|
}
|
|
|
|
// If allEnsures is true, then report all activities of given ensure, otherwise just the latest
|
|
first := len(ensures) - 1
|
|
if allEnsures {
|
|
first = 0
|
|
}
|
|
var responseData []*debugTimings
|
|
var changeTimings map[string]*changeTimings
|
|
for _, ensureTm := range ensures[first:] {
|
|
ensureChangeID := ensureTm.Tags["change-id"]
|
|
// change is optional for ensure timings
|
|
if ensureChangeID != "" {
|
|
// ignore an error when getting a change, it may no longer be present in the state
|
|
changeTimings, _ = collectChangeTimings(st, ensureChangeID)
|
|
}
|
|
debugTm := &debugTimings{
|
|
ChangeID: ensureChangeID,
|
|
ChangeTimings: changeTimings,
|
|
EnsureTimings: ensureTm.NestedTimings,
|
|
TotalDuration: ensureTm.Duration,
|
|
}
|
|
responseData = append(responseData, debugTm)
|
|
}
|
|
|
|
return responseData, nil
|
|
}
|
|
|
|
func collectStartupTimings(st *state.State, startupTag string, allStarts bool) ([]*debugTimings, error) {
|
|
starts, err := timings.Get(st, -1, func(tags map[string]string) bool {
|
|
return tags["startup"] == startupTag
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot get timings of startup %s: %v", startupTag, err)
|
|
}
|
|
if len(starts) == 0 {
|
|
return nil, fmt.Errorf("cannot find startup: %v", startupTag)
|
|
}
|
|
|
|
// If allStarts is true, then report all activities of given startup, otherwise just the latest
|
|
first := len(starts) - 1
|
|
if allStarts {
|
|
first = 0
|
|
}
|
|
var responseData []*debugTimings
|
|
for _, startTm := range starts[first:] {
|
|
debugTm := &debugTimings{
|
|
StartupTimings: startTm.NestedTimings,
|
|
TotalDuration: startTm.Duration,
|
|
}
|
|
responseData = append(responseData, debugTm)
|
|
}
|
|
|
|
return responseData, nil
|
|
}
|
|
|
|
func getChangeTimings(st *state.State, changeID, ensureTag, startupTag string, all bool) Response {
|
|
// If ensure tag was passed by the client, find its related changes;
|
|
// we can have many ensure executions and their changes in the responseData array.
|
|
if ensureTag != "" {
|
|
responseData, err := collectEnsureTimings(st, ensureTag, all)
|
|
if err != nil {
|
|
return BadRequest(err.Error())
|
|
}
|
|
return SyncResponse(responseData)
|
|
}
|
|
|
|
if startupTag != "" {
|
|
responseData, err := collectStartupTimings(st, startupTag, all)
|
|
if err != nil {
|
|
return BadRequest(err.Error())
|
|
}
|
|
return SyncResponse(responseData)
|
|
}
|
|
|
|
// timings for single change ID
|
|
changeTimings, err := collectChangeTimings(st, changeID)
|
|
if err != nil {
|
|
return BadRequest(err.Error())
|
|
}
|
|
|
|
responseData := []*debugTimings{
|
|
{
|
|
ChangeID: changeID,
|
|
ChangeTimings: changeTimings,
|
|
},
|
|
}
|
|
return SyncResponse(responseData)
|
|
}
|
|
|
|
func getGadgetDiskMapping(st *state.State) Response {
|
|
deviceCtx, err := devicestate.DeviceCtx(st, nil, nil)
|
|
if err != nil {
|
|
return InternalError("cannot get device context: %v", err)
|
|
}
|
|
gadgetInfo, err := snapstate.GadgetInfo(st, deviceCtx)
|
|
if err != nil {
|
|
return InternalError("cannot get gadget info: %v", err)
|
|
}
|
|
gadgetDir := gadgetInfo.MountDir()
|
|
|
|
kernelInfo, err := snapstate.KernelInfo(st, deviceCtx)
|
|
if err != nil {
|
|
return InternalError("cannot get kernel info: %v", err)
|
|
}
|
|
kernelDir := kernelInfo.MountDir()
|
|
|
|
mod := deviceCtx.Model()
|
|
|
|
// Find out if we are encrypted
|
|
encType := secboot.EncryptionTypeNone
|
|
sealingMethod, err := device.SealedKeysMethod(dirs.GlobalRootDir)
|
|
if err != nil {
|
|
if err != device.ErrNoSealedKeys {
|
|
return InternalError("cannot find out crypto state: %v", err)
|
|
}
|
|
// no sealed keys, so no encryption
|
|
} else {
|
|
// TODO: is there a better way to find the encType
|
|
// than indirectly via the sealedKeyMethods? does not
|
|
// matter right now because there really is only one
|
|
// encryption type
|
|
switch sealingMethod {
|
|
case device.SealingMethodLegacyTPM, device.SealingMethodTPM, device.SealingMethodFDESetupHook:
|
|
// LUKS and LUKS-with-ICE are the same for what is
|
|
// required here
|
|
encType = secboot.EncryptionTypeLUKS
|
|
default:
|
|
return InternalError("unknown sealing method: %s", sealingMethod)
|
|
}
|
|
}
|
|
|
|
_, allLaidOutVols, err := gadget.LaidOutVolumesFromGadget(gadgetDir, kernelDir, mod, encType)
|
|
if err != nil {
|
|
return InternalError("cannot get all disk volume device traits: cannot layout volumes: %v", err)
|
|
}
|
|
|
|
// TODO: allow passing in encrypted options info here
|
|
|
|
// allow implicit system-data on pre-uc20 only
|
|
optsMap := map[string]*gadget.DiskVolumeValidationOptions{}
|
|
for vol := range allLaidOutVols {
|
|
optsMap[vol] = &gadget.DiskVolumeValidationOptions{
|
|
AllowImplicitSystemData: mod.Grade() == asserts.ModelGradeUnset,
|
|
}
|
|
}
|
|
|
|
res, err := gadget.AllDiskVolumeDeviceTraits(allLaidOutVols, optsMap)
|
|
if err != nil {
|
|
return InternalError("cannot get all disk volume device traits: %v", err)
|
|
}
|
|
|
|
return SyncResponse(res)
|
|
}
|
|
|
|
func getDisks(st *state.State) Response {
|
|
|
|
disks, err := disks.AllPhysicalDisks()
|
|
if err != nil {
|
|
return InternalError("cannot get all physical disks: %v", err)
|
|
}
|
|
vols := make([]*gadget.OnDiskVolume, 0, len(disks))
|
|
for _, d := range disks {
|
|
vol, err := gadget.OnDiskVolumeFromDisk(d)
|
|
if err != nil {
|
|
return InternalError("cannot get on disk volume for device %s: %v", d.KernelDeviceNode(), err)
|
|
}
|
|
vols = append(vols, vol)
|
|
}
|
|
|
|
return SyncResponse(vols)
|
|
}
|
|
|
|
func createRecovery(st *state.State, label string) Response {
|
|
if label == "" {
|
|
return BadRequest("cannot create a recovery system with no label")
|
|
}
|
|
chg, err := devicestate.CreateRecoverySystem(st, label)
|
|
if err != nil {
|
|
return InternalError("cannot create recovery system %q: %v", label, err)
|
|
}
|
|
ensureStateSoon(st)
|
|
return AsyncResponse(nil, chg.ID())
|
|
}
|
|
|
|
func getDebug(c *Command, r *http.Request, user *auth.UserState) Response {
|
|
query := r.URL.Query()
|
|
aspect := query.Get("aspect")
|
|
st := c.d.overlord.State()
|
|
st.Lock()
|
|
defer st.Unlock()
|
|
switch aspect {
|
|
case "base-declaration":
|
|
return getBaseDeclaration(st)
|
|
case "connectivity":
|
|
return checkConnectivity(st)
|
|
case "model":
|
|
model, err := c.d.overlord.DeviceManager().Model()
|
|
if err != nil {
|
|
return InternalError("cannot get model: %v", err)
|
|
}
|
|
return SyncResponse(map[string]interface{}{
|
|
"model": string(asserts.Encode(model)),
|
|
})
|
|
|
|
case "change-timings":
|
|
chgID := query.Get("change-id")
|
|
ensureTag := query.Get("ensure")
|
|
startupTag := query.Get("startup")
|
|
all := query.Get("all")
|
|
return getChangeTimings(st, chgID, ensureTag, startupTag, all == "true")
|
|
case "seeding":
|
|
return getSeedingInfo(st)
|
|
case "gadget-disk-mapping":
|
|
return getGadgetDiskMapping(st)
|
|
case "disks":
|
|
return getDisks(st)
|
|
default:
|
|
return BadRequest("unknown debug aspect %q", aspect)
|
|
}
|
|
}
|
|
|
|
func postDebug(c *Command, r *http.Request, user *auth.UserState) Response {
|
|
var a debugAction
|
|
decoder := json.NewDecoder(r.Body)
|
|
if err := decoder.Decode(&a); err != nil {
|
|
return BadRequest("cannot decode request body into a debug action: %v", err)
|
|
}
|
|
|
|
st := c.d.overlord.State()
|
|
st.Lock()
|
|
defer st.Unlock()
|
|
|
|
switch a.Action {
|
|
case "add-warning":
|
|
st.Warnf("%v", a.Message)
|
|
return SyncResponse(true)
|
|
case "unshow-warnings":
|
|
st.UnshowAllWarnings()
|
|
return SyncResponse(true)
|
|
case "ensure-state-soon":
|
|
ensureStateSoon(st)
|
|
return SyncResponse(true)
|
|
case "can-manage-refreshes":
|
|
return SyncResponse(devicestate.CanManageRefreshes(st))
|
|
case "prune":
|
|
opTime, err := c.d.overlord.DeviceManager().StartOfOperationTime()
|
|
if err != nil {
|
|
return BadRequest("cannot get start of operation time: %s", err)
|
|
}
|
|
st.Prune(opTime, 0, 0, 0)
|
|
return SyncResponse(true)
|
|
case "stacktraces":
|
|
return getStacktraces()
|
|
case "create-recovery-system":
|
|
return createRecovery(st, a.Params.RecoverySystemLabel)
|
|
case "migrate-home":
|
|
return migrateHome(st, a.Snaps)
|
|
default:
|
|
return BadRequest("unknown debug action: %v", a.Action)
|
|
}
|
|
}
|