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 }