github.com/freetocompute/snapd@v0.0.0-20210618182524-2fb355d72fd9/overlord/servicestate/quota_control_test.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2021 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 servicestate_test
    21  
    22  import (
    23  	"fmt"
    24  	"path/filepath"
    25  
    26  	. "gopkg.in/check.v1"
    27  
    28  	"github.com/snapcore/snapd/dirs"
    29  	"github.com/snapcore/snapd/gadget/quantity"
    30  	"github.com/snapcore/snapd/overlord/configstate/config"
    31  	"github.com/snapcore/snapd/overlord/servicestate"
    32  	"github.com/snapcore/snapd/overlord/snapstate"
    33  	"github.com/snapcore/snapd/overlord/state"
    34  	"github.com/snapcore/snapd/snap"
    35  	"github.com/snapcore/snapd/snap/snaptest"
    36  	"github.com/snapcore/snapd/snapdenv"
    37  	"github.com/snapcore/snapd/systemd"
    38  	"github.com/snapcore/snapd/testutil"
    39  )
    40  
    41  type quotaControlSuite struct {
    42  	baseServiceMgrTestSuite
    43  }
    44  
    45  var _ = Suite(&quotaControlSuite{})
    46  
    47  func (s *quotaControlSuite) SetUpTest(c *C) {
    48  	s.baseServiceMgrTestSuite.SetUpTest(c)
    49  
    50  	// we don't need the EnsureSnapServices ensure loop to run by default
    51  	servicestate.MockEnsuredSnapServices(s.mgr, true)
    52  
    53  	// we enable quota-groups by default
    54  	s.state.Lock()
    55  	defer s.state.Unlock()
    56  	tr := config.NewTransaction(s.state)
    57  	tr.Set("core", "experimental.quota-groups", true)
    58  	tr.Commit()
    59  
    60  	// mock that we have a new enough version of systemd by default
    61  	r := servicestate.MockSystemdVersion(248)
    62  	s.AddCleanup(r)
    63  }
    64  
    65  type quotaGroupState struct {
    66  	MemoryLimit quantity.Size
    67  	SubGroups   []string
    68  	ParentGroup string
    69  	Snaps       []string
    70  }
    71  
    72  func checkQuotaState(c *C, st *state.State, exp map[string]quotaGroupState) {
    73  	m, err := servicestate.AllQuotas(st)
    74  	c.Assert(err, IsNil)
    75  	c.Assert(m, HasLen, len(exp))
    76  	for name, grp := range m {
    77  		expGrp, ok := exp[name]
    78  		c.Assert(ok, Equals, true, Commentf("unexpected group %q in state", name))
    79  		c.Assert(grp.MemoryLimit, Equals, expGrp.MemoryLimit)
    80  		c.Assert(grp.ParentGroup, Equals, expGrp.ParentGroup)
    81  
    82  		c.Assert(grp.Snaps, HasLen, len(expGrp.Snaps))
    83  		if len(expGrp.Snaps) != 0 {
    84  			c.Assert(grp.Snaps, DeepEquals, expGrp.Snaps)
    85  
    86  			// also check on the service file states
    87  			for _, sn := range expGrp.Snaps {
    88  				// meh assume all services are named svc1
    89  				slicePath := name
    90  				if grp.ParentGroup != "" {
    91  					slicePath = grp.ParentGroup + "/" + name
    92  				}
    93  				checkSvcAndSliceState(c, sn+".svc1", slicePath, grp.MemoryLimit)
    94  			}
    95  		}
    96  
    97  		c.Assert(grp.SubGroups, HasLen, len(expGrp.SubGroups))
    98  		if len(expGrp.SubGroups) != 0 {
    99  			c.Assert(grp.SubGroups, DeepEquals, expGrp.SubGroups)
   100  		}
   101  	}
   102  }
   103  
   104  func checkSvcAndSliceState(c *C, snapSvc string, slicePath string, sliceMem quantity.Size) {
   105  	slicePath = systemd.EscapeUnitNamePath(slicePath)
   106  	// make sure the service file exists
   107  	svcFileName := filepath.Join(dirs.SnapServicesDir, "snap."+snapSvc+".service")
   108  	c.Assert(svcFileName, testutil.FilePresent)
   109  
   110  	if sliceMem != 0 {
   111  		// the service file should mention this slice
   112  		c.Assert(svcFileName, testutil.FileContains, fmt.Sprintf("\nSlice=snap.%s.slice\n", slicePath))
   113  	} else {
   114  		c.Assert(svcFileName, Not(testutil.FileContains), fmt.Sprintf("Slice=snap.%s.slice", slicePath))
   115  	}
   116  	checkSliceState(c, slicePath, sliceMem)
   117  }
   118  
   119  func checkSliceState(c *C, sliceName string, sliceMem quantity.Size) {
   120  	sliceFileName := filepath.Join(dirs.SnapServicesDir, "snap."+sliceName+".slice")
   121  	if sliceMem != 0 {
   122  		c.Assert(sliceFileName, testutil.FilePresent)
   123  		c.Assert(sliceFileName, testutil.FileContains, fmt.Sprintf("\nMemoryMax=%s\n", sliceMem.String()))
   124  	} else {
   125  		c.Assert(sliceFileName, testutil.FileAbsent)
   126  	}
   127  }
   128  
   129  func systemctlCallsForSliceStart(name string) []expectedSystemctl {
   130  	name = systemd.EscapeUnitNamePath(name)
   131  	slice := "snap." + name + ".slice"
   132  	return []expectedSystemctl{
   133  		{expArgs: []string{"start", slice}},
   134  	}
   135  }
   136  
   137  func systemctlCallsForSliceStop(name string) []expectedSystemctl {
   138  	name = systemd.EscapeUnitNamePath(name)
   139  	slice := "snap." + name + ".slice"
   140  	return []expectedSystemctl{
   141  		{expArgs: []string{"stop", slice}},
   142  		{
   143  			expArgs: []string{"show", "--property=ActiveState", slice},
   144  			output:  "ActiveState=inactive",
   145  		},
   146  	}
   147  }
   148  
   149  func systemctlCallsForServiceRestart(name string) []expectedSystemctl {
   150  	svc := "snap." + name + ".svc1.service"
   151  	return []expectedSystemctl{
   152  		{
   153  			expArgs: []string{"show", "--property=Id,ActiveState,UnitFileState,Type", svc},
   154  			output:  fmt.Sprintf("Id=%s\nActiveState=active\nUnitFileState=enabled\nType=simple\n", svc),
   155  		},
   156  		{expArgs: []string{"stop", svc}},
   157  		{
   158  			expArgs: []string{"show", "--property=ActiveState", svc},
   159  			output:  "ActiveState=inactive",
   160  		},
   161  		{expArgs: []string{"start", svc}},
   162  	}
   163  }
   164  
   165  func systemctlCallsForCreateQuota(groupName string, snapNames ...string) []expectedSystemctl {
   166  	calls := join(
   167  		[]expectedSystemctl{{expArgs: []string{"daemon-reload"}}},
   168  		systemctlCallsForSliceStart(groupName),
   169  	)
   170  	for _, snapName := range snapNames {
   171  		calls = join(calls, systemctlCallsForServiceRestart(snapName))
   172  	}
   173  
   174  	return calls
   175  }
   176  
   177  func systemctlCallsVersion(version int) []expectedSystemctl {
   178  	return []expectedSystemctl{
   179  		{
   180  			expArgs: []string{"--version"},
   181  			output:  fmt.Sprintf("systemd %d\n+FOO +BAR\n", version),
   182  		},
   183  	}
   184  }
   185  
   186  func join(calls ...[]expectedSystemctl) []expectedSystemctl {
   187  	fullCall := []expectedSystemctl{}
   188  	for _, call := range calls {
   189  		fullCall = append(fullCall, call...)
   190  	}
   191  
   192  	return fullCall
   193  }
   194  
   195  func (s *quotaControlSuite) TestCreateQuotaNotEnabled(c *C) {
   196  	s.state.Lock()
   197  	defer s.state.Unlock()
   198  	tr := config.NewTransaction(s.state)
   199  	tr.Set("core", "experimental.quota-groups", false)
   200  	tr.Commit()
   201  
   202  	// try to create an empty quota group
   203  	err := servicestate.CreateQuota(s.state, "foo", "", nil, quantity.SizeGiB)
   204  	c.Assert(err, ErrorMatches, `experimental feature disabled - test it by setting 'experimental.quota-groups' to true`)
   205  }
   206  
   207  func (s *quotaControlSuite) TestCreateQuotaSystemdTooOld(c *C) {
   208  	s.state.Lock()
   209  	defer s.state.Unlock()
   210  
   211  	r := s.mockSystemctlCalls(c, systemctlCallsVersion(204))
   212  	defer r()
   213  
   214  	err := servicestate.CheckSystemdVersion()
   215  	c.Assert(err, IsNil)
   216  
   217  	err = servicestate.CreateQuota(s.state, "foo", "", nil, quantity.SizeGiB)
   218  	c.Assert(err, ErrorMatches, `systemd version too old: snap quotas requires systemd 205 and newer \(currently have 204\)`)
   219  }
   220  
   221  func (s *quotaControlSuite) TestRemoveQuotaPreseeding(c *C) {
   222  	r := snapdenv.MockPreseeding(true)
   223  	defer r()
   224  
   225  	st := s.state
   226  	st.Lock()
   227  	defer st.Unlock()
   228  
   229  	snapstate.Set(s.state, "test-snap", s.testSnapState)
   230  	snaptest.MockSnapCurrent(c, testYaml, s.testSnapSideInfo)
   231  
   232  	// create a quota group
   233  	err := servicestate.CreateQuota(s.state, "foo", "", []string{"test-snap"}, quantity.SizeGiB)
   234  	c.Assert(err, IsNil)
   235  
   236  	// check that the quota groups were created in the state
   237  	checkQuotaState(c, st, map[string]quotaGroupState{
   238  		"foo": {
   239  			MemoryLimit: quantity.SizeGiB,
   240  			Snaps:       []string{"test-snap"},
   241  		},
   242  	})
   243  
   244  	// but removing a quota doesn't work, since it just doesn't make sense to be
   245  	// able to remove a quota group while preseeding, so we purposely fail
   246  	err = servicestate.RemoveQuota(st, "foo")
   247  	c.Assert(err, ErrorMatches, `removing quota groups not supported while preseeding`)
   248  }
   249  
   250  func (s *quotaControlSuite) TestCreateUpdateRemoveQuotaHappy(c *C) {
   251  	r := s.mockSystemctlCalls(c, join(
   252  		// CreateQuota for foo - success
   253  		systemctlCallsForCreateQuota("foo", "test-snap"),
   254  
   255  		// UpdateQuota for foo
   256  		[]expectedSystemctl{{expArgs: []string{"daemon-reload"}}},
   257  
   258  		// RemoveQuota for foo
   259  		[]expectedSystemctl{{expArgs: []string{"daemon-reload"}}},
   260  		systemctlCallsForSliceStop("foo"),
   261  		[]expectedSystemctl{{expArgs: []string{"daemon-reload"}}},
   262  		systemctlCallsForServiceRestart("test-snap"),
   263  	))
   264  	defer r()
   265  
   266  	st := s.state
   267  	st.Lock()
   268  	defer st.Unlock()
   269  
   270  	// setup the snap so it exists
   271  	snapstate.Set(s.state, "test-snap", s.testSnapState)
   272  	snaptest.MockSnapCurrent(c, testYaml, s.testSnapSideInfo)
   273  
   274  	// create the quota group
   275  	err := servicestate.CreateQuota(st, "foo", "", []string{"test-snap"}, quantity.SizeGiB)
   276  	c.Assert(err, IsNil)
   277  
   278  	// check that the quota groups were created in the state
   279  	checkQuotaState(c, st, map[string]quotaGroupState{
   280  		"foo": {
   281  			MemoryLimit: quantity.SizeGiB,
   282  			Snaps:       []string{"test-snap"},
   283  		},
   284  	})
   285  
   286  	// increase the memory limit
   287  	err = servicestate.UpdateQuota(st, "foo", servicestate.QuotaGroupUpdate{NewMemoryLimit: 2 * quantity.SizeGiB})
   288  	c.Assert(err, IsNil)
   289  
   290  	checkQuotaState(c, st, map[string]quotaGroupState{
   291  		"foo": {
   292  			MemoryLimit: 2 * quantity.SizeGiB,
   293  			Snaps:       []string{"test-snap"},
   294  		},
   295  	})
   296  
   297  	// remove the quota
   298  	err = servicestate.RemoveQuota(st, "foo")
   299  	c.Assert(err, IsNil)
   300  	checkQuotaState(c, st, nil)
   301  }
   302  
   303  func (s *quotaControlSuite) TestEnsureSnapAbsentFromQuotaGroup(c *C) {
   304  	r := s.mockSystemctlCalls(c, join(
   305  		// CreateQuota for foo
   306  		systemctlCallsForCreateQuota("foo", "test-snap", "test-snap2"),
   307  
   308  		// EnsureSnapAbsentFromQuota with just test-snap restarted since it is
   309  		// no longer in the group
   310  		[]expectedSystemctl{{expArgs: []string{"daemon-reload"}}},
   311  		systemctlCallsForServiceRestart("test-snap"),
   312  
   313  		// another identical call to EnsureSnapAbsentFromQuota does nothing
   314  		// since the function is idempotent
   315  
   316  		// EnsureSnapAbsentFromQuota with just test-snap2 restarted since it is no
   317  		// longer in the group
   318  		[]expectedSystemctl{{expArgs: []string{"daemon-reload"}}},
   319  		systemctlCallsForServiceRestart("test-snap2"),
   320  	))
   321  	defer r()
   322  
   323  	st := s.state
   324  	st.Lock()
   325  	defer st.Unlock()
   326  	// setup test-snap
   327  	snapstate.Set(s.state, "test-snap", s.testSnapState)
   328  	snaptest.MockSnapCurrent(c, testYaml, s.testSnapSideInfo)
   329  	// and test-snap2
   330  	si2 := &snap.SideInfo{RealName: "test-snap2", Revision: snap.R(42)}
   331  	snapst2 := &snapstate.SnapState{
   332  		Sequence: []*snap.SideInfo{si2},
   333  		Current:  si2.Revision,
   334  		Active:   true,
   335  		SnapType: "app",
   336  	}
   337  	snapstate.Set(s.state, "test-snap2", snapst2)
   338  	snaptest.MockSnapCurrent(c, testYaml2, si2)
   339  
   340  	// create a quota group
   341  	err := servicestate.CreateQuota(s.state, "foo", "", []string{"test-snap", "test-snap2"}, quantity.SizeGiB)
   342  	c.Assert(err, IsNil)
   343  
   344  	checkQuotaState(c, st, map[string]quotaGroupState{
   345  		"foo": {
   346  			MemoryLimit: quantity.SizeGiB,
   347  			Snaps:       []string{"test-snap", "test-snap2"},
   348  		},
   349  	})
   350  
   351  	// remove test-snap from the group
   352  	err = servicestate.EnsureSnapAbsentFromQuota(s.state, "test-snap")
   353  	c.Assert(err, IsNil)
   354  
   355  	checkQuotaState(c, st, map[string]quotaGroupState{
   356  		"foo": {
   357  			MemoryLimit: quantity.SizeGiB,
   358  			Snaps:       []string{"test-snap2"},
   359  		},
   360  	})
   361  
   362  	// removing the same snap twice works as well but does nothing
   363  	err = servicestate.EnsureSnapAbsentFromQuota(s.state, "test-snap")
   364  	c.Assert(err, IsNil)
   365  
   366  	// now remove test-snap2 too
   367  	err = servicestate.EnsureSnapAbsentFromQuota(s.state, "test-snap2")
   368  	c.Assert(err, IsNil)
   369  
   370  	// and check that it got updated in the state
   371  	checkQuotaState(c, st, map[string]quotaGroupState{
   372  		"foo": {
   373  			MemoryLimit: quantity.SizeGiB,
   374  		},
   375  	})
   376  
   377  	// it's not an error to call EnsureSnapAbsentFromQuotaGroup on a snap that
   378  	// is not in any quota group
   379  	err = servicestate.EnsureSnapAbsentFromQuota(s.state, "test-snap33333")
   380  	c.Assert(err, IsNil)
   381  }