github.com/hugh712/snapd@v0.0.0-20200910133618-1a99902bd583/overlord/snapstate/backend_test.go (about)

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