github.com/stolowski/snapd@v0.0.0-20210407085831-115137ce5a22/wrappers/services_gen_test.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2014-2016 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 wrappers_test 21 22 import ( 23 "fmt" 24 "os" 25 "os/exec" 26 "path/filepath" 27 "strings" 28 "time" 29 30 . "gopkg.in/check.v1" 31 32 "github.com/snapcore/snapd/dirs" 33 "github.com/snapcore/snapd/release" 34 "github.com/snapcore/snapd/snap" 35 "github.com/snapcore/snapd/snap/snaptest" 36 "github.com/snapcore/snapd/testutil" 37 "github.com/snapcore/snapd/timeout" 38 "github.com/snapcore/snapd/timeutil" 39 "github.com/snapcore/snapd/wrappers" 40 ) 41 42 type servicesWrapperGenSuite struct { 43 testutil.BaseTest 44 } 45 46 var _ = Suite(&servicesWrapperGenSuite{}) 47 48 const expectedServiceFmt = `[Unit] 49 # Auto-generated, DO NOT EDIT 50 Description=Service for snap application snap.app 51 Requires=%s-snap-44.mount 52 Wants=network.target 53 After=%s-snap-44.mount network.target snapd.apparmor.service 54 X-Snappy=yes 55 56 [Service] 57 EnvironmentFile=-/etc/environment 58 ExecStart=/usr/bin/snap run snap.app 59 SyslogIdentifier=snap.app 60 Restart=%s 61 WorkingDirectory=/var/snap/snap/44 62 ExecStop=/usr/bin/snap run --command=stop snap.app 63 ExecReload=/usr/bin/snap run --command=reload snap.app 64 ExecStopPost=/usr/bin/snap run --command=post-stop snap.app 65 TimeoutStopSec=10 66 Type=%s 67 %s` 68 69 const expectedInstallSection = ` 70 [Install] 71 WantedBy=multi-user.target 72 ` 73 74 const expectedUserServiceFmt = `[Unit] 75 # Auto-generated, DO NOT EDIT 76 Description=Service for snap application snap.app 77 X-Snappy=yes 78 79 [Service] 80 EnvironmentFile=-/etc/environment 81 ExecStart=/usr/bin/snap run snap.app 82 SyslogIdentifier=snap.app 83 Restart=%s 84 WorkingDirectory=/var/snap/snap/44 85 ExecStop=/usr/bin/snap run --command=stop snap.app 86 ExecReload=/usr/bin/snap run --command=reload snap.app 87 ExecStopPost=/usr/bin/snap run --command=post-stop snap.app 88 TimeoutStopSec=10 89 Type=%s 90 91 [Install] 92 WantedBy=default.target 93 ` 94 95 var ( 96 mountUnitPrefix = strings.Replace(dirs.SnapMountDir[1:], "/", "-", -1) 97 ) 98 99 var ( 100 expectedAppService = fmt.Sprintf(expectedServiceFmt, mountUnitPrefix, mountUnitPrefix, "on-failure", "simple", expectedInstallSection) 101 expectedDbusService = fmt.Sprintf(expectedServiceFmt, mountUnitPrefix, mountUnitPrefix, "on-failure", "dbus\nBusName=foo.bar.baz", "") 102 expectedOneshotService = fmt.Sprintf(expectedServiceFmt, mountUnitPrefix, mountUnitPrefix, "no", "oneshot\nRemainAfterExit=yes", expectedInstallSection) 103 expectedUserAppService = fmt.Sprintf(expectedUserServiceFmt, "on-failure", "simple") 104 ) 105 106 var ( 107 expectedServiceWrapperFmt = `[Unit] 108 # Auto-generated, DO NOT EDIT 109 Description=Service for snap application xkcd-webserver.xkcd-webserver 110 Requires=%s-xkcd\x2dwebserver-44.mount 111 Wants=network.target 112 After=%s-xkcd\x2dwebserver-44.mount network.target snapd.apparmor.service 113 X-Snappy=yes 114 115 [Service] 116 EnvironmentFile=-/etc/environment 117 ExecStart=/usr/bin/snap run xkcd-webserver 118 SyslogIdentifier=xkcd-webserver.xkcd-webserver 119 Restart=on-failure 120 WorkingDirectory=/var/snap/xkcd-webserver/44 121 ExecStop=/usr/bin/snap run --command=stop xkcd-webserver 122 ExecReload=/usr/bin/snap run --command=reload xkcd-webserver 123 ExecStopPost=/usr/bin/snap run --command=post-stop xkcd-webserver 124 TimeoutStopSec=30 125 Type=%s 126 %s` 127 expectedTypeForkingWrapper = fmt.Sprintf(expectedServiceWrapperFmt, mountUnitPrefix, mountUnitPrefix, "forking", expectedInstallSection) 128 ) 129 130 func (s *servicesWrapperGenSuite) SetUpTest(c *C) { 131 s.BaseTest.SetUpTest(c) 132 s.BaseTest.AddCleanup(snap.MockSanitizePlugsSlots(func(snapInfo *snap.Info) {})) 133 } 134 135 func (s *servicesWrapperGenSuite) TearDownTest(c *C) { 136 s.BaseTest.TearDownTest(c) 137 } 138 139 func (s *servicesWrapperGenSuite) TestGenerateSnapServiceFileOnClassic(c *C) { 140 yamlText := ` 141 name: snap 142 version: 1.0 143 apps: 144 app: 145 command: bin/start 146 stop-command: bin/stop 147 reload-command: bin/reload 148 post-stop-command: bin/stop --post 149 stop-timeout: 10s 150 daemon: simple 151 ` 152 info, err := snap.InfoFromSnapYaml([]byte(yamlText)) 153 c.Assert(err, IsNil) 154 info.Revision = snap.R(44) 155 app := info.Apps["app"] 156 157 generatedWrapper, err := wrappers.GenerateSnapServiceFile(app, nil) 158 c.Assert(err, IsNil) 159 c.Check(string(generatedWrapper), Equals, expectedAppService) 160 } 161 162 func (s *servicesWrapperGenSuite) TestGenerateSnapServiceOnCore(c *C) { 163 defer func() { dirs.SetRootDir("/") }() 164 165 expectedAppServiceOnCore := `[Unit] 166 # Auto-generated, DO NOT EDIT 167 Description=Service for snap application foo.app 168 Requires=snap-foo-44.mount 169 Wants=network.target 170 After=snap-foo-44.mount network.target snapd.apparmor.service 171 X-Snappy=yes 172 173 [Service] 174 EnvironmentFile=-/etc/environment 175 ExecStart=/usr/bin/snap run foo.app 176 SyslogIdentifier=foo.app 177 Restart=on-failure 178 WorkingDirectory=/var/snap/foo/44 179 TimeoutStopSec=30 180 Type=simple 181 182 [Install] 183 WantedBy=multi-user.target 184 ` 185 186 yamlText := ` 187 name: foo 188 version: 1.0 189 apps: 190 app: 191 command: bin/start 192 daemon: simple 193 ` 194 info, err := snap.InfoFromSnapYaml([]byte(yamlText)) 195 c.Assert(err, IsNil) 196 info.Revision = snap.R(44) 197 app := info.Apps["app"] 198 199 // we are on core 200 restore := release.MockOnClassic(false) 201 defer restore() 202 restore = release.MockReleaseInfo(&release.OS{ID: "ubuntu-core"}) 203 defer restore() 204 dirs.SetRootDir("/") 205 206 opts := wrappers.AddSnapServicesOptions{ 207 RequireMountedSnapdSnap: false, 208 } 209 generatedWrapper, err := wrappers.GenerateSnapServiceFile(app, &opts) 210 c.Assert(err, IsNil) 211 c.Check(string(generatedWrapper), Equals, expectedAppServiceOnCore) 212 213 // now with additional dependency on tooling 214 opts = wrappers.AddSnapServicesOptions{ 215 RequireMountedSnapdSnap: true, 216 } 217 generatedWrapper, err = wrappers.GenerateSnapServiceFile(app, &opts) 218 c.Assert(err, IsNil) 219 // we gain additional Requires= & After= on usr-lib-snapd.mount 220 expectedAppServiceOnCoreWithSnapd := `[Unit] 221 # Auto-generated, DO NOT EDIT 222 Description=Service for snap application foo.app 223 Requires=snap-foo-44.mount 224 Wants=network.target 225 After=snap-foo-44.mount network.target snapd.apparmor.service 226 Requires=usr-lib-snapd.mount 227 After=usr-lib-snapd.mount 228 X-Snappy=yes 229 230 [Service] 231 EnvironmentFile=-/etc/environment 232 ExecStart=/usr/bin/snap run foo.app 233 SyslogIdentifier=foo.app 234 Restart=on-failure 235 WorkingDirectory=/var/snap/foo/44 236 TimeoutStopSec=30 237 Type=simple 238 239 [Install] 240 WantedBy=multi-user.target 241 ` 242 243 c.Check(string(generatedWrapper), Equals, expectedAppServiceOnCoreWithSnapd) 244 } 245 246 func (s *servicesWrapperGenSuite) TestGenerateSnapServiceFileWithStartTimeout(c *C) { 247 yamlText := ` 248 name: snap 249 version: 1.0 250 apps: 251 app: 252 command: bin/start 253 start-timeout: 10m 254 daemon: simple 255 ` 256 info, err := snap.InfoFromSnapYaml([]byte(yamlText)) 257 c.Assert(err, IsNil) 258 info.Revision = snap.R(44) 259 app := info.Apps["app"] 260 261 generatedWrapper, err := wrappers.GenerateSnapServiceFile(app, nil) 262 c.Assert(err, IsNil) 263 c.Check(string(generatedWrapper), testutil.Contains, "\nTimeoutStartSec=600\n") 264 } 265 266 func (s *servicesWrapperGenSuite) TestGenerateSnapServiceFileRestart(c *C) { 267 yamlTextTemplate := ` 268 name: snap 269 apps: 270 app: 271 daemon: simple 272 restart-condition: %s 273 ` 274 for name, cond := range snap.RestartMap { 275 yamlText := fmt.Sprintf(yamlTextTemplate, cond) 276 277 info, err := snap.InfoFromSnapYaml([]byte(yamlText)) 278 c.Assert(err, IsNil) 279 info.Revision = snap.R(44) 280 app := info.Apps["app"] 281 282 generatedWrapper, err := wrappers.GenerateSnapServiceFile(app, nil) 283 c.Assert(err, IsNil) 284 wrapperText := string(generatedWrapper) 285 if cond == snap.RestartNever { 286 c.Check(wrapperText, Matches, 287 `(?ms).*^Restart=no$.*`, Commentf(name)) 288 } else { 289 c.Check(wrapperText, Matches, 290 `(?ms).*^Restart=`+name+`$.*`, Commentf(name)) 291 } 292 } 293 } 294 295 func (s *servicesWrapperGenSuite) TestGenerateSnapServiceFileTypeForking(c *C) { 296 service := &snap.AppInfo{ 297 Snap: &snap.Info{ 298 SuggestedName: "xkcd-webserver", 299 Version: "0.3.4", 300 SideInfo: snap.SideInfo{Revision: snap.R(44)}, 301 }, 302 Name: "xkcd-webserver", 303 Command: "bin/foo start", 304 StopCommand: "bin/foo stop", 305 ReloadCommand: "bin/foo reload", 306 PostStopCommand: "bin/foo post-stop", 307 StopTimeout: timeout.DefaultTimeout, 308 Daemon: "forking", 309 DaemonScope: snap.SystemDaemon, 310 } 311 312 generatedWrapper, err := wrappers.GenerateSnapServiceFile(service, nil) 313 c.Assert(err, IsNil) 314 c.Assert(string(generatedWrapper), Equals, expectedTypeForkingWrapper) 315 } 316 317 func (s *servicesWrapperGenSuite) TestGenerateSnapServiceFileIllegalChars(c *C) { 318 service := &snap.AppInfo{ 319 Snap: &snap.Info{ 320 SuggestedName: "xkcd-webserver", 321 Version: "0.3.4", 322 SideInfo: snap.SideInfo{Revision: snap.R(44)}, 323 }, 324 Name: "xkcd-webserver", 325 Command: "bin/foo start\n", 326 StopCommand: "bin/foo stop", 327 ReloadCommand: "bin/foo reload", 328 PostStopCommand: "bin/foo post-stop", 329 StopTimeout: timeout.DefaultTimeout, 330 Daemon: "simple", 331 DaemonScope: snap.SystemDaemon, 332 } 333 334 _, err := wrappers.GenerateSnapServiceFile(service, nil) 335 c.Assert(err, NotNil) 336 } 337 338 func (s *servicesWrapperGenSuite) TestGenServiceFileWithBusName(c *C) { 339 yamlText := ` 340 name: snap 341 version: 1.0 342 slots: 343 dbus-slot: 344 interface: dbus 345 bus: system 346 name: org.example.Foo 347 apps: 348 app: 349 command: bin/start 350 stop-command: bin/stop 351 reload-command: bin/reload 352 post-stop-command: bin/stop --post 353 stop-timeout: 10s 354 bus-name: foo.bar.baz 355 daemon: dbus 356 activates-on: [dbus-slot] 357 ` 358 359 info, err := snap.InfoFromSnapYaml([]byte(yamlText)) 360 c.Assert(err, IsNil) 361 info.Revision = snap.R(44) 362 app := info.Apps["app"] 363 364 generatedWrapper, err := wrappers.GenerateSnapServiceFile(app, nil) 365 c.Assert(err, IsNil) 366 367 c.Assert(string(generatedWrapper), Equals, expectedDbusService) 368 } 369 370 func (s *servicesWrapperGenSuite) TestGenServiceFileWithBusNameOnly(c *C) { 371 372 yamlText := ` 373 name: snap 374 version: 1.0 375 apps: 376 app: 377 command: bin/start 378 stop-command: bin/stop 379 reload-command: bin/reload 380 post-stop-command: bin/stop --post 381 stop-timeout: 10s 382 bus-name: foo.bar.baz 383 daemon: dbus 384 ` 385 386 info, err := snap.InfoFromSnapYaml([]byte(yamlText)) 387 c.Assert(err, IsNil) 388 info.Revision = snap.R(44) 389 app := info.Apps["app"] 390 391 generatedWrapper, err := wrappers.GenerateSnapServiceFile(app, nil) 392 c.Assert(err, IsNil) 393 394 expectedDbusService := fmt.Sprintf(expectedServiceFmt, mountUnitPrefix, mountUnitPrefix, "on-failure", "dbus\nBusName=foo.bar.baz", expectedInstallSection) 395 c.Assert(string(generatedWrapper), Equals, expectedDbusService) 396 } 397 398 func (s *servicesWrapperGenSuite) TestGenServiceFileWithBusNameFromSlot(c *C) { 399 400 yamlText := ` 401 name: snap 402 version: 1.0 403 slots: 404 dbus-slot1: 405 interface: dbus 406 bus: system 407 name: org.example.Foo 408 dbus-slot2: 409 interface: dbus 410 bus: system 411 name: foo.bar.baz 412 apps: 413 app: 414 command: bin/start 415 stop-command: bin/stop 416 reload-command: bin/reload 417 post-stop-command: bin/stop --post 418 stop-timeout: 10s 419 daemon: dbus 420 activates-on: [dbus-slot1, dbus-slot2] 421 ` 422 423 info, err := snap.InfoFromSnapYaml([]byte(yamlText)) 424 c.Assert(err, IsNil) 425 info.Revision = snap.R(44) 426 app := info.Apps["app"] 427 428 generatedWrapper, err := wrappers.GenerateSnapServiceFile(app, nil) 429 c.Assert(err, IsNil) 430 431 // Bus name defaults to the name from the last slot the daemon 432 // activates on. 433 c.Assert(string(generatedWrapper), Equals, expectedDbusService) 434 } 435 436 func (s *servicesWrapperGenSuite) TestGenOneshotServiceFile(c *C) { 437 438 info := snaptest.MockInfo(c, ` 439 name: snap 440 version: 1.0 441 apps: 442 app: 443 command: bin/start 444 stop-command: bin/stop 445 reload-command: bin/reload 446 post-stop-command: bin/stop --post 447 stop-timeout: 10s 448 daemon: oneshot 449 `, &snap.SideInfo{Revision: snap.R(44)}) 450 451 app := info.Apps["app"] 452 453 generatedWrapper, err := wrappers.GenerateSnapServiceFile(app, nil) 454 c.Assert(err, IsNil) 455 456 c.Assert(string(generatedWrapper), Equals, expectedOneshotService) 457 } 458 459 func (s *servicesWrapperGenSuite) TestGenerateSnapUserServiceFile(c *C) { 460 yamlText := ` 461 name: snap 462 version: 1.0 463 apps: 464 app: 465 command: bin/start 466 stop-command: bin/stop 467 reload-command: bin/reload 468 post-stop-command: bin/stop --post 469 stop-timeout: 10s 470 daemon: simple 471 daemon-scope: user 472 ` 473 info, err := snap.InfoFromSnapYaml([]byte(yamlText)) 474 c.Assert(err, IsNil) 475 info.Revision = snap.R(44) 476 app := info.Apps["app"] 477 478 generatedWrapper, err := wrappers.GenerateSnapServiceFile(app, nil) 479 c.Assert(err, IsNil) 480 c.Check(string(generatedWrapper), Equals, expectedUserAppService) 481 } 482 483 func (s *servicesWrapperGenSuite) TestGenerateSnapServiceWithSockets(c *C) { 484 const sock1ExpectedFmt = `[Unit] 485 # Auto-generated, DO NOT EDIT 486 Description=Socket sock1 for snap application some-snap.app 487 Requires=%s-some\x2dsnap-44.mount 488 After=%s-some\x2dsnap-44.mount 489 X-Snappy=yes 490 491 [Socket] 492 Service=snap.some-snap.app.service 493 FileDescriptorName=sock1 494 ListenStream=%s/sock1.socket 495 SocketMode=0666 496 497 [Install] 498 WantedBy=sockets.target 499 ` 500 const sock2ExpectedFmt = `[Unit] 501 # Auto-generated, DO NOT EDIT 502 Description=Socket sock2 for snap application some-snap.app 503 Requires=%s-some\x2dsnap-44.mount 504 After=%s-some\x2dsnap-44.mount 505 X-Snappy=yes 506 507 [Socket] 508 Service=snap.some-snap.app.service 509 FileDescriptorName=sock2 510 ListenStream=%s/sock2.socket 511 512 [Install] 513 WantedBy=sockets.target 514 ` 515 516 si := &snap.Info{ 517 SuggestedName: "some-snap", 518 Version: "1.0", 519 SideInfo: snap.SideInfo{Revision: snap.R(44)}, 520 } 521 service := &snap.AppInfo{ 522 Snap: si, 523 Name: "app", 524 Command: "bin/foo start", 525 Daemon: "simple", 526 DaemonScope: snap.SystemDaemon, 527 Plugs: map[string]*snap.PlugInfo{"network-bind": {Interface: "network-bind"}}, 528 Sockets: map[string]*snap.SocketInfo{ 529 "sock1": { 530 Name: "sock1", 531 ListenStream: "$SNAP_DATA/sock1.socket", 532 SocketMode: 0666, 533 }, 534 "sock2": { 535 Name: "sock2", 536 ListenStream: "$SNAP_DATA/sock2.socket", 537 }, 538 }, 539 } 540 service.Sockets["sock1"].App = service 541 service.Sockets["sock2"].App = service 542 543 sock1Path := filepath.Join(dirs.SnapServicesDir, "snap.some-snap.app.sock1.socket") 544 sock2Path := filepath.Join(dirs.SnapServicesDir, "snap.some-snap.app.sock2.socket") 545 sock1Expected := fmt.Sprintf(sock1ExpectedFmt, mountUnitPrefix, mountUnitPrefix, si.DataDir()) 546 sock2Expected := fmt.Sprintf(sock2ExpectedFmt, mountUnitPrefix, mountUnitPrefix, si.DataDir()) 547 548 generatedWrapper, err := wrappers.GenerateSnapServiceFile(service, nil) 549 c.Assert(err, IsNil) 550 c.Assert(strings.Contains(string(generatedWrapper), "[Install]"), Equals, false) 551 c.Assert(strings.Contains(string(generatedWrapper), "WantedBy=multi-user.target"), Equals, false) 552 553 generatedSockets, err := wrappers.GenerateSnapSocketFiles(service) 554 c.Assert(err, IsNil) 555 c.Assert(generatedSockets, Not(IsNil)) 556 c.Assert(*generatedSockets, HasLen, 2) 557 c.Assert(*generatedSockets, DeepEquals, map[string][]byte{ 558 sock1Path: []byte(sock1Expected), 559 sock2Path: []byte(sock2Expected), 560 }) 561 } 562 563 func (s *servicesWrapperGenSuite) TestServiceAfterBefore(c *C) { 564 const expectedServiceFmt = `[Unit] 565 # Auto-generated, DO NOT EDIT 566 Description=Service for snap application snap.app 567 Requires=%s-snap-44.mount 568 Wants=network.target 569 After=%s-snap-44.mount network.target %s snapd.apparmor.service 570 Before=%s 571 X-Snappy=yes 572 573 [Service] 574 EnvironmentFile=-/etc/environment 575 ExecStart=/usr/bin/snap run snap.app 576 SyslogIdentifier=snap.app 577 Restart=%s 578 WorkingDirectory=/var/snap/snap/44 579 TimeoutStopSec=30 580 Type=%s 581 582 [Install] 583 WantedBy=multi-user.target 584 ` 585 586 service := &snap.AppInfo{ 587 Snap: &snap.Info{ 588 SuggestedName: "snap", 589 Version: "0.3.4", 590 SideInfo: snap.SideInfo{Revision: snap.R(44)}, 591 Apps: map[string]*snap.AppInfo{ 592 "foo": { 593 Name: "foo", 594 Snap: &snap.Info{SuggestedName: "snap"}, 595 Daemon: "forking", 596 DaemonScope: snap.SystemDaemon, 597 }, 598 "bar": { 599 Name: "bar", 600 Snap: &snap.Info{SuggestedName: "snap"}, 601 Daemon: "forking", 602 DaemonScope: snap.SystemDaemon, 603 }, 604 "zed": { 605 Name: "zed", 606 Snap: &snap.Info{SuggestedName: "snap"}, 607 Daemon: "forking", 608 DaemonScope: snap.SystemDaemon, 609 }, 610 "baz": { 611 Name: "baz", 612 Snap: &snap.Info{SuggestedName: "snap"}, 613 Daemon: "forking", 614 DaemonScope: snap.SystemDaemon, 615 }, 616 }, 617 }, 618 Name: "app", 619 Command: "bin/foo start", 620 Daemon: "simple", 621 DaemonScope: snap.SystemDaemon, 622 StopTimeout: timeout.DefaultTimeout, 623 } 624 625 for _, tc := range []struct { 626 after []string 627 before []string 628 generatedAfter string 629 generatedBefore string 630 }{{ 631 after: []string{"bar", "zed"}, 632 generatedAfter: "snap.snap.bar.service snap.snap.zed.service", 633 before: []string{"foo", "baz"}, 634 generatedBefore: "snap.snap.foo.service snap.snap.baz.service", 635 }, { 636 after: []string{"bar"}, 637 generatedAfter: "snap.snap.bar.service", 638 before: []string{"foo"}, 639 generatedBefore: "snap.snap.foo.service", 640 }, 641 } { 642 c.Logf("tc: %v", tc) 643 service.After = tc.after 644 service.Before = tc.before 645 generatedWrapper, err := wrappers.GenerateSnapServiceFile(service, nil) 646 c.Assert(err, IsNil) 647 648 expectedService := fmt.Sprintf(expectedServiceFmt, mountUnitPrefix, mountUnitPrefix, 649 tc.generatedAfter, tc.generatedBefore, "on-failure", "simple") 650 c.Assert(string(generatedWrapper), Equals, expectedService) 651 } 652 } 653 654 func (s *servicesWrapperGenSuite) TestServiceTimerUnit(c *C) { 655 const expectedServiceFmt = `[Unit] 656 # Auto-generated, DO NOT EDIT 657 Description=Timer app for snap application snap.app 658 Requires=%s-snap-44.mount 659 After=%s-snap-44.mount 660 X-Snappy=yes 661 662 [Timer] 663 Unit=snap.snap.app.service 664 OnCalendar=*-*-* 10:00 665 OnCalendar=*-*-* 11:00 666 667 [Install] 668 WantedBy=timers.target 669 ` 670 671 expectedService := fmt.Sprintf(expectedServiceFmt, mountUnitPrefix, mountUnitPrefix) 672 service := &snap.AppInfo{ 673 Snap: &snap.Info{ 674 SuggestedName: "snap", 675 Version: "0.3.4", 676 SideInfo: snap.SideInfo{Revision: snap.R(44)}, 677 }, 678 Name: "app", 679 Command: "bin/foo start", 680 Daemon: "simple", 681 DaemonScope: snap.SystemDaemon, 682 StopTimeout: timeout.DefaultTimeout, 683 Timer: &snap.TimerInfo{ 684 Timer: "10:00-12:00/2", 685 }, 686 } 687 service.Timer.App = service 688 689 generatedWrapper, err := wrappers.GenerateSnapTimerFile(service) 690 c.Assert(err, IsNil) 691 692 c.Logf("timer: \n%v\n", string(generatedWrapper)) 693 c.Assert(string(generatedWrapper), Equals, expectedService) 694 } 695 696 func (s *servicesWrapperGenSuite) TestServiceTimerUnitBadTimer(c *C) { 697 service := &snap.AppInfo{ 698 Snap: &snap.Info{ 699 SuggestedName: "snap", 700 Version: "0.3.4", 701 SideInfo: snap.SideInfo{Revision: snap.R(44)}, 702 }, 703 Name: "app", 704 Command: "bin/foo start", 705 Daemon: "simple", 706 DaemonScope: snap.SystemDaemon, 707 StopTimeout: timeout.DefaultTimeout, 708 Timer: &snap.TimerInfo{ 709 Timer: "bad-timer", 710 }, 711 } 712 service.Timer.App = service 713 714 generatedWrapper, err := wrappers.GenerateSnapTimerFile(service) 715 c.Assert(err, ErrorMatches, `cannot parse "bad-timer": "bad" is not a valid weekday`) 716 c.Assert(generatedWrapper, IsNil) 717 } 718 719 func (s *servicesWrapperGenSuite) TestServiceTimerServiceUnit(c *C) { 720 const expectedServiceFmt = `[Unit] 721 # Auto-generated, DO NOT EDIT 722 Description=Service for snap application snap.app 723 Requires=%s-snap-44.mount 724 Wants=network.target 725 After=%s-snap-44.mount network.target snapd.apparmor.service 726 X-Snappy=yes 727 728 [Service] 729 EnvironmentFile=-/etc/environment 730 ExecStart=/usr/bin/snap run --timer="10:00-12:00,,mon,23:00~01:00/2" snap.app 731 SyslogIdentifier=snap.app 732 Restart=%s 733 WorkingDirectory=/var/snap/snap/44 734 TimeoutStopSec=30 735 Type=%s 736 ` 737 738 expectedService := fmt.Sprintf(expectedServiceFmt, mountUnitPrefix, mountUnitPrefix, "on-failure", "simple") 739 service := &snap.AppInfo{ 740 Snap: &snap.Info{ 741 SuggestedName: "snap", 742 Version: "0.3.4", 743 SideInfo: snap.SideInfo{Revision: snap.R(44)}, 744 }, 745 Name: "app", 746 Command: "bin/foo start", 747 Daemon: "simple", 748 DaemonScope: snap.SystemDaemon, 749 StopTimeout: timeout.DefaultTimeout, 750 Timer: &snap.TimerInfo{ 751 Timer: "10:00-12:00,,mon,23:00~01:00/2", 752 }, 753 } 754 755 generatedWrapper, err := wrappers.GenerateSnapServiceFile(service, nil) 756 c.Assert(err, IsNil) 757 758 c.Logf("service: \n%v\n", string(generatedWrapper)) 759 c.Assert(string(generatedWrapper), Equals, expectedService) 760 } 761 762 func (s *servicesWrapperGenSuite) TestTimerGenerateSchedules(c *C) { 763 systemdAnalyzePath, _ := exec.LookPath("systemd-analyze") 764 if systemdAnalyzePath != "" { 765 // systemd-analyze is in the path, but it will fail if the 766 // daemon is not running (as it happens in LP builds) and writes 767 // the following to stderr: 768 // Failed to create bus connection: No such file or directory 769 cmd := exec.Command(systemdAnalyzePath, "calendar", "12:00") 770 err := cmd.Run() 771 if err != nil { 772 // turns out it's not usable, disable extra verification 773 fmt.Fprintln(os.Stderr, `WARNING: systemd-analyze not usable, cannot validate a known schedule "12:00"`) 774 systemdAnalyzePath = "" 775 } 776 } 777 778 if systemdAnalyzePath == "" { 779 fmt.Fprintln(os.Stderr, "WARNING: generated schedules will not be validated by systemd-analyze") 780 } 781 782 for _, t := range []struct { 783 in string 784 expected []string 785 randomized bool 786 }{{ 787 in: "9:00-11:00,,20:00-22:00", 788 expected: []string{"*-*-* 09:00", "*-*-* 20:00"}, 789 }, { 790 in: "9:00-11:00/2,,20:00", 791 expected: []string{"*-*-* 09:00", "*-*-* 10:00", "*-*-* 20:00"}, 792 }, { 793 in: "9:00~11:00/2,,20:00", 794 expected: []string{`\*-\*-\* 09:[0-5][0-9]`, `\*-\*-\* 10:[0-5][0-9]`, `\*-\*-\* 20:00`}, 795 randomized: true, 796 }, { 797 in: "mon,10:00,,fri,15:00", 798 expected: []string{"Mon *-*-* 10:00", "Fri *-*-* 15:00"}, 799 }, { 800 in: "mon-fri,10:00-11:00", 801 expected: []string{"Mon,Tue,Wed,Thu,Fri *-*-* 10:00"}, 802 }, { 803 in: "fri-mon,10:00-11:00", 804 expected: []string{"Fri,Sat,Sun,Mon *-*-* 10:00"}, 805 }, { 806 in: "mon5,10:00", 807 expected: []string{"Mon *-*-22,23,24,25,26,27,28,29,30,31 10:00"}, 808 }, { 809 in: "mon2,10:00", 810 expected: []string{"Mon *-*-8,9,10,11,12,13,14 10:00"}, 811 }, { 812 in: "mon2,mon1,10:00", 813 expected: []string{"Mon *-*-8,9,10,11,12,13,14 10:00", "Mon *-*-1,2,3,4,5,6,7 10:00"}, 814 }, { 815 // (deprecated syntax, reduced to mon1-mon) 816 // NOTE: non-representable, assumes that service runner does the 817 // filtering of when to run the timer 818 in: "mon1-mon3,10:00", 819 expected: []string{"*-*-8,9,10,11,12,13,14 10:00", "*-*-1,2,3,4,5,6,7 10:00"}, 820 }, { 821 in: "mon,10:00~12:00,,fri,15:00", 822 expected: []string{`Mon \*-\*-\* 1[01]:[0-5][0-9]`, `Fri \*-\*-\* 15:00`}, 823 randomized: true, 824 }, { 825 in: "23:00~24:00/4", 826 expected: []string{`\*-\*-\* 23:[01][0-9]`, `\*-\*-\* 23:[12][0-9]`, `\*-\*-\* 23:[34][0-9]`, `*-*-* 23:[45][0-9]`}, 827 randomized: true, 828 }, { 829 in: "23:00~01:00/4", 830 expected: []string{`\*-\*-\* 23:[0-2][0-9]`, `\*-\*-\* 23:[3-5][0-9]`, `\*-\*-\* 00:[0-2][0-9]`, `\*-\*-\* 00:[3-5][0-9]`}, 831 randomized: true, 832 }, { 833 in: "23:00-01:00/4", 834 expected: []string{`*-*-* 23:00`, `*-*-* 23:30`, `*-*-* 00:00`, `*-*-* 00:30`}, 835 }, { 836 in: "24:00", 837 expected: []string{`*-*-* 00:00`}, 838 }, { 839 // NOTE: non-representable, assumes that service runner does the 840 // filtering of when to run the timer 841 in: "fri-mon1,10:00", 842 expected: []string{"*-*-22,23,24,25,26,27,28,29,30,31 10:00", "*-*-1,2,3,4,5,6,7 10:00"}, 843 }, { 844 // NOTE: non-representable, assumes that service runner does the 845 // filtering of when to run the timer 846 in: "mon5-fri,10:00", 847 expected: []string{"*-*-29,30,31 10:00", "*-*-1,2,3,4,5,6,7 10:00", "*-*-22,23,24,25,26,27,28 10:00"}, 848 }, { 849 // NOTE: non-representable, assumes that service runner does the 850 // filtering of when to run the timer 851 in: "mon4-fri,10:00", 852 expected: []string{"*-*-29,30,31 10:00", "*-*-1,2,3,4,5,6,7 10:00", "*-*-22,23,24,25,26,27,28 10:00"}, 853 }, { 854 // NOTE: non-representable, assumes that service runner does the 855 // filtering of when to run the timer 856 in: "mon-fri2,10:00", 857 expected: []string{"*-*-1,2,3,4,5,6,7 10:00", "*-*-8,9,10,11,12,13,14 10:00"}, 858 }, { 859 // NOTE: non-representable, assumes that service runner does the 860 // filtering of when to run the timer 861 in: "mon-fri5,10:00", 862 expected: []string{"*-*-29,30,31 10:00", "*-*-22,23,24,25,26,27,28 10:00"}, 863 }, { 864 // NOTE: non-representable, assumes that service runner does the 865 // filtering of when to run the timer 866 in: "mon1-mon,10:00", 867 expected: []string{"*-*-8,9,10,11,12,13,14 10:00", "*-*-1,2,3,4,5,6,7 10:00"}, 868 }, { 869 in: "mon", 870 expected: []string{"Mon *-*-*"}, 871 }, { 872 in: "mon,fri", 873 expected: []string{"Mon *-*-*", "Fri *-*-*"}, 874 }, { 875 in: "mon2,mon1", 876 expected: []string{"Mon *-*-8,9,10,11,12,13,14", "Mon *-*-1,2,3,4,5,6,7"}, 877 }} { 878 c.Logf("trying %+v", t) 879 880 schedule, err := timeutil.ParseSchedule(t.in) 881 c.Check(err, IsNil) 882 883 timer := wrappers.GenerateOnCalendarSchedules(schedule) 884 c.Check(timer, Not(IsNil)) 885 if !t.randomized { 886 c.Check(timer, DeepEquals, t.expected) 887 } else { 888 c.Assert(timer, HasLen, len(t.expected)) 889 for i := range timer { 890 c.Check(timer[i], Matches, t.expected[i]) 891 } 892 } 893 894 if systemdAnalyzePath != "" { 895 cmd := exec.Command(systemdAnalyzePath, append([]string{"calendar"}, timer...)...) 896 out, err := cmd.CombinedOutput() 897 c.Check(err, IsNil, Commentf("systemd-analyze failed with output:\n%s", string(out))) 898 } 899 } 900 } 901 902 func (s *servicesWrapperGenSuite) TestKillModeSig(c *C) { 903 for _, rm := range []string{"sigterm", "sighup", "sigusr1", "sigusr2"} { 904 service := &snap.AppInfo{ 905 Snap: &snap.Info{ 906 SuggestedName: "snap", 907 Version: "0.3.4", 908 SideInfo: snap.SideInfo{Revision: snap.R(44)}, 909 }, 910 Name: "app", 911 Command: "bin/foo start", 912 Daemon: "simple", 913 DaemonScope: snap.SystemDaemon, 914 StopMode: snap.StopModeType(rm), 915 } 916 917 generatedWrapper, err := wrappers.GenerateSnapServiceFile(service, nil) 918 c.Assert(err, IsNil) 919 920 c.Check(string(generatedWrapper), Equals, fmt.Sprintf(`[Unit] 921 # Auto-generated, DO NOT EDIT 922 Description=Service for snap application snap.app 923 Requires=%s-snap-44.mount 924 Wants=network.target 925 After=%s-snap-44.mount network.target snapd.apparmor.service 926 X-Snappy=yes 927 928 [Service] 929 EnvironmentFile=-/etc/environment 930 ExecStart=/usr/bin/snap run snap.app 931 SyslogIdentifier=snap.app 932 Restart=on-failure 933 WorkingDirectory=/var/snap/snap/44 934 TimeoutStopSec=30 935 Type=simple 936 KillMode=process 937 KillSignal=%s 938 939 [Install] 940 WantedBy=multi-user.target 941 `, mountUnitPrefix, mountUnitPrefix, strings.ToUpper(rm))) 942 } 943 } 944 945 func (s *servicesWrapperGenSuite) TestRestartDelay(c *C) { 946 service := &snap.AppInfo{ 947 Snap: &snap.Info{ 948 SuggestedName: "snap", 949 Version: "0.3.4", 950 SideInfo: snap.SideInfo{Revision: snap.R(44)}, 951 }, 952 Name: "app", 953 Command: "bin/foo start", 954 Daemon: "simple", 955 DaemonScope: snap.SystemDaemon, 956 RestartDelay: timeout.Timeout(20 * time.Second), 957 } 958 959 generatedWrapper, err := wrappers.GenerateSnapServiceFile(service, nil) 960 c.Assert(err, IsNil) 961 962 c.Check(string(generatedWrapper), Equals, fmt.Sprintf(`[Unit] 963 # Auto-generated, DO NOT EDIT 964 Description=Service for snap application snap.app 965 Requires=%s-snap-44.mount 966 Wants=network.target 967 After=%s-snap-44.mount network.target snapd.apparmor.service 968 X-Snappy=yes 969 970 [Service] 971 EnvironmentFile=-/etc/environment 972 ExecStart=/usr/bin/snap run snap.app 973 SyslogIdentifier=snap.app 974 Restart=on-failure 975 RestartSec=20 976 WorkingDirectory=/var/snap/snap/44 977 TimeoutStopSec=30 978 Type=simple 979 980 [Install] 981 WantedBy=multi-user.target 982 `, mountUnitPrefix, mountUnitPrefix)) 983 } 984 985 func (s *servicesWrapperGenSuite) TestVitalityScore(c *C) { 986 service := &snap.AppInfo{ 987 Snap: &snap.Info{ 988 SuggestedName: "snap", 989 Version: "0.3.4", 990 SideInfo: snap.SideInfo{Revision: snap.R(44)}, 991 }, 992 Name: "app", 993 Command: "bin/foo start", 994 Daemon: "simple", 995 DaemonScope: snap.SystemDaemon, 996 RestartDelay: timeout.Timeout(20 * time.Second), 997 } 998 999 opts := &wrappers.AddSnapServicesOptions{VitalityRank: 1} 1000 generatedWrapper, err := wrappers.GenerateSnapServiceFile(service, opts) 1001 c.Assert(err, IsNil) 1002 1003 c.Check(string(generatedWrapper), Equals, fmt.Sprintf(`[Unit] 1004 # Auto-generated, DO NOT EDIT 1005 Description=Service for snap application snap.app 1006 Requires=%s-snap-44.mount 1007 Wants=network.target 1008 After=%s-snap-44.mount network.target snapd.apparmor.service 1009 X-Snappy=yes 1010 1011 [Service] 1012 EnvironmentFile=-/etc/environment 1013 ExecStart=/usr/bin/snap run snap.app 1014 SyslogIdentifier=snap.app 1015 Restart=on-failure 1016 RestartSec=20 1017 WorkingDirectory=/var/snap/snap/44 1018 TimeoutStopSec=30 1019 Type=simple 1020 OOMScoreAdjust=-899 1021 1022 [Install] 1023 WantedBy=multi-user.target 1024 `, mountUnitPrefix, mountUnitPrefix)) 1025 }