gopkg.in/ubuntu-core/snappy.v0@v0.0.0-20210902073436-25a8614f10a6/overlord/snapstate/backend_test.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2016-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 snapstate_test 21 22 import ( 23 "context" 24 "errors" 25 "fmt" 26 "io" 27 "os" 28 "path/filepath" 29 "sort" 30 "strings" 31 "sync" 32 33 . "gopkg.in/check.v1" 34 35 "github.com/snapcore/snapd/boot" 36 "github.com/snapcore/snapd/cmd/snaplock/runinhibit" 37 "github.com/snapcore/snapd/osutil" 38 "github.com/snapcore/snapd/overlord/auth" 39 "github.com/snapcore/snapd/overlord/snapstate" 40 "github.com/snapcore/snapd/overlord/snapstate/backend" 41 "github.com/snapcore/snapd/overlord/state" 42 "github.com/snapcore/snapd/progress" 43 "github.com/snapcore/snapd/snap" 44 "github.com/snapcore/snapd/snap/snapfile" 45 "github.com/snapcore/snapd/store" 46 "github.com/snapcore/snapd/store/storetest" 47 "github.com/snapcore/snapd/strutil" 48 "github.com/snapcore/snapd/timings" 49 ) 50 51 type fakeOp struct { 52 op string 53 54 name string 55 path string 56 revno snap.Revision 57 sinfo snap.SideInfo 58 stype snap.Type 59 60 curSnaps []store.CurrentSnap 61 action store.SnapAction 62 63 old string 64 65 aliases []*backend.Alias 66 rmAliases []*backend.Alias 67 68 userID int 69 70 otherInstances bool 71 unlinkFirstInstallUndo bool 72 skipKernelExtraction bool 73 74 services []string 75 disabledServices []string 76 77 vitalityRank int 78 79 inhibitHint runinhibit.Hint 80 81 requireSnapdTooling bool 82 } 83 84 type fakeOps []fakeOp 85 86 func (ops fakeOps) MustFindOp(c *C, opName string) *fakeOp { 87 for _, op := range ops { 88 if op.op == opName { 89 return &op 90 } 91 } 92 c.Errorf("cannot find operation with op: %q, all ops: %v", opName, ops.Ops()) 93 c.FailNow() 94 return nil 95 } 96 97 func (ops fakeOps) Ops() []string { 98 opsOps := make([]string, len(ops)) 99 for i, op := range ops { 100 opsOps[i] = op.op 101 } 102 103 return opsOps 104 } 105 106 func (ops fakeOps) Count(op string) int { 107 n := 0 108 for i := range ops { 109 if ops[i].op == op { 110 n++ 111 } 112 } 113 return n 114 } 115 116 func (ops fakeOps) First(op string) *fakeOp { 117 for i := range ops { 118 if ops[i].op == op { 119 return &ops[i] 120 } 121 } 122 123 return nil 124 } 125 126 type fakeDownload struct { 127 name string 128 macaroon string 129 target string 130 opts *store.DownloadOptions 131 } 132 133 type byName []store.CurrentSnap 134 135 func (bna byName) Len() int { return len(bna) } 136 func (bna byName) Swap(i, j int) { bna[i], bna[j] = bna[j], bna[i] } 137 func (bna byName) Less(i, j int) bool { 138 return bna[i].InstanceName < bna[j].InstanceName 139 } 140 141 type byAction []*store.SnapAction 142 143 func (ba byAction) Len() int { return len(ba) } 144 func (ba byAction) Swap(i, j int) { ba[i], ba[j] = ba[j], ba[i] } 145 func (ba byAction) Less(i, j int) bool { 146 if ba[i].Action == ba[j].Action { 147 if ba[i].Action == "refresh" { 148 return ba[i].SnapID < ba[j].SnapID 149 } else { 150 return ba[i].InstanceName < ba[j].InstanceName 151 } 152 } 153 return ba[i].Action < ba[j].Action 154 } 155 156 type fakeStore struct { 157 storetest.Store 158 159 downloads []fakeDownload 160 refreshRevnos map[string]snap.Revision 161 fakeBackend *fakeSnappyBackend 162 fakeCurrentProgress int 163 fakeTotalProgress int 164 state *state.State 165 seenPrivacyKeys map[string]bool 166 } 167 168 func (f *fakeStore) pokeStateLock() { 169 // the store should be called without the state lock held. Try 170 // to acquire it. 171 f.state.Lock() 172 f.state.Unlock() 173 } 174 175 func (f *fakeStore) SnapInfo(ctx context.Context, spec store.SnapSpec, user *auth.UserState) (*snap.Info, error) { 176 f.pokeStateLock() 177 178 _, instanceKey := snap.SplitInstanceName(spec.Name) 179 if instanceKey != "" { 180 return nil, fmt.Errorf("internal error: unexpected instance name: %q", spec.Name) 181 } 182 sspec := snapSpec{ 183 Name: spec.Name, 184 } 185 info, err := f.snap(sspec, user) 186 187 userID := 0 188 if user != nil { 189 userID = user.ID 190 } 191 f.fakeBackend.appendOp(&fakeOp{op: "storesvc-snap", name: spec.Name, revno: info.Revision, userID: userID}) 192 193 return info, err 194 } 195 196 type snapSpec struct { 197 Name string 198 Channel string 199 Revision snap.Revision 200 Cohort string 201 } 202 203 func (f *fakeStore) snap(spec snapSpec, user *auth.UserState) (*snap.Info, error) { 204 if spec.Revision.Unset() { 205 switch { 206 case spec.Cohort != "": 207 spec.Revision = snap.R(666) 208 case spec.Channel == "channel-for-7": 209 spec.Revision = snap.R(7) 210 default: 211 spec.Revision = snap.R(11) 212 } 213 } 214 215 confinement := snap.StrictConfinement 216 217 typ := snap.TypeApp 218 epoch := snap.E("1*") 219 switch spec.Name { 220 case "core", "core16", "ubuntu-core", "some-core": 221 typ = snap.TypeOS 222 case "some-base", "other-base", "some-other-base", "yet-another-base", "core18": 223 typ = snap.TypeBase 224 case "some-kernel": 225 typ = snap.TypeKernel 226 case "some-gadget", "brand-gadget": 227 typ = snap.TypeGadget 228 case "some-snapd": 229 typ = snap.TypeSnapd 230 case "snapd": 231 typ = snap.TypeSnapd 232 case "some-snap-now-classic": 233 confinement = "classic" 234 case "some-epoch-snap": 235 epoch = snap.E("42") 236 } 237 238 if spec.Name == "snap-unknown" { 239 return nil, store.ErrSnapNotFound 240 } 241 242 info := &snap.Info{ 243 Architectures: []string{"all"}, 244 SideInfo: snap.SideInfo{ 245 RealName: spec.Name, 246 Channel: spec.Channel, 247 SnapID: spec.Name + "-id", 248 Revision: spec.Revision, 249 }, 250 Version: spec.Name, 251 DownloadInfo: snap.DownloadInfo{ 252 DownloadURL: "https://some-server.com/some/path.snap", 253 Size: 5, 254 }, 255 Confinement: confinement, 256 SnapType: typ, 257 Epoch: epoch, 258 } 259 switch spec.Channel { 260 case "channel-no-revision": 261 return nil, &store.RevisionNotAvailableError{} 262 case "channel-for-devmode": 263 info.Confinement = snap.DevModeConfinement 264 case "channel-for-classic": 265 info.Confinement = snap.ClassicConfinement 266 case "channel-for-paid": 267 info.Prices = map[string]float64{"USD": 0.77} 268 info.SideInfo.Paid = true 269 case "channel-for-private": 270 info.SideInfo.Private = true 271 case "channel-for-layout": 272 info.Layout = map[string]*snap.Layout{ 273 "/usr": { 274 Snap: info, 275 Path: "/usr", 276 Symlink: "$SNAP/usr", 277 }, 278 } 279 case "channel-for-user-daemon": 280 info.Apps = map[string]*snap.AppInfo{ 281 "user-daemon": { 282 Snap: info, 283 Name: "user-daemon", 284 Daemon: "simple", 285 DaemonScope: "user", 286 }, 287 } 288 case "channel-for-dbus-activation": 289 slot := &snap.SlotInfo{ 290 Snap: info, 291 Name: "dbus-slot", 292 Interface: "dbus", 293 Attrs: map[string]interface{}{ 294 "bus": "system", 295 "name": "org.example.Foo", 296 }, 297 Apps: make(map[string]*snap.AppInfo), 298 } 299 info.Apps = map[string]*snap.AppInfo{ 300 "dbus-daemon": { 301 Snap: info, 302 Name: "dbus-daemon", 303 Daemon: "simple", 304 DaemonScope: snap.SystemDaemon, 305 ActivatesOn: []*snap.SlotInfo{slot}, 306 Slots: map[string]*snap.SlotInfo{ 307 slot.Name: slot, 308 }, 309 }, 310 } 311 slot.Apps["dbus-daemon"] = info.Apps["dbus-daemon"] 312 } 313 314 return info, nil 315 } 316 317 type refreshCand struct { 318 snapID string 319 channel string 320 revision snap.Revision 321 block []snap.Revision 322 ignoreValidation bool 323 } 324 325 func (f *fakeStore) lookupRefresh(cand refreshCand) (*snap.Info, error) { 326 var name string 327 328 typ := snap.TypeApp 329 epoch := snap.E("1*") 330 switch cand.snapID { 331 case "": 332 panic("store refresh APIs expect snap-ids") 333 case "other-snap-id": 334 return nil, store.ErrNoUpdateAvailable 335 case "fakestore-please-error-on-refresh": 336 return nil, fmt.Errorf("failing as requested") 337 case "services-snap-id": 338 name = "services-snap" 339 case "some-snap-id": 340 name = "some-snap" 341 case "some-other-snap-id": 342 name = "some-other-snap" 343 case "some-epoch-snap-id": 344 name = "some-epoch-snap" 345 epoch = snap.E("42") 346 case "some-snap-now-classic-id": 347 name = "some-snap-now-classic" 348 case "some-snap-was-classic-id": 349 name = "some-snap-was-classic" 350 case "core-snap-id": 351 name = "core" 352 typ = snap.TypeOS 353 case "core18-snap-id": 354 name = "core18" 355 typ = snap.TypeBase 356 case "snap-with-snapd-control-id": 357 name = "snap-with-snapd-control" 358 case "producer-id": 359 name = "producer" 360 case "consumer-id": 361 name = "consumer" 362 case "some-base-id": 363 name = "some-base" 364 typ = snap.TypeBase 365 case "snap-content-plug-id": 366 name = "snap-content-plug" 367 case "snap-content-slot-id": 368 name = "snap-content-slot" 369 case "snapd-snap-id": 370 name = "snapd" 371 typ = snap.TypeSnapd 372 case "kernel-id": 373 name = "kernel" 374 typ = snap.TypeKernel 375 case "brand-gadget-id": 376 name = "brand-gadget" 377 typ = snap.TypeGadget 378 case "alias-snap-id": 379 name = "snap-id" 380 default: 381 panic(fmt.Sprintf("refresh: unknown snap-id: %s", cand.snapID)) 382 } 383 384 revno := snap.R(11) 385 if r := f.refreshRevnos[cand.snapID]; !r.Unset() { 386 revno = r 387 } 388 confinement := snap.StrictConfinement 389 switch cand.channel { 390 case "channel-for-7/stable": 391 revno = snap.R(7) 392 case "channel-for-classic/stable": 393 confinement = snap.ClassicConfinement 394 case "channel-for-devmode/stable": 395 confinement = snap.DevModeConfinement 396 } 397 if name == "some-snap-now-classic" { 398 confinement = "classic" 399 } 400 401 info := &snap.Info{ 402 SnapType: typ, 403 SideInfo: snap.SideInfo{ 404 RealName: name, 405 Channel: cand.channel, 406 SnapID: cand.snapID, 407 Revision: revno, 408 }, 409 Version: name, 410 DownloadInfo: snap.DownloadInfo{ 411 DownloadURL: "https://some-server.com/some/path.snap", 412 }, 413 Confinement: confinement, 414 Architectures: []string{"all"}, 415 Epoch: epoch, 416 } 417 switch cand.channel { 418 case "channel-for-layout/stable": 419 info.Layout = map[string]*snap.Layout{ 420 "/usr": { 421 Snap: info, 422 Path: "/usr", 423 Symlink: "$SNAP/usr", 424 }, 425 } 426 case "channel-for-base/stable": 427 info.Base = "some-base" 428 } 429 430 var hit snap.Revision 431 if cand.revision != revno { 432 hit = revno 433 } 434 for _, blocked := range cand.block { 435 if blocked == revno { 436 hit = snap.Revision{} 437 break 438 } 439 } 440 441 if !hit.Unset() { 442 return info, nil 443 } 444 445 return nil, store.ErrNoUpdateAvailable 446 } 447 448 func (f *fakeStore) SnapAction(ctx context.Context, currentSnaps []*store.CurrentSnap, actions []*store.SnapAction, assertQuery store.AssertionQuery, user *auth.UserState, opts *store.RefreshOptions) ([]store.SnapActionResult, []store.AssertionResult, error) { 449 if ctx == nil { 450 panic("context required") 451 } 452 f.pokeStateLock() 453 if assertQuery != nil { 454 panic("no assertion query support") 455 } 456 457 if len(currentSnaps) == 0 && len(actions) == 0 { 458 return nil, nil, nil 459 } 460 if len(actions) > 4 { 461 panic("fake SnapAction unexpectedly called with more than 3 actions") 462 } 463 464 curByInstanceName := make(map[string]*store.CurrentSnap, len(currentSnaps)) 465 curSnaps := make(byName, len(currentSnaps)) 466 for i, cur := range currentSnaps { 467 if cur.InstanceName == "" || cur.SnapID == "" || cur.Revision.Unset() { 468 return nil, nil, fmt.Errorf("internal error: incomplete current snap info") 469 } 470 curByInstanceName[cur.InstanceName] = cur 471 curSnaps[i] = *cur 472 } 473 sort.Sort(curSnaps) 474 475 userID := 0 476 if user != nil { 477 userID = user.ID 478 } 479 if len(curSnaps) == 0 { 480 curSnaps = nil 481 } 482 f.fakeBackend.appendOp(&fakeOp{ 483 op: "storesvc-snap-action", 484 curSnaps: curSnaps, 485 userID: userID, 486 }) 487 488 if f.seenPrivacyKeys == nil { 489 // so that checks don't topple over this being uninitialized 490 f.seenPrivacyKeys = make(map[string]bool) 491 } 492 if opts != nil && opts.PrivacyKey != "" { 493 f.seenPrivacyKeys[opts.PrivacyKey] = true 494 } 495 496 sorted := make(byAction, len(actions)) 497 copy(sorted, actions) 498 sort.Sort(sorted) 499 500 refreshErrors := make(map[string]error) 501 installErrors := make(map[string]error) 502 var res []store.SnapActionResult 503 for _, a := range sorted { 504 if a.Action != "install" && a.Action != "refresh" { 505 panic("not supported") 506 } 507 if a.InstanceName == "" { 508 return nil, nil, fmt.Errorf("internal error: action without instance name") 509 } 510 511 snapName, instanceKey := snap.SplitInstanceName(a.InstanceName) 512 513 if a.Action == "install" { 514 spec := snapSpec{ 515 Name: snapName, 516 Channel: a.Channel, 517 Revision: a.Revision, 518 Cohort: a.CohortKey, 519 } 520 info, err := f.snap(spec, user) 521 if err != nil { 522 installErrors[a.InstanceName] = err 523 continue 524 } 525 f.fakeBackend.appendOp(&fakeOp{ 526 op: "storesvc-snap-action:action", 527 action: *a, 528 revno: info.Revision, 529 userID: userID, 530 }) 531 if !a.Revision.Unset() { 532 info.Channel = "" 533 } 534 info.InstanceKey = instanceKey 535 sar := store.SnapActionResult{Info: info} 536 if strings.HasSuffix(snapName, "-with-default-track") && strutil.ListContains([]string{"stable", "candidate", "beta", "edge"}, a.Channel) { 537 sar.RedirectChannel = "2.0/" + a.Channel 538 } 539 res = append(res, sar) 540 continue 541 } 542 543 // refresh 544 545 cur := curByInstanceName[a.InstanceName] 546 if cur == nil { 547 return nil, nil, fmt.Errorf("internal error: no matching current snap for %q", a.InstanceName) 548 } 549 channel := a.Channel 550 if channel == "" { 551 channel = cur.TrackingChannel 552 } 553 ignoreValidation := cur.IgnoreValidation 554 if a.Flags&store.SnapActionIgnoreValidation != 0 { 555 ignoreValidation = true 556 } else if a.Flags&store.SnapActionEnforceValidation != 0 { 557 ignoreValidation = false 558 } 559 cand := refreshCand{ 560 snapID: a.SnapID, 561 channel: channel, 562 revision: cur.Revision, 563 block: cur.Block, 564 ignoreValidation: ignoreValidation, 565 } 566 info, err := f.lookupRefresh(cand) 567 var hit snap.Revision 568 if info != nil { 569 if !a.Revision.Unset() { 570 info.Revision = a.Revision 571 } 572 hit = info.Revision 573 } 574 f.fakeBackend.ops = append(f.fakeBackend.ops, fakeOp{ 575 op: "storesvc-snap-action:action", 576 action: *a, 577 revno: hit, 578 userID: userID, 579 }) 580 if err == store.ErrNoUpdateAvailable { 581 refreshErrors[cur.InstanceName] = err 582 continue 583 } 584 if err != nil { 585 return nil, nil, err 586 } 587 if !a.Revision.Unset() { 588 info.Channel = "" 589 } 590 info.InstanceKey = instanceKey 591 res = append(res, store.SnapActionResult{Info: info}) 592 } 593 594 if len(refreshErrors)+len(installErrors) > 0 || len(res) == 0 { 595 if len(refreshErrors) == 0 { 596 refreshErrors = nil 597 } 598 if len(installErrors) == 0 { 599 installErrors = nil 600 } 601 return res, nil, &store.SnapActionError{ 602 NoResults: len(refreshErrors)+len(installErrors)+len(res) == 0, 603 Refresh: refreshErrors, 604 Install: installErrors, 605 } 606 } 607 608 return res, nil, nil 609 } 610 611 func (f *fakeStore) SuggestedCurrency() string { 612 f.pokeStateLock() 613 614 return "XTS" 615 } 616 617 func (f *fakeStore) Download(ctx context.Context, name, targetFn string, snapInfo *snap.DownloadInfo, pb progress.Meter, user *auth.UserState, dlOpts *store.DownloadOptions) error { 618 f.pokeStateLock() 619 620 if _, key := snap.SplitInstanceName(name); key != "" { 621 return fmt.Errorf("internal error: unsupported download with instance name %q", name) 622 } 623 var macaroon string 624 if user != nil { 625 macaroon = user.StoreMacaroon 626 } 627 // only add the options if they contain anything interesting 628 if *dlOpts == (store.DownloadOptions{}) { 629 dlOpts = nil 630 } 631 f.downloads = append(f.downloads, fakeDownload{ 632 macaroon: macaroon, 633 name: name, 634 target: targetFn, 635 opts: dlOpts, 636 }) 637 f.fakeBackend.appendOp(&fakeOp{op: "storesvc-download", name: name}) 638 639 pb.SetTotal(float64(f.fakeTotalProgress)) 640 pb.Set(float64(f.fakeCurrentProgress)) 641 642 return nil 643 } 644 645 func (f *fakeStore) WriteCatalogs(ctx context.Context, _ io.Writer, _ store.SnapAdder) error { 646 if ctx == nil { 647 panic("context required") 648 } 649 f.pokeStateLock() 650 651 f.fakeBackend.appendOp(&fakeOp{ 652 op: "x-commands", 653 }) 654 655 return nil 656 } 657 658 func (f *fakeStore) Sections(ctx context.Context, _ *auth.UserState) ([]string, error) { 659 if ctx == nil { 660 panic("context required") 661 } 662 f.pokeStateLock() 663 664 f.fakeBackend.appendOp(&fakeOp{ 665 op: "x-sections", 666 }) 667 668 return nil, nil 669 } 670 671 type fakeSnappyBackend struct { 672 ops fakeOps 673 mu sync.Mutex 674 675 linkSnapWaitCh chan int 676 linkSnapWaitTrigger string 677 linkSnapFailTrigger string 678 linkSnapMaybeReboot bool 679 680 copySnapDataFailTrigger string 681 emptyContainer snap.Container 682 683 servicesCurrentlyDisabled []string 684 685 lockDir string 686 687 // TODO cleanup triggers above 688 maybeInjectErr func(*fakeOp) error 689 } 690 691 func (f *fakeSnappyBackend) maybeErrForLastOp() error { 692 if f.maybeInjectErr == nil { 693 return nil 694 } 695 if len(f.ops) == 0 { 696 return nil 697 } 698 return f.maybeInjectErr(&f.ops[len(f.ops)-1]) 699 } 700 701 func (f *fakeSnappyBackend) OpenSnapFile(snapFilePath string, si *snap.SideInfo) (*snap.Info, snap.Container, error) { 702 op := fakeOp{ 703 op: "open-snap-file", 704 path: snapFilePath, 705 } 706 707 if si != nil { 708 op.sinfo = *si 709 } 710 711 var info *snap.Info 712 if !osutil.IsDirectory(snapFilePath) { 713 name := filepath.Base(snapFilePath) 714 split := strings.Split(name, "_") 715 if len(split) >= 2 { 716 // <snap>_<rev>.snap 717 // <snap>_<instance-key>_<rev>.snap 718 name = split[0] 719 } 720 721 info = &snap.Info{SuggestedName: name, Architectures: []string{"all"}} 722 if name == "some-snap-now-classic" { 723 info.Confinement = "classic" 724 } 725 if name == "some-epoch-snap" { 726 info.Epoch = snap.E("42") 727 } else { 728 info.Epoch = snap.E("1*") 729 } 730 } else { 731 // for snap try only 732 snapf, err := snapfile.Open(snapFilePath) 733 if err != nil { 734 return nil, nil, err 735 } 736 737 info, err = snap.ReadInfoFromSnapFile(snapf, si) 738 if err != nil { 739 return nil, nil, err 740 } 741 } 742 743 if info == nil { 744 return nil, nil, fmt.Errorf("internal error: no mocked snap for %q", snapFilePath) 745 } 746 f.appendOp(&op) 747 return info, f.emptyContainer, nil 748 } 749 750 // XXX: this is now something that is overridden by tests that need a 751 // different service setup so it should be configurable and part 752 // of the fakeSnappyBackend? 753 var servicesSnapYaml = `name: services-snap 754 apps: 755 svc1: 756 daemon: simple 757 before: [svc3] 758 svc2: 759 daemon: simple 760 after: [svc1] 761 svc3: 762 daemon: simple 763 before: [svc2] 764 ` 765 766 func (f *fakeSnappyBackend) SetupSnap(snapFilePath, instanceName string, si *snap.SideInfo, dev boot.Device, opts *backend.SetupSnapOptions, p progress.Meter) (snap.Type, *backend.InstallRecord, error) { 767 p.Notify("setup-snap") 768 revno := snap.R(0) 769 if si != nil { 770 revno = si.Revision 771 } 772 f.appendOp(&fakeOp{ 773 op: "setup-snap", 774 name: instanceName, 775 path: snapFilePath, 776 revno: revno, 777 778 skipKernelExtraction: opts != nil && opts.SkipKernelExtraction, 779 }) 780 snapType := snap.TypeApp 781 switch si.RealName { 782 case "core": 783 snapType = snap.TypeOS 784 case "gadget": 785 snapType = snap.TypeGadget 786 } 787 if instanceName == "borken-in-setup" { 788 return snapType, nil, fmt.Errorf("cannot install snap %q", instanceName) 789 } 790 if instanceName == "some-snap-no-install-record" { 791 return snapType, nil, nil 792 } 793 return snapType, &backend.InstallRecord{}, nil 794 } 795 796 func (f *fakeSnappyBackend) ReadInfo(name string, si *snap.SideInfo) (*snap.Info, error) { 797 if name == "borken" && si.Revision == snap.R(2) { 798 return nil, errors.New(`cannot read info for "borken" snap`) 799 } 800 if name == "borken-undo-setup" && si.Revision == snap.R(2) { 801 return nil, errors.New(`cannot read info for "borken-undo-setup" snap`) 802 } 803 if name == "not-there" && si.Revision == snap.R(2) { 804 return nil, &snap.NotFoundError{Snap: name, Revision: si.Revision} 805 } 806 snapName, instanceKey := snap.SplitInstanceName(name) 807 // naive emulation for now, always works 808 info := &snap.Info{ 809 SuggestedName: snapName, 810 SideInfo: *si, 811 Architectures: []string{"all"}, 812 SnapType: snap.TypeApp, 813 Epoch: snap.E("1*"), 814 } 815 if strings.Contains(snapName, "alias-snap") { 816 // only for the switch below 817 snapName = "alias-snap" 818 } 819 switch snapName { 820 case "snap-with-empty-epoch": 821 info.Epoch = snap.Epoch{} 822 case "some-epoch-snap": 823 info.Epoch = snap.E("13") 824 case "some-snap-with-base": 825 info.Base = "core18" 826 case "gadget", "brand-gadget": 827 info.SnapType = snap.TypeGadget 828 case "core": 829 info.SnapType = snap.TypeOS 830 case "snapd": 831 info.SnapType = snap.TypeSnapd 832 case "services-snap": 833 var err error 834 // fix services after/before so that there is only one solution 835 // to dependency ordering 836 info, err = snap.InfoFromSnapYaml([]byte(servicesSnapYaml)) 837 if err != nil { 838 panic(err) 839 } 840 info.SideInfo = *si 841 case "alias-snap": 842 var err error 843 info, err = snap.InfoFromSnapYaml([]byte(`name: alias-snap 844 apps: 845 cmd1: 846 cmd2: 847 cmd3: 848 cmd4: 849 cmd5: 850 cmddaemon: 851 daemon: simple 852 `)) 853 if err != nil { 854 panic(err) 855 } 856 info.SideInfo = *si 857 } 858 859 info.InstanceKey = instanceKey 860 return info, nil 861 } 862 863 func (f *fakeSnappyBackend) ClearTrashedData(si *snap.Info) { 864 f.appendOp(&fakeOp{ 865 op: "cleanup-trash", 866 name: si.InstanceName(), 867 revno: si.Revision, 868 }) 869 } 870 871 func (f *fakeSnappyBackend) StoreInfo(st *state.State, name, channel string, userID int, flags snapstate.Flags) (*snap.Info, error) { 872 return f.ReadInfo(name, &snap.SideInfo{ 873 RealName: name, 874 }) 875 } 876 877 func (f *fakeSnappyBackend) CopySnapData(newInfo, oldInfo *snap.Info, p progress.Meter) error { 878 p.Notify("copy-data") 879 old := "<no-old>" 880 if oldInfo != nil { 881 old = oldInfo.MountDir() 882 } 883 884 if newInfo.MountDir() == f.copySnapDataFailTrigger { 885 f.appendOp(&fakeOp{ 886 op: "copy-data.failed", 887 path: newInfo.MountDir(), 888 old: old, 889 }) 890 return errors.New("fail") 891 } 892 893 f.appendOp(&fakeOp{ 894 op: "copy-data", 895 path: newInfo.MountDir(), 896 old: old, 897 }) 898 return f.maybeErrForLastOp() 899 } 900 901 func (f *fakeSnappyBackend) LinkSnap(info *snap.Info, dev boot.Device, linkCtx backend.LinkContext, tm timings.Measurer) (rebootRequired bool, err error) { 902 if info.MountDir() == f.linkSnapWaitTrigger { 903 f.linkSnapWaitCh <- 1 904 <-f.linkSnapWaitCh 905 } 906 907 vitalityRank := 0 908 if linkCtx.ServiceOptions != nil { 909 vitalityRank = linkCtx.ServiceOptions.VitalityRank 910 } 911 912 op := fakeOp{ 913 op: "link-snap", 914 path: info.MountDir(), 915 916 vitalityRank: vitalityRank, 917 requireSnapdTooling: linkCtx.RequireMountedSnapdSnap, 918 } 919 920 if info.MountDir() == f.linkSnapFailTrigger { 921 op.op = "link-snap.failed" 922 f.ops = append(f.ops, op) 923 return false, errors.New("fail") 924 } 925 926 f.appendOp(&op) 927 928 reboot := false 929 if f.linkSnapMaybeReboot { 930 reboot = info.InstanceName() == dev.Base() 931 } 932 933 return reboot, nil 934 } 935 936 func svcSnapMountDir(svcs []*snap.AppInfo) string { 937 if len(svcs) == 0 { 938 return "<no services>" 939 } 940 if svcs[0].Snap == nil { 941 return "<snapless service>" 942 } 943 return svcs[0].Snap.MountDir() 944 } 945 946 func (f *fakeSnappyBackend) StartServices(svcs []*snap.AppInfo, disabledSvcs []string, meter progress.Meter, tm timings.Measurer) error { 947 services := make([]string, 0, len(svcs)) 948 for _, svc := range svcs { 949 services = append(services, svc.Name) 950 } 951 op := fakeOp{ 952 op: "start-snap-services", 953 path: svcSnapMountDir(svcs), 954 services: services, 955 } 956 // only add the services to the op if there's something to add 957 if len(disabledSvcs) != 0 { 958 op.disabledServices = disabledSvcs 959 } 960 f.appendOp(&op) 961 return f.maybeErrForLastOp() 962 } 963 964 func (f *fakeSnappyBackend) StopServices(svcs []*snap.AppInfo, reason snap.ServiceStopReason, meter progress.Meter, tm timings.Measurer) error { 965 f.appendOp(&fakeOp{ 966 op: fmt.Sprintf("stop-snap-services:%s", reason), 967 path: svcSnapMountDir(svcs), 968 }) 969 return f.maybeErrForLastOp() 970 } 971 972 func (f *fakeSnappyBackend) ServicesEnableState(info *snap.Info, meter progress.Meter) (map[string]bool, error) { 973 // return the disabled services as disabled and nothing else 974 m := make(map[string]bool) 975 for _, svc := range f.servicesCurrentlyDisabled { 976 m[svc] = false 977 } 978 979 f.appendOp(&fakeOp{ 980 op: "current-snap-service-states", 981 disabledServices: f.servicesCurrentlyDisabled, 982 }) 983 984 return m, f.maybeErrForLastOp() 985 } 986 987 func (f *fakeSnappyBackend) QueryDisabledServices(info *snap.Info, meter progress.Meter) ([]string, error) { 988 var l []string 989 990 m, err := f.ServicesEnableState(info, meter) 991 if err != nil { 992 return nil, err 993 } 994 for name, enabled := range m { 995 if !enabled { 996 l = append(l, name) 997 } 998 } 999 1000 // XXX: add a fakeOp here? 1001 1002 return l, nil 1003 } 1004 1005 func (f *fakeSnappyBackend) UndoSetupSnap(s snap.PlaceInfo, typ snap.Type, installRecord *backend.InstallRecord, dev boot.Device, p progress.Meter) error { 1006 p.Notify("setup-snap") 1007 f.appendOp(&fakeOp{ 1008 op: "undo-setup-snap", 1009 name: s.InstanceName(), 1010 path: s.MountDir(), 1011 stype: typ, 1012 }) 1013 if s.InstanceName() == "borken-undo-setup" { 1014 return errors.New(`cannot undo setup of "borken-undo-setup" snap`) 1015 } 1016 return f.maybeErrForLastOp() 1017 } 1018 1019 func (f *fakeSnappyBackend) UndoCopySnapData(newInfo *snap.Info, oldInfo *snap.Info, p progress.Meter) error { 1020 p.Notify("undo-copy-data") 1021 old := "<no-old>" 1022 if oldInfo != nil { 1023 old = oldInfo.MountDir() 1024 } 1025 f.appendOp(&fakeOp{ 1026 op: "undo-copy-snap-data", 1027 path: newInfo.MountDir(), 1028 old: old, 1029 }) 1030 return f.maybeErrForLastOp() 1031 } 1032 1033 func (f *fakeSnappyBackend) UnlinkSnap(info *snap.Info, linkCtx backend.LinkContext, meter progress.Meter) error { 1034 meter.Notify("unlink") 1035 f.appendOp(&fakeOp{ 1036 op: "unlink-snap", 1037 path: info.MountDir(), 1038 1039 unlinkFirstInstallUndo: linkCtx.FirstInstall, 1040 }) 1041 return f.maybeErrForLastOp() 1042 } 1043 1044 func (f *fakeSnappyBackend) RemoveSnapFiles(s snap.PlaceInfo, typ snap.Type, installRecord *backend.InstallRecord, dev boot.Device, meter progress.Meter) error { 1045 meter.Notify("remove-snap-files") 1046 f.appendOp(&fakeOp{ 1047 op: "remove-snap-files", 1048 path: s.MountDir(), 1049 stype: typ, 1050 }) 1051 return f.maybeErrForLastOp() 1052 } 1053 1054 func (f *fakeSnappyBackend) RemoveSnapData(info *snap.Info) error { 1055 f.appendOp(&fakeOp{ 1056 op: "remove-snap-data", 1057 path: info.MountDir(), 1058 }) 1059 return f.maybeErrForLastOp() 1060 } 1061 1062 func (f *fakeSnappyBackend) RemoveSnapCommonData(info *snap.Info) error { 1063 f.appendOp(&fakeOp{ 1064 op: "remove-snap-common-data", 1065 path: info.MountDir(), 1066 }) 1067 return f.maybeErrForLastOp() 1068 } 1069 1070 func (f *fakeSnappyBackend) RemoveSnapDataDir(info *snap.Info, otherInstances bool) error { 1071 f.ops = append(f.ops, fakeOp{ 1072 op: "remove-snap-data-dir", 1073 name: info.InstanceName(), 1074 path: snap.BaseDataDir(info.SnapName()), 1075 otherInstances: otherInstances, 1076 }) 1077 return f.maybeErrForLastOp() 1078 } 1079 1080 func (f *fakeSnappyBackend) RemoveSnapDir(s snap.PlaceInfo, otherInstances bool) error { 1081 f.ops = append(f.ops, fakeOp{ 1082 op: "remove-snap-dir", 1083 name: s.InstanceName(), 1084 path: snap.BaseDir(s.SnapName()), 1085 otherInstances: otherInstances, 1086 }) 1087 return f.maybeErrForLastOp() 1088 } 1089 1090 func (f *fakeSnappyBackend) DiscardSnapNamespace(snapName string) error { 1091 f.appendOp(&fakeOp{ 1092 op: "discard-namespace", 1093 name: snapName, 1094 }) 1095 return f.maybeErrForLastOp() 1096 } 1097 1098 func (f *fakeSnappyBackend) RemoveSnapInhibitLock(snapName string) error { 1099 f.appendOp(&fakeOp{ 1100 op: "remove-inhibit-lock", 1101 name: snapName, 1102 }) 1103 return f.maybeErrForLastOp() 1104 } 1105 1106 func (f *fakeSnappyBackend) Candidate(sideInfo *snap.SideInfo) { 1107 var sinfo snap.SideInfo 1108 if sideInfo != nil { 1109 sinfo = *sideInfo 1110 } 1111 f.appendOp(&fakeOp{ 1112 op: "candidate", 1113 sinfo: sinfo, 1114 }) 1115 } 1116 1117 func (f *fakeSnappyBackend) CurrentInfo(curInfo *snap.Info) { 1118 old := "<no-current>" 1119 if curInfo != nil { 1120 old = curInfo.MountDir() 1121 } 1122 f.appendOp(&fakeOp{ 1123 op: "current", 1124 old: old, 1125 }) 1126 } 1127 1128 func (f *fakeSnappyBackend) ForeignTask(kind string, status state.Status, snapsup *snapstate.SnapSetup) { 1129 f.appendOp(&fakeOp{ 1130 op: kind + ":" + status.String(), 1131 name: snapsup.InstanceName(), 1132 revno: snapsup.Revision(), 1133 }) 1134 } 1135 1136 type byAlias []*backend.Alias 1137 1138 func (ba byAlias) Len() int { return len(ba) } 1139 func (ba byAlias) Swap(i, j int) { ba[i], ba[j] = ba[j], ba[i] } 1140 func (ba byAlias) Less(i, j int) bool { 1141 return ba[i].Name < ba[j].Name 1142 } 1143 1144 func (f *fakeSnappyBackend) UpdateAliases(add []*backend.Alias, remove []*backend.Alias) error { 1145 if len(add) != 0 { 1146 add = append([]*backend.Alias(nil), add...) 1147 sort.Sort(byAlias(add)) 1148 } 1149 if len(remove) != 0 { 1150 remove = append([]*backend.Alias(nil), remove...) 1151 sort.Sort(byAlias(remove)) 1152 } 1153 f.appendOp(&fakeOp{ 1154 op: "update-aliases", 1155 aliases: add, 1156 rmAliases: remove, 1157 }) 1158 return f.maybeErrForLastOp() 1159 } 1160 1161 func (f *fakeSnappyBackend) RemoveSnapAliases(snapName string) error { 1162 f.appendOp(&fakeOp{ 1163 op: "remove-snap-aliases", 1164 name: snapName, 1165 }) 1166 return f.maybeErrForLastOp() 1167 } 1168 1169 func (f *fakeSnappyBackend) RunInhibitSnapForUnlink(info *snap.Info, hint runinhibit.Hint, decision func() error) (lock *osutil.FileLock, err error) { 1170 f.appendOp(&fakeOp{ 1171 op: "run-inhibit-snap-for-unlink", 1172 name: info.InstanceName(), 1173 inhibitHint: hint, 1174 }) 1175 if err := decision(); err != nil { 1176 return nil, err 1177 } 1178 if f.lockDir == "" { 1179 f.lockDir = os.TempDir() 1180 } 1181 // XXX: returning a real lock is somewhat annoying 1182 return osutil.NewFileLock(filepath.Join(f.lockDir, info.InstanceName()+".lock")) 1183 } 1184 1185 func (f *fakeSnappyBackend) appendOp(op *fakeOp) { 1186 f.mu.Lock() 1187 defer f.mu.Unlock() 1188 f.ops = append(f.ops, *op) 1189 }