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 }