github.com/Lephar/snapd@v0.0.0-20210825215435-c7fba9cef4d2/overlord/configstate/configcore/vitality_test.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2020 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 configcore_test
    21  
    22  import (
    23  	"path/filepath"
    24  	"strconv"
    25  	"strings"
    26  
    27  	. "gopkg.in/check.v1"
    28  
    29  	"github.com/snapcore/snapd/asserts"
    30  	"github.com/snapcore/snapd/asserts/assertstest"
    31  	"github.com/snapcore/snapd/dirs"
    32  	"github.com/snapcore/snapd/gadget/quantity"
    33  	"github.com/snapcore/snapd/overlord/configstate/config"
    34  	"github.com/snapcore/snapd/overlord/configstate/configcore"
    35  	"github.com/snapcore/snapd/overlord/servicestate"
    36  	"github.com/snapcore/snapd/overlord/servicestate/servicestatetest"
    37  	"github.com/snapcore/snapd/overlord/snapstate"
    38  	"github.com/snapcore/snapd/overlord/snapstate/snapstatetest"
    39  	"github.com/snapcore/snapd/snap"
    40  	"github.com/snapcore/snapd/snap/snaptest"
    41  	"github.com/snapcore/snapd/systemd"
    42  	"github.com/snapcore/snapd/systemd/systemdtest"
    43  	"github.com/snapcore/snapd/testutil"
    44  )
    45  
    46  type vitalitySuite struct {
    47  	configcoreSuite
    48  }
    49  
    50  var _ = Suite(&vitalitySuite{})
    51  
    52  func (s *vitalitySuite) SetUpTest(c *C) {
    53  	s.configcoreSuite.SetUpTest(c)
    54  
    55  	uc18model := assertstest.FakeAssertion(map[string]interface{}{
    56  		"type":         "model",
    57  		"authority-id": "canonical",
    58  		"series":       "16",
    59  		"brand-id":     "canonical",
    60  		"model":        "pc",
    61  		"gadget":       "pc",
    62  		"kernel":       "kernel",
    63  		"architecture": "amd64",
    64  		"base":         "core18",
    65  	}).(*asserts.Model)
    66  
    67  	s.AddCleanup(snapstatetest.MockDeviceModel(uc18model))
    68  }
    69  
    70  func (s *vitalitySuite) TestConfigureVitalityUnhappyName(c *C) {
    71  	err := configcore.Run(classicDev, &mockConf{
    72  		state: s.state,
    73  		changes: map[string]interface{}{
    74  			"resilience.vitality-hint": "-invalid-snap-name!yf",
    75  		},
    76  	})
    77  	c.Assert(err, ErrorMatches, `cannot set "resilience.vitality-hint": invalid snap name: ".*"`)
    78  }
    79  
    80  func (s *vitalitySuite) TestConfigureVitalityNoSnapd(c *C) {
    81  	err := configcore.Run(classicDev, &mockConf{
    82  		state: s.state,
    83  		changes: map[string]interface{}{
    84  			"resilience.vitality-hint": "snapd",
    85  		},
    86  	})
    87  	c.Assert(err, ErrorMatches, `cannot set "resilience.vitality-hint": snapd snap vitality cannot be changed`)
    88  }
    89  
    90  func (s *vitalitySuite) TestConfigureVitalityhappyName(c *C) {
    91  	err := configcore.Run(classicDev, &mockConf{
    92  		state: s.state,
    93  		changes: map[string]interface{}{
    94  			"resilience.vitality-hint": "valid-snapname",
    95  		},
    96  	})
    97  	c.Assert(err, IsNil)
    98  	// no snap named "valid-snapname" is installed, so no systemd action
    99  	c.Check(s.systemctlArgs, HasLen, 0)
   100  }
   101  
   102  var mockSnapWithService = `name: test-snap
   103  version: 1.0
   104  apps:
   105   foo:
   106    daemon: simple
   107  `
   108  
   109  func (s *vitalitySuite) TestConfigureVitalityWithValidSnapUC16(c *C) {
   110  	uc16model := assertstest.FakeAssertion(map[string]interface{}{
   111  		"type":         "model",
   112  		"authority-id": "canonical",
   113  		"series":       "16",
   114  		"brand-id":     "canonical",
   115  		"model":        "pc",
   116  		"gadget":       "pc",
   117  		"kernel":       "kernel",
   118  		"architecture": "amd64",
   119  	}).(*asserts.Model)
   120  
   121  	defer snapstatetest.MockDeviceModel(uc16model)()
   122  
   123  	s.testConfigureVitalityWithValidSnap(c, false)
   124  }
   125  
   126  func (s *vitalitySuite) TestConfigureVitalityWithValidSnapUC18(c *C) {
   127  	s.testConfigureVitalityWithValidSnap(c, true)
   128  }
   129  
   130  func (s *vitalitySuite) testConfigureVitalityWithValidSnap(c *C, uc18 bool) {
   131  	si := &snap.SideInfo{RealName: "test-snap", Revision: snap.R(1)}
   132  	snaptest.MockSnap(c, mockSnapWithService, si)
   133  	s.state.Lock()
   134  	snapstate.Set(s.state, "test-snap", &snapstate.SnapState{
   135  		Sequence: []*snap.SideInfo{si},
   136  		Current:  snap.R(1),
   137  		Active:   true,
   138  		SnapType: "app",
   139  	})
   140  	s.state.Unlock()
   141  
   142  	err := configcore.Run(classicDev, &mockConf{
   143  		state: s.state,
   144  		changes: map[string]interface{}{
   145  			"resilience.vitality-hint": "unrelated,test-snap",
   146  		},
   147  	})
   148  	c.Assert(err, IsNil)
   149  	svcName := "snap.test-snap.foo.service"
   150  	c.Check(s.systemctlArgs, DeepEquals, [][]string{
   151  		{"daemon-reload"},
   152  		{"is-enabled", "snap.test-snap.foo.service"},
   153  		{"enable", "snap.test-snap.foo.service"},
   154  		{"start", "snap.test-snap.foo.service"},
   155  	})
   156  	svcPath := filepath.Join(dirs.SnapServicesDir, svcName)
   157  	c.Check(svcPath, testutil.FileContains, "\nOOMScoreAdjust=-898\n")
   158  	if uc18 {
   159  		c.Check(svcPath, testutil.FileContains, "\nWants=usr-lib-snapd.mount\n")
   160  	}
   161  }
   162  
   163  func (s *vitalitySuite) TestConfigureVitalityWithQuotaGroup(c *C) {
   164  	r := servicestate.MockSystemdVersion(248)
   165  	defer r()
   166  
   167  	si := &snap.SideInfo{RealName: "test-snap", Revision: snap.R(1)}
   168  	snaptest.MockSnap(c, mockSnapWithService, si)
   169  	s.state.Lock()
   170  	snapstate.Set(s.state, "test-snap", &snapstate.SnapState{
   171  		Sequence: []*snap.SideInfo{si},
   172  		Current:  snap.R(1),
   173  		Active:   true,
   174  		SnapType: "app",
   175  	})
   176  
   177  	// CreateQuota is calling "systemctl.Restart", which needs to be mocked
   178  	systemctlRestorer := systemd.MockSystemctl(func(cmd ...string) (buf []byte, err error) {
   179  		s.systemctlArgs = append(s.systemctlArgs, cmd)
   180  		if out := systemdtest.HandleMockAllUnitsActiveOutput(cmd, nil); out != nil {
   181  			return out, nil
   182  		}
   183  
   184  		if cmd[0] == "show" {
   185  			return []byte("ActiveState=inactive\n"), nil
   186  		}
   187  		return nil, nil
   188  	})
   189  	s.AddCleanup(systemctlRestorer)
   190  	tr := config.NewTransaction(s.state)
   191  	tr.Set("core", "experimental.quota-groups", true)
   192  	tr.Commit()
   193  
   194  	// make a new quota group with this snap in it
   195  	err := servicestatetest.MockQuotaInState(s.state, "foogroup", "", []string{"test-snap"}, quantity.SizeMiB)
   196  	c.Assert(err, IsNil)
   197  
   198  	s.state.Unlock()
   199  
   200  	err = configcore.Run(classicDev, &mockConf{
   201  		state: s.state,
   202  		changes: map[string]interface{}{
   203  			"resilience.vitality-hint": "unrelated,test-snap",
   204  		},
   205  	})
   206  	c.Assert(err, IsNil)
   207  	svcName := "snap.test-snap.foo.service"
   208  	c.Check(s.systemctlArgs, DeepEquals, [][]string{
   209  		{"daemon-reload"},
   210  		{"is-enabled", "snap.test-snap.foo.service"},
   211  		{"enable", "snap.test-snap.foo.service"},
   212  		{"start", "snap.test-snap.foo.service"},
   213  	})
   214  	svcPath := filepath.Join(dirs.SnapServicesDir, svcName)
   215  	c.Check(svcPath, testutil.FileContains, "\nOOMScoreAdjust=-898\n")
   216  	c.Check(svcPath, testutil.FileContains, "\nSlice=snap.foogroup.slice\n")
   217  }
   218  
   219  func (s *vitalitySuite) TestConfigureVitalityHintTooMany(c *C) {
   220  	l := make([]string, 101)
   221  	for i := range l {
   222  		l[i] = strconv.Itoa(i)
   223  	}
   224  	manyStr := strings.Join(l, ",")
   225  	err := configcore.Run(classicDev, &mockConf{
   226  		state: s.state,
   227  		changes: map[string]interface{}{
   228  			"resilience.vitality-hint": manyStr,
   229  		},
   230  	})
   231  	c.Assert(err, ErrorMatches, `cannot set more than 100 snaps in "resilience.vitality-hint": got 101`)
   232  }
   233  
   234  func (s *vitalitySuite) TestConfigureVitalityManySnaps(c *C) {
   235  	for _, snapName := range []string{"snap1", "snap2", "snap3"} {
   236  		si := &snap.SideInfo{RealName: snapName, Revision: snap.R(1)}
   237  		snaptest.MockSnap(c, mockSnapWithService, si)
   238  		s.state.Lock()
   239  		snapstate.Set(s.state, snapName, &snapstate.SnapState{
   240  			Sequence: []*snap.SideInfo{si},
   241  			Current:  snap.R(1),
   242  			Active:   true,
   243  			SnapType: "app",
   244  		})
   245  		s.state.Unlock()
   246  	}
   247  
   248  	// snap1,snap2,snap3
   249  	err := configcore.Run(classicDev, &mockConf{
   250  		state: s.state,
   251  		changes: map[string]interface{}{
   252  			"resilience.vitality-hint": "snap1,snap2,snap3",
   253  		},
   254  	})
   255  	c.Assert(err, IsNil)
   256  	// test
   257  	svcPath := filepath.Join(dirs.SnapServicesDir, "snap.snap1.foo.service")
   258  	c.Check(svcPath, testutil.FileContains, "\nOOMScoreAdjust=-899\n")
   259  	svcPath = filepath.Join(dirs.SnapServicesDir, "snap.snap2.foo.service")
   260  	c.Check(svcPath, testutil.FileContains, "\nOOMScoreAdjust=-898\n")
   261  	svcPath = filepath.Join(dirs.SnapServicesDir, "snap.snap3.foo.service")
   262  	c.Check(svcPath, testutil.FileContains, "\nOOMScoreAdjust=-897\n")
   263  }
   264  
   265  func (s *vitalitySuite) TestConfigureVitalityManySnapsDelta(c *C) {
   266  	for _, snapName := range []string{"snap1", "snap2", "snap3"} {
   267  		si := &snap.SideInfo{RealName: snapName, Revision: snap.R(1)}
   268  		snaptest.MockSnap(c, mockSnapWithService, si)
   269  		s.state.Lock()
   270  		snapstate.Set(s.state, snapName, &snapstate.SnapState{
   271  			Sequence: []*snap.SideInfo{si},
   272  			Current:  snap.R(1),
   273  			Active:   true,
   274  			SnapType: "app",
   275  		})
   276  		s.state.Unlock()
   277  	}
   278  
   279  	// snap1,snap2,snap3 switch to snap3,snap1
   280  	err := configcore.Run(classicDev, &mockConf{
   281  		state: s.state,
   282  		conf: map[string]interface{}{
   283  			"resilience.vitality-hint": "snap1,snap2,snap3",
   284  		},
   285  		changes: map[string]interface{}{
   286  			"resilience.vitality-hint": "snap3,snap1",
   287  		},
   288  	})
   289  	c.Assert(err, IsNil)
   290  	// test that snap1,snap3 got the new rank
   291  	svcPath := filepath.Join(dirs.SnapServicesDir, "snap.snap1.foo.service")
   292  	c.Check(svcPath, testutil.FileContains, "\nOOMScoreAdjust=-898")
   293  	// and that snap2 no longer has a OOMScoreAdjust setting
   294  	svcPath = filepath.Join(dirs.SnapServicesDir, "snap.snap2.foo.service")
   295  	c.Check(svcPath, Not(testutil.FileContains), "\nOOMScoreAdjust=")
   296  	svcPath = filepath.Join(dirs.SnapServicesDir, "snap.snap3.foo.service")
   297  	c.Check(svcPath, testutil.FileContains, "\nOOMScoreAdjust=-899\n")
   298  }
   299  
   300  func (s *vitalitySuite) TestConfigureVitalityManySnapsOneRemovedOneUnchanged(c *C) {
   301  	for _, snapName := range []string{"snap1", "snap2", "snap3"} {
   302  		si := &snap.SideInfo{RealName: snapName, Revision: snap.R(1)}
   303  		snaptest.MockSnap(c, mockSnapWithService, si)
   304  		s.state.Lock()
   305  		snapstate.Set(s.state, snapName, &snapstate.SnapState{
   306  			Sequence: []*snap.SideInfo{si},
   307  			Current:  snap.R(1),
   308  			Active:   true,
   309  			SnapType: "app",
   310  		})
   311  		s.state.Unlock()
   312  	}
   313  
   314  	// first run generates the snap1,snap2 configs
   315  	err := configcore.Run(classicDev, &mockConf{
   316  		state: s.state,
   317  		changes: map[string]interface{}{
   318  			"resilience.vitality-hint": "snap1,snap2",
   319  		},
   320  	})
   321  	c.Assert(err, IsNil)
   322  	svcPath := filepath.Join(dirs.SnapServicesDir, "snap.snap1.foo.service")
   323  	c.Check(svcPath, testutil.FileContains, "\nOOMScoreAdjust=-899")
   324  	svcPath = filepath.Join(dirs.SnapServicesDir, "snap.snap2.foo.service")
   325  	c.Check(svcPath, testutil.FileContains, "\nOOMScoreAdjust=-898\n")
   326  	c.Check(s.systemctlArgs, testutil.DeepContains, []string{"start", "snap.snap1.foo.service"})
   327  	c.Check(s.systemctlArgs, testutil.DeepContains, []string{"start", "snap.snap2.foo.service"})
   328  	s.systemctlArgs = nil
   329  
   330  	// now we change the configuration and set snap1,snap3
   331  	err = configcore.Run(classicDev, &mockConf{
   332  		state: s.state,
   333  		conf: map[string]interface{}{
   334  			"resilience.vitality-hint": "snap1,snap2",
   335  		},
   336  		changes: map[string]interface{}{
   337  			"resilience.vitality-hint": "snap1,snap3",
   338  		},
   339  	})
   340  	c.Assert(err, IsNil)
   341  	// test that snap1 is unchanged
   342  	svcPath = filepath.Join(dirs.SnapServicesDir, "snap.snap1.foo.service")
   343  	c.Check(svcPath, testutil.FileContains, "\nOOMScoreAdjust=-899")
   344  	// and that snap2 no longer has a OOMScoreAdjust setting
   345  	svcPath = filepath.Join(dirs.SnapServicesDir, "snap.snap2.foo.service")
   346  	c.Check(svcPath, Not(testutil.FileContains), "\nOOMScoreAdjust=")
   347  	// snap3 got added
   348  	svcPath = filepath.Join(dirs.SnapServicesDir, "snap.snap3.foo.service")
   349  	c.Check(svcPath, testutil.FileContains, "\nOOMScoreAdjust=-898\n")
   350  
   351  	// ensure that snap1 did not get started again (it is unchanged)
   352  	c.Check(s.systemctlArgs, Not(testutil.DeepContains), []string{"start", "snap.snap1.foo.service"})
   353  	// snap2 changed (no OOMScoreAdjust anymore) so needs restart
   354  	c.Check(s.systemctlArgs, testutil.DeepContains, []string{"start", "snap.snap2.foo.service"})
   355  	// snap3 changed so needs restart
   356  	c.Check(s.systemctlArgs, testutil.DeepContains, []string{"start", "snap.snap3.foo.service"})
   357  }
   358  
   359  func (s *vitalitySuite) TestConfigureVitalityNotActiveSnap(c *C) {
   360  	si := &snap.SideInfo{RealName: "test-snap", Revision: snap.R(1)}
   361  	snaptest.MockSnap(c, mockSnapWithService, si)
   362  	s.state.Lock()
   363  	snapstate.Set(s.state, "test-snap", &snapstate.SnapState{
   364  		Sequence: []*snap.SideInfo{si},
   365  		Current:  snap.R(1),
   366  		Active:   false,
   367  		SnapType: "app",
   368  	})
   369  	s.state.Unlock()
   370  
   371  	err := configcore.Run(classicDev, &mockConf{
   372  		state: s.state,
   373  		changes: map[string]interface{}{
   374  			"resilience.vitality-hint": "unrelated,test-snap",
   375  		},
   376  	})
   377  	c.Assert(err, IsNil)
   378  	c.Check(s.systemctlArgs, HasLen, 0)
   379  }