github.com/Lephar/snapd@v0.0.0-20210825215435-c7fba9cef4d2/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  }