github.com/bugraaydogar/snapd@v0.0.0-20210315170335-8c70bb858939/daemon/api_debug.go (about)

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