github.com/chipaca/snappy@v0.0.0-20210104084008-1f06296fe8ad/daemon/api.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 "context" 24 "encoding/json" 25 "errors" 26 "fmt" 27 "io" 28 "io/ioutil" 29 "mime" 30 "mime/multipart" 31 "net/http" 32 "os" 33 "path/filepath" 34 "strconv" 35 "strings" 36 37 "github.com/gorilla/mux" 38 39 "github.com/snapcore/snapd/asserts" 40 "github.com/snapcore/snapd/asserts/snapasserts" 41 "github.com/snapcore/snapd/client" 42 "github.com/snapcore/snapd/dirs" 43 "github.com/snapcore/snapd/i18n" 44 "github.com/snapcore/snapd/logger" 45 "github.com/snapcore/snapd/osutil" 46 "github.com/snapcore/snapd/overlord/assertstate" 47 "github.com/snapcore/snapd/overlord/auth" 48 "github.com/snapcore/snapd/overlord/servicestate" 49 "github.com/snapcore/snapd/overlord/snapshotstate" 50 "github.com/snapcore/snapd/overlord/snapstate" 51 "github.com/snapcore/snapd/overlord/state" 52 "github.com/snapcore/snapd/progress" 53 "github.com/snapcore/snapd/sandbox" 54 "github.com/snapcore/snapd/snap" 55 "github.com/snapcore/snapd/snap/channel" 56 "github.com/snapcore/snapd/snap/snapfile" 57 "github.com/snapcore/snapd/strutil" 58 ) 59 60 var api = []*Command{ 61 rootCmd, 62 sysInfoCmd, 63 loginCmd, 64 logoutCmd, 65 appIconCmd, 66 findCmd, 67 snapsCmd, 68 snapCmd, 69 snapFileCmd, 70 snapDownloadCmd, 71 snapConfCmd, 72 interfacesCmd, 73 assertsCmd, 74 assertsFindManyCmd, 75 stateChangeCmd, 76 stateChangesCmd, 77 createUserCmd, 78 buyCmd, 79 readyToBuyCmd, 80 snapctlCmd, 81 usersCmd, 82 sectionsCmd, 83 aliasesCmd, 84 appsCmd, 85 logsCmd, 86 warningsCmd, 87 debugPprofCmd, 88 debugCmd, 89 snapshotCmd, 90 snapshotExportCmd, 91 connectionsCmd, 92 modelCmd, 93 cohortsCmd, 94 serialModelCmd, 95 systemsCmd, 96 systemsActionCmd, 97 validationSetsListCmd, 98 validationSetsCmd, 99 routineConsoleConfStartCmd, 100 systemRecoveryKeysCmd, 101 } 102 103 var ( 104 // see daemon.go:canAccess for details how the access is controlled 105 snapsCmd = &Command{ 106 Path: "/v2/snaps", 107 UserOK: true, 108 PolkitOK: "io.snapcraft.snapd.manage", 109 GET: getSnapsInfo, 110 POST: postSnaps, 111 } 112 113 snapCmd = &Command{ 114 Path: "/v2/snaps/{name}", 115 UserOK: true, 116 PolkitOK: "io.snapcraft.snapd.manage", 117 GET: getSnapInfo, 118 POST: postSnap, 119 } 120 ) 121 122 // UserFromRequest extracts user information from request and return the respective user in state, if valid 123 // It requires the state to be locked 124 func UserFromRequest(st *state.State, req *http.Request) (*auth.UserState, error) { 125 // extract macaroons data from request 126 header := req.Header.Get("Authorization") 127 if header == "" { 128 return nil, auth.ErrInvalidAuth 129 } 130 131 authorizationData := strings.SplitN(header, " ", 2) 132 if len(authorizationData) != 2 || authorizationData[0] != "Macaroon" { 133 return nil, fmt.Errorf("authorization header misses Macaroon prefix") 134 } 135 136 var macaroon string 137 var discharges []string 138 for _, field := range strutil.CommaSeparatedList(authorizationData[1]) { 139 if strings.HasPrefix(field, `root="`) { 140 macaroon = strings.TrimSuffix(field[6:], `"`) 141 } 142 if strings.HasPrefix(field, `discharge="`) { 143 discharges = append(discharges, strings.TrimSuffix(field[11:], `"`)) 144 } 145 } 146 147 if macaroon == "" { 148 return nil, fmt.Errorf("invalid authorization header") 149 } 150 151 user, err := auth.CheckMacaroon(st, macaroon, discharges) 152 return user, err 153 } 154 155 var muxVars = mux.Vars 156 157 func getSnapInfo(c *Command, r *http.Request, user *auth.UserState) Response { 158 vars := muxVars(r) 159 name := vars["name"] 160 161 about, err := localSnapInfo(c.d.overlord.State(), name) 162 if err != nil { 163 if err == errNoSnap { 164 return SnapNotFound(name, err) 165 } 166 167 return InternalError("%v", err) 168 } 169 170 route := c.d.router.Get(c.Path) 171 if route == nil { 172 return InternalError("cannot find route for %q snap", name) 173 } 174 175 url, err := route.URL("name", name) 176 if err != nil { 177 return InternalError("cannot build URL for %q snap: %v", name, err) 178 } 179 180 sd := servicestate.NewStatusDecorator(progress.Null) 181 182 result := webify(mapLocal(about, sd), url.String()) 183 184 return SyncResponse(result, nil) 185 } 186 187 func webify(result *client.Snap, resource string) *client.Snap { 188 if result.Icon == "" || strings.HasPrefix(result.Icon, "http") { 189 return result 190 } 191 result.Icon = "" 192 193 route := appIconCmd.d.router.Get(appIconCmd.Path) 194 if route != nil { 195 url, err := route.URL("name", result.Name) 196 if err == nil { 197 result.Icon = url.String() 198 } 199 } 200 201 return result 202 } 203 204 func getStore(c *Command) snapstate.StoreService { 205 st := c.d.overlord.State() 206 st.Lock() 207 defer st.Unlock() 208 209 return snapstate.Store(st, nil) 210 } 211 212 // plural! 213 func getSnapsInfo(c *Command, r *http.Request, user *auth.UserState) Response { 214 215 if shouldSearchStore(r) { 216 logger.Noticef("Jumping to \"find\" to better support legacy request %q", r.URL) 217 return searchStore(c, r, user) 218 } 219 220 route := c.d.router.Get(snapCmd.Path) 221 if route == nil { 222 return InternalError("cannot find route for snaps") 223 } 224 225 query := r.URL.Query() 226 var all bool 227 sel := query.Get("select") 228 switch sel { 229 case "all": 230 all = true 231 case "enabled", "": 232 all = false 233 default: 234 return BadRequest("invalid select parameter: %q", sel) 235 } 236 var wanted map[string]bool 237 if ns := query.Get("snaps"); len(ns) > 0 { 238 nsl := strutil.CommaSeparatedList(ns) 239 wanted = make(map[string]bool, len(nsl)) 240 for _, name := range nsl { 241 wanted[name] = true 242 } 243 } 244 245 found, err := allLocalSnapInfos(c.d.overlord.State(), all, wanted) 246 if err != nil { 247 return InternalError("cannot list local snaps! %v", err) 248 } 249 250 results := make([]*json.RawMessage, len(found)) 251 252 sd := servicestate.NewStatusDecorator(progress.Null) 253 for i, x := range found { 254 name := x.info.InstanceName() 255 rev := x.info.Revision 256 257 url, err := route.URL("name", name) 258 if err != nil { 259 logger.Noticef("Cannot build URL for snap %q revision %s: %v", name, rev, err) 260 continue 261 } 262 263 data, err := json.Marshal(webify(mapLocal(x, sd), url.String())) 264 if err != nil { 265 return InternalError("cannot serialize snap %q revision %s: %v", name, rev, err) 266 } 267 raw := json.RawMessage(data) 268 results[i] = &raw 269 } 270 271 return SyncResponse(results, &Meta{Sources: []string{"local"}}) 272 } 273 274 func shouldSearchStore(r *http.Request) bool { 275 // we should jump to the old behaviour iff q is given, or if 276 // sources is given and either empty or contains the word 277 // 'store'. Otherwise, local results only. 278 279 query := r.URL.Query() 280 281 if _, ok := query["q"]; ok { 282 logger.Debugf("use of obsolete \"q\" parameter: %q", r.URL) 283 return true 284 } 285 286 if src, ok := query["sources"]; ok { 287 logger.Debugf("use of obsolete \"sources\" parameter: %q", r.URL) 288 if len(src) == 0 || strings.Contains(src[0], "store") { 289 return true 290 } 291 } 292 293 return false 294 } 295 296 // licenseData holds details about the snap license, and may be 297 // marshaled back as an error when the license agreement is pending, 298 // and is expected as input to accept (or not) that license 299 // agreement. As such, its field names are part of the API. 300 type licenseData struct { 301 Intro string `json:"intro"` 302 License string `json:"license"` 303 Agreed bool `json:"agreed"` 304 } 305 306 func (*licenseData) Error() string { 307 return "license agreement required" 308 } 309 310 type snapRevisionOptions struct { 311 Channel string `json:"channel"` 312 Revision snap.Revision `json:"revision"` 313 314 CohortKey string `json:"cohort-key"` 315 LeaveCohort bool `json:"leave-cohort"` 316 } 317 318 func (ropt *snapRevisionOptions) validate() error { 319 if ropt.CohortKey != "" { 320 if ropt.LeaveCohort { 321 return fmt.Errorf("cannot specify both cohort-key and leave-cohort") 322 } 323 if !ropt.Revision.Unset() { 324 return fmt.Errorf("cannot specify both cohort-key and revision") 325 } 326 } 327 328 if ropt.Channel != "" { 329 _, err := channel.Parse(ropt.Channel, "-") 330 if err != nil { 331 return err 332 } 333 } 334 return nil 335 } 336 337 type snapInstruction struct { 338 progress.NullMeter 339 340 Action string `json:"action"` 341 Amend bool `json:"amend"` 342 snapRevisionOptions 343 DevMode bool `json:"devmode"` 344 JailMode bool `json:"jailmode"` 345 Classic bool `json:"classic"` 346 IgnoreValidation bool `json:"ignore-validation"` 347 IgnoreRunning bool `json:"ignore-running"` 348 Unaliased bool `json:"unaliased"` 349 Purge bool `json:"purge,omitempty"` 350 // dropping support temporarely until flag confusion is sorted, 351 // this isn't supported by client atm anyway 352 LeaveOld bool `json:"temp-dropped-leave-old"` 353 License *licenseData `json:"license"` 354 Snaps []string `json:"snaps"` 355 Users []string `json:"users"` 356 357 // The fields below should not be unmarshalled into. Do not export them. 358 userID int 359 ctx context.Context 360 } 361 362 func (inst *snapInstruction) revnoOpts() *snapstate.RevisionOptions { 363 return &snapstate.RevisionOptions{ 364 Channel: inst.Channel, 365 Revision: inst.Revision, 366 CohortKey: inst.CohortKey, 367 LeaveCohort: inst.LeaveCohort, 368 } 369 } 370 371 func (inst *snapInstruction) modeFlags() (snapstate.Flags, error) { 372 return modeFlags(inst.DevMode, inst.JailMode, inst.Classic) 373 } 374 375 func (inst *snapInstruction) installFlags() (snapstate.Flags, error) { 376 flags, err := inst.modeFlags() 377 if err != nil { 378 return snapstate.Flags{}, err 379 } 380 if inst.Unaliased { 381 flags.Unaliased = true 382 } 383 if inst.IgnoreRunning { 384 flags.IgnoreRunning = true 385 } 386 387 return flags, nil 388 } 389 390 func (inst *snapInstruction) validate() error { 391 if inst.CohortKey != "" { 392 if inst.Action != "install" && inst.Action != "refresh" && inst.Action != "switch" { 393 return fmt.Errorf("cohort-key can only be specified for install, refresh, or switch") 394 } 395 } 396 if inst.LeaveCohort { 397 if inst.Action != "refresh" && inst.Action != "switch" { 398 return fmt.Errorf("leave-cohort can only be specified for refresh or switch") 399 } 400 } 401 if inst.Action == "install" { 402 for _, snapName := range inst.Snaps { 403 // FIXME: alternatively we could simply mutate *inst 404 // and s/ubuntu-core/core/ ? 405 if snapName == "ubuntu-core" { 406 return fmt.Errorf(`cannot install "ubuntu-core", please use "core" instead`) 407 } 408 } 409 } 410 411 return inst.snapRevisionOptions.validate() 412 } 413 414 type snapInstructionResult struct { 415 Summary string 416 Affected []string 417 Tasksets []*state.TaskSet 418 Result map[string]interface{} 419 } 420 421 var ( 422 snapstateInstall = snapstate.Install 423 snapstateInstallPath = snapstate.InstallPath 424 snapstateRefreshCandidates = snapstate.RefreshCandidates 425 snapstateTryPath = snapstate.TryPath 426 snapstateUpdate = snapstate.Update 427 snapstateUpdateMany = snapstate.UpdateMany 428 snapstateInstallMany = snapstate.InstallMany 429 snapstateRemoveMany = snapstate.RemoveMany 430 snapstateRevert = snapstate.Revert 431 snapstateRevertToRevision = snapstate.RevertToRevision 432 snapstateSwitch = snapstate.Switch 433 434 snapshotList = snapshotstate.List 435 snapshotCheck = snapshotstate.Check 436 snapshotForget = snapshotstate.Forget 437 snapshotRestore = snapshotstate.Restore 438 snapshotSave = snapshotstate.Save 439 snapshotExport = snapshotstate.Export 440 snapshotImport = snapshotstate.Import 441 442 assertstateRefreshSnapDeclarations = assertstate.RefreshSnapDeclarations 443 ) 444 445 func ensureStateSoonImpl(st *state.State) { 446 st.EnsureBefore(0) 447 } 448 449 var ensureStateSoon = ensureStateSoonImpl 450 451 var errDevJailModeConflict = errors.New("cannot use devmode and jailmode flags together") 452 var errClassicDevmodeConflict = errors.New("cannot use classic and devmode flags together") 453 var errNoJailMode = errors.New("this system cannot honour the jailmode flag") 454 455 func modeFlags(devMode, jailMode, classic bool) (snapstate.Flags, error) { 456 flags := snapstate.Flags{} 457 devModeOS := sandbox.ForceDevMode() 458 switch { 459 case jailMode && devModeOS: 460 return flags, errNoJailMode 461 case jailMode && devMode: 462 return flags, errDevJailModeConflict 463 case devMode && classic: 464 return flags, errClassicDevmodeConflict 465 } 466 // NOTE: jailmode and classic are allowed together. In that setting, 467 // jailmode overrides classic and the app gets regular (non-classic) 468 // confinement. 469 flags.JailMode = jailMode 470 flags.Classic = classic 471 flags.DevMode = devMode 472 return flags, nil 473 } 474 475 func snapUpdateMany(inst *snapInstruction, st *state.State) (*snapInstructionResult, error) { 476 // 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 477 if err := assertstateRefreshSnapDeclarations(st, inst.userID); err != nil { 478 return nil, err 479 } 480 481 // TODO: use a per-request context 482 updated, tasksets, err := snapstateUpdateMany(context.TODO(), st, inst.Snaps, inst.userID, nil) 483 if err != nil { 484 return nil, err 485 } 486 487 var msg string 488 switch len(updated) { 489 case 0: 490 if len(inst.Snaps) != 0 { 491 // TRANSLATORS: the %s is a comma-separated list of quoted snap names 492 msg = fmt.Sprintf(i18n.G("Refresh snaps %s: no updates"), strutil.Quoted(inst.Snaps)) 493 } else { 494 msg = i18n.G("Refresh all snaps: no updates") 495 } 496 case 1: 497 msg = fmt.Sprintf(i18n.G("Refresh snap %q"), updated[0]) 498 default: 499 quoted := strutil.Quoted(updated) 500 // TRANSLATORS: the %s is a comma-separated list of quoted snap names 501 msg = fmt.Sprintf(i18n.G("Refresh snaps %s"), quoted) 502 } 503 504 return &snapInstructionResult{ 505 Summary: msg, 506 Affected: updated, 507 Tasksets: tasksets, 508 }, nil 509 } 510 511 func snapInstallMany(inst *snapInstruction, st *state.State) (*snapInstructionResult, error) { 512 for _, name := range inst.Snaps { 513 if len(name) == 0 { 514 return nil, fmt.Errorf(i18n.G("cannot install snap with empty name")) 515 } 516 } 517 installed, tasksets, err := snapstateInstallMany(st, inst.Snaps, inst.userID) 518 if err != nil { 519 return nil, err 520 } 521 522 var msg string 523 switch len(inst.Snaps) { 524 case 0: 525 return nil, fmt.Errorf("cannot install zero snaps") 526 case 1: 527 msg = fmt.Sprintf(i18n.G("Install snap %q"), inst.Snaps[0]) 528 default: 529 quoted := strutil.Quoted(inst.Snaps) 530 // TRANSLATORS: the %s is a comma-separated list of quoted snap names 531 msg = fmt.Sprintf(i18n.G("Install snaps %s"), quoted) 532 } 533 534 return &snapInstructionResult{ 535 Summary: msg, 536 Affected: installed, 537 Tasksets: tasksets, 538 }, nil 539 } 540 541 func snapInstall(inst *snapInstruction, st *state.State) (string, []*state.TaskSet, error) { 542 if len(inst.Snaps[0]) == 0 { 543 return "", nil, fmt.Errorf(i18n.G("cannot install snap with empty name")) 544 } 545 546 flags, err := inst.installFlags() 547 if err != nil { 548 return "", nil, err 549 } 550 551 var ckey string 552 if inst.CohortKey == "" { 553 logger.Noticef("Installing snap %q revision %s", inst.Snaps[0], inst.Revision) 554 } else { 555 ckey = strutil.ElliptLeft(inst.CohortKey, 10) 556 logger.Noticef("Installing snap %q from cohort %q", inst.Snaps[0], ckey) 557 } 558 tset, err := snapstateInstall(inst.ctx, st, inst.Snaps[0], inst.revnoOpts(), inst.userID, flags) 559 if err != nil { 560 return "", nil, err 561 } 562 563 msg := fmt.Sprintf(i18n.G("Install %q snap"), inst.Snaps[0]) 564 if inst.Channel != "stable" && inst.Channel != "" { 565 msg += fmt.Sprintf(" from %q channel", inst.Channel) 566 } 567 if inst.CohortKey != "" { 568 msg += fmt.Sprintf(" from %q cohort", ckey) 569 } 570 return msg, []*state.TaskSet{tset}, nil 571 } 572 573 func snapUpdate(inst *snapInstruction, st *state.State) (string, []*state.TaskSet, error) { 574 // TODO: bail if revision is given (and != current?), *or* behave as with install --revision? 575 flags, err := inst.modeFlags() 576 if err != nil { 577 return "", nil, err 578 } 579 if inst.IgnoreValidation { 580 flags.IgnoreValidation = true 581 } 582 if inst.IgnoreRunning { 583 flags.IgnoreRunning = true 584 } 585 if inst.Amend { 586 flags.Amend = true 587 } 588 589 // we need refreshed snap-declarations to enforce refresh-control as best as we can 590 if err = assertstateRefreshSnapDeclarations(st, inst.userID); err != nil { 591 return "", nil, err 592 } 593 594 ts, err := snapstateUpdate(st, inst.Snaps[0], inst.revnoOpts(), inst.userID, flags) 595 if err != nil { 596 return "", nil, err 597 } 598 599 msg := fmt.Sprintf(i18n.G("Refresh %q snap"), inst.Snaps[0]) 600 if inst.Channel != "stable" && inst.Channel != "" { 601 msg = fmt.Sprintf(i18n.G("Refresh %q snap from %q channel"), inst.Snaps[0], inst.Channel) 602 } 603 604 return msg, []*state.TaskSet{ts}, nil 605 } 606 607 func snapRemoveMany(inst *snapInstruction, st *state.State) (*snapInstructionResult, error) { 608 removed, tasksets, err := snapstateRemoveMany(st, inst.Snaps) 609 if err != nil { 610 return nil, err 611 } 612 613 var msg string 614 switch len(inst.Snaps) { 615 case 0: 616 return nil, fmt.Errorf("cannot remove zero snaps") 617 case 1: 618 msg = fmt.Sprintf(i18n.G("Remove snap %q"), inst.Snaps[0]) 619 default: 620 quoted := strutil.Quoted(inst.Snaps) 621 // TRANSLATORS: the %s is a comma-separated list of quoted snap names 622 msg = fmt.Sprintf(i18n.G("Remove snaps %s"), quoted) 623 } 624 625 return &snapInstructionResult{ 626 Summary: msg, 627 Affected: removed, 628 Tasksets: tasksets, 629 }, nil 630 } 631 632 func snapRemove(inst *snapInstruction, st *state.State) (string, []*state.TaskSet, error) { 633 ts, err := snapstate.Remove(st, inst.Snaps[0], inst.Revision, &snapstate.RemoveFlags{Purge: inst.Purge}) 634 if err != nil { 635 return "", nil, err 636 } 637 638 msg := fmt.Sprintf(i18n.G("Remove %q snap"), inst.Snaps[0]) 639 return msg, []*state.TaskSet{ts}, nil 640 } 641 642 func snapRevert(inst *snapInstruction, st *state.State) (string, []*state.TaskSet, error) { 643 var ts *state.TaskSet 644 645 flags, err := inst.modeFlags() 646 if err != nil { 647 return "", nil, err 648 } 649 650 if inst.Revision.Unset() { 651 ts, err = snapstateRevert(st, inst.Snaps[0], flags) 652 } else { 653 ts, err = snapstateRevertToRevision(st, inst.Snaps[0], inst.Revision, flags) 654 } 655 if err != nil { 656 return "", nil, err 657 } 658 659 msg := fmt.Sprintf(i18n.G("Revert %q snap"), inst.Snaps[0]) 660 return msg, []*state.TaskSet{ts}, nil 661 } 662 663 func snapEnable(inst *snapInstruction, st *state.State) (string, []*state.TaskSet, error) { 664 if !inst.Revision.Unset() { 665 return "", nil, errors.New("enable takes no revision") 666 } 667 ts, err := snapstate.Enable(st, inst.Snaps[0]) 668 if err != nil { 669 return "", nil, err 670 } 671 672 msg := fmt.Sprintf(i18n.G("Enable %q snap"), inst.Snaps[0]) 673 return msg, []*state.TaskSet{ts}, nil 674 } 675 676 func snapDisable(inst *snapInstruction, st *state.State) (string, []*state.TaskSet, error) { 677 if !inst.Revision.Unset() { 678 return "", nil, errors.New("disable takes no revision") 679 } 680 ts, err := snapstate.Disable(st, inst.Snaps[0]) 681 if err != nil { 682 return "", nil, err 683 } 684 685 msg := fmt.Sprintf(i18n.G("Disable %q snap"), inst.Snaps[0]) 686 return msg, []*state.TaskSet{ts}, nil 687 } 688 689 func snapSwitch(inst *snapInstruction, st *state.State) (string, []*state.TaskSet, error) { 690 if !inst.Revision.Unset() { 691 return "", nil, errors.New("switch takes no revision") 692 } 693 ts, err := snapstateSwitch(st, inst.Snaps[0], inst.revnoOpts()) 694 if err != nil { 695 return "", nil, err 696 } 697 698 var msg string 699 switch { 700 case inst.LeaveCohort && inst.Channel != "": 701 msg = fmt.Sprintf(i18n.G("Switch %q snap to channel %q and away from cohort"), inst.Snaps[0], inst.Channel) 702 case inst.LeaveCohort: 703 msg = fmt.Sprintf(i18n.G("Switch %q snap away from cohort"), inst.Snaps[0]) 704 case inst.CohortKey == "" && inst.Channel != "": 705 msg = fmt.Sprintf(i18n.G("Switch %q snap to channel %q"), inst.Snaps[0], inst.Channel) 706 case inst.CohortKey != "" && inst.Channel == "": 707 msg = fmt.Sprintf(i18n.G("Switch %q snap to cohort %q"), inst.Snaps[0], strutil.ElliptLeft(inst.CohortKey, 10)) 708 default: 709 msg = fmt.Sprintf(i18n.G("Switch %q snap to channel %q and cohort %q"), inst.Snaps[0], inst.Channel, strutil.ElliptLeft(inst.CohortKey, 10)) 710 } 711 return msg, []*state.TaskSet{ts}, nil 712 } 713 714 func snapshotMany(inst *snapInstruction, st *state.State) (*snapInstructionResult, error) { 715 setID, snapshotted, ts, err := snapshotSave(st, inst.Snaps, inst.Users) 716 if err != nil { 717 return nil, err 718 } 719 720 var msg string 721 if len(inst.Snaps) == 0 { 722 msg = i18n.G("Snapshot all snaps") 723 } else { 724 // TRANSLATORS: the %s is a comma-separated list of quoted snap names 725 msg = fmt.Sprintf(i18n.G("Snapshot snaps %s"), strutil.Quoted(inst.Snaps)) 726 } 727 728 return &snapInstructionResult{ 729 Summary: msg, 730 Affected: snapshotted, 731 Tasksets: []*state.TaskSet{ts}, 732 Result: map[string]interface{}{"set-id": setID}, 733 }, nil 734 } 735 736 type snapActionFunc func(*snapInstruction, *state.State) (string, []*state.TaskSet, error) 737 type snapsActionFunc func(*snapInstruction, *state.State) (*snapInstructionResult, error) 738 739 var snapInstructionDispTable = map[string]snapActionFunc{ 740 "install": snapInstall, 741 "refresh": snapUpdate, 742 "remove": snapRemove, 743 "revert": snapRevert, 744 "enable": snapEnable, 745 "disable": snapDisable, 746 "switch": snapSwitch, 747 } 748 749 func (inst *snapInstruction) dispatch() snapActionFunc { 750 if len(inst.Snaps) != 1 { 751 logger.Panicf("dispatch only handles single-snap ops; got %d", len(inst.Snaps)) 752 } 753 return snapInstructionDispTable[inst.Action] 754 } 755 756 func (inst *snapInstruction) dispatchForMany() (op snapsActionFunc) { 757 switch inst.Action { 758 case "refresh": 759 op = snapUpdateMany 760 case "install": 761 op = snapInstallMany 762 case "remove": 763 op = snapRemoveMany 764 case "snapshot": 765 op = snapshotMany 766 } 767 return op 768 } 769 770 func (inst *snapInstruction) errToResponse(err error) Response { 771 if len(inst.Snaps) == 0 { 772 return errToResponse(err, nil, BadRequest, "cannot %s: %v", inst.Action) 773 } 774 775 return errToResponse(err, inst.Snaps, BadRequest, "cannot %s %s: %v", inst.Action, strutil.Quoted(inst.Snaps)) 776 } 777 778 func postSnap(c *Command, r *http.Request, user *auth.UserState) Response { 779 route := c.d.router.Get(stateChangeCmd.Path) 780 if route == nil { 781 return InternalError("cannot find route for change") 782 } 783 784 decoder := json.NewDecoder(r.Body) 785 var inst snapInstruction 786 if err := decoder.Decode(&inst); err != nil { 787 return BadRequest("cannot decode request body into snap instruction: %v", err) 788 } 789 inst.ctx = r.Context() 790 791 state := c.d.overlord.State() 792 state.Lock() 793 defer state.Unlock() 794 795 if user != nil { 796 inst.userID = user.ID 797 } 798 799 vars := muxVars(r) 800 inst.Snaps = []string{vars["name"]} 801 802 if err := inst.validate(); err != nil { 803 return BadRequest("%s", err) 804 } 805 806 impl := inst.dispatch() 807 if impl == nil { 808 return BadRequest("unknown action %s", inst.Action) 809 } 810 811 msg, tsets, err := impl(&inst, state) 812 if err != nil { 813 return inst.errToResponse(err) 814 } 815 816 chg := newChange(state, inst.Action+"-snap", msg, tsets, inst.Snaps) 817 818 ensureStateSoon(state) 819 820 return AsyncResponse(nil, &Meta{Change: chg.ID()}) 821 } 822 823 func newChange(st *state.State, kind, summary string, tsets []*state.TaskSet, snapNames []string) *state.Change { 824 chg := st.NewChange(kind, summary) 825 for _, ts := range tsets { 826 chg.AddAll(ts) 827 } 828 if snapNames != nil { 829 chg.Set("snap-names", snapNames) 830 } 831 return chg 832 } 833 834 const maxReadBuflen = 1024 * 1024 835 836 func trySnap(st *state.State, r *http.Request, user *auth.UserState, trydir string, flags snapstate.Flags) Response { 837 st.Lock() 838 defer st.Unlock() 839 840 if !filepath.IsAbs(trydir) { 841 return BadRequest("cannot try %q: need an absolute path", trydir) 842 } 843 if !osutil.IsDirectory(trydir) { 844 return BadRequest("cannot try %q: not a snap directory", trydir) 845 } 846 847 // the developer asked us to do this with a trusted snap dir 848 info, err := unsafeReadSnapInfo(trydir) 849 if _, ok := err.(snap.NotSnapError); ok { 850 return SyncResponse(&resp{ 851 Type: ResponseTypeError, 852 Result: &errorResult{ 853 Message: err.Error(), 854 Kind: client.ErrorKindNotSnap, 855 }, 856 Status: 400, 857 }, nil) 858 } 859 if err != nil { 860 return BadRequest("cannot read snap info for %s: %s", trydir, err) 861 } 862 863 tset, err := snapstateTryPath(st, info.InstanceName(), trydir, flags) 864 if err != nil { 865 return errToResponse(err, []string{info.InstanceName()}, BadRequest, "cannot try %s: %s", trydir) 866 } 867 868 msg := fmt.Sprintf(i18n.G("Try %q snap from %s"), info.InstanceName(), trydir) 869 chg := newChange(st, "try-snap", msg, []*state.TaskSet{tset}, []string{info.InstanceName()}) 870 chg.Set("api-data", map[string]string{"snap-name": info.InstanceName()}) 871 872 ensureStateSoon(st) 873 874 return AsyncResponse(nil, &Meta{Change: chg.ID()}) 875 } 876 877 func isTrue(form *multipart.Form, key string) bool { 878 value := form.Value[key] 879 if len(value) == 0 { 880 return false 881 } 882 b, err := strconv.ParseBool(value[0]) 883 if err != nil { 884 return false 885 } 886 887 return b 888 } 889 890 func snapsOp(c *Command, r *http.Request, user *auth.UserState) Response { 891 route := c.d.router.Get(stateChangeCmd.Path) 892 if route == nil { 893 return InternalError("cannot find route for change") 894 } 895 896 decoder := json.NewDecoder(r.Body) 897 var inst snapInstruction 898 if err := decoder.Decode(&inst); err != nil { 899 return BadRequest("cannot decode request body into snap instruction: %v", err) 900 } 901 902 // TODO: inst.Amend, etc? 903 if inst.Channel != "" || !inst.Revision.Unset() || inst.DevMode || inst.JailMode || inst.CohortKey != "" || inst.LeaveCohort || inst.Purge { 904 return BadRequest("unsupported option provided for multi-snap operation") 905 } 906 if err := inst.validate(); err != nil { 907 return BadRequest("%v", err) 908 } 909 910 st := c.d.overlord.State() 911 st.Lock() 912 defer st.Unlock() 913 914 if user != nil { 915 inst.userID = user.ID 916 } 917 918 op := inst.dispatchForMany() 919 if op == nil { 920 return BadRequest("unsupported multi-snap operation %q", inst.Action) 921 } 922 res, err := op(&inst, st) 923 if err != nil { 924 return inst.errToResponse(err) 925 } 926 927 var chg *state.Change 928 if len(res.Tasksets) == 0 { 929 chg = st.NewChange(inst.Action+"-snap", res.Summary) 930 chg.SetStatus(state.DoneStatus) 931 } else { 932 chg = newChange(st, inst.Action+"-snap", res.Summary, res.Tasksets, res.Affected) 933 ensureStateSoon(st) 934 } 935 936 chg.Set("api-data", map[string]interface{}{"snap-names": res.Affected}) 937 938 return AsyncResponse(res.Result, &Meta{Change: chg.ID()}) 939 } 940 941 func postSnaps(c *Command, r *http.Request, user *auth.UserState) Response { 942 contentType := r.Header.Get("Content-Type") 943 944 mediaType, params, err := mime.ParseMediaType(contentType) 945 if err != nil { 946 return BadRequest("cannot parse content type: %v", err) 947 } 948 949 if mediaType == "application/json" { 950 charset := strings.ToUpper(params["charset"]) 951 if charset != "" && charset != "UTF-8" { 952 return BadRequest("unknown charset in content type: %s", contentType) 953 } 954 return snapsOp(c, r, user) 955 } 956 957 if !strings.HasPrefix(contentType, "multipart/") { 958 return BadRequest("unknown content type: %s", contentType) 959 } 960 961 route := c.d.router.Get(stateChangeCmd.Path) 962 if route == nil { 963 return InternalError("cannot find route for change") 964 } 965 966 // POSTs to sideload snaps must be a multipart/form-data file upload. 967 form, err := multipart.NewReader(r.Body, params["boundary"]).ReadForm(maxReadBuflen) 968 if err != nil { 969 return BadRequest("cannot read POST form: %v", err) 970 } 971 972 dangerousOK := isTrue(form, "dangerous") 973 flags, err := modeFlags(isTrue(form, "devmode"), isTrue(form, "jailmode"), isTrue(form, "classic")) 974 if err != nil { 975 return BadRequest(err.Error()) 976 } 977 978 if len(form.Value["action"]) > 0 && form.Value["action"][0] == "try" { 979 if len(form.Value["snap-path"]) == 0 { 980 return BadRequest("need 'snap-path' value in form") 981 } 982 return trySnap(c.d.overlord.State(), r, user, form.Value["snap-path"][0], flags) 983 } 984 flags.RemoveSnapPath = true 985 986 flags.Unaliased = isTrue(form, "unaliased") 987 flags.IgnoreRunning = isTrue(form, "ignore-running") 988 989 // find the file for the "snap" form field 990 var snapBody multipart.File 991 var origPath string 992 out: 993 for name, fheaders := range form.File { 994 if name != "snap" { 995 continue 996 } 997 for _, fheader := range fheaders { 998 snapBody, err = fheader.Open() 999 origPath = fheader.Filename 1000 if err != nil { 1001 return BadRequest(`cannot open uploaded "snap" file: %v`, err) 1002 } 1003 defer snapBody.Close() 1004 1005 break out 1006 } 1007 } 1008 defer form.RemoveAll() 1009 1010 if snapBody == nil { 1011 return BadRequest(`cannot find "snap" file field in provided multipart/form-data payload`) 1012 } 1013 1014 // we are in charge of the tempfile life cycle until we hand it off to the change 1015 changeTriggered := false 1016 // if you change this prefix, look for it in the tests 1017 // also see localInstallCleanup in snapstate/snapmgr.go 1018 tmpf, err := ioutil.TempFile(dirs.SnapBlobDir, dirs.LocalInstallBlobTempPrefix) 1019 if err != nil { 1020 return InternalError("cannot create temporary file: %v", err) 1021 } 1022 1023 tempPath := tmpf.Name() 1024 1025 defer func() { 1026 if !changeTriggered { 1027 os.Remove(tempPath) 1028 } 1029 }() 1030 1031 if _, err := io.Copy(tmpf, snapBody); err != nil { 1032 return InternalError("cannot copy request into temporary file: %v", err) 1033 } 1034 tmpf.Sync() 1035 1036 if len(form.Value["snap-path"]) > 0 { 1037 origPath = form.Value["snap-path"][0] 1038 } 1039 1040 var instanceName string 1041 1042 if len(form.Value["name"]) > 0 { 1043 // caller has specified desired instance name 1044 instanceName = form.Value["name"][0] 1045 if err := snap.ValidateInstanceName(instanceName); err != nil { 1046 return BadRequest(err.Error()) 1047 } 1048 } 1049 1050 st := c.d.overlord.State() 1051 st.Lock() 1052 defer st.Unlock() 1053 1054 var snapName string 1055 var sideInfo *snap.SideInfo 1056 1057 if !dangerousOK { 1058 si, err := snapasserts.DeriveSideInfo(tempPath, assertstate.DB(st)) 1059 switch { 1060 case err == nil: 1061 snapName = si.RealName 1062 sideInfo = si 1063 case asserts.IsNotFound(err): 1064 // with devmode we try to find assertions but it's ok 1065 // if they are not there (implies --dangerous) 1066 if !isTrue(form, "devmode") { 1067 msg := "cannot find signatures with metadata for snap" 1068 if origPath != "" { 1069 msg = fmt.Sprintf("%s %q", msg, origPath) 1070 } 1071 return BadRequest(msg) 1072 } 1073 // TODO: set a warning if devmode 1074 default: 1075 return BadRequest(err.Error()) 1076 } 1077 } 1078 1079 if snapName == "" { 1080 // potentially dangerous but dangerous or devmode params were set 1081 info, err := unsafeReadSnapInfo(tempPath) 1082 if err != nil { 1083 return BadRequest("cannot read snap file: %v", err) 1084 } 1085 snapName = info.SnapName() 1086 sideInfo = &snap.SideInfo{RealName: snapName} 1087 } 1088 1089 if instanceName != "" { 1090 requestedSnapName := snap.InstanceSnap(instanceName) 1091 if requestedSnapName != snapName { 1092 return BadRequest(fmt.Sprintf("instance name %q does not match snap name %q", instanceName, snapName)) 1093 } 1094 } else { 1095 instanceName = snapName 1096 } 1097 1098 msg := fmt.Sprintf(i18n.G("Install %q snap from file"), instanceName) 1099 if origPath != "" { 1100 msg = fmt.Sprintf(i18n.G("Install %q snap from file %q"), instanceName, origPath) 1101 } 1102 1103 tset, _, err := snapstateInstallPath(st, sideInfo, tempPath, instanceName, "", flags) 1104 if err != nil { 1105 return errToResponse(err, []string{snapName}, InternalError, "cannot install snap file: %v") 1106 } 1107 1108 chg := newChange(st, "install-snap", msg, []*state.TaskSet{tset}, []string{instanceName}) 1109 chg.Set("api-data", map[string]string{"snap-name": instanceName}) 1110 1111 ensureStateSoon(st) 1112 1113 // only when the unlock succeeds (as opposed to panicing) is the handoff done 1114 // but this is good enough 1115 changeTriggered = true 1116 1117 return AsyncResponse(nil, &Meta{Change: chg.ID()}) 1118 } 1119 1120 func unsafeReadSnapInfoImpl(snapPath string) (*snap.Info, error) { 1121 // Condider using DeriveSideInfo before falling back to this! 1122 snapf, err := snapfile.Open(snapPath) 1123 if err != nil { 1124 return nil, err 1125 } 1126 return snap.ReadInfoFromSnapFile(snapf, nil) 1127 } 1128 1129 var unsafeReadSnapInfo = unsafeReadSnapInfoImpl