github.com/hugh712/snapd@v0.0.0-20200910133618-1a99902bd583/daemon/api.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2015-2018 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 "context" 25 "encoding/json" 26 "errors" 27 "fmt" 28 "io" 29 "io/ioutil" 30 "mime" 31 "mime/multipart" 32 "net" 33 "net/http" 34 "net/url" 35 "os" 36 "os/exec" 37 "path/filepath" 38 "sort" 39 "strconv" 40 "strings" 41 "time" 42 43 "github.com/gorilla/mux" 44 "github.com/jessevdk/go-flags" 45 46 "github.com/snapcore/snapd/arch" 47 "github.com/snapcore/snapd/asserts" 48 "github.com/snapcore/snapd/asserts/snapasserts" 49 "github.com/snapcore/snapd/client" 50 "github.com/snapcore/snapd/client/clientutil" 51 "github.com/snapcore/snapd/dirs" 52 "github.com/snapcore/snapd/httputil" 53 "github.com/snapcore/snapd/i18n" 54 "github.com/snapcore/snapd/interfaces" 55 "github.com/snapcore/snapd/jsonutil" 56 "github.com/snapcore/snapd/logger" 57 "github.com/snapcore/snapd/osutil" 58 "github.com/snapcore/snapd/overlord/assertstate" 59 "github.com/snapcore/snapd/overlord/auth" 60 "github.com/snapcore/snapd/overlord/configstate" 61 "github.com/snapcore/snapd/overlord/configstate/config" 62 "github.com/snapcore/snapd/overlord/hookstate/ctlcmd" 63 "github.com/snapcore/snapd/overlord/ifacestate" 64 "github.com/snapcore/snapd/overlord/servicestate" 65 "github.com/snapcore/snapd/overlord/snapshotstate" 66 "github.com/snapcore/snapd/overlord/snapstate" 67 "github.com/snapcore/snapd/overlord/state" 68 "github.com/snapcore/snapd/progress" 69 "github.com/snapcore/snapd/release" 70 "github.com/snapcore/snapd/sandbox" 71 "github.com/snapcore/snapd/snap" 72 "github.com/snapcore/snapd/snap/channel" 73 "github.com/snapcore/snapd/snap/snapfile" 74 "github.com/snapcore/snapd/store" 75 "github.com/snapcore/snapd/strutil" 76 "github.com/snapcore/snapd/systemd" 77 ) 78 79 var api = []*Command{ 80 rootCmd, 81 sysInfoCmd, 82 loginCmd, 83 logoutCmd, 84 appIconCmd, 85 findCmd, 86 snapsCmd, 87 snapCmd, 88 snapFileCmd, 89 snapDownloadCmd, 90 snapConfCmd, 91 interfacesCmd, 92 assertsCmd, 93 assertsFindManyCmd, 94 stateChangeCmd, 95 stateChangesCmd, 96 createUserCmd, 97 buyCmd, 98 readyToBuyCmd, 99 snapctlCmd, 100 usersCmd, 101 sectionsCmd, 102 aliasesCmd, 103 appsCmd, 104 logsCmd, 105 warningsCmd, 106 debugPprofCmd, 107 debugCmd, 108 snapshotCmd, 109 snapshotExportCmd, 110 connectionsCmd, 111 modelCmd, 112 cohortsCmd, 113 serialModelCmd, 114 systemsCmd, 115 systemsActionCmd, 116 } 117 118 var servicestateControl = servicestate.Control 119 120 var ( 121 // see daemon.go:canAccess for details how the access is controlled 122 rootCmd = &Command{ 123 Path: "/", 124 GuestOK: true, 125 GET: tbd, 126 } 127 128 sysInfoCmd = &Command{ 129 Path: "/v2/system-info", 130 GuestOK: true, 131 GET: sysInfo, 132 } 133 134 appIconCmd = &Command{ 135 Path: "/v2/icons/{name}/icon", 136 UserOK: true, 137 GET: appIconGet, 138 } 139 140 findCmd = &Command{ 141 Path: "/v2/find", 142 UserOK: true, 143 GET: searchStore, 144 } 145 146 snapsCmd = &Command{ 147 Path: "/v2/snaps", 148 UserOK: true, 149 PolkitOK: "io.snapcraft.snapd.manage", 150 GET: getSnapsInfo, 151 POST: postSnaps, 152 } 153 154 snapCmd = &Command{ 155 Path: "/v2/snaps/{name}", 156 UserOK: true, 157 PolkitOK: "io.snapcraft.snapd.manage", 158 GET: getSnapInfo, 159 POST: postSnap, 160 } 161 162 appsCmd = &Command{ 163 Path: "/v2/apps", 164 UserOK: true, 165 GET: getAppsInfo, 166 POST: postApps, 167 } 168 169 logsCmd = &Command{ 170 Path: "/v2/logs", 171 PolkitOK: "io.snapcraft.snapd.manage", 172 GET: getLogs, 173 } 174 175 snapConfCmd = &Command{ 176 Path: "/v2/snaps/{name}/conf", 177 GET: getSnapConf, 178 PUT: setSnapConf, 179 } 180 181 interfacesCmd = &Command{ 182 Path: "/v2/interfaces", 183 UserOK: true, 184 PolkitOK: "io.snapcraft.snapd.manage-interfaces", 185 GET: interfacesConnectionsMultiplexer, 186 POST: changeInterfaces, 187 } 188 189 stateChangeCmd = &Command{ 190 Path: "/v2/changes/{id}", 191 UserOK: true, 192 PolkitOK: "io.snapcraft.snapd.manage", 193 GET: getChange, 194 POST: abortChange, 195 } 196 197 stateChangesCmd = &Command{ 198 Path: "/v2/changes", 199 UserOK: true, 200 GET: getChanges, 201 } 202 203 buyCmd = &Command{ 204 Path: "/v2/buy", 205 POST: postBuy, 206 } 207 208 readyToBuyCmd = &Command{ 209 Path: "/v2/buy/ready", 210 GET: readyToBuy, 211 } 212 213 snapctlCmd = &Command{ 214 Path: "/v2/snapctl", 215 SnapOK: true, 216 POST: runSnapctl, 217 } 218 219 sectionsCmd = &Command{ 220 Path: "/v2/sections", 221 UserOK: true, 222 GET: getSections, 223 } 224 225 aliasesCmd = &Command{ 226 Path: "/v2/aliases", 227 UserOK: true, 228 GET: getAliases, 229 POST: changeAliases, 230 } 231 232 warningsCmd = &Command{ 233 Path: "/v2/warnings", 234 UserOK: true, 235 PolkitOK: "io.snapcraft.snapd.manage", 236 GET: getWarnings, 237 POST: ackWarnings, 238 } 239 240 buildID = "unknown" 241 ) 242 243 var systemdVirt = "" 244 245 func init() { 246 // cache the build-id on startup to ensure that changes in 247 // the underlying binary do not affect us 248 if bid, err := osutil.MyBuildID(); err == nil { 249 buildID = bid 250 } 251 // cache systemd-detect-virt output as it's unlikely to change :-) 252 if buf, err := exec.Command("systemd-detect-virt").CombinedOutput(); err == nil { 253 systemdVirt = string(bytes.TrimSpace(buf)) 254 } 255 } 256 257 func tbd(c *Command, r *http.Request, user *auth.UserState) Response { 258 return SyncResponse([]string{"TBD"}, nil) 259 } 260 261 func formatRefreshTime(t time.Time) string { 262 if t.IsZero() { 263 return "" 264 } 265 return fmt.Sprintf("%s", t.Truncate(time.Minute).Format(time.RFC3339)) 266 } 267 268 func sysInfo(c *Command, r *http.Request, user *auth.UserState) Response { 269 st := c.d.overlord.State() 270 snapMgr := c.d.overlord.SnapManager() 271 deviceMgr := c.d.overlord.DeviceManager() 272 st.Lock() 273 defer st.Unlock() 274 nextRefresh := snapMgr.NextRefresh() 275 lastRefresh, _ := snapMgr.LastRefresh() 276 refreshHold, _ := snapMgr.EffectiveRefreshHold() 277 refreshScheduleStr, legacySchedule, err := snapMgr.RefreshSchedule() 278 if err != nil { 279 return InternalError("cannot get refresh schedule: %s", err) 280 } 281 users, err := auth.Users(st) 282 if err != nil && err != state.ErrNoState { 283 return InternalError("cannot get user auth data: %s", err) 284 } 285 286 refreshInfo := client.RefreshInfo{ 287 Last: formatRefreshTime(lastRefresh), 288 Hold: formatRefreshTime(refreshHold), 289 Next: formatRefreshTime(nextRefresh), 290 } 291 if !legacySchedule { 292 refreshInfo.Timer = refreshScheduleStr 293 } else { 294 refreshInfo.Schedule = refreshScheduleStr 295 } 296 297 m := map[string]interface{}{ 298 "series": release.Series, 299 "version": c.d.Version, 300 "build-id": buildID, 301 "os-release": release.ReleaseInfo, 302 "on-classic": release.OnClassic, 303 "managed": len(users) > 0, 304 "kernel-version": osutil.KernelVersion(), 305 "locations": map[string]interface{}{ 306 "snap-mount-dir": dirs.SnapMountDir, 307 "snap-bin-dir": dirs.SnapBinariesDir, 308 }, 309 "refresh": refreshInfo, 310 "architecture": arch.DpkgArchitecture(), 311 "system-mode": deviceMgr.SystemMode(), 312 } 313 if systemdVirt != "" { 314 m["virtualization"] = systemdVirt 315 } 316 317 // NOTE: Right now we don't have a good way to differentiate if we 318 // only have partial confinement (ala AppArmor disabled and Seccomp 319 // enabled) or no confinement at all. Once we have a better system 320 // in place how we can dynamically retrieve these information from 321 // snapd we will use this here. 322 if sandbox.ForceDevMode() { 323 m["confinement"] = "partial" 324 } else { 325 m["confinement"] = "strict" 326 } 327 328 // Convey richer information about features of available security backends. 329 if features := sandboxFeatures(c.d.overlord.InterfaceManager().Repository().Backends()); features != nil { 330 m["sandbox-features"] = features 331 } 332 333 return SyncResponse(m, nil) 334 } 335 336 func sandboxFeatures(backends []interfaces.SecurityBackend) map[string][]string { 337 result := make(map[string][]string, len(backends)+1) 338 for _, backend := range backends { 339 features := backend.SandboxFeatures() 340 if len(features) > 0 { 341 sort.Strings(features) 342 result[string(backend.Name())] = features 343 } 344 } 345 346 // Add information about supported confinement types as a fake backend 347 features := make([]string, 1, 3) 348 features[0] = "devmode" 349 if !sandbox.ForceDevMode() { 350 features = append(features, "strict") 351 } 352 if dirs.SupportsClassicConfinement() { 353 features = append(features, "classic") 354 } 355 sort.Strings(features) 356 result["confinement-options"] = features 357 358 return result 359 } 360 361 // UserFromRequest extracts user information from request and return the respective user in state, if valid 362 // It requires the state to be locked 363 func UserFromRequest(st *state.State, req *http.Request) (*auth.UserState, error) { 364 // extract macaroons data from request 365 header := req.Header.Get("Authorization") 366 if header == "" { 367 return nil, auth.ErrInvalidAuth 368 } 369 370 authorizationData := strings.SplitN(header, " ", 2) 371 if len(authorizationData) != 2 || authorizationData[0] != "Macaroon" { 372 return nil, fmt.Errorf("authorization header misses Macaroon prefix") 373 } 374 375 var macaroon string 376 var discharges []string 377 for _, field := range strutil.CommaSeparatedList(authorizationData[1]) { 378 if strings.HasPrefix(field, `root="`) { 379 macaroon = strings.TrimSuffix(field[6:], `"`) 380 } 381 if strings.HasPrefix(field, `discharge="`) { 382 discharges = append(discharges, strings.TrimSuffix(field[11:], `"`)) 383 } 384 } 385 386 if macaroon == "" { 387 return nil, fmt.Errorf("invalid authorization header") 388 } 389 390 user, err := auth.CheckMacaroon(st, macaroon, discharges) 391 return user, err 392 } 393 394 var muxVars = mux.Vars 395 396 func getSnapInfo(c *Command, r *http.Request, user *auth.UserState) Response { 397 vars := muxVars(r) 398 name := vars["name"] 399 400 about, err := localSnapInfo(c.d.overlord.State(), name) 401 if err != nil { 402 if err == errNoSnap { 403 return SnapNotFound(name, err) 404 } 405 406 return InternalError("%v", err) 407 } 408 409 route := c.d.router.Get(c.Path) 410 if route == nil { 411 return InternalError("cannot find route for %q snap", name) 412 } 413 414 url, err := route.URL("name", name) 415 if err != nil { 416 return InternalError("cannot build URL for %q snap: %v", name, err) 417 } 418 419 sd := servicestate.NewStatusDecorator(progress.Null) 420 421 result := webify(mapLocal(about, sd), url.String()) 422 423 return SyncResponse(result, nil) 424 } 425 426 func webify(result *client.Snap, resource string) *client.Snap { 427 if result.Icon == "" || strings.HasPrefix(result.Icon, "http") { 428 return result 429 } 430 result.Icon = "" 431 432 route := appIconCmd.d.router.Get(appIconCmd.Path) 433 if route != nil { 434 url, err := route.URL("name", result.Name) 435 if err == nil { 436 result.Icon = url.String() 437 } 438 } 439 440 return result 441 } 442 443 func getStore(c *Command) snapstate.StoreService { 444 st := c.d.overlord.State() 445 st.Lock() 446 defer st.Unlock() 447 448 return snapstate.Store(st, nil) 449 } 450 451 func getSections(c *Command, r *http.Request, user *auth.UserState) Response { 452 route := c.d.router.Get(snapCmd.Path) 453 if route == nil { 454 return InternalError("cannot find route for snaps") 455 } 456 457 theStore := getStore(c) 458 459 // TODO: use a per-request context 460 sections, err := theStore.Sections(context.TODO(), user) 461 switch err { 462 case nil: 463 // pass 464 case store.ErrBadQuery: 465 return SyncResponse(&resp{ 466 Type: ResponseTypeError, 467 Result: &errorResult{Message: err.Error(), Kind: client.ErrorKindBadQuery}, 468 Status: 400, 469 }, nil) 470 case store.ErrUnauthenticated, store.ErrInvalidCredentials: 471 return Unauthorized("%v", err) 472 default: 473 return InternalError("%v", err) 474 } 475 476 return SyncResponse(sections, nil) 477 } 478 479 func searchStore(c *Command, r *http.Request, user *auth.UserState) Response { 480 route := c.d.router.Get(snapCmd.Path) 481 if route == nil { 482 return InternalError("cannot find route for snaps") 483 } 484 query := r.URL.Query() 485 q := query.Get("q") 486 commonID := query.Get("common-id") 487 // TODO: support both "category" (search v2) and "section" 488 section := query.Get("section") 489 name := query.Get("name") 490 scope := query.Get("scope") 491 private := false 492 prefix := false 493 494 if sel := query.Get("select"); sel != "" { 495 switch sel { 496 case "refresh": 497 if commonID != "" { 498 return BadRequest("cannot use 'common-id' with 'select=refresh'") 499 } 500 if name != "" { 501 return BadRequest("cannot use 'name' with 'select=refresh'") 502 } 503 if q != "" { 504 return BadRequest("cannot use 'q' with 'select=refresh'") 505 } 506 return storeUpdates(c, r, user) 507 case "private": 508 private = true 509 } 510 } 511 512 if name != "" { 513 if q != "" { 514 return BadRequest("cannot use 'q' and 'name' together") 515 } 516 if commonID != "" { 517 return BadRequest("cannot use 'common-id' and 'name' together") 518 } 519 520 if name[len(name)-1] != '*' { 521 return findOne(c, r, user, name) 522 } 523 524 prefix = true 525 q = name[:len(name)-1] 526 } 527 528 if commonID != "" && q != "" { 529 return BadRequest("cannot use 'common-id' and 'q' together") 530 } 531 532 theStore := getStore(c) 533 ctx := store.WithClientUserAgent(r.Context(), r) 534 found, err := theStore.Find(ctx, &store.Search{ 535 Query: q, 536 Prefix: prefix, 537 CommonID: commonID, 538 Category: section, 539 Private: private, 540 Scope: scope, 541 }, user) 542 switch err { 543 case nil: 544 // pass 545 case store.ErrBadQuery: 546 return SyncResponse(&resp{ 547 Type: ResponseTypeError, 548 Result: &errorResult{Message: err.Error(), Kind: client.ErrorKindBadQuery}, 549 Status: 400, 550 }, nil) 551 case store.ErrUnauthenticated, store.ErrInvalidCredentials: 552 return Unauthorized(err.Error()) 553 default: 554 if e, ok := err.(*url.Error); ok { 555 if neterr, ok := e.Err.(*net.OpError); ok { 556 if dnserr, ok := neterr.Err.(*net.DNSError); ok { 557 return SyncResponse(&resp{ 558 Type: ResponseTypeError, 559 Result: &errorResult{Message: dnserr.Error(), Kind: client.ErrorKindDNSFailure}, 560 Status: 400, 561 }, nil) 562 } 563 } 564 } 565 if e, ok := err.(net.Error); ok && e.Timeout() { 566 return SyncResponse(&resp{ 567 Type: ResponseTypeError, 568 Result: &errorResult{Message: err.Error(), Kind: client.ErrorKindNetworkTimeout}, 569 Status: 400, 570 }, nil) 571 } 572 if e, ok := err.(*httputil.PerstistentNetworkError); ok { 573 return SyncResponse(&resp{ 574 Type: ResponseTypeError, 575 Result: &errorResult{Message: e.Error(), Kind: client.ErrorKindDNSFailure}, 576 Status: 400, 577 }, nil) 578 } 579 580 return InternalError("%v", err) 581 } 582 583 meta := &Meta{ 584 SuggestedCurrency: theStore.SuggestedCurrency(), 585 Sources: []string{"store"}, 586 } 587 588 return sendStorePackages(route, meta, found) 589 } 590 591 func findOne(c *Command, r *http.Request, user *auth.UserState, name string) Response { 592 if err := snap.ValidateName(name); err != nil { 593 return BadRequest(err.Error()) 594 } 595 596 theStore := getStore(c) 597 spec := store.SnapSpec{ 598 Name: name, 599 } 600 ctx := store.WithClientUserAgent(r.Context(), r) 601 snapInfo, err := theStore.SnapInfo(ctx, spec, user) 602 switch err { 603 case nil: 604 // pass 605 case store.ErrInvalidCredentials: 606 return Unauthorized("%v", err) 607 case store.ErrSnapNotFound: 608 return SnapNotFound(name, err) 609 default: 610 return InternalError("%v", err) 611 } 612 613 meta := &Meta{ 614 SuggestedCurrency: theStore.SuggestedCurrency(), 615 Sources: []string{"store"}, 616 } 617 618 results := make([]*json.RawMessage, 1) 619 data, err := json.Marshal(webify(mapRemote(snapInfo), r.URL.String())) 620 if err != nil { 621 return InternalError(err.Error()) 622 } 623 results[0] = (*json.RawMessage)(&data) 624 return SyncResponse(results, meta) 625 } 626 627 func shouldSearchStore(r *http.Request) bool { 628 // we should jump to the old behaviour iff q is given, or if 629 // sources is given and either empty or contains the word 630 // 'store'. Otherwise, local results only. 631 632 query := r.URL.Query() 633 634 if _, ok := query["q"]; ok { 635 logger.Debugf("use of obsolete \"q\" parameter: %q", r.URL) 636 return true 637 } 638 639 if src, ok := query["sources"]; ok { 640 logger.Debugf("use of obsolete \"sources\" parameter: %q", r.URL) 641 if len(src) == 0 || strings.Contains(src[0], "store") { 642 return true 643 } 644 } 645 646 return false 647 } 648 649 func storeUpdates(c *Command, r *http.Request, user *auth.UserState) Response { 650 route := c.d.router.Get(snapCmd.Path) 651 if route == nil { 652 return InternalError("cannot find route for snaps") 653 } 654 655 state := c.d.overlord.State() 656 state.Lock() 657 updates, err := snapstateRefreshCandidates(state, user) 658 state.Unlock() 659 if err != nil { 660 return InternalError("cannot list updates: %v", err) 661 } 662 663 return sendStorePackages(route, nil, updates) 664 } 665 666 func sendStorePackages(route *mux.Route, meta *Meta, found []*snap.Info) Response { 667 results := make([]*json.RawMessage, 0, len(found)) 668 for _, x := range found { 669 url, err := route.URL("name", x.InstanceName()) 670 if err != nil { 671 logger.Noticef("Cannot build URL for snap %q revision %s: %v", x.InstanceName(), x.Revision, err) 672 continue 673 } 674 675 data, err := json.Marshal(webify(mapRemote(x), url.String())) 676 if err != nil { 677 return InternalError("%v", err) 678 } 679 raw := json.RawMessage(data) 680 results = append(results, &raw) 681 } 682 683 return SyncResponse(results, meta) 684 } 685 686 // plural! 687 func getSnapsInfo(c *Command, r *http.Request, user *auth.UserState) Response { 688 689 if shouldSearchStore(r) { 690 logger.Noticef("Jumping to \"find\" to better support legacy request %q", r.URL) 691 return searchStore(c, r, user) 692 } 693 694 route := c.d.router.Get(snapCmd.Path) 695 if route == nil { 696 return InternalError("cannot find route for snaps") 697 } 698 699 query := r.URL.Query() 700 var all bool 701 sel := query.Get("select") 702 switch sel { 703 case "all": 704 all = true 705 case "enabled", "": 706 all = false 707 default: 708 return BadRequest("invalid select parameter: %q", sel) 709 } 710 var wanted map[string]bool 711 if ns := query.Get("snaps"); len(ns) > 0 { 712 nsl := strutil.CommaSeparatedList(ns) 713 wanted = make(map[string]bool, len(nsl)) 714 for _, name := range nsl { 715 wanted[name] = true 716 } 717 } 718 719 found, err := allLocalSnapInfos(c.d.overlord.State(), all, wanted) 720 if err != nil { 721 return InternalError("cannot list local snaps! %v", err) 722 } 723 724 results := make([]*json.RawMessage, len(found)) 725 726 sd := servicestate.NewStatusDecorator(progress.Null) 727 for i, x := range found { 728 name := x.info.InstanceName() 729 rev := x.info.Revision 730 731 url, err := route.URL("name", name) 732 if err != nil { 733 logger.Noticef("Cannot build URL for snap %q revision %s: %v", name, rev, err) 734 continue 735 } 736 737 data, err := json.Marshal(webify(mapLocal(x, sd), url.String())) 738 if err != nil { 739 return InternalError("cannot serialize snap %q revision %s: %v", name, rev, err) 740 } 741 raw := json.RawMessage(data) 742 results[i] = &raw 743 } 744 745 return SyncResponse(results, &Meta{Sources: []string{"local"}}) 746 } 747 748 // licenseData holds details about the snap license, and may be 749 // marshaled back as an error when the license agreement is pending, 750 // and is expected as input to accept (or not) that license 751 // agreement. As such, its field names are part of the API. 752 type licenseData struct { 753 Intro string `json:"intro"` 754 License string `json:"license"` 755 Agreed bool `json:"agreed"` 756 } 757 758 func (*licenseData) Error() string { 759 return "license agreement required" 760 } 761 762 type snapRevisionOptions struct { 763 Channel string `json:"channel"` 764 Revision snap.Revision `json:"revision"` 765 766 CohortKey string `json:"cohort-key"` 767 LeaveCohort bool `json:"leave-cohort"` 768 } 769 770 func (ropt *snapRevisionOptions) validate() error { 771 if ropt.CohortKey != "" { 772 if ropt.LeaveCohort { 773 return fmt.Errorf("cannot specify both cohort-key and leave-cohort") 774 } 775 if !ropt.Revision.Unset() { 776 return fmt.Errorf("cannot specify both cohort-key and revision") 777 } 778 } 779 780 if ropt.Channel != "" { 781 _, err := channel.Parse(ropt.Channel, "-") 782 if err != nil { 783 return err 784 } 785 } 786 return nil 787 } 788 789 type snapInstruction struct { 790 progress.NullMeter 791 792 Action string `json:"action"` 793 Amend bool `json:"amend"` 794 snapRevisionOptions 795 DevMode bool `json:"devmode"` 796 JailMode bool `json:"jailmode"` 797 Classic bool `json:"classic"` 798 IgnoreValidation bool `json:"ignore-validation"` 799 Unaliased bool `json:"unaliased"` 800 Purge bool `json:"purge,omitempty"` 801 // dropping support temporarely until flag confusion is sorted, 802 // this isn't supported by client atm anyway 803 LeaveOld bool `json:"temp-dropped-leave-old"` 804 License *licenseData `json:"license"` 805 Snaps []string `json:"snaps"` 806 Users []string `json:"users"` 807 808 // The fields below should not be unmarshalled into. Do not export them. 809 userID int 810 ctx context.Context 811 } 812 813 func (inst *snapInstruction) revnoOpts() *snapstate.RevisionOptions { 814 return &snapstate.RevisionOptions{ 815 Channel: inst.Channel, 816 Revision: inst.Revision, 817 CohortKey: inst.CohortKey, 818 LeaveCohort: inst.LeaveCohort, 819 } 820 } 821 822 func (inst *snapInstruction) modeFlags() (snapstate.Flags, error) { 823 return modeFlags(inst.DevMode, inst.JailMode, inst.Classic) 824 } 825 826 func (inst *snapInstruction) installFlags() (snapstate.Flags, error) { 827 flags, err := inst.modeFlags() 828 if err != nil { 829 return snapstate.Flags{}, err 830 } 831 if inst.Unaliased { 832 flags.Unaliased = true 833 } 834 return flags, nil 835 } 836 837 func (inst *snapInstruction) validate() error { 838 if inst.CohortKey != "" { 839 if inst.Action != "install" && inst.Action != "refresh" && inst.Action != "switch" { 840 return fmt.Errorf("cohort-key can only be specified for install, refresh, or switch") 841 } 842 } 843 if inst.LeaveCohort { 844 if inst.Action != "refresh" && inst.Action != "switch" { 845 return fmt.Errorf("leave-cohort can only be specified for refresh or switch") 846 } 847 } 848 if inst.Action == "install" { 849 for _, snapName := range inst.Snaps { 850 // FIXME: alternatively we could simply mutate *inst 851 // and s/ubuntu-core/core/ ? 852 if snapName == "ubuntu-core" { 853 return fmt.Errorf(`cannot install "ubuntu-core", please use "core" instead`) 854 } 855 } 856 } 857 858 return inst.snapRevisionOptions.validate() 859 } 860 861 type snapInstructionResult struct { 862 Summary string 863 Affected []string 864 Tasksets []*state.TaskSet 865 Result map[string]interface{} 866 } 867 868 var ( 869 snapstateInstall = snapstate.Install 870 snapstateInstallPath = snapstate.InstallPath 871 snapstateRefreshCandidates = snapstate.RefreshCandidates 872 snapstateTryPath = snapstate.TryPath 873 snapstateUpdate = snapstate.Update 874 snapstateUpdateMany = snapstate.UpdateMany 875 snapstateInstallMany = snapstate.InstallMany 876 snapstateRemoveMany = snapstate.RemoveMany 877 snapstateRevert = snapstate.Revert 878 snapstateRevertToRevision = snapstate.RevertToRevision 879 snapstateSwitch = snapstate.Switch 880 881 snapshotList = snapshotstate.List 882 snapshotCheck = snapshotstate.Check 883 snapshotForget = snapshotstate.Forget 884 snapshotRestore = snapshotstate.Restore 885 snapshotSave = snapshotstate.Save 886 snapshotExport = snapshotstate.Export 887 888 assertstateRefreshSnapDeclarations = assertstate.RefreshSnapDeclarations 889 ) 890 891 func ensureStateSoonImpl(st *state.State) { 892 st.EnsureBefore(0) 893 } 894 895 var ensureStateSoon = ensureStateSoonImpl 896 897 var errDevJailModeConflict = errors.New("cannot use devmode and jailmode flags together") 898 var errClassicDevmodeConflict = errors.New("cannot use classic and devmode flags together") 899 var errNoJailMode = errors.New("this system cannot honour the jailmode flag") 900 901 func modeFlags(devMode, jailMode, classic bool) (snapstate.Flags, error) { 902 flags := snapstate.Flags{} 903 devModeOS := sandbox.ForceDevMode() 904 switch { 905 case jailMode && devModeOS: 906 return flags, errNoJailMode 907 case jailMode && devMode: 908 return flags, errDevJailModeConflict 909 case devMode && classic: 910 return flags, errClassicDevmodeConflict 911 } 912 // NOTE: jailmode and classic are allowed together. In that setting, 913 // jailmode overrides classic and the app gets regular (non-classic) 914 // confinement. 915 flags.JailMode = jailMode 916 flags.Classic = classic 917 flags.DevMode = devMode 918 return flags, nil 919 } 920 921 func snapUpdateMany(inst *snapInstruction, st *state.State) (*snapInstructionResult, error) { 922 // we need refreshed snap-declarations to enforce refresh-control as best as we can, this also ensures that snap-declarations and their prerequisite assertions are updated regularly 923 if err := assertstateRefreshSnapDeclarations(st, inst.userID); err != nil { 924 return nil, err 925 } 926 927 // TODO: use a per-request context 928 updated, tasksets, err := snapstateUpdateMany(context.TODO(), st, inst.Snaps, inst.userID, nil) 929 if err != nil { 930 return nil, err 931 } 932 933 var msg string 934 switch len(updated) { 935 case 0: 936 if len(inst.Snaps) != 0 { 937 // TRANSLATORS: the %s is a comma-separated list of quoted snap names 938 msg = fmt.Sprintf(i18n.G("Refresh snaps %s: no updates"), strutil.Quoted(inst.Snaps)) 939 } else { 940 msg = i18n.G("Refresh all snaps: no updates") 941 } 942 case 1: 943 msg = fmt.Sprintf(i18n.G("Refresh snap %q"), updated[0]) 944 default: 945 quoted := strutil.Quoted(updated) 946 // TRANSLATORS: the %s is a comma-separated list of quoted snap names 947 msg = fmt.Sprintf(i18n.G("Refresh snaps %s"), quoted) 948 } 949 950 return &snapInstructionResult{ 951 Summary: msg, 952 Affected: updated, 953 Tasksets: tasksets, 954 }, nil 955 } 956 957 func snapInstallMany(inst *snapInstruction, st *state.State) (*snapInstructionResult, error) { 958 for _, name := range inst.Snaps { 959 if len(name) == 0 { 960 return nil, fmt.Errorf(i18n.G("cannot install snap with empty name")) 961 } 962 } 963 installed, tasksets, err := snapstateInstallMany(st, inst.Snaps, inst.userID) 964 if err != nil { 965 return nil, err 966 } 967 968 var msg string 969 switch len(inst.Snaps) { 970 case 0: 971 return nil, fmt.Errorf("cannot install zero snaps") 972 case 1: 973 msg = fmt.Sprintf(i18n.G("Install snap %q"), inst.Snaps[0]) 974 default: 975 quoted := strutil.Quoted(inst.Snaps) 976 // TRANSLATORS: the %s is a comma-separated list of quoted snap names 977 msg = fmt.Sprintf(i18n.G("Install snaps %s"), quoted) 978 } 979 980 return &snapInstructionResult{ 981 Summary: msg, 982 Affected: installed, 983 Tasksets: tasksets, 984 }, nil 985 } 986 987 func snapInstall(inst *snapInstruction, st *state.State) (string, []*state.TaskSet, error) { 988 if len(inst.Snaps[0]) == 0 { 989 return "", nil, fmt.Errorf(i18n.G("cannot install snap with empty name")) 990 } 991 992 flags, err := inst.installFlags() 993 if err != nil { 994 return "", nil, err 995 } 996 997 var ckey string 998 if inst.CohortKey == "" { 999 logger.Noticef("Installing snap %q revision %s", inst.Snaps[0], inst.Revision) 1000 } else { 1001 ckey = strutil.ElliptLeft(inst.CohortKey, 10) 1002 logger.Noticef("Installing snap %q from cohort %q", inst.Snaps[0], ckey) 1003 } 1004 tset, err := snapstateInstall(inst.ctx, st, inst.Snaps[0], inst.revnoOpts(), inst.userID, flags) 1005 if err != nil { 1006 return "", nil, err 1007 } 1008 1009 msg := fmt.Sprintf(i18n.G("Install %q snap"), inst.Snaps[0]) 1010 if inst.Channel != "stable" && inst.Channel != "" { 1011 msg += fmt.Sprintf(" from %q channel", inst.Channel) 1012 } 1013 if inst.CohortKey != "" { 1014 msg += fmt.Sprintf(" from %q cohort", ckey) 1015 } 1016 return msg, []*state.TaskSet{tset}, nil 1017 } 1018 1019 func snapUpdate(inst *snapInstruction, st *state.State) (string, []*state.TaskSet, error) { 1020 // TODO: bail if revision is given (and != current?), *or* behave as with install --revision? 1021 flags, err := inst.modeFlags() 1022 if err != nil { 1023 return "", nil, err 1024 } 1025 if inst.IgnoreValidation { 1026 flags.IgnoreValidation = true 1027 } 1028 if inst.Amend { 1029 flags.Amend = true 1030 } 1031 1032 // we need refreshed snap-declarations to enforce refresh-control as best as we can 1033 if err = assertstateRefreshSnapDeclarations(st, inst.userID); err != nil { 1034 return "", nil, err 1035 } 1036 1037 ts, err := snapstateUpdate(st, inst.Snaps[0], inst.revnoOpts(), inst.userID, flags) 1038 if err != nil { 1039 return "", nil, err 1040 } 1041 1042 msg := fmt.Sprintf(i18n.G("Refresh %q snap"), inst.Snaps[0]) 1043 if inst.Channel != "stable" && inst.Channel != "" { 1044 msg = fmt.Sprintf(i18n.G("Refresh %q snap from %q channel"), inst.Snaps[0], inst.Channel) 1045 } 1046 1047 return msg, []*state.TaskSet{ts}, nil 1048 } 1049 1050 func snapRemoveMany(inst *snapInstruction, st *state.State) (*snapInstructionResult, error) { 1051 removed, tasksets, err := snapstateRemoveMany(st, inst.Snaps) 1052 if err != nil { 1053 return nil, err 1054 } 1055 1056 var msg string 1057 switch len(inst.Snaps) { 1058 case 0: 1059 return nil, fmt.Errorf("cannot remove zero snaps") 1060 case 1: 1061 msg = fmt.Sprintf(i18n.G("Remove snap %q"), inst.Snaps[0]) 1062 default: 1063 quoted := strutil.Quoted(inst.Snaps) 1064 // TRANSLATORS: the %s is a comma-separated list of quoted snap names 1065 msg = fmt.Sprintf(i18n.G("Remove snaps %s"), quoted) 1066 } 1067 1068 return &snapInstructionResult{ 1069 Summary: msg, 1070 Affected: removed, 1071 Tasksets: tasksets, 1072 }, nil 1073 } 1074 1075 func snapRemove(inst *snapInstruction, st *state.State) (string, []*state.TaskSet, error) { 1076 ts, err := snapstate.Remove(st, inst.Snaps[0], inst.Revision, &snapstate.RemoveFlags{Purge: inst.Purge}) 1077 if err != nil { 1078 return "", nil, err 1079 } 1080 1081 msg := fmt.Sprintf(i18n.G("Remove %q snap"), inst.Snaps[0]) 1082 return msg, []*state.TaskSet{ts}, nil 1083 } 1084 1085 func snapRevert(inst *snapInstruction, st *state.State) (string, []*state.TaskSet, error) { 1086 var ts *state.TaskSet 1087 1088 flags, err := inst.modeFlags() 1089 if err != nil { 1090 return "", nil, err 1091 } 1092 1093 if inst.Revision.Unset() { 1094 ts, err = snapstateRevert(st, inst.Snaps[0], flags) 1095 } else { 1096 ts, err = snapstateRevertToRevision(st, inst.Snaps[0], inst.Revision, flags) 1097 } 1098 if err != nil { 1099 return "", nil, err 1100 } 1101 1102 msg := fmt.Sprintf(i18n.G("Revert %q snap"), inst.Snaps[0]) 1103 return msg, []*state.TaskSet{ts}, nil 1104 } 1105 1106 func snapEnable(inst *snapInstruction, st *state.State) (string, []*state.TaskSet, error) { 1107 if !inst.Revision.Unset() { 1108 return "", nil, errors.New("enable takes no revision") 1109 } 1110 ts, err := snapstate.Enable(st, inst.Snaps[0]) 1111 if err != nil { 1112 return "", nil, err 1113 } 1114 1115 msg := fmt.Sprintf(i18n.G("Enable %q snap"), inst.Snaps[0]) 1116 return msg, []*state.TaskSet{ts}, nil 1117 } 1118 1119 func snapDisable(inst *snapInstruction, st *state.State) (string, []*state.TaskSet, error) { 1120 if !inst.Revision.Unset() { 1121 return "", nil, errors.New("disable takes no revision") 1122 } 1123 ts, err := snapstate.Disable(st, inst.Snaps[0]) 1124 if err != nil { 1125 return "", nil, err 1126 } 1127 1128 msg := fmt.Sprintf(i18n.G("Disable %q snap"), inst.Snaps[0]) 1129 return msg, []*state.TaskSet{ts}, nil 1130 } 1131 1132 func snapSwitch(inst *snapInstruction, st *state.State) (string, []*state.TaskSet, error) { 1133 if !inst.Revision.Unset() { 1134 return "", nil, errors.New("switch takes no revision") 1135 } 1136 ts, err := snapstateSwitch(st, inst.Snaps[0], inst.revnoOpts()) 1137 if err != nil { 1138 return "", nil, err 1139 } 1140 1141 var msg string 1142 switch { 1143 case inst.LeaveCohort && inst.Channel != "": 1144 msg = fmt.Sprintf(i18n.G("Switch %q snap to channel %q and away from cohort"), inst.Snaps[0], inst.Channel) 1145 case inst.LeaveCohort: 1146 msg = fmt.Sprintf(i18n.G("Switch %q snap away from cohort"), inst.Snaps[0]) 1147 case inst.CohortKey == "" && inst.Channel != "": 1148 msg = fmt.Sprintf(i18n.G("Switch %q snap to channel %q"), inst.Snaps[0], inst.Channel) 1149 case inst.CohortKey != "" && inst.Channel == "": 1150 msg = fmt.Sprintf(i18n.G("Switch %q snap to cohort %q"), inst.Snaps[0], strutil.ElliptLeft(inst.CohortKey, 10)) 1151 default: 1152 msg = fmt.Sprintf(i18n.G("Switch %q snap to channel %q and cohort %q"), inst.Snaps[0], inst.Channel, strutil.ElliptLeft(inst.CohortKey, 10)) 1153 } 1154 return msg, []*state.TaskSet{ts}, nil 1155 } 1156 1157 func snapshotMany(inst *snapInstruction, st *state.State) (*snapInstructionResult, error) { 1158 setID, snapshotted, ts, err := snapshotSave(st, inst.Snaps, inst.Users) 1159 if err != nil { 1160 return nil, err 1161 } 1162 1163 var msg string 1164 if len(inst.Snaps) == 0 { 1165 msg = i18n.G("Snapshot all snaps") 1166 } else { 1167 // TRANSLATORS: the %s is a comma-separated list of quoted snap names 1168 msg = fmt.Sprintf(i18n.G("Snapshot snaps %s"), strutil.Quoted(inst.Snaps)) 1169 } 1170 1171 return &snapInstructionResult{ 1172 Summary: msg, 1173 Affected: snapshotted, 1174 Tasksets: []*state.TaskSet{ts}, 1175 Result: map[string]interface{}{"set-id": setID}, 1176 }, nil 1177 } 1178 1179 type snapActionFunc func(*snapInstruction, *state.State) (string, []*state.TaskSet, error) 1180 1181 var snapInstructionDispTable = map[string]snapActionFunc{ 1182 "install": snapInstall, 1183 "refresh": snapUpdate, 1184 "remove": snapRemove, 1185 "revert": snapRevert, 1186 "enable": snapEnable, 1187 "disable": snapDisable, 1188 "switch": snapSwitch, 1189 } 1190 1191 func (inst *snapInstruction) dispatch() snapActionFunc { 1192 if len(inst.Snaps) != 1 { 1193 logger.Panicf("dispatch only handles single-snap ops; got %d", len(inst.Snaps)) 1194 } 1195 return snapInstructionDispTable[inst.Action] 1196 } 1197 1198 func (inst *snapInstruction) errToResponse(err error) Response { 1199 if len(inst.Snaps) == 0 { 1200 return errToResponse(err, nil, BadRequest, "cannot %s: %v", inst.Action) 1201 } 1202 1203 return errToResponse(err, inst.Snaps, BadRequest, "cannot %s %s: %v", inst.Action, strutil.Quoted(inst.Snaps)) 1204 } 1205 1206 func postSnap(c *Command, r *http.Request, user *auth.UserState) Response { 1207 route := c.d.router.Get(stateChangeCmd.Path) 1208 if route == nil { 1209 return InternalError("cannot find route for change") 1210 } 1211 1212 decoder := json.NewDecoder(r.Body) 1213 var inst snapInstruction 1214 if err := decoder.Decode(&inst); err != nil { 1215 return BadRequest("cannot decode request body into snap instruction: %v", err) 1216 } 1217 inst.ctx = r.Context() 1218 1219 state := c.d.overlord.State() 1220 state.Lock() 1221 defer state.Unlock() 1222 1223 if user != nil { 1224 inst.userID = user.ID 1225 } 1226 1227 vars := muxVars(r) 1228 inst.Snaps = []string{vars["name"]} 1229 1230 if err := inst.validate(); err != nil { 1231 return BadRequest("%s", err) 1232 } 1233 1234 impl := inst.dispatch() 1235 if impl == nil { 1236 return BadRequest("unknown action %s", inst.Action) 1237 } 1238 1239 msg, tsets, err := impl(&inst, state) 1240 if err != nil { 1241 return inst.errToResponse(err) 1242 } 1243 1244 chg := newChange(state, inst.Action+"-snap", msg, tsets, inst.Snaps) 1245 1246 ensureStateSoon(state) 1247 1248 return AsyncResponse(nil, &Meta{Change: chg.ID()}) 1249 } 1250 1251 func newChange(st *state.State, kind, summary string, tsets []*state.TaskSet, snapNames []string) *state.Change { 1252 chg := st.NewChange(kind, summary) 1253 for _, ts := range tsets { 1254 chg.AddAll(ts) 1255 } 1256 if snapNames != nil { 1257 chg.Set("snap-names", snapNames) 1258 } 1259 return chg 1260 } 1261 1262 const maxReadBuflen = 1024 * 1024 1263 1264 func trySnap(c *Command, r *http.Request, user *auth.UserState, trydir string, flags snapstate.Flags) Response { 1265 st := c.d.overlord.State() 1266 st.Lock() 1267 defer st.Unlock() 1268 1269 if !filepath.IsAbs(trydir) { 1270 return BadRequest("cannot try %q: need an absolute path", trydir) 1271 } 1272 if !osutil.IsDirectory(trydir) { 1273 return BadRequest("cannot try %q: not a snap directory", trydir) 1274 } 1275 1276 // the developer asked us to do this with a trusted snap dir 1277 info, err := unsafeReadSnapInfo(trydir) 1278 if _, ok := err.(snap.NotSnapError); ok { 1279 return SyncResponse(&resp{ 1280 Type: ResponseTypeError, 1281 Result: &errorResult{ 1282 Message: err.Error(), 1283 Kind: client.ErrorKindNotSnap, 1284 }, 1285 Status: 400, 1286 }, nil) 1287 } 1288 if err != nil { 1289 return BadRequest("cannot read snap info for %s: %s", trydir, err) 1290 } 1291 1292 tset, err := snapstateTryPath(st, info.InstanceName(), trydir, flags) 1293 if err != nil { 1294 return errToResponse(err, []string{info.InstanceName()}, BadRequest, "cannot try %s: %s", trydir) 1295 } 1296 1297 msg := fmt.Sprintf(i18n.G("Try %q snap from %s"), info.InstanceName(), trydir) 1298 chg := newChange(st, "try-snap", msg, []*state.TaskSet{tset}, []string{info.InstanceName()}) 1299 chg.Set("api-data", map[string]string{"snap-name": info.InstanceName()}) 1300 1301 ensureStateSoon(st) 1302 1303 return AsyncResponse(nil, &Meta{Change: chg.ID()}) 1304 } 1305 1306 func isTrue(form *multipart.Form, key string) bool { 1307 value := form.Value[key] 1308 if len(value) == 0 { 1309 return false 1310 } 1311 b, err := strconv.ParseBool(value[0]) 1312 if err != nil { 1313 return false 1314 } 1315 1316 return b 1317 } 1318 1319 func snapsOp(c *Command, r *http.Request, user *auth.UserState) Response { 1320 route := c.d.router.Get(stateChangeCmd.Path) 1321 if route == nil { 1322 return InternalError("cannot find route for change") 1323 } 1324 1325 decoder := json.NewDecoder(r.Body) 1326 var inst snapInstruction 1327 if err := decoder.Decode(&inst); err != nil { 1328 return BadRequest("cannot decode request body into snap instruction: %v", err) 1329 } 1330 1331 // TODO: inst.Amend, etc? 1332 if inst.Channel != "" || !inst.Revision.Unset() || inst.DevMode || inst.JailMode || inst.CohortKey != "" || inst.LeaveCohort || inst.Purge { 1333 return BadRequest("unsupported option provided for multi-snap operation") 1334 } 1335 if err := inst.validate(); err != nil { 1336 return BadRequest("%v", err) 1337 } 1338 1339 st := c.d.overlord.State() 1340 st.Lock() 1341 defer st.Unlock() 1342 1343 if user != nil { 1344 inst.userID = user.ID 1345 } 1346 1347 var op func(*snapInstruction, *state.State) (*snapInstructionResult, error) 1348 1349 switch inst.Action { 1350 case "refresh": 1351 op = snapUpdateMany 1352 case "install": 1353 op = snapInstallMany 1354 case "remove": 1355 op = snapRemoveMany 1356 case "snapshot": 1357 op = snapshotMany 1358 default: 1359 return BadRequest("unsupported multi-snap operation %q", inst.Action) 1360 } 1361 res, err := op(&inst, st) 1362 if err != nil { 1363 return inst.errToResponse(err) 1364 } 1365 1366 var chg *state.Change 1367 if len(res.Tasksets) == 0 { 1368 chg = st.NewChange(inst.Action+"-snap", res.Summary) 1369 chg.SetStatus(state.DoneStatus) 1370 } else { 1371 chg = newChange(st, inst.Action+"-snap", res.Summary, res.Tasksets, res.Affected) 1372 ensureStateSoon(st) 1373 } 1374 1375 chg.Set("api-data", map[string]interface{}{"snap-names": res.Affected}) 1376 1377 return AsyncResponse(res.Result, &Meta{Change: chg.ID()}) 1378 } 1379 1380 func postSnaps(c *Command, r *http.Request, user *auth.UserState) Response { 1381 contentType := r.Header.Get("Content-Type") 1382 1383 mediaType, params, err := mime.ParseMediaType(contentType) 1384 if err != nil { 1385 return BadRequest("cannot parse content type: %v", err) 1386 } 1387 1388 if mediaType == "application/json" { 1389 charset := strings.ToUpper(params["charset"]) 1390 if charset != "" && charset != "UTF-8" { 1391 return BadRequest("unknown charset in content type: %s", contentType) 1392 } 1393 return snapsOp(c, r, user) 1394 } 1395 1396 if !strings.HasPrefix(contentType, "multipart/") { 1397 return BadRequest("unknown content type: %s", contentType) 1398 } 1399 1400 route := c.d.router.Get(stateChangeCmd.Path) 1401 if route == nil { 1402 return InternalError("cannot find route for change") 1403 } 1404 1405 // POSTs to sideload snaps must be a multipart/form-data file upload. 1406 form, err := multipart.NewReader(r.Body, params["boundary"]).ReadForm(maxReadBuflen) 1407 if err != nil { 1408 return BadRequest("cannot read POST form: %v", err) 1409 } 1410 1411 dangerousOK := isTrue(form, "dangerous") 1412 flags, err := modeFlags(isTrue(form, "devmode"), isTrue(form, "jailmode"), isTrue(form, "classic")) 1413 if err != nil { 1414 return BadRequest(err.Error()) 1415 } 1416 1417 if len(form.Value["action"]) > 0 && form.Value["action"][0] == "try" { 1418 if len(form.Value["snap-path"]) == 0 { 1419 return BadRequest("need 'snap-path' value in form") 1420 } 1421 return trySnap(c, r, user, form.Value["snap-path"][0], flags) 1422 } 1423 flags.RemoveSnapPath = true 1424 1425 flags.Unaliased = isTrue(form, "unaliased") 1426 1427 // find the file for the "snap" form field 1428 var snapBody multipart.File 1429 var origPath string 1430 out: 1431 for name, fheaders := range form.File { 1432 if name != "snap" { 1433 continue 1434 } 1435 for _, fheader := range fheaders { 1436 snapBody, err = fheader.Open() 1437 origPath = fheader.Filename 1438 if err != nil { 1439 return BadRequest(`cannot open uploaded "snap" file: %v`, err) 1440 } 1441 defer snapBody.Close() 1442 1443 break out 1444 } 1445 } 1446 defer form.RemoveAll() 1447 1448 if snapBody == nil { 1449 return BadRequest(`cannot find "snap" file field in provided multipart/form-data payload`) 1450 } 1451 1452 // we are in charge of the tempfile life cycle until we hand it off to the change 1453 changeTriggered := false 1454 // if you change this prefix, look for it in the tests 1455 // also see localInstallCleanup in snapstate/snapmgr.go 1456 tmpf, err := ioutil.TempFile(dirs.SnapBlobDir, dirs.LocalInstallBlobTempPrefix) 1457 if err != nil { 1458 return InternalError("cannot create temporary file: %v", err) 1459 } 1460 1461 tempPath := tmpf.Name() 1462 1463 defer func() { 1464 if !changeTriggered { 1465 os.Remove(tempPath) 1466 } 1467 }() 1468 1469 if _, err := io.Copy(tmpf, snapBody); err != nil { 1470 return InternalError("cannot copy request into temporary file: %v", err) 1471 } 1472 tmpf.Sync() 1473 1474 if len(form.Value["snap-path"]) > 0 { 1475 origPath = form.Value["snap-path"][0] 1476 } 1477 1478 var instanceName string 1479 1480 if len(form.Value["name"]) > 0 { 1481 // caller has specified desired instance name 1482 instanceName = form.Value["name"][0] 1483 if err := snap.ValidateInstanceName(instanceName); err != nil { 1484 return BadRequest(err.Error()) 1485 } 1486 } 1487 1488 st := c.d.overlord.State() 1489 st.Lock() 1490 defer st.Unlock() 1491 1492 var snapName string 1493 var sideInfo *snap.SideInfo 1494 1495 if !dangerousOK { 1496 si, err := snapasserts.DeriveSideInfo(tempPath, assertstate.DB(st)) 1497 switch { 1498 case err == nil: 1499 snapName = si.RealName 1500 sideInfo = si 1501 case asserts.IsNotFound(err): 1502 // with devmode we try to find assertions but it's ok 1503 // if they are not there (implies --dangerous) 1504 if !isTrue(form, "devmode") { 1505 msg := "cannot find signatures with metadata for snap" 1506 if origPath != "" { 1507 msg = fmt.Sprintf("%s %q", msg, origPath) 1508 } 1509 return BadRequest(msg) 1510 } 1511 // TODO: set a warning if devmode 1512 default: 1513 return BadRequest(err.Error()) 1514 } 1515 } 1516 1517 if snapName == "" { 1518 // potentially dangerous but dangerous or devmode params were set 1519 info, err := unsafeReadSnapInfo(tempPath) 1520 if err != nil { 1521 return BadRequest("cannot read snap file: %v", err) 1522 } 1523 snapName = info.SnapName() 1524 sideInfo = &snap.SideInfo{RealName: snapName} 1525 } 1526 1527 if instanceName != "" { 1528 requestedSnapName := snap.InstanceSnap(instanceName) 1529 if requestedSnapName != snapName { 1530 return BadRequest(fmt.Sprintf("instance name %q does not match snap name %q", instanceName, snapName)) 1531 } 1532 } else { 1533 instanceName = snapName 1534 } 1535 1536 msg := fmt.Sprintf(i18n.G("Install %q snap from file"), instanceName) 1537 if origPath != "" { 1538 msg = fmt.Sprintf(i18n.G("Install %q snap from file %q"), instanceName, origPath) 1539 } 1540 1541 tset, _, err := snapstateInstallPath(st, sideInfo, tempPath, instanceName, "", flags) 1542 if err != nil { 1543 return errToResponse(err, []string{snapName}, InternalError, "cannot install snap file: %v") 1544 } 1545 1546 chg := newChange(st, "install-snap", msg, []*state.TaskSet{tset}, []string{instanceName}) 1547 chg.Set("api-data", map[string]string{"snap-name": instanceName}) 1548 1549 ensureStateSoon(st) 1550 1551 // only when the unlock succeeds (as opposed to panicing) is the handoff done 1552 // but this is good enough 1553 changeTriggered = true 1554 1555 return AsyncResponse(nil, &Meta{Change: chg.ID()}) 1556 } 1557 1558 func unsafeReadSnapInfoImpl(snapPath string) (*snap.Info, error) { 1559 // Condider using DeriveSideInfo before falling back to this! 1560 snapf, err := snapfile.Open(snapPath) 1561 if err != nil { 1562 return nil, err 1563 } 1564 return snap.ReadInfoFromSnapFile(snapf, nil) 1565 } 1566 1567 var unsafeReadSnapInfo = unsafeReadSnapInfoImpl 1568 1569 func iconGet(st *state.State, name string) Response { 1570 st.Lock() 1571 defer st.Unlock() 1572 1573 var snapst snapstate.SnapState 1574 err := snapstate.Get(st, name, &snapst) 1575 if err != nil { 1576 if err == state.ErrNoState { 1577 return SnapNotFound(name, err) 1578 } 1579 return InternalError("cannot consult state: %v", err) 1580 } 1581 sideInfo := snapst.CurrentSideInfo() 1582 if sideInfo == nil { 1583 return NotFound("snap has no current revision") 1584 } 1585 1586 icon := snapIcon(snap.MinimalPlaceInfo(name, sideInfo.Revision)) 1587 1588 if icon == "" { 1589 return NotFound("local snap has no icon") 1590 } 1591 1592 return fileResponse(icon) 1593 } 1594 1595 func appIconGet(c *Command, r *http.Request, user *auth.UserState) Response { 1596 vars := muxVars(r) 1597 name := vars["name"] 1598 1599 return iconGet(c.d.overlord.State(), name) 1600 } 1601 1602 func getSnapConf(c *Command, r *http.Request, user *auth.UserState) Response { 1603 vars := muxVars(r) 1604 snapName := configstate.RemapSnapFromRequest(vars["name"]) 1605 1606 keys := strutil.CommaSeparatedList(r.URL.Query().Get("keys")) 1607 1608 s := c.d.overlord.State() 1609 s.Lock() 1610 tr := config.NewTransaction(s) 1611 s.Unlock() 1612 1613 currentConfValues := make(map[string]interface{}) 1614 // Special case - return root document 1615 if len(keys) == 0 { 1616 keys = []string{""} 1617 } 1618 for _, key := range keys { 1619 var value interface{} 1620 if err := tr.Get(snapName, key, &value); err != nil { 1621 if config.IsNoOption(err) { 1622 if key == "" { 1623 // no configuration - return empty document 1624 currentConfValues = make(map[string]interface{}) 1625 break 1626 } 1627 return SyncResponse(&resp{ 1628 Type: ResponseTypeError, 1629 Result: &errorResult{ 1630 Message: err.Error(), 1631 Kind: client.ErrorKindConfigNoSuchOption, 1632 Value: err, 1633 }, 1634 Status: 400, 1635 }, nil) 1636 } else { 1637 return InternalError("%v", err) 1638 } 1639 } 1640 if key == "" { 1641 if len(keys) > 1 { 1642 return BadRequest("keys contains zero-length string") 1643 } 1644 return SyncResponse(value, nil) 1645 } 1646 1647 currentConfValues[key] = value 1648 } 1649 1650 return SyncResponse(currentConfValues, nil) 1651 } 1652 1653 func setSnapConf(c *Command, r *http.Request, user *auth.UserState) Response { 1654 vars := muxVars(r) 1655 snapName := configstate.RemapSnapFromRequest(vars["name"]) 1656 1657 var patchValues map[string]interface{} 1658 if err := jsonutil.DecodeWithNumber(r.Body, &patchValues); err != nil { 1659 return BadRequest("cannot decode request body into patch values: %v", err) 1660 } 1661 1662 st := c.d.overlord.State() 1663 st.Lock() 1664 defer st.Unlock() 1665 1666 taskset, err := configstate.ConfigureInstalled(st, snapName, patchValues, 0) 1667 if err != nil { 1668 // TODO: just return snap-not-installed instead ? 1669 if _, ok := err.(*snap.NotInstalledError); ok { 1670 return SnapNotFound(snapName, err) 1671 } 1672 return errToResponse(err, []string{snapName}, InternalError, "%v") 1673 } 1674 1675 summary := fmt.Sprintf("Change configuration of %q snap", snapName) 1676 change := newChange(st, "configure-snap", summary, []*state.TaskSet{taskset}, []string{snapName}) 1677 1678 st.EnsureBefore(0) 1679 1680 return AsyncResponse(nil, &Meta{Change: change.ID()}) 1681 } 1682 1683 // interfacesConnectionsMultiplexer multiplexes to either legacy (connection) or modern behavior (interfaces). 1684 func interfacesConnectionsMultiplexer(c *Command, r *http.Request, user *auth.UserState) Response { 1685 query := r.URL.Query() 1686 qselect := query.Get("select") 1687 if qselect == "" { 1688 return getLegacyConnections(c, r, user) 1689 } else { 1690 return getInterfaces(c, r, user) 1691 } 1692 } 1693 1694 func getInterfaces(c *Command, r *http.Request, user *auth.UserState) Response { 1695 // Collect query options from request arguments. 1696 q := r.URL.Query() 1697 pselect := q.Get("select") 1698 if pselect != "all" && pselect != "connected" { 1699 return BadRequest("unsupported select qualifier") 1700 } 1701 var names []string // Interface names 1702 namesStr := q.Get("names") 1703 if namesStr != "" { 1704 names = strings.Split(namesStr, ",") 1705 } 1706 opts := &interfaces.InfoOptions{ 1707 Names: names, 1708 Doc: q.Get("doc") == "true", 1709 Plugs: q.Get("plugs") == "true", 1710 Slots: q.Get("slots") == "true", 1711 Connected: pselect == "connected", 1712 } 1713 // Query the interface repository (this returns []*interface.Info). 1714 infos := c.d.overlord.InterfaceManager().Repository().Info(opts) 1715 infoJSONs := make([]*interfaceJSON, 0, len(infos)) 1716 1717 for _, info := range infos { 1718 // Convert interfaces.Info into interfaceJSON 1719 plugs := make([]*plugJSON, 0, len(info.Plugs)) 1720 for _, plug := range info.Plugs { 1721 plugs = append(plugs, &plugJSON{ 1722 Snap: plug.Snap.InstanceName(), 1723 Name: plug.Name, 1724 Attrs: plug.Attrs, 1725 Label: plug.Label, 1726 }) 1727 } 1728 slots := make([]*slotJSON, 0, len(info.Slots)) 1729 for _, slot := range info.Slots { 1730 slots = append(slots, &slotJSON{ 1731 Snap: slot.Snap.InstanceName(), 1732 Name: slot.Name, 1733 Attrs: slot.Attrs, 1734 Label: slot.Label, 1735 }) 1736 } 1737 infoJSONs = append(infoJSONs, &interfaceJSON{ 1738 Name: info.Name, 1739 Summary: info.Summary, 1740 DocURL: info.DocURL, 1741 Plugs: plugs, 1742 Slots: slots, 1743 }) 1744 } 1745 return SyncResponse(infoJSONs, nil) 1746 } 1747 1748 func getLegacyConnections(c *Command, r *http.Request, user *auth.UserState) Response { 1749 connsjson, err := collectConnections(c.d.overlord.InterfaceManager(), collectFilter{}) 1750 if err != nil { 1751 return InternalError("collecting connection information failed: %v", err) 1752 } 1753 legacyconnsjson := legacyConnectionsJSON{ 1754 Plugs: connsjson.Plugs, 1755 Slots: connsjson.Slots, 1756 } 1757 return SyncResponse(legacyconnsjson, nil) 1758 } 1759 1760 func snapNamesFromConns(conns []*interfaces.ConnRef) []string { 1761 m := make(map[string]bool) 1762 for _, conn := range conns { 1763 m[conn.PlugRef.Snap] = true 1764 m[conn.SlotRef.Snap] = true 1765 } 1766 l := make([]string, 0, len(m)) 1767 for name := range m { 1768 l = append(l, name) 1769 } 1770 sort.Strings(l) 1771 return l 1772 } 1773 1774 // changeInterfaces controls the interfaces system. 1775 // Plugs can be connected to and disconnected from slots. 1776 func changeInterfaces(c *Command, r *http.Request, user *auth.UserState) Response { 1777 var a interfaceAction 1778 decoder := json.NewDecoder(r.Body) 1779 if err := decoder.Decode(&a); err != nil { 1780 return BadRequest("cannot decode request body into an interface action: %v", err) 1781 } 1782 if a.Action == "" { 1783 return BadRequest("interface action not specified") 1784 } 1785 if len(a.Plugs) > 1 || len(a.Slots) > 1 { 1786 return NotImplemented("many-to-many operations are not implemented") 1787 } 1788 if a.Action != "connect" && a.Action != "disconnect" { 1789 return BadRequest("unsupported interface action: %q", a.Action) 1790 } 1791 if len(a.Plugs) == 0 || len(a.Slots) == 0 { 1792 return BadRequest("at least one plug and slot is required") 1793 } 1794 1795 var summary string 1796 var err error 1797 1798 var tasksets []*state.TaskSet 1799 var affected []string 1800 1801 st := c.d.overlord.State() 1802 st.Lock() 1803 defer st.Unlock() 1804 1805 for i := range a.Plugs { 1806 a.Plugs[i].Snap = ifacestate.RemapSnapFromRequest(a.Plugs[i].Snap) 1807 } 1808 for i := range a.Slots { 1809 a.Slots[i].Snap = ifacestate.RemapSnapFromRequest(a.Slots[i].Snap) 1810 } 1811 1812 switch a.Action { 1813 case "connect": 1814 var connRef *interfaces.ConnRef 1815 repo := c.d.overlord.InterfaceManager().Repository() 1816 connRef, err = repo.ResolveConnect(a.Plugs[0].Snap, a.Plugs[0].Name, a.Slots[0].Snap, a.Slots[0].Name) 1817 if err == nil { 1818 var ts *state.TaskSet 1819 affected = snapNamesFromConns([]*interfaces.ConnRef{connRef}) 1820 summary = fmt.Sprintf("Connect %s:%s to %s:%s", connRef.PlugRef.Snap, connRef.PlugRef.Name, connRef.SlotRef.Snap, connRef.SlotRef.Name) 1821 ts, err = ifacestate.Connect(st, connRef.PlugRef.Snap, connRef.PlugRef.Name, connRef.SlotRef.Snap, connRef.SlotRef.Name) 1822 if _, ok := err.(*ifacestate.ErrAlreadyConnected); ok { 1823 change := newChange(st, a.Action+"-snap", summary, nil, affected) 1824 change.SetStatus(state.DoneStatus) 1825 return AsyncResponse(nil, &Meta{Change: change.ID()}) 1826 } 1827 tasksets = append(tasksets, ts) 1828 } 1829 case "disconnect": 1830 var conns []*interfaces.ConnRef 1831 summary = fmt.Sprintf("Disconnect %s:%s from %s:%s", a.Plugs[0].Snap, a.Plugs[0].Name, a.Slots[0].Snap, a.Slots[0].Name) 1832 conns, err = c.d.overlord.InterfaceManager().ResolveDisconnect(a.Plugs[0].Snap, a.Plugs[0].Name, a.Slots[0].Snap, a.Slots[0].Name, a.Forget) 1833 if err == nil { 1834 if len(conns) == 0 { 1835 return InterfacesUnchanged("nothing to do") 1836 } 1837 repo := c.d.overlord.InterfaceManager().Repository() 1838 for _, connRef := range conns { 1839 var ts *state.TaskSet 1840 var conn *interfaces.Connection 1841 if a.Forget { 1842 ts, err = ifacestate.Forget(st, repo, connRef) 1843 } else { 1844 conn, err = repo.Connection(connRef) 1845 if err != nil { 1846 break 1847 } 1848 ts, err = ifacestate.Disconnect(st, conn) 1849 if err != nil { 1850 break 1851 } 1852 } 1853 if err != nil { 1854 break 1855 } 1856 ts.JoinLane(st.NewLane()) 1857 tasksets = append(tasksets, ts) 1858 } 1859 affected = snapNamesFromConns(conns) 1860 } 1861 } 1862 if err != nil { 1863 return errToResponse(err, nil, BadRequest, "%v") 1864 } 1865 1866 change := newChange(st, a.Action+"-snap", summary, tasksets, affected) 1867 st.EnsureBefore(0) 1868 1869 return AsyncResponse(nil, &Meta{Change: change.ID()}) 1870 } 1871 1872 type changeInfo struct { 1873 ID string `json:"id"` 1874 Kind string `json:"kind"` 1875 Summary string `json:"summary"` 1876 Status string `json:"status"` 1877 Tasks []*taskInfo `json:"tasks,omitempty"` 1878 Ready bool `json:"ready"` 1879 Err string `json:"err,omitempty"` 1880 1881 SpawnTime time.Time `json:"spawn-time,omitempty"` 1882 ReadyTime *time.Time `json:"ready-time,omitempty"` 1883 1884 Data map[string]*json.RawMessage `json:"data,omitempty"` 1885 } 1886 1887 type taskInfo struct { 1888 ID string `json:"id"` 1889 Kind string `json:"kind"` 1890 Summary string `json:"summary"` 1891 Status string `json:"status"` 1892 Log []string `json:"log,omitempty"` 1893 Progress taskInfoProgress `json:"progress"` 1894 1895 SpawnTime time.Time `json:"spawn-time,omitempty"` 1896 ReadyTime *time.Time `json:"ready-time,omitempty"` 1897 } 1898 1899 type taskInfoProgress struct { 1900 Label string `json:"label"` 1901 Done int `json:"done"` 1902 Total int `json:"total"` 1903 } 1904 1905 func change2changeInfo(chg *state.Change) *changeInfo { 1906 status := chg.Status() 1907 chgInfo := &changeInfo{ 1908 ID: chg.ID(), 1909 Kind: chg.Kind(), 1910 Summary: chg.Summary(), 1911 Status: status.String(), 1912 Ready: status.Ready(), 1913 1914 SpawnTime: chg.SpawnTime(), 1915 } 1916 readyTime := chg.ReadyTime() 1917 if !readyTime.IsZero() { 1918 chgInfo.ReadyTime = &readyTime 1919 } 1920 if err := chg.Err(); err != nil { 1921 chgInfo.Err = err.Error() 1922 } 1923 1924 tasks := chg.Tasks() 1925 taskInfos := make([]*taskInfo, len(tasks)) 1926 for j, t := range tasks { 1927 label, done, total := t.Progress() 1928 1929 taskInfo := &taskInfo{ 1930 ID: t.ID(), 1931 Kind: t.Kind(), 1932 Summary: t.Summary(), 1933 Status: t.Status().String(), 1934 Log: t.Log(), 1935 Progress: taskInfoProgress{ 1936 Label: label, 1937 Done: done, 1938 Total: total, 1939 }, 1940 SpawnTime: t.SpawnTime(), 1941 } 1942 readyTime := t.ReadyTime() 1943 if !readyTime.IsZero() { 1944 taskInfo.ReadyTime = &readyTime 1945 } 1946 taskInfos[j] = taskInfo 1947 } 1948 chgInfo.Tasks = taskInfos 1949 1950 var data map[string]*json.RawMessage 1951 if chg.Get("api-data", &data) == nil { 1952 chgInfo.Data = data 1953 } 1954 1955 return chgInfo 1956 } 1957 1958 func getChange(c *Command, r *http.Request, user *auth.UserState) Response { 1959 chID := muxVars(r)["id"] 1960 state := c.d.overlord.State() 1961 state.Lock() 1962 defer state.Unlock() 1963 chg := state.Change(chID) 1964 if chg == nil { 1965 return NotFound("cannot find change with id %q", chID) 1966 } 1967 1968 return SyncResponse(change2changeInfo(chg), nil) 1969 } 1970 1971 func getChanges(c *Command, r *http.Request, user *auth.UserState) Response { 1972 query := r.URL.Query() 1973 qselect := query.Get("select") 1974 if qselect == "" { 1975 qselect = "in-progress" 1976 } 1977 var filter func(*state.Change) bool 1978 switch qselect { 1979 case "all": 1980 filter = func(*state.Change) bool { return true } 1981 case "in-progress": 1982 filter = func(chg *state.Change) bool { return !chg.Status().Ready() } 1983 case "ready": 1984 filter = func(chg *state.Change) bool { return chg.Status().Ready() } 1985 default: 1986 return BadRequest("select should be one of: all,in-progress,ready") 1987 } 1988 1989 if wantedName := query.Get("for"); wantedName != "" { 1990 outerFilter := filter 1991 filter = func(chg *state.Change) bool { 1992 if !outerFilter(chg) { 1993 return false 1994 } 1995 1996 var snapNames []string 1997 if err := chg.Get("snap-names", &snapNames); err != nil { 1998 logger.Noticef("Cannot get snap-name for change %v", chg.ID()) 1999 return false 2000 } 2001 2002 for _, name := range snapNames { 2003 // due to 2004 // https://bugs.launchpad.net/snapd/+bug/1880560 2005 // the snap-names in service-control changes 2006 // could have included <snap>.<app> 2007 snapName, _ := snap.SplitSnapApp(name) 2008 if snapName == wantedName { 2009 return true 2010 } 2011 } 2012 return false 2013 } 2014 } 2015 2016 state := c.d.overlord.State() 2017 state.Lock() 2018 defer state.Unlock() 2019 chgs := state.Changes() 2020 chgInfos := make([]*changeInfo, 0, len(chgs)) 2021 for _, chg := range chgs { 2022 if !filter(chg) { 2023 continue 2024 } 2025 chgInfos = append(chgInfos, change2changeInfo(chg)) 2026 } 2027 return SyncResponse(chgInfos, nil) 2028 } 2029 2030 func abortChange(c *Command, r *http.Request, user *auth.UserState) Response { 2031 chID := muxVars(r)["id"] 2032 state := c.d.overlord.State() 2033 state.Lock() 2034 defer state.Unlock() 2035 chg := state.Change(chID) 2036 if chg == nil { 2037 return NotFound("cannot find change with id %q", chID) 2038 } 2039 2040 var reqData struct { 2041 Action string `json:"action"` 2042 } 2043 2044 decoder := json.NewDecoder(r.Body) 2045 if err := decoder.Decode(&reqData); err != nil { 2046 return BadRequest("cannot decode data from request body: %v", err) 2047 } 2048 2049 if reqData.Action != "abort" { 2050 return BadRequest("change action %q is unsupported", reqData.Action) 2051 } 2052 2053 if chg.Status().Ready() { 2054 return BadRequest("cannot abort change %s with nothing pending", chID) 2055 } 2056 2057 // flag the change 2058 chg.Abort() 2059 2060 // actually ask to proceed with the abort 2061 ensureStateSoon(state) 2062 2063 return SyncResponse(change2changeInfo(chg), nil) 2064 } 2065 2066 var ( 2067 runSnapctlUcrednetGet = ucrednetGet 2068 ctlcmdRun = ctlcmd.Run 2069 ) 2070 2071 func convertBuyError(err error) Response { 2072 switch err { 2073 case nil: 2074 return nil 2075 case store.ErrInvalidCredentials: 2076 return Unauthorized(err.Error()) 2077 case store.ErrUnauthenticated: 2078 return SyncResponse(&resp{ 2079 Type: ResponseTypeError, 2080 Result: &errorResult{ 2081 Message: err.Error(), 2082 Kind: client.ErrorKindLoginRequired, 2083 }, 2084 Status: 400, 2085 }, nil) 2086 case store.ErrTOSNotAccepted: 2087 return SyncResponse(&resp{ 2088 Type: ResponseTypeError, 2089 Result: &errorResult{ 2090 Message: err.Error(), 2091 Kind: client.ErrorKindTermsNotAccepted, 2092 }, 2093 Status: 400, 2094 }, nil) 2095 case store.ErrNoPaymentMethods: 2096 return SyncResponse(&resp{ 2097 Type: ResponseTypeError, 2098 Result: &errorResult{ 2099 Message: err.Error(), 2100 Kind: client.ErrorKindNoPaymentMethods, 2101 }, 2102 Status: 400, 2103 }, nil) 2104 case store.ErrPaymentDeclined: 2105 return SyncResponse(&resp{ 2106 Type: ResponseTypeError, 2107 Result: &errorResult{ 2108 Message: err.Error(), 2109 Kind: client.ErrorKindPaymentDeclined, 2110 }, 2111 Status: 400, 2112 }, nil) 2113 default: 2114 return InternalError("%v", err) 2115 } 2116 } 2117 2118 func postBuy(c *Command, r *http.Request, user *auth.UserState) Response { 2119 var opts client.BuyOptions 2120 2121 decoder := json.NewDecoder(r.Body) 2122 err := decoder.Decode(&opts) 2123 if err != nil { 2124 return BadRequest("cannot decode buy options from request body: %v", err) 2125 } 2126 2127 s := getStore(c) 2128 2129 buyResult, err := s.Buy(&opts, user) 2130 2131 if resp := convertBuyError(err); resp != nil { 2132 return resp 2133 } 2134 2135 return SyncResponse(buyResult, nil) 2136 } 2137 2138 func readyToBuy(c *Command, r *http.Request, user *auth.UserState) Response { 2139 s := getStore(c) 2140 2141 if resp := convertBuyError(s.ReadyToBuy(user)); resp != nil { 2142 return resp 2143 } 2144 2145 return SyncResponse(true, nil) 2146 } 2147 2148 func runSnapctl(c *Command, r *http.Request, user *auth.UserState) Response { 2149 var snapctlOptions client.SnapCtlOptions 2150 if err := jsonutil.DecodeWithNumber(r.Body, &snapctlOptions); err != nil { 2151 return BadRequest("cannot decode snapctl request: %s", err) 2152 } 2153 2154 if len(snapctlOptions.Args) == 0 { 2155 return BadRequest("snapctl cannot run without args") 2156 } 2157 2158 _, uid, _, err := runSnapctlUcrednetGet(r.RemoteAddr) 2159 if err != nil { 2160 return Forbidden("cannot get remote user: %s", err) 2161 } 2162 2163 // Ignore missing context error to allow 'snapctl -h' without a context; 2164 // Actual context is validated later by get/set. 2165 context, _ := c.d.overlord.HookManager().Context(snapctlOptions.ContextID) 2166 stdout, stderr, err := ctlcmdRun(context, snapctlOptions.Args, uid) 2167 if err != nil { 2168 if e, ok := err.(*ctlcmd.UnsuccessfulError); ok { 2169 result := map[string]interface{}{ 2170 "stdout": string(stdout), 2171 "stderr": string(stderr), 2172 "exit-code": e.ExitCode, 2173 } 2174 return &resp{ 2175 Type: ResponseTypeError, 2176 Result: &errorResult{ 2177 Message: e.Error(), 2178 Kind: client.ErrorKindUnsuccessful, 2179 Value: result, 2180 }, 2181 Status: 200, 2182 } 2183 } 2184 if e, ok := err.(*ctlcmd.ForbiddenCommandError); ok { 2185 return Forbidden(e.Error()) 2186 } 2187 if e, ok := err.(*flags.Error); ok && e.Type == flags.ErrHelp { 2188 stdout = []byte(e.Error()) 2189 } else { 2190 return BadRequest("error running snapctl: %s", err) 2191 } 2192 } 2193 2194 if context != nil && context.IsEphemeral() { 2195 context.Lock() 2196 defer context.Unlock() 2197 if err := context.Done(); err != nil { 2198 return BadRequest(i18n.G("set failed: %v"), err) 2199 } 2200 } 2201 2202 result := map[string]string{ 2203 "stdout": string(stdout), 2204 "stderr": string(stderr), 2205 } 2206 2207 return SyncResponse(result, nil) 2208 } 2209 2210 // aliasAction is an action performed on aliases 2211 type aliasAction struct { 2212 Action string `json:"action"` 2213 Snap string `json:"snap"` 2214 App string `json:"app"` 2215 Alias string `json:"alias"` 2216 // old now unsupported api 2217 Aliases []string `json:"aliases"` 2218 } 2219 2220 func changeAliases(c *Command, r *http.Request, user *auth.UserState) Response { 2221 var a aliasAction 2222 decoder := json.NewDecoder(r.Body) 2223 if err := decoder.Decode(&a); err != nil { 2224 return BadRequest("cannot decode request body into an alias action: %v", err) 2225 } 2226 if len(a.Aliases) != 0 { 2227 return BadRequest("cannot interpret request, snaps can no longer be expected to declare their aliases") 2228 } 2229 2230 var taskset *state.TaskSet 2231 var err error 2232 2233 st := c.d.overlord.State() 2234 st.Lock() 2235 defer st.Unlock() 2236 2237 switch a.Action { 2238 default: 2239 return BadRequest("unsupported alias action: %q", a.Action) 2240 case "alias": 2241 taskset, err = snapstate.Alias(st, a.Snap, a.App, a.Alias) 2242 case "unalias": 2243 if a.Alias == a.Snap { 2244 // Do What I mean: 2245 // check if a snap is referred/intended 2246 // or just an alias 2247 var snapst snapstate.SnapState 2248 err := snapstate.Get(st, a.Snap, &snapst) 2249 if err != nil && err != state.ErrNoState { 2250 return InternalError("%v", err) 2251 } 2252 if err == state.ErrNoState { // not a snap 2253 a.Snap = "" 2254 } 2255 } 2256 if a.Snap != "" { 2257 a.Alias = "" 2258 taskset, err = snapstate.DisableAllAliases(st, a.Snap) 2259 } else { 2260 taskset, a.Snap, err = snapstate.RemoveManualAlias(st, a.Alias) 2261 } 2262 case "prefer": 2263 taskset, err = snapstate.Prefer(st, a.Snap) 2264 } 2265 if err != nil { 2266 return errToResponse(err, nil, BadRequest, "%v") 2267 } 2268 2269 var summary string 2270 switch a.Action { 2271 case "alias": 2272 summary = fmt.Sprintf(i18n.G("Setup alias %q => %q for snap %q"), a.Alias, a.App, a.Snap) 2273 case "unalias": 2274 if a.Alias != "" { 2275 summary = fmt.Sprintf(i18n.G("Remove manual alias %q for snap %q"), a.Alias, a.Snap) 2276 } else { 2277 summary = fmt.Sprintf(i18n.G("Disable all aliases for snap %q"), a.Snap) 2278 } 2279 case "prefer": 2280 summary = fmt.Sprintf(i18n.G("Prefer aliases of snap %q"), a.Snap) 2281 } 2282 2283 change := newChange(st, a.Action, summary, []*state.TaskSet{taskset}, []string{a.Snap}) 2284 st.EnsureBefore(0) 2285 2286 return AsyncResponse(nil, &Meta{Change: change.ID()}) 2287 } 2288 2289 type aliasStatus struct { 2290 Command string `json:"command"` 2291 Status string `json:"status"` 2292 Manual string `json:"manual,omitempty"` 2293 Auto string `json:"auto,omitempty"` 2294 } 2295 2296 // getAliases produces a response with a map snap -> alias -> aliasStatus 2297 func getAliases(c *Command, r *http.Request, user *auth.UserState) Response { 2298 state := c.d.overlord.State() 2299 state.Lock() 2300 defer state.Unlock() 2301 2302 res := make(map[string]map[string]aliasStatus) 2303 2304 allStates, err := snapstate.All(state) 2305 if err != nil { 2306 return InternalError("cannot list local snaps: %v", err) 2307 } 2308 2309 for snapName, snapst := range allStates { 2310 if err != nil { 2311 return InternalError("cannot retrieve info for snap %q: %v", snapName, err) 2312 } 2313 if len(snapst.Aliases) != 0 { 2314 snapAliases := make(map[string]aliasStatus) 2315 res[snapName] = snapAliases 2316 autoDisabled := snapst.AutoAliasesDisabled 2317 for alias, aliasTarget := range snapst.Aliases { 2318 aliasStatus := aliasStatus{ 2319 Manual: aliasTarget.Manual, 2320 Auto: aliasTarget.Auto, 2321 } 2322 status := "auto" 2323 tgt := aliasTarget.Effective(autoDisabled) 2324 if tgt == "" { 2325 status = "disabled" 2326 tgt = aliasTarget.Auto 2327 } else if aliasTarget.Manual != "" { 2328 status = "manual" 2329 } 2330 aliasStatus.Status = status 2331 aliasStatus.Command = snap.JoinSnapApp(snapName, tgt) 2332 snapAliases[alias] = aliasStatus 2333 } 2334 } 2335 } 2336 2337 return SyncResponse(res, nil) 2338 } 2339 2340 func getAppsInfo(c *Command, r *http.Request, user *auth.UserState) Response { 2341 query := r.URL.Query() 2342 2343 opts := appInfoOptions{} 2344 switch sel := query.Get("select"); sel { 2345 case "": 2346 // nothing to do 2347 case "service": 2348 opts.service = true 2349 default: 2350 return BadRequest("invalid select parameter: %q", sel) 2351 } 2352 2353 appInfos, rsp := appInfosFor(c.d.overlord.State(), strutil.CommaSeparatedList(query.Get("names")), opts) 2354 if rsp != nil { 2355 return rsp 2356 } 2357 2358 sd := servicestate.NewStatusDecorator(progress.Null) 2359 2360 clientAppInfos, err := clientutil.ClientAppInfosFromSnapAppInfos(appInfos, sd) 2361 if err != nil { 2362 return InternalError("%v", err) 2363 } 2364 2365 return SyncResponse(clientAppInfos, nil) 2366 } 2367 2368 func getLogs(c *Command, r *http.Request, user *auth.UserState) Response { 2369 query := r.URL.Query() 2370 n := 10 2371 if s := query.Get("n"); s != "" { 2372 m, err := strconv.ParseInt(s, 0, 32) 2373 if err != nil { 2374 return BadRequest(`invalid value for n: %q: %v`, s, err) 2375 } 2376 n = int(m) 2377 } 2378 follow := false 2379 if s := query.Get("follow"); s != "" { 2380 f, err := strconv.ParseBool(s) 2381 if err != nil { 2382 return BadRequest(`invalid value for follow: %q: %v`, s, err) 2383 } 2384 follow = f 2385 } 2386 2387 // only services have logs for now 2388 opts := appInfoOptions{service: true} 2389 appInfos, rsp := appInfosFor(c.d.overlord.State(), strutil.CommaSeparatedList(query.Get("names")), opts) 2390 if rsp != nil { 2391 return rsp 2392 } 2393 if len(appInfos) == 0 { 2394 return AppNotFound("no matching services") 2395 } 2396 2397 serviceNames := make([]string, len(appInfos)) 2398 for i, appInfo := range appInfos { 2399 serviceNames[i] = appInfo.ServiceName() 2400 } 2401 2402 sysd := systemd.New(dirs.GlobalRootDir, systemd.SystemMode, progress.Null) 2403 reader, err := sysd.LogReader(serviceNames, n, follow) 2404 if err != nil { 2405 return InternalError("cannot get logs: %v", err) 2406 } 2407 2408 return &journalLineReaderSeqResponse{ 2409 ReadCloser: reader, 2410 follow: follow, 2411 } 2412 } 2413 2414 func namesToSnapNames(inst *servicestate.Instruction) []string { 2415 seen := make(map[string]struct{}, len(inst.Names)) 2416 for _, snapOrSnapDotApp := range inst.Names { 2417 snapName, _ := snap.SplitSnapApp(snapOrSnapDotApp) 2418 seen[snapName] = struct{}{} 2419 } 2420 names := make([]string, 0, len(seen)) 2421 for k := range seen { 2422 names = append(names, k) 2423 } 2424 // keep stable ordering 2425 sort.Strings(names) 2426 return names 2427 } 2428 2429 func postApps(c *Command, r *http.Request, user *auth.UserState) Response { 2430 var inst servicestate.Instruction 2431 decoder := json.NewDecoder(r.Body) 2432 if err := decoder.Decode(&inst); err != nil { 2433 return BadRequest("cannot decode request body into service operation: %v", err) 2434 } 2435 // XXX: decoder.More() 2436 if len(inst.Names) == 0 { 2437 // on POST, don't allow empty to mean all 2438 return BadRequest("cannot perform operation on services without a list of services to operate on") 2439 } 2440 2441 st := c.d.overlord.State() 2442 appInfos, rsp := appInfosFor(st, inst.Names, appInfoOptions{service: true}) 2443 if rsp != nil { 2444 return rsp 2445 } 2446 if len(appInfos) == 0 { 2447 // can't happen: appInfosFor with a non-empty list of services 2448 // shouldn't ever return an empty appInfos with no error response 2449 return InternalError("no services found") 2450 } 2451 2452 tss, err := servicestateControl(st, appInfos, &inst, nil) 2453 if err != nil { 2454 // TODO: use errToResponse here too and introduce a proper error kind ? 2455 if _, ok := err.(servicestate.ServiceActionConflictError); ok { 2456 return Conflict(err.Error()) 2457 } 2458 return BadRequest(err.Error()) 2459 } 2460 st.Lock() 2461 defer st.Unlock() 2462 // names received in the request can be snap or snap.app, we need to 2463 // extract the actual snap names before associating them with a change 2464 chg := newChange(st, "service-control", fmt.Sprintf("Running service command"), tss, namesToSnapNames(&inst)) 2465 st.EnsureBefore(0) 2466 return AsyncResponse(nil, &Meta{Change: chg.ID()}) 2467 } 2468 2469 var ( 2470 stateOkayWarnings = (*state.State).OkayWarnings 2471 stateAllWarnings = (*state.State).AllWarnings 2472 statePendingWarnings = (*state.State).PendingWarnings 2473 ) 2474 2475 func ackWarnings(c *Command, r *http.Request, _ *auth.UserState) Response { 2476 defer r.Body.Close() 2477 var op struct { 2478 Action string `json:"action"` 2479 Timestamp time.Time `json:"timestamp"` 2480 } 2481 decoder := json.NewDecoder(r.Body) 2482 if err := decoder.Decode(&op); err != nil { 2483 return BadRequest("cannot decode request body into warnings operation: %v", err) 2484 } 2485 if op.Action != "okay" { 2486 return BadRequest("unknown warning action %q", op.Action) 2487 } 2488 st := c.d.overlord.State() 2489 st.Lock() 2490 defer st.Unlock() 2491 n := stateOkayWarnings(st, op.Timestamp) 2492 2493 return SyncResponse(n, nil) 2494 } 2495 2496 func getWarnings(c *Command, r *http.Request, _ *auth.UserState) Response { 2497 query := r.URL.Query() 2498 var all bool 2499 sel := query.Get("select") 2500 switch sel { 2501 case "all": 2502 all = true 2503 case "pending", "": 2504 all = false 2505 default: 2506 return BadRequest("invalid select parameter: %q", sel) 2507 } 2508 2509 st := c.d.overlord.State() 2510 st.Lock() 2511 defer st.Unlock() 2512 2513 var ws []*state.Warning 2514 if all { 2515 ws = stateAllWarnings(st) 2516 } else { 2517 ws, _ = statePendingWarnings(st) 2518 } 2519 if len(ws) == 0 { 2520 // no need to confuse the issue 2521 return SyncResponse([]state.Warning{}, nil) 2522 } 2523 2524 return SyncResponse(ws, nil) 2525 }