github.com/rigado/snapd@v2.42.5-go-mod+incompatible/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  	DoingTime      time.Duration         `json:"doing-time,omitempty"`
    90  	UndoingTime    time.Duration         `json:"undoing-time,omitempty"`
    91  	DoingTimings   []*timings.TimingJSON `json:"doing-timings,omitempty"`
    92  	UndoingTimings []*timings.TimingJSON `json:"undoing-timings,omitempty"`
    93  }
    94  
    95  type debugTimings struct {
    96  	ChangeID       string                    `json:"change-id"`
    97  	EnsureTimings  []*timings.TimingJSON     `json:"ensure-timings,omitempty"`
    98  	StartupTimings []*timings.TimingJSON     `json:"startup-timings,omitempty"`
    99  	ChangeTimings  map[string]*changeTimings `json:"change-timings,omitempty"`
   100  }
   101  
   102  func collectChangeTimings(st *state.State, changeID string) (map[string]*changeTimings, error) {
   103  	chg := st.Change(changeID)
   104  	if chg == nil {
   105  		return nil, fmt.Errorf("cannot find change: %v", changeID)
   106  	}
   107  
   108  	// collect "timings" for tasks of given change
   109  	stateTimings, err := timings.Get(st, -1, func(tags map[string]string) bool { return tags["change-id"] == changeID })
   110  	if err != nil {
   111  		return nil, fmt.Errorf("cannot get timings of change %s: %v", changeID, err)
   112  	}
   113  
   114  	doingTimingsByTask := make(map[string][]*timings.TimingJSON)
   115  	undoingTimingsByTask := make(map[string][]*timings.TimingJSON)
   116  	for _, tm := range stateTimings {
   117  		taskID := tm.Tags["task-id"]
   118  		if status, ok := tm.Tags["task-status"]; ok {
   119  			switch {
   120  			case status == state.DoingStatus.String():
   121  				doingTimingsByTask[taskID] = tm.NestedTimings
   122  			case status == state.UndoingStatus.String():
   123  				undoingTimingsByTask[taskID] = tm.NestedTimings
   124  			default:
   125  				return nil, fmt.Errorf("unexpected task status %q for timing of task %s", status, taskID)
   126  			}
   127  		}
   128  	}
   129  
   130  	m := map[string]*changeTimings{}
   131  	for _, t := range chg.Tasks() {
   132  		m[t.ID()] = &changeTimings{
   133  			DoingTime:      t.DoingTime(),
   134  			UndoingTime:    t.UndoingTime(),
   135  			DoingTimings:   doingTimingsByTask[t.ID()],
   136  			UndoingTimings: undoingTimingsByTask[t.ID()],
   137  		}
   138  	}
   139  	return m, nil
   140  }
   141  
   142  func collectEnsureTimings(st *state.State, ensureTag string, allEnsures bool) ([]*debugTimings, error) {
   143  	ensures, err := timings.Get(st, -1, func(tags map[string]string) bool {
   144  		return tags["ensure"] == ensureTag
   145  	})
   146  	if err != nil {
   147  		return nil, fmt.Errorf("cannot get timings of ensure %s: %v", ensureTag, err)
   148  	}
   149  	if len(ensures) == 0 {
   150  		return nil, fmt.Errorf("cannot find ensure: %v", ensureTag)
   151  	}
   152  
   153  	// If allEnsures is true, then report all activities of given ensure, otherwise just the latest
   154  	first := len(ensures) - 1
   155  	if allEnsures {
   156  		first = 0
   157  	}
   158  	var responseData []*debugTimings
   159  	var changeTimings map[string]*changeTimings
   160  	for _, ensureTm := range ensures[first:] {
   161  		ensureChangeID := ensureTm.Tags["change-id"]
   162  		// change is optional for ensure timings
   163  		if ensureChangeID != "" {
   164  			// ignore an error when getting a change, it may no longer be present in the state
   165  			changeTimings, _ = collectChangeTimings(st, ensureChangeID)
   166  		}
   167  		debugTm := &debugTimings{
   168  			ChangeID:      ensureChangeID,
   169  			ChangeTimings: changeTimings,
   170  			EnsureTimings: ensureTm.NestedTimings,
   171  		}
   172  		responseData = append(responseData, debugTm)
   173  	}
   174  
   175  	return responseData, nil
   176  }
   177  
   178  func collectStartupTimings(st *state.State, startupTag string, allStarts bool) ([]*debugTimings, error) {
   179  	starts, err := timings.Get(st, -1, func(tags map[string]string) bool {
   180  		return tags["startup"] == startupTag
   181  	})
   182  	if err != nil {
   183  		return nil, fmt.Errorf("cannot get timings of startup %s: %v", startupTag, err)
   184  	}
   185  	if len(starts) == 0 {
   186  		return nil, fmt.Errorf("cannot find startup: %v", startupTag)
   187  	}
   188  
   189  	// If allStarts is true, then report all activities of given startup, otherwise just the latest
   190  	first := len(starts) - 1
   191  	if allStarts {
   192  		first = 0
   193  	}
   194  	var responseData []*debugTimings
   195  	for _, startTm := range starts[first:] {
   196  		debugTm := &debugTimings{
   197  			StartupTimings: startTm.NestedTimings,
   198  		}
   199  		responseData = append(responseData, debugTm)
   200  	}
   201  
   202  	return responseData, nil
   203  }
   204  
   205  func getChangeTimings(st *state.State, changeID, ensureTag, startupTag string, all bool) Response {
   206  	// If ensure tag was passed by the client, find its related changes;
   207  	// we can have many ensure executions and their changes in the responseData array.
   208  	if ensureTag != "" {
   209  		responseData, err := collectEnsureTimings(st, ensureTag, all)
   210  		if err != nil {
   211  			return BadRequest(err.Error())
   212  		}
   213  		return SyncResponse(responseData, nil)
   214  	}
   215  
   216  	if startupTag != "" {
   217  		responseData, err := collectStartupTimings(st, startupTag, all)
   218  		if err != nil {
   219  			return BadRequest(err.Error())
   220  		}
   221  		return SyncResponse(responseData, nil)
   222  	}
   223  
   224  	// timings for single change ID
   225  	changeTimings, err := collectChangeTimings(st, changeID)
   226  	if err != nil {
   227  		return BadRequest(err.Error())
   228  	}
   229  
   230  	responseData := []*debugTimings{
   231  		{
   232  			ChangeID:      changeID,
   233  			ChangeTimings: changeTimings,
   234  		},
   235  	}
   236  	return SyncResponse(responseData, nil)
   237  }
   238  
   239  func getDebug(c *Command, r *http.Request, user *auth.UserState) Response {
   240  	query := r.URL.Query()
   241  	aspect := query.Get("aspect")
   242  	st := c.d.overlord.State()
   243  	st.Lock()
   244  	defer st.Unlock()
   245  	switch aspect {
   246  	case "base-declaration":
   247  		return getBaseDeclaration(st)
   248  	case "connectivity":
   249  		return checkConnectivity(st)
   250  	case "model":
   251  		model, err := c.d.overlord.DeviceManager().Model()
   252  		if err != nil {
   253  			return InternalError("cannot get model: %v", err)
   254  		}
   255  		return SyncResponse(map[string]interface{}{
   256  			"model": string(asserts.Encode(model)),
   257  		}, nil)
   258  	case "change-timings":
   259  		chgID := query.Get("change-id")
   260  		ensureTag := query.Get("ensure")
   261  		startupTag := query.Get("startup")
   262  		all := query.Get("all")
   263  		return getChangeTimings(st, chgID, ensureTag, startupTag, all == "true")
   264  	default:
   265  		return BadRequest("unknown debug aspect %q", aspect)
   266  	}
   267  }
   268  
   269  func postDebug(c *Command, r *http.Request, user *auth.UserState) Response {
   270  	var a debugAction
   271  	decoder := json.NewDecoder(r.Body)
   272  	if err := decoder.Decode(&a); err != nil {
   273  		return BadRequest("cannot decode request body into a debug action: %v", err)
   274  	}
   275  
   276  	st := c.d.overlord.State()
   277  	st.Lock()
   278  	defer st.Unlock()
   279  
   280  	switch a.Action {
   281  	case "add-warning":
   282  		st.Warnf("%v", a.Message)
   283  		return SyncResponse(true, nil)
   284  	case "unshow-warnings":
   285  		st.UnshowAllWarnings()
   286  		return SyncResponse(true, nil)
   287  	case "ensure-state-soon":
   288  		ensureStateSoon(st)
   289  		return SyncResponse(true, nil)
   290  	case "get-base-declaration":
   291  		return getBaseDeclaration(st)
   292  	case "can-manage-refreshes":
   293  		return SyncResponse(devicestate.CanManageRefreshes(st), nil)
   294  	case "connectivity":
   295  		return checkConnectivity(st)
   296  	default:
   297  		return BadRequest("unknown debug action: %v", a.Action)
   298  	}
   299  }