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