github.com/Lephar/snapd@v0.0.0-20210825215435-c7fba9cef4d2/overlord/hookstate/ctlcmd/services_test.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2017 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 ctlcmd_test
    21  
    22  import (
    23  	"context"
    24  	"fmt"
    25  	"sort"
    26  
    27  	. "gopkg.in/check.v1"
    28  
    29  	"github.com/snapcore/snapd/client"
    30  	"github.com/snapcore/snapd/dirs"
    31  	"github.com/snapcore/snapd/overlord/auth"
    32  	"github.com/snapcore/snapd/overlord/hookstate"
    33  	"github.com/snapcore/snapd/overlord/hookstate/ctlcmd"
    34  	"github.com/snapcore/snapd/overlord/hookstate/hooktest"
    35  	"github.com/snapcore/snapd/overlord/servicestate"
    36  	"github.com/snapcore/snapd/overlord/snapstate"
    37  	"github.com/snapcore/snapd/overlord/snapstate/snapstatetest"
    38  	"github.com/snapcore/snapd/overlord/state"
    39  	"github.com/snapcore/snapd/snap"
    40  	"github.com/snapcore/snapd/snap/snaptest"
    41  	"github.com/snapcore/snapd/store"
    42  	"github.com/snapcore/snapd/store/storetest"
    43  	"github.com/snapcore/snapd/systemd"
    44  	"github.com/snapcore/snapd/testutil"
    45  )
    46  
    47  type fakeStore struct {
    48  	storetest.Store
    49  }
    50  
    51  func (f *fakeStore) SnapAction(_ context.Context, currentSnaps []*store.CurrentSnap, actions []*store.SnapAction, assertQuery store.AssertionQuery, user *auth.UserState, opts *store.RefreshOptions) ([]store.SnapActionResult, []store.AssertionResult, error) {
    52  	if assertQuery != nil {
    53  		panic("no assertion query support")
    54  	}
    55  	if actions[0].Action == "install" {
    56  		installs := make([]store.SnapActionResult, 0, len(actions))
    57  		for _, a := range actions {
    58  			snapName, instanceKey := snap.SplitInstanceName(a.InstanceName)
    59  			if instanceKey != "" {
    60  				panic(fmt.Sprintf("unexpected instance name %q in snap install action", a.InstanceName))
    61  			}
    62  
    63  			installs = append(installs, store.SnapActionResult{Info: &snap.Info{
    64  				DownloadInfo: snap.DownloadInfo{
    65  					Size: 1,
    66  				},
    67  				SideInfo: snap.SideInfo{
    68  					RealName: snapName,
    69  					Revision: snap.R(2),
    70  				},
    71  				Architectures: []string{"all"},
    72  			}})
    73  		}
    74  
    75  		return installs, nil, nil
    76  	}
    77  
    78  	snaps := []store.SnapActionResult{{Info: &snap.Info{
    79  		SideInfo: snap.SideInfo{
    80  			RealName: "test-snap",
    81  			Revision: snap.R(2),
    82  			SnapID:   "test-snap-id",
    83  		},
    84  		Architectures: []string{"all"},
    85  	}}, {Info: &snap.Info{
    86  		SideInfo: snap.SideInfo{
    87  			RealName: "other-snap",
    88  			Revision: snap.R(2),
    89  			SnapID:   "other-snap-id",
    90  		},
    91  		Architectures: []string{"all"},
    92  	}}}
    93  	return snaps, nil, nil
    94  }
    95  
    96  type servicectlSuite struct {
    97  	testutil.BaseTest
    98  	st          *state.State
    99  	fakeStore   fakeStore
   100  	mockContext *hookstate.Context
   101  	mockHandler *hooktest.MockHandler
   102  }
   103  
   104  var _ = Suite(&servicectlSuite{})
   105  
   106  const testSnapYaml = `name: test-snap
   107  version: 1.0
   108  summary: test-snap
   109  apps:
   110   normal-app:
   111    command: bin/dummy
   112   test-service:
   113    command: bin/service
   114    daemon: simple
   115    reload-command: bin/reload
   116   another-service:
   117    command: bin/service
   118    daemon: simple
   119    reload-command: bin/reload
   120   user-service:
   121    command: bin/user-service
   122    daemon: simple
   123    daemon-scope: user
   124  `
   125  
   126  const otherSnapYaml = `name: other-snap
   127  version: 1.0
   128  summary: other-snap
   129  apps:
   130   test-service:
   131    command: bin/service
   132    daemon: simple
   133    reload-command: bin/reload
   134  `
   135  
   136  func mockServiceChangeFunc(testServiceControlInputs func(appInfos []*snap.AppInfo, inst *servicestate.Instruction)) func() {
   137  	return ctlcmd.MockServicestateControlFunc(func(st *state.State, appInfos []*snap.AppInfo, inst *servicestate.Instruction, flags *servicestate.Flags, context *hookstate.Context) ([]*state.TaskSet, error) {
   138  		testServiceControlInputs(appInfos, inst)
   139  		return nil, fmt.Errorf("forced error")
   140  	})
   141  }
   142  
   143  func (s *servicectlSuite) SetUpTest(c *C) {
   144  	s.BaseTest.SetUpTest(c)
   145  	oldRoot := dirs.GlobalRootDir
   146  	dirs.SetRootDir(c.MkDir())
   147  
   148  	testutil.MockCommand(c, "systemctl", "")
   149  
   150  	s.BaseTest.AddCleanup(func() {
   151  		dirs.SetRootDir(oldRoot)
   152  	})
   153  	s.BaseTest.AddCleanup(snap.MockSanitizePlugsSlots(func(snapInfo *snap.Info) {}))
   154  
   155  	s.mockHandler = hooktest.NewMockHandler()
   156  
   157  	s.st = state.New(nil)
   158  	s.st.Lock()
   159  	defer s.st.Unlock()
   160  
   161  	snapstate.ReplaceStore(s.st, &s.fakeStore)
   162  
   163  	// mock installed snaps
   164  	info1 := snaptest.MockSnapCurrent(c, string(testSnapYaml), &snap.SideInfo{
   165  		Revision: snap.R(1),
   166  	})
   167  	info2 := snaptest.MockSnapCurrent(c, string(otherSnapYaml), &snap.SideInfo{
   168  		Revision: snap.R(1),
   169  	})
   170  	snapstate.Set(s.st, info1.InstanceName(), &snapstate.SnapState{
   171  		Active: true,
   172  		Sequence: []*snap.SideInfo{
   173  			{
   174  				RealName: info1.SnapName(),
   175  				Revision: info1.Revision,
   176  				SnapID:   "test-snap-id",
   177  			},
   178  		},
   179  		Current: info1.Revision,
   180  	})
   181  	snapstate.Set(s.st, info2.InstanceName(), &snapstate.SnapState{
   182  		Active: true,
   183  		Sequence: []*snap.SideInfo{
   184  			{
   185  				RealName: info2.SnapName(),
   186  				Revision: info2.Revision,
   187  				SnapID:   "other-snap-id",
   188  			},
   189  		},
   190  		Current: info2.Revision,
   191  	})
   192  
   193  	task := s.st.NewTask("test-task", "my test task")
   194  	setup := &hookstate.HookSetup{Snap: "test-snap", Revision: snap.R(1), Hook: "test-hook"}
   195  
   196  	var err error
   197  	s.mockContext, err = hookstate.NewContext(task, task.State(), setup, s.mockHandler, "")
   198  	c.Assert(err, IsNil)
   199  
   200  	s.st.Set("seeded", true)
   201  	s.st.Set("refresh-privacy-key", "privacy-key")
   202  	s.AddCleanup(snapstatetest.UseFallbackDeviceModel())
   203  }
   204  
   205  func (s *servicectlSuite) TestStopCommand(c *C) {
   206  	var serviceChangeFuncCalled bool
   207  	restore := mockServiceChangeFunc(func(appInfos []*snap.AppInfo, inst *servicestate.Instruction) {
   208  		serviceChangeFuncCalled = true
   209  		c.Assert(appInfos, HasLen, 1)
   210  		c.Assert(appInfos[0].Name, Equals, "test-service")
   211  		c.Assert(inst, DeepEquals, &servicestate.Instruction{
   212  			Action: "stop",
   213  			Names:  []string{"test-snap.test-service"},
   214  			StopOptions: client.StopOptions{
   215  				Disable: false,
   216  			},
   217  		},
   218  		)
   219  	})
   220  	defer restore()
   221  	_, _, err := ctlcmd.Run(s.mockContext, []string{"stop", "test-snap.test-service"}, 0)
   222  	c.Assert(err, NotNil)
   223  	c.Check(err, ErrorMatches, "forced error")
   224  	c.Assert(serviceChangeFuncCalled, Equals, true)
   225  }
   226  
   227  func (s *servicectlSuite) TestStopCommandUnknownService(c *C) {
   228  	var serviceChangeFuncCalled bool
   229  	restore := mockServiceChangeFunc(func(appInfos []*snap.AppInfo, inst *servicestate.Instruction) {
   230  		serviceChangeFuncCalled = true
   231  	})
   232  	defer restore()
   233  	_, _, err := ctlcmd.Run(s.mockContext, []string{"stop", "test-snap.fooservice"}, 0)
   234  	c.Assert(err, NotNil)
   235  	c.Assert(err, ErrorMatches, `unknown service: "test-snap.fooservice"`)
   236  	c.Assert(serviceChangeFuncCalled, Equals, false)
   237  }
   238  
   239  func (s *servicectlSuite) TestStopCommandFailsOnOtherSnap(c *C) {
   240  	var serviceChangeFuncCalled bool
   241  	restore := mockServiceChangeFunc(func(appInfos []*snap.AppInfo, inst *servicestate.Instruction) {
   242  		serviceChangeFuncCalled = true
   243  	})
   244  	defer restore()
   245  	// verify that snapctl is not allowed to control services of other snaps (only the one of its hook)
   246  	_, _, err := ctlcmd.Run(s.mockContext, []string{"stop", "other-snap.test-service"}, 0)
   247  	c.Check(err, NotNil)
   248  	c.Assert(err, ErrorMatches, `unknown service: "other-snap.test-service"`)
   249  	c.Assert(serviceChangeFuncCalled, Equals, false)
   250  }
   251  
   252  func (s *servicectlSuite) TestStartCommand(c *C) {
   253  	var serviceChangeFuncCalled bool
   254  	restore := mockServiceChangeFunc(func(appInfos []*snap.AppInfo, inst *servicestate.Instruction) {
   255  		serviceChangeFuncCalled = true
   256  		c.Assert(appInfos, HasLen, 1)
   257  		c.Assert(appInfos[0].Name, Equals, "test-service")
   258  		c.Assert(inst, DeepEquals, &servicestate.Instruction{
   259  			Action: "start",
   260  			Names:  []string{"test-snap.test-service"},
   261  			StartOptions: client.StartOptions{
   262  				Enable: false,
   263  			},
   264  		},
   265  		)
   266  	})
   267  	defer restore()
   268  	_, _, err := ctlcmd.Run(s.mockContext, []string{"start", "test-snap.test-service"}, 0)
   269  	c.Check(err, NotNil)
   270  	c.Check(err, ErrorMatches, "forced error")
   271  	c.Assert(serviceChangeFuncCalled, Equals, true)
   272  }
   273  
   274  func (s *servicectlSuite) TestRestartCommand(c *C) {
   275  	var serviceChangeFuncCalled bool
   276  	restore := mockServiceChangeFunc(func(appInfos []*snap.AppInfo, inst *servicestate.Instruction) {
   277  		serviceChangeFuncCalled = true
   278  		c.Assert(appInfos, HasLen, 1)
   279  		c.Assert(appInfos[0].Name, Equals, "test-service")
   280  		c.Assert(inst, DeepEquals, &servicestate.Instruction{
   281  			Action: "restart",
   282  			Names:  []string{"test-snap.test-service"},
   283  			RestartOptions: client.RestartOptions{
   284  				Reload: false,
   285  			},
   286  		},
   287  		)
   288  	})
   289  	defer restore()
   290  	_, _, err := ctlcmd.Run(s.mockContext, []string{"restart", "test-snap.test-service"}, 0)
   291  	c.Check(err, NotNil)
   292  	c.Check(err, ErrorMatches, "forced error")
   293  	c.Assert(serviceChangeFuncCalled, Equals, true)
   294  }
   295  
   296  func (s *servicectlSuite) TestConflictingChange(c *C) {
   297  	s.st.Lock()
   298  	task := s.st.NewTask("link-snap", "conflicting task")
   299  	snapsup := snapstate.SnapSetup{
   300  		SideInfo: &snap.SideInfo{
   301  			RealName: "test-snap",
   302  			SnapID:   "test-snap-id-1",
   303  			Revision: snap.R(1),
   304  		},
   305  	}
   306  	task.Set("snap-setup", snapsup)
   307  	chg := s.st.NewChange("conflicting change", "install change")
   308  	chg.AddTask(task)
   309  	s.st.Unlock()
   310  
   311  	_, _, err := ctlcmd.Run(s.mockContext, []string{"start", "test-snap.test-service"}, 0)
   312  	c.Check(err, NotNil)
   313  	c.Check(err, ErrorMatches, `snap "test-snap" has "conflicting change" change in progress`)
   314  }
   315  
   316  var (
   317  	installTaskKinds = []string{
   318  		"prerequisites",
   319  		"download-snap",
   320  		"validate-snap",
   321  		"mount-snap",
   322  		"copy-snap-data",
   323  		"setup-profiles",
   324  		"link-snap",
   325  		"auto-connect",
   326  		"set-auto-aliases",
   327  		"setup-aliases",
   328  		"run-hook[install]",
   329  		"start-snap-services",
   330  		"run-hook[configure]",
   331  		"run-hook[check-health]",
   332  	}
   333  
   334  	refreshTaskKinds = []string{
   335  		"prerequisites",
   336  		"download-snap",
   337  		"validate-snap",
   338  		"mount-snap",
   339  		"run-hook[pre-refresh]",
   340  		"stop-snap-services",
   341  		"remove-aliases",
   342  		"unlink-current-snap",
   343  		"copy-snap-data",
   344  		"setup-profiles",
   345  		"link-snap",
   346  		"auto-connect",
   347  		"set-auto-aliases",
   348  		"setup-aliases",
   349  		"run-hook[post-refresh]",
   350  		"start-snap-services",
   351  		"cleanup",
   352  		"run-hook[configure]",
   353  		"run-hook[check-health]",
   354  	}
   355  )
   356  
   357  func (s *servicectlSuite) TestQueuedCommands(c *C) {
   358  	s.st.Lock()
   359  
   360  	chg := s.st.NewChange("install change", "install change")
   361  	installed, tts, err := snapstate.InstallMany(s.st, []string{"one", "two"}, 0)
   362  	c.Assert(err, IsNil)
   363  	c.Check(installed, DeepEquals, []string{"one", "two"})
   364  	c.Assert(tts, HasLen, 2)
   365  	c.Assert(taskKinds(tts[0].Tasks()), DeepEquals, installTaskKinds)
   366  	c.Assert(taskKinds(tts[1].Tasks()), DeepEquals, installTaskKinds)
   367  	chg.AddAll(tts[0])
   368  	chg.AddAll(tts[1])
   369  
   370  	s.st.Unlock()
   371  
   372  	for _, ts := range tts {
   373  		tsTasks := ts.Tasks()
   374  		// assumes configure task is last
   375  		task := tsTasks[len(tsTasks)-1]
   376  		c.Assert(task.Kind(), Equals, "run-hook")
   377  		setup := &hookstate.HookSetup{Snap: "test-snap", Revision: snap.R(1), Hook: "configure"}
   378  		context, err := hookstate.NewContext(task, task.State(), setup, s.mockHandler, "")
   379  		c.Assert(err, IsNil)
   380  
   381  		_, _, err = ctlcmd.Run(context, []string{"stop", "test-snap.test-service"}, 0)
   382  		c.Check(err, IsNil)
   383  		_, _, err = ctlcmd.Run(context, []string{"start", "test-snap.test-service"}, 0)
   384  		c.Check(err, IsNil)
   385  		_, _, err = ctlcmd.Run(context, []string{"restart", "test-snap.test-service"}, 0)
   386  		c.Check(err, IsNil)
   387  	}
   388  
   389  	s.st.Lock()
   390  	defer s.st.Unlock()
   391  
   392  	expectedTaskKinds := append(installTaskKinds, "exec-command", "service-control", "exec-command", "service-control", "exec-command", "service-control")
   393  	checkLaneTasks := func(lane int) {
   394  		laneTasks := chg.LaneTasks(lane)
   395  		c.Assert(taskKinds(laneTasks), DeepEquals, expectedTaskKinds)
   396  		c.Check(laneTasks[12].Summary(), Matches, `Run configure hook of .* snap if present`)
   397  		c.Check(laneTasks[14].Summary(), Equals, "stop of [test-snap.test-service]")
   398  		c.Check(laneTasks[16].Summary(), Equals, "start of [test-snap.test-service]")
   399  		c.Check(laneTasks[18].Summary(), Equals, "restart of [test-snap.test-service]")
   400  	}
   401  	checkLaneTasks(1)
   402  	checkLaneTasks(2)
   403  }
   404  
   405  func (s *servicectlSuite) testQueueCommandsOrdering(c *C, finalTaskKind string) {
   406  	s.st.Lock()
   407  
   408  	chg := s.st.NewChange("seeding change", "seeding change")
   409  	finalTask := s.st.NewTask(finalTaskKind, "")
   410  	chg.AddTask(finalTask)
   411  	configure := s.st.NewTask("run-hook", "")
   412  	chg.AddTask(configure)
   413  
   414  	s.st.Unlock()
   415  
   416  	setup := &hookstate.HookSetup{Snap: "test-snap", Revision: snap.R(1), Hook: "configure"}
   417  	context, err := hookstate.NewContext(configure, configure.State(), setup, s.mockHandler, "")
   418  	c.Assert(err, IsNil)
   419  
   420  	_, _, err = ctlcmd.Run(context, []string{"stop", "test-snap.test-service"}, 0)
   421  	c.Check(err, IsNil)
   422  	_, _, err = ctlcmd.Run(context, []string{"start", "test-snap.test-service"}, 0)
   423  	c.Check(err, IsNil)
   424  
   425  	s.st.Lock()
   426  	defer s.st.Unlock()
   427  
   428  	var finalWaitTasks []string
   429  	for _, t := range finalTask.WaitTasks() {
   430  		taskInfo := fmt.Sprintf("%s:%s", t.Kind(), t.Summary())
   431  		finalWaitTasks = append(finalWaitTasks, taskInfo)
   432  
   433  		var wait []string
   434  		var hasRunHook bool
   435  		for _, wt := range t.WaitTasks() {
   436  			if wt.Kind() != "run-hook" {
   437  				taskInfo = fmt.Sprintf("%s:%s", wt.Kind(), wt.Summary())
   438  				wait = append(wait, taskInfo)
   439  			} else {
   440  				hasRunHook = true
   441  			}
   442  		}
   443  		c.Assert(hasRunHook, Equals, true)
   444  
   445  		switch t.Kind() {
   446  		case "exec-command":
   447  			var argv []string
   448  			c.Assert(t.Get("argv", &argv), IsNil)
   449  			c.Check(argv, HasLen, 3)
   450  			switch argv[1] {
   451  			case "stop":
   452  				c.Check(wait, HasLen, 0)
   453  			case "start":
   454  				c.Check(wait, DeepEquals, []string{
   455  					`exec-command:stop of [test-snap.test-service]`,
   456  					`service-control:Run service command "stop" for services ["test-service"] of snap "test-snap"`})
   457  			default:
   458  				c.Fatalf("unexpected command: %q", argv[1])
   459  			}
   460  		case "service-control":
   461  			var sa servicestate.ServiceAction
   462  			c.Assert(t.Get("service-action", &sa), IsNil)
   463  			c.Check(sa.Services, DeepEquals, []string{"test-service"})
   464  			switch sa.Action {
   465  			case "stop":
   466  				c.Check(wait, DeepEquals, []string{
   467  					"exec-command:stop of [test-snap.test-service]"})
   468  			case "start":
   469  				c.Check(wait, DeepEquals, []string{
   470  					"exec-command:start of [test-snap.test-service]",
   471  					"exec-command:stop of [test-snap.test-service]",
   472  					`service-control:Run service command "stop" for services ["test-service"] of snap "test-snap"`})
   473  			}
   474  		default:
   475  			c.Fatalf("unexpected task: %s", t.Kind())
   476  		}
   477  
   478  	}
   479  	c.Check(finalWaitTasks, DeepEquals, []string{
   480  		`exec-command:stop of [test-snap.test-service]`,
   481  		`service-control:Run service command "stop" for services ["test-service"] of snap "test-snap"`,
   482  		`exec-command:start of [test-snap.test-service]`,
   483  		`service-control:Run service command "start" for services ["test-service"] of snap "test-snap"`})
   484  	c.Check(finalTask.HaltTasks(), HasLen, 0)
   485  }
   486  
   487  func (s *servicectlSuite) TestQueuedCommandsRunBeforeMarkSeeded(c *C) {
   488  	s.testQueueCommandsOrdering(c, "mark-seeded")
   489  }
   490  
   491  func (s *servicectlSuite) TestQueuedCommandsRunBeforeSetModel(c *C) {
   492  	s.testQueueCommandsOrdering(c, "set-model")
   493  }
   494  
   495  func (s *servicectlSuite) TestQueuedCommandsUpdateMany(c *C) {
   496  	oldAutoAliases := snapstate.AutoAliases
   497  	snapstate.AutoAliases = func(*state.State, *snap.Info) (map[string]string, error) {
   498  		return nil, nil
   499  	}
   500  	defer func() { snapstate.AutoAliases = oldAutoAliases }()
   501  
   502  	s.st.Lock()
   503  
   504  	chg := s.st.NewChange("update many change", "update change")
   505  	installed, tts, err := snapstate.UpdateMany(context.Background(), s.st, []string{"test-snap", "other-snap"}, 0, nil)
   506  	c.Assert(err, IsNil)
   507  	sort.Strings(installed)
   508  	c.Check(installed, DeepEquals, []string{"other-snap", "test-snap"})
   509  	c.Assert(tts, HasLen, 3)
   510  	c.Assert(taskKinds(tts[0].Tasks()), DeepEquals, refreshTaskKinds)
   511  	c.Assert(taskKinds(tts[1].Tasks()), DeepEquals, refreshTaskKinds)
   512  	c.Assert(taskKinds(tts[2].Tasks()), DeepEquals, []string{"check-rerefresh"})
   513  	c.Assert(tts[2].Tasks()[0].Kind(), Equals, "check-rerefresh")
   514  	chg.AddAll(tts[0])
   515  	chg.AddAll(tts[1])
   516  
   517  	s.st.Unlock()
   518  
   519  	for _, ts := range tts[:2] {
   520  		tsTasks := ts.Tasks()
   521  		// assumes configure task is last
   522  		task := tsTasks[len(tsTasks)-1]
   523  		c.Assert(task.Kind(), Equals, "run-hook")
   524  		setup := &hookstate.HookSetup{Snap: "test-snap", Revision: snap.R(1), Hook: "configure"}
   525  		context, err := hookstate.NewContext(task, task.State(), setup, s.mockHandler, "")
   526  		c.Assert(err, IsNil)
   527  
   528  		_, _, err = ctlcmd.Run(context, []string{"stop", "test-snap.test-service"}, 0)
   529  		c.Check(err, IsNil)
   530  		_, _, err = ctlcmd.Run(context, []string{"start", "test-snap.test-service"}, 0)
   531  		c.Check(err, IsNil)
   532  		_, _, err = ctlcmd.Run(context, []string{"restart", "test-snap.test-service"}, 0)
   533  		c.Check(err, IsNil)
   534  	}
   535  
   536  	s.st.Lock()
   537  	defer s.st.Unlock()
   538  
   539  	expectedTaskKinds := append(refreshTaskKinds, "exec-command", "service-control", "exec-command", "service-control", "exec-command", "service-control")
   540  	for i := 1; i <= 2; i++ {
   541  		laneTasks := chg.LaneTasks(i)
   542  		c.Assert(taskKinds(laneTasks), DeepEquals, expectedTaskKinds)
   543  		c.Check(laneTasks[17].Summary(), Matches, `Run configure hook of .* snap if present`)
   544  		c.Check(laneTasks[19].Summary(), Equals, "stop of [test-snap.test-service]")
   545  		c.Check(laneTasks[20].Summary(), Equals, `Run service command "stop" for services ["test-service"] of snap "test-snap"`)
   546  		c.Check(laneTasks[21].Summary(), Equals, "start of [test-snap.test-service]")
   547  		c.Check(laneTasks[22].Summary(), Equals, `Run service command "start" for services ["test-service"] of snap "test-snap"`)
   548  		c.Check(laneTasks[23].Summary(), Equals, "restart of [test-snap.test-service]")
   549  		c.Check(laneTasks[24].Summary(), Equals, `Run service command "restart" for services ["test-service"] of snap "test-snap"`)
   550  	}
   551  }
   552  
   553  func (s *servicectlSuite) TestQueuedCommandsSingleLane(c *C) {
   554  	s.st.Lock()
   555  
   556  	chg := s.st.NewChange("install change", "install change")
   557  	ts, err := snapstate.Install(context.Background(), s.st, "one", &snapstate.RevisionOptions{Revision: snap.R(1)}, 0, snapstate.Flags{})
   558  	c.Assert(err, IsNil)
   559  	c.Assert(taskKinds(ts.Tasks()), DeepEquals, installTaskKinds)
   560  	chg.AddAll(ts)
   561  
   562  	s.st.Unlock()
   563  
   564  	tsTasks := ts.Tasks()
   565  	// assumes configure task is last
   566  	task := tsTasks[len(tsTasks)-1]
   567  	c.Assert(task.Kind(), Equals, "run-hook")
   568  	setup := &hookstate.HookSetup{Snap: "test-snap", Revision: snap.R(1), Hook: "configure"}
   569  	context, err := hookstate.NewContext(task, task.State(), setup, s.mockHandler, "")
   570  	c.Assert(err, IsNil)
   571  
   572  	_, _, err = ctlcmd.Run(context, []string{"stop", "test-snap.test-service"}, 0)
   573  	c.Check(err, IsNil)
   574  	_, _, err = ctlcmd.Run(context, []string{"start", "test-snap.test-service"}, 0)
   575  	c.Check(err, IsNil)
   576  	_, _, err = ctlcmd.Run(context, []string{"restart", "test-snap.test-service"}, 0)
   577  	c.Check(err, IsNil)
   578  
   579  	s.st.Lock()
   580  	defer s.st.Unlock()
   581  
   582  	laneTasks := chg.LaneTasks(0)
   583  	c.Assert(taskKinds(laneTasks), DeepEquals, append(installTaskKinds, "exec-command", "service-control", "exec-command", "service-control", "exec-command", "service-control"))
   584  	c.Check(laneTasks[12].Summary(), Matches, `Run configure hook of .* snap if present`)
   585  	c.Check(laneTasks[14].Summary(), Equals, "stop of [test-snap.test-service]")
   586  	c.Check(laneTasks[16].Summary(), Equals, "start of [test-snap.test-service]")
   587  	c.Check(laneTasks[18].Summary(), Equals, "restart of [test-snap.test-service]")
   588  }
   589  
   590  func (s *servicectlSuite) TestTwoServices(c *C) {
   591  	restore := systemd.MockSystemctl(func(args ...string) (buf []byte, err error) {
   592  		switch args[0] {
   593  		case "show":
   594  			c.Check(args[2], Matches, `snap\.test-snap\.\w+-service\.service`)
   595  			return []byte(fmt.Sprintf(`Id=%s
   596  Type=simple
   597  ActiveState=active
   598  UnitFileState=enabled
   599  `, args[2])), nil
   600  		case "--user":
   601  			c.Check(args[1], Equals, "--global")
   602  			c.Check(args[2], Equals, "is-enabled")
   603  			return []byte("enabled\n"), nil
   604  		default:
   605  			c.Errorf("unexpected systemctl command: %v", args)
   606  			return nil, fmt.Errorf("should not be reached")
   607  		}
   608  	})
   609  	defer restore()
   610  
   611  	stdout, stderr, err := ctlcmd.Run(s.mockContext, []string{"services"}, 0)
   612  	c.Assert(err, IsNil)
   613  	c.Check(string(stdout), Equals, `
   614  Service                    Startup  Current  Notes
   615  test-snap.another-service  enabled  active   -
   616  test-snap.test-service     enabled  active   -
   617  test-snap.user-service     enabled  -        user
   618  `[1:])
   619  	c.Check(string(stderr), Equals, "")
   620  }
   621  
   622  func (s *servicectlSuite) TestServices(c *C) {
   623  	restore := systemd.MockSystemctl(func(args ...string) (buf []byte, err error) {
   624  		c.Assert(args[0], Equals, "show")
   625  		c.Check(args[2], Equals, "snap.test-snap.test-service.service")
   626  		return []byte(`Id=snap.test-snap.test-service.service
   627  Type=simple
   628  ActiveState=active
   629  UnitFileState=enabled
   630  `), nil
   631  	})
   632  	defer restore()
   633  
   634  	stdout, stderr, err := ctlcmd.Run(s.mockContext, []string{"services", "test-snap.test-service"}, 0)
   635  	c.Assert(err, IsNil)
   636  	c.Check(string(stdout), Equals, `
   637  Service                 Startup  Current  Notes
   638  test-snap.test-service  enabled  active   -
   639  `[1:])
   640  	c.Check(string(stderr), Equals, "")
   641  }