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