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