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