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  }