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 }