github.com/anonymouse64/snapd@v0.0.0-20210824153203-04c4c42d842d/daemon/api_debug.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2015-2021 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  	"encoding/json"
    24  	"fmt"
    25  	"net/http"
    26  	"sort"
    27  	"time"
    28  
    29  	"github.com/snapcore/snapd/asserts"
    30  	"github.com/snapcore/snapd/overlord/assertstate"
    31  	"github.com/snapcore/snapd/overlord/auth"
    32  	"github.com/snapcore/snapd/overlord/devicestate"
    33  	"github.com/snapcore/snapd/overlord/snapstate"
    34  	"github.com/snapcore/snapd/overlord/state"
    35  	"github.com/snapcore/snapd/timings"
    36  )
    37  
    38  var debugCmd = &Command{
    39  	Path:        "/v2/debug",
    40  	GET:         getDebug,
    41  	POST:        postDebug,
    42  	ReadAccess:  openAccess{},
    43  	WriteAccess: rootAccess{},
    44  }
    45  
    46  type debugAction struct {
    47  	Action  string `json:"action"`
    48  	Message string `json:"message"`
    49  	Params  struct {
    50  		ChgID string `json:"chg-id"`
    51  
    52  		RecoverySystemLabel string `json:"recovery-system-label"`
    53  	} `json:"params"`
    54  }
    55  
    56  type connectivityStatus struct {
    57  	Connectivity bool     `json:"connectivity"`
    58  	Unreachable  []string `json:"unreachable,omitempty"`
    59  }
    60  
    61  func getBaseDeclaration(st *state.State) Response {
    62  	bd, err := assertstate.BaseDeclaration(st)
    63  	if err != nil {
    64  		return InternalError("cannot get base declaration: %s", err)
    65  	}
    66  	return SyncResponse(map[string]interface{}{
    67  		"base-declaration": string(asserts.Encode(bd)),
    68  	})
    69  
    70  }
    71  
    72  func checkConnectivity(st *state.State) Response {
    73  	theStore := snapstate.Store(st, nil)
    74  	st.Unlock()
    75  	defer st.Lock()
    76  	checkResult, err := theStore.ConnectivityCheck()
    77  	if err != nil {
    78  		return InternalError("cannot run connectivity check: %v", err)
    79  	}
    80  	status := connectivityStatus{Connectivity: true}
    81  	for host, reachable := range checkResult {
    82  		if !reachable {
    83  			status.Connectivity = false
    84  			status.Unreachable = append(status.Unreachable, host)
    85  		}
    86  	}
    87  	sort.Strings(status.Unreachable)
    88  
    89  	return SyncResponse(status)
    90  }
    91  
    92  type changeTimings struct {
    93  	Status         string                `json:"status,omitempty"`
    94  	Kind           string                `json:"kind,omitempty"`
    95  	Summary        string                `json:"summary,omitempty"`
    96  	Lane           int                   `json:"lane,omitempty"`
    97  	ReadyTime      time.Time             `json:"ready-time,omitempty"`
    98  	DoingTime      time.Duration         `json:"doing-time,omitempty"`
    99  	UndoingTime    time.Duration         `json:"undoing-time,omitempty"`
   100  	DoingTimings   []*timings.TimingJSON `json:"doing-timings,omitempty"`
   101  	UndoingTimings []*timings.TimingJSON `json:"undoing-timings,omitempty"`
   102  }
   103  
   104  type debugTimings struct {
   105  	ChangeID string `json:"change-id"`
   106  	// total duration of the activity - present for ensure and startup timings only
   107  	TotalDuration  time.Duration         `json:"total-duration,omitempty"`
   108  	EnsureTimings  []*timings.TimingJSON `json:"ensure-timings,omitempty"`
   109  	StartupTimings []*timings.TimingJSON `json:"startup-timings,omitempty"`
   110  	// ChangeTimings are indexed by task id
   111  	ChangeTimings map[string]*changeTimings `json:"change-timings,omitempty"`
   112  }
   113  
   114  // minLane determines the lowest lane number for the task
   115  func minLane(t *state.Task) int {
   116  	lanes := t.Lanes()
   117  	minLane := lanes[0]
   118  	for _, l := range lanes[1:] {
   119  		if l < minLane {
   120  			minLane = l
   121  		}
   122  	}
   123  	return minLane
   124  }
   125  
   126  func collectChangeTimings(st *state.State, changeID string) (map[string]*changeTimings, error) {
   127  	chg := st.Change(changeID)
   128  	if chg == nil {
   129  		return nil, fmt.Errorf("cannot find change: %v", changeID)
   130  	}
   131  
   132  	// collect "timings" for tasks of given change
   133  	stateTimings, err := timings.Get(st, -1, func(tags map[string]string) bool { return tags["change-id"] == changeID })
   134  	if err != nil {
   135  		return nil, fmt.Errorf("cannot get timings of change %s: %v", changeID, err)
   136  	}
   137  
   138  	doingTimingsByTask := make(map[string][]*timings.TimingJSON)
   139  	undoingTimingsByTask := make(map[string][]*timings.TimingJSON)
   140  	for _, tm := range stateTimings {
   141  		taskID := tm.Tags["task-id"]
   142  		if status, ok := tm.Tags["task-status"]; ok {
   143  			switch {
   144  			case status == state.DoingStatus.String():
   145  				doingTimingsByTask[taskID] = tm.NestedTimings
   146  			case status == state.UndoingStatus.String():
   147  				undoingTimingsByTask[taskID] = tm.NestedTimings
   148  			default:
   149  				return nil, fmt.Errorf("unexpected task status %q for timing of task %s", status, taskID)
   150  			}
   151  		}
   152  	}
   153  
   154  	m := map[string]*changeTimings{}
   155  	for _, t := range chg.Tasks() {
   156  		m[t.ID()] = &changeTimings{
   157  			Kind:           t.Kind(),
   158  			Status:         t.Status().String(),
   159  			Summary:        t.Summary(),
   160  			Lane:           minLane(t),
   161  			ReadyTime:      t.ReadyTime(),
   162  			DoingTime:      t.DoingTime(),
   163  			UndoingTime:    t.UndoingTime(),
   164  			DoingTimings:   doingTimingsByTask[t.ID()],
   165  			UndoingTimings: undoingTimingsByTask[t.ID()],
   166  		}
   167  	}
   168  	return m, nil
   169  }
   170  
   171  func collectEnsureTimings(st *state.State, ensureTag string, allEnsures bool) ([]*debugTimings, error) {
   172  	ensures, err := timings.Get(st, -1, func(tags map[string]string) bool {
   173  		return tags["ensure"] == ensureTag
   174  	})
   175  	if err != nil {
   176  		return nil, fmt.Errorf("cannot get timings of ensure %s: %v", ensureTag, err)
   177  	}
   178  	if len(ensures) == 0 {
   179  		return nil, fmt.Errorf("cannot find ensure: %v", ensureTag)
   180  	}
   181  
   182  	// If allEnsures is true, then report all activities of given ensure, otherwise just the latest
   183  	first := len(ensures) - 1
   184  	if allEnsures {
   185  		first = 0
   186  	}
   187  	var responseData []*debugTimings
   188  	var changeTimings map[string]*changeTimings
   189  	for _, ensureTm := range ensures[first:] {
   190  		ensureChangeID := ensureTm.Tags["change-id"]
   191  		// change is optional for ensure timings
   192  		if ensureChangeID != "" {
   193  			// ignore an error when getting a change, it may no longer be present in the state
   194  			changeTimings, _ = collectChangeTimings(st, ensureChangeID)
   195  		}
   196  		debugTm := &debugTimings{
   197  			ChangeID:      ensureChangeID,
   198  			ChangeTimings: changeTimings,
   199  			EnsureTimings: ensureTm.NestedTimings,
   200  			TotalDuration: ensureTm.Duration,
   201  		}
   202  		responseData = append(responseData, debugTm)
   203  	}
   204  
   205  	return responseData, nil
   206  }
   207  
   208  func collectStartupTimings(st *state.State, startupTag string, allStarts bool) ([]*debugTimings, error) {
   209  	starts, err := timings.Get(st, -1, func(tags map[string]string) bool {
   210  		return tags["startup"] == startupTag
   211  	})
   212  	if err != nil {
   213  		return nil, fmt.Errorf("cannot get timings of startup %s: %v", startupTag, err)
   214  	}
   215  	if len(starts) == 0 {
   216  		return nil, fmt.Errorf("cannot find startup: %v", startupTag)
   217  	}
   218  
   219  	// If allStarts is true, then report all activities of given startup, otherwise just the latest
   220  	first := len(starts) - 1
   221  	if allStarts {
   222  		first = 0
   223  	}
   224  	var responseData []*debugTimings
   225  	for _, startTm := range starts[first:] {
   226  		debugTm := &debugTimings{
   227  			StartupTimings: startTm.NestedTimings,
   228  			TotalDuration:  startTm.Duration,
   229  		}
   230  		responseData = append(responseData, debugTm)
   231  	}
   232  
   233  	return responseData, nil
   234  }
   235  
   236  func getChangeTimings(st *state.State, changeID, ensureTag, startupTag string, all bool) Response {
   237  	// If ensure tag was passed by the client, find its related changes;
   238  	// we can have many ensure executions and their changes in the responseData array.
   239  	if ensureTag != "" {
   240  		responseData, err := collectEnsureTimings(st, ensureTag, all)
   241  		if err != nil {
   242  			return BadRequest(err.Error())
   243  		}
   244  		return SyncResponse(responseData)
   245  	}
   246  
   247  	if startupTag != "" {
   248  		responseData, err := collectStartupTimings(st, startupTag, all)
   249  		if err != nil {
   250  			return BadRequest(err.Error())
   251  		}
   252  		return SyncResponse(responseData)
   253  	}
   254  
   255  	// timings for single change ID
   256  	changeTimings, err := collectChangeTimings(st, changeID)
   257  	if err != nil {
   258  		return BadRequest(err.Error())
   259  	}
   260  
   261  	responseData := []*debugTimings{
   262  		{
   263  			ChangeID:      changeID,
   264  			ChangeTimings: changeTimings,
   265  		},
   266  	}
   267  	return SyncResponse(responseData)
   268  }
   269  
   270  func createRecovery(st *state.State, label string) Response {
   271  	if label == "" {
   272  		return BadRequest("cannot create a recovery system with no label")
   273  	}
   274  	chg, err := devicestate.CreateRecoverySystem(st, label)
   275  	if err != nil {
   276  		return InternalError("cannot create recovery system %q: %v", label, err)
   277  	}
   278  	ensureStateSoon(st)
   279  	return AsyncResponse(nil, chg.ID())
   280  }
   281  
   282  func getDebug(c *Command, r *http.Request, user *auth.UserState) Response {
   283  	query := r.URL.Query()
   284  	aspect := query.Get("aspect")
   285  	st := c.d.overlord.State()
   286  	st.Lock()
   287  	defer st.Unlock()
   288  	switch aspect {
   289  	case "base-declaration":
   290  		return getBaseDeclaration(st)
   291  	case "connectivity":
   292  		return checkConnectivity(st)
   293  	case "model":
   294  		model, err := c.d.overlord.DeviceManager().Model()
   295  		if err != nil {
   296  			return InternalError("cannot get model: %v", err)
   297  		}
   298  		return SyncResponse(map[string]interface{}{
   299  			"model": string(asserts.Encode(model)),
   300  		})
   301  
   302  	case "change-timings":
   303  		chgID := query.Get("change-id")
   304  		ensureTag := query.Get("ensure")
   305  		startupTag := query.Get("startup")
   306  		all := query.Get("all")
   307  		return getChangeTimings(st, chgID, ensureTag, startupTag, all == "true")
   308  	case "seeding":
   309  		return getSeedingInfo(st)
   310  	default:
   311  		return BadRequest("unknown debug aspect %q", aspect)
   312  	}
   313  }
   314  
   315  func postDebug(c *Command, r *http.Request, user *auth.UserState) Response {
   316  	var a debugAction
   317  	decoder := json.NewDecoder(r.Body)
   318  	if err := decoder.Decode(&a); err != nil {
   319  		return BadRequest("cannot decode request body into a debug action: %v", err)
   320  	}
   321  
   322  	st := c.d.overlord.State()
   323  	st.Lock()
   324  	defer st.Unlock()
   325  
   326  	switch a.Action {
   327  	case "add-warning":
   328  		st.Warnf("%v", a.Message)
   329  		return SyncResponse(true)
   330  	case "unshow-warnings":
   331  		st.UnshowAllWarnings()
   332  		return SyncResponse(true)
   333  	case "ensure-state-soon":
   334  		ensureStateSoon(st)
   335  		return SyncResponse(true)
   336  	case "can-manage-refreshes":
   337  		return SyncResponse(devicestate.CanManageRefreshes(st))
   338  	case "prune":
   339  		opTime, err := c.d.overlord.DeviceManager().StartOfOperationTime()
   340  		if err != nil {
   341  			return BadRequest("cannot get start of operation time: %s", err)
   342  		}
   343  		st.Prune(opTime, 0, 0, 0)
   344  		return SyncResponse(true)
   345  	case "stacktraces":
   346  		return getStacktraces()
   347  	case "create-recovery-system":
   348  		return createRecovery(st, a.Params.RecoverySystemLabel)
   349  	default:
   350  		return BadRequest("unknown debug action: %v", a.Action)
   351  	}
   352  }