github.com/meulengracht/snapd@v0.0.0-20210719210640-8bde69bcc84e/daemon/api_general.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2015-2020 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 "bytes" 24 "encoding/json" 25 "net/http" 26 "os/exec" 27 "sort" 28 "time" 29 30 "github.com/snapcore/snapd/arch" 31 "github.com/snapcore/snapd/client" 32 "github.com/snapcore/snapd/dirs" 33 "github.com/snapcore/snapd/interfaces" 34 "github.com/snapcore/snapd/logger" 35 "github.com/snapcore/snapd/osutil" 36 "github.com/snapcore/snapd/overlord/auth" 37 "github.com/snapcore/snapd/overlord/devicestate" 38 "github.com/snapcore/snapd/overlord/state" 39 "github.com/snapcore/snapd/release" 40 "github.com/snapcore/snapd/sandbox" 41 "github.com/snapcore/snapd/snap" 42 ) 43 44 var ( 45 // see daemon.go:canAccess for details how the access is controlled 46 rootCmd = &Command{ 47 Path: "/", 48 GET: tbd, 49 ReadAccess: openAccess{}, 50 } 51 52 sysInfoCmd = &Command{ 53 Path: "/v2/system-info", 54 GET: sysInfo, 55 ReadAccess: openAccess{}, 56 } 57 58 stateChangeCmd = &Command{ 59 Path: "/v2/changes/{id}", 60 GET: getChange, 61 POST: abortChange, 62 ReadAccess: openAccess{}, 63 WriteAccess: authenticatedAccess{Polkit: polkitActionManage}, 64 } 65 66 stateChangesCmd = &Command{ 67 Path: "/v2/changes", 68 GET: getChanges, 69 ReadAccess: openAccess{}, 70 } 71 72 warningsCmd = &Command{ 73 Path: "/v2/warnings", 74 GET: getWarnings, 75 POST: ackWarnings, 76 ReadAccess: openAccess{}, 77 WriteAccess: authenticatedAccess{Polkit: polkitActionManage}, 78 } 79 ) 80 81 var ( 82 buildID = "unknown" 83 systemdVirt = "" 84 ) 85 86 func init() { 87 // cache the build-id on startup to ensure that changes in 88 // the underlying binary do not affect us 89 if bid, err := osutil.MyBuildID(); err == nil { 90 buildID = bid 91 } 92 // cache systemd-detect-virt output as it's unlikely to change :-) 93 if buf, err := exec.Command("systemd-detect-virt").CombinedOutput(); err == nil { 94 systemdVirt = string(bytes.TrimSpace(buf)) 95 } 96 } 97 98 func tbd(c *Command, r *http.Request, user *auth.UserState) Response { 99 return SyncResponse([]string{"TBD"}) 100 } 101 102 func sysInfo(c *Command, r *http.Request, user *auth.UserState) Response { 103 st := c.d.overlord.State() 104 snapMgr := c.d.overlord.SnapManager() 105 deviceMgr := c.d.overlord.DeviceManager() 106 st.Lock() 107 defer st.Unlock() 108 nextRefresh := snapMgr.NextRefresh() 109 lastRefresh, _ := snapMgr.LastRefresh() 110 refreshHold, _ := snapMgr.EffectiveRefreshHold() 111 refreshScheduleStr, legacySchedule, err := snapMgr.RefreshSchedule() 112 if err != nil { 113 return InternalError("cannot get refresh schedule: %s", err) 114 } 115 users, err := auth.Users(st) 116 if err != nil && err != state.ErrNoState { 117 return InternalError("cannot get user auth data: %s", err) 118 } 119 120 refreshInfo := client.RefreshInfo{ 121 Last: formatRefreshTime(lastRefresh), 122 Hold: formatRefreshTime(refreshHold), 123 Next: formatRefreshTime(nextRefresh), 124 } 125 if !legacySchedule { 126 refreshInfo.Timer = refreshScheduleStr 127 } else { 128 refreshInfo.Schedule = refreshScheduleStr 129 } 130 131 m := map[string]interface{}{ 132 "series": release.Series, 133 "version": c.d.Version, 134 "build-id": buildID, 135 "os-release": release.ReleaseInfo, 136 "on-classic": release.OnClassic, 137 "managed": len(users) > 0, 138 "kernel-version": osutil.KernelVersion(), 139 "locations": map[string]interface{}{ 140 "snap-mount-dir": dirs.SnapMountDir, 141 "snap-bin-dir": dirs.SnapBinariesDir, 142 }, 143 "refresh": refreshInfo, 144 "architecture": arch.DpkgArchitecture(), 145 "system-mode": deviceMgr.SystemMode(devicestate.SysAny), 146 } 147 if systemdVirt != "" { 148 m["virtualization"] = systemdVirt 149 } 150 151 // NOTE: Right now we don't have a good way to differentiate if we 152 // only have partial confinement (ala AppArmor disabled and Seccomp 153 // enabled) or no confinement at all. Once we have a better system 154 // in place how we can dynamically retrieve these information from 155 // snapd we will use this here. 156 if sandbox.ForceDevMode() { 157 m["confinement"] = "partial" 158 } else { 159 m["confinement"] = "strict" 160 } 161 162 // Convey richer information about features of available security backends. 163 if features := sandboxFeatures(c.d.overlord.InterfaceManager().Repository().Backends()); features != nil { 164 m["sandbox-features"] = features 165 } 166 167 return SyncResponse(m) 168 } 169 170 func formatRefreshTime(t time.Time) string { 171 if t.IsZero() { 172 return "" 173 } 174 return t.Truncate(time.Minute).Format(time.RFC3339) 175 } 176 177 func sandboxFeatures(backends []interfaces.SecurityBackend) map[string][]string { 178 result := make(map[string][]string, len(backends)+1) 179 for _, backend := range backends { 180 features := backend.SandboxFeatures() 181 if len(features) > 0 { 182 sort.Strings(features) 183 result[string(backend.Name())] = features 184 } 185 } 186 187 // Add information about supported confinement types as a fake backend 188 features := make([]string, 1, 3) 189 features[0] = "devmode" 190 if !sandbox.ForceDevMode() { 191 features = append(features, "strict") 192 } 193 if dirs.SupportsClassicConfinement() { 194 features = append(features, "classic") 195 } 196 sort.Strings(features) 197 result["confinement-options"] = features 198 199 return result 200 } 201 202 func getChange(c *Command, r *http.Request, user *auth.UserState) Response { 203 chID := muxVars(r)["id"] 204 state := c.d.overlord.State() 205 state.Lock() 206 defer state.Unlock() 207 chg := state.Change(chID) 208 if chg == nil { 209 return NotFound("cannot find change with id %q", chID) 210 } 211 212 return SyncResponse(change2changeInfo(chg)) 213 } 214 215 func getChanges(c *Command, r *http.Request, user *auth.UserState) Response { 216 query := r.URL.Query() 217 qselect := query.Get("select") 218 if qselect == "" { 219 qselect = "in-progress" 220 } 221 var filter func(*state.Change) bool 222 switch qselect { 223 case "all": 224 filter = func(*state.Change) bool { return true } 225 case "in-progress": 226 filter = func(chg *state.Change) bool { return !chg.Status().Ready() } 227 case "ready": 228 filter = func(chg *state.Change) bool { return chg.Status().Ready() } 229 default: 230 return BadRequest("select should be one of: all,in-progress,ready") 231 } 232 233 if wantedName := query.Get("for"); wantedName != "" { 234 outerFilter := filter 235 filter = func(chg *state.Change) bool { 236 if !outerFilter(chg) { 237 return false 238 } 239 240 var snapNames []string 241 if err := chg.Get("snap-names", &snapNames); err != nil { 242 logger.Noticef("Cannot get snap-name for change %v", chg.ID()) 243 return false 244 } 245 246 for _, name := range snapNames { 247 // due to 248 // https://bugs.launchpad.net/snapd/+bug/1880560 249 // the snap-names in service-control changes 250 // could have included <snap>.<app> 251 snapName, _ := snap.SplitSnapApp(name) 252 if snapName == wantedName { 253 return true 254 } 255 } 256 return false 257 } 258 } 259 260 state := c.d.overlord.State() 261 state.Lock() 262 defer state.Unlock() 263 chgs := state.Changes() 264 chgInfos := make([]*changeInfo, 0, len(chgs)) 265 for _, chg := range chgs { 266 if !filter(chg) { 267 continue 268 } 269 chgInfos = append(chgInfos, change2changeInfo(chg)) 270 } 271 return SyncResponse(chgInfos) 272 } 273 274 func abortChange(c *Command, r *http.Request, user *auth.UserState) Response { 275 chID := muxVars(r)["id"] 276 state := c.d.overlord.State() 277 state.Lock() 278 defer state.Unlock() 279 chg := state.Change(chID) 280 if chg == nil { 281 return NotFound("cannot find change with id %q", chID) 282 } 283 284 var reqData struct { 285 Action string `json:"action"` 286 } 287 288 decoder := json.NewDecoder(r.Body) 289 if err := decoder.Decode(&reqData); err != nil { 290 return BadRequest("cannot decode data from request body: %v", err) 291 } 292 293 if reqData.Action != "abort" { 294 return BadRequest("change action %q is unsupported", reqData.Action) 295 } 296 297 if chg.Status().Ready() { 298 return BadRequest("cannot abort change %s with nothing pending", chID) 299 } 300 301 // flag the change 302 chg.Abort() 303 304 // actually ask to proceed with the abort 305 ensureStateSoon(state) 306 307 return SyncResponse(change2changeInfo(chg)) 308 } 309 310 type changeInfo struct { 311 ID string `json:"id"` 312 Kind string `json:"kind"` 313 Summary string `json:"summary"` 314 Status string `json:"status"` 315 Tasks []*taskInfo `json:"tasks,omitempty"` 316 Ready bool `json:"ready"` 317 Err string `json:"err,omitempty"` 318 319 SpawnTime time.Time `json:"spawn-time,omitempty"` 320 ReadyTime *time.Time `json:"ready-time,omitempty"` 321 322 Data map[string]*json.RawMessage `json:"data,omitempty"` 323 } 324 325 type taskInfo struct { 326 ID string `json:"id"` 327 Kind string `json:"kind"` 328 Summary string `json:"summary"` 329 Status string `json:"status"` 330 Log []string `json:"log,omitempty"` 331 Progress taskInfoProgress `json:"progress"` 332 333 SpawnTime time.Time `json:"spawn-time,omitempty"` 334 ReadyTime *time.Time `json:"ready-time,omitempty"` 335 } 336 337 type taskInfoProgress struct { 338 Label string `json:"label"` 339 Done int `json:"done"` 340 Total int `json:"total"` 341 } 342 343 func change2changeInfo(chg *state.Change) *changeInfo { 344 status := chg.Status() 345 chgInfo := &changeInfo{ 346 ID: chg.ID(), 347 Kind: chg.Kind(), 348 Summary: chg.Summary(), 349 Status: status.String(), 350 Ready: status.Ready(), 351 352 SpawnTime: chg.SpawnTime(), 353 } 354 readyTime := chg.ReadyTime() 355 if !readyTime.IsZero() { 356 chgInfo.ReadyTime = &readyTime 357 } 358 if err := chg.Err(); err != nil { 359 chgInfo.Err = err.Error() 360 } 361 362 tasks := chg.Tasks() 363 taskInfos := make([]*taskInfo, len(tasks)) 364 for j, t := range tasks { 365 label, done, total := t.Progress() 366 367 taskInfo := &taskInfo{ 368 ID: t.ID(), 369 Kind: t.Kind(), 370 Summary: t.Summary(), 371 Status: t.Status().String(), 372 Log: t.Log(), 373 Progress: taskInfoProgress{ 374 Label: label, 375 Done: done, 376 Total: total, 377 }, 378 SpawnTime: t.SpawnTime(), 379 } 380 readyTime := t.ReadyTime() 381 if !readyTime.IsZero() { 382 taskInfo.ReadyTime = &readyTime 383 } 384 taskInfos[j] = taskInfo 385 } 386 chgInfo.Tasks = taskInfos 387 388 var data map[string]*json.RawMessage 389 if chg.Get("api-data", &data) == nil { 390 chgInfo.Data = data 391 } 392 393 return chgInfo 394 } 395 396 var ( 397 stateOkayWarnings = (*state.State).OkayWarnings 398 stateAllWarnings = (*state.State).AllWarnings 399 statePendingWarnings = (*state.State).PendingWarnings 400 ) 401 402 func getWarnings(c *Command, r *http.Request, _ *auth.UserState) Response { 403 query := r.URL.Query() 404 var all bool 405 sel := query.Get("select") 406 switch sel { 407 case "all": 408 all = true 409 case "pending", "": 410 all = false 411 default: 412 return BadRequest("invalid select parameter: %q", sel) 413 } 414 415 st := c.d.overlord.State() 416 st.Lock() 417 defer st.Unlock() 418 419 var ws []*state.Warning 420 if all { 421 ws = stateAllWarnings(st) 422 } else { 423 ws, _ = statePendingWarnings(st) 424 } 425 if len(ws) == 0 { 426 // no need to confuse the issue 427 return SyncResponse([]state.Warning{}) 428 } 429 430 return SyncResponse(ws) 431 } 432 433 func ackWarnings(c *Command, r *http.Request, _ *auth.UserState) Response { 434 defer r.Body.Close() 435 var op struct { 436 Action string `json:"action"` 437 Timestamp time.Time `json:"timestamp"` 438 } 439 decoder := json.NewDecoder(r.Body) 440 if err := decoder.Decode(&op); err != nil { 441 return BadRequest("cannot decode request body into warnings operation: %v", err) 442 } 443 if op.Action != "okay" { 444 return BadRequest("unknown warning action %q", op.Action) 445 } 446 st := c.d.overlord.State() 447 st.Lock() 448 defer st.Unlock() 449 n := stateOkayWarnings(st, op.Timestamp) 450 451 return SyncResponse(n) 452 }