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