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