github.com/david-imola/snapd@v0.0.0-20210611180407-2de8ddeece6d/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  func (s *quotaControlSuite) TestCreateQuotaNotEnabled(c *C) {
    66  	s.state.Lock()
    67  	defer s.state.Unlock()
    68  	tr := config.NewTransaction(s.state)
    69  	tr.Set("core", "experimental.quota-groups", false)
    70  	tr.Commit()
    71  
    72  	// try to create an empty quota group
    73  	err := servicestate.CreateQuota(s.state, "foo", "", nil, quantity.SizeGiB)
    74  	c.Assert(err, ErrorMatches, `experimental feature disabled - test it by setting 'experimental.quota-groups' to true`)
    75  }
    76  
    77  func (s *quotaControlSuite) TestCreateQuotaSystemdTooOld(c *C) {
    78  	s.state.Lock()
    79  	defer s.state.Unlock()
    80  
    81  	r := s.mockSystemctlCalls(c, systemctlCallsVersion(204))
    82  	defer r()
    83  
    84  	err := servicestate.CheckSystemdVersion()
    85  	c.Assert(err, IsNil)
    86  
    87  	err = servicestate.CreateQuota(s.state, "foo", "", nil, quantity.SizeGiB)
    88  	c.Assert(err, ErrorMatches, `systemd version too old: snap quotas requires systemd 205 and newer \(currently have 204\)`)
    89  }
    90  
    91  type quotaGroupState struct {
    92  	MemoryLimit quantity.Size
    93  	SubGroups   []string
    94  	ParentGroup string
    95  	Snaps       []string
    96  }
    97  
    98  func checkQuotaState(c *C, st *state.State, exp map[string]quotaGroupState) {
    99  	m, err := servicestate.AllQuotas(st)
   100  	c.Assert(err, IsNil)
   101  	c.Assert(m, HasLen, len(exp))
   102  	for name, grp := range m {
   103  		expGrp, ok := exp[name]
   104  		c.Assert(ok, Equals, true, Commentf("unexpected group %q in state", name))
   105  		c.Assert(grp.MemoryLimit, Equals, expGrp.MemoryLimit)
   106  		c.Assert(grp.ParentGroup, Equals, expGrp.ParentGroup)
   107  
   108  		c.Assert(grp.Snaps, HasLen, len(expGrp.Snaps))
   109  		if len(expGrp.Snaps) != 0 {
   110  			c.Assert(grp.Snaps, DeepEquals, expGrp.Snaps)
   111  
   112  			// also check on the service file states
   113  			for _, sn := range expGrp.Snaps {
   114  				// meh assume all services are named svc1
   115  				slicePath := name
   116  				if grp.ParentGroup != "" {
   117  					slicePath = grp.ParentGroup + "/" + name
   118  				}
   119  				checkSvcAndSliceState(c, sn+".svc1", slicePath, grp.MemoryLimit)
   120  			}
   121  		}
   122  
   123  		c.Assert(grp.SubGroups, HasLen, len(expGrp.SubGroups))
   124  		if len(expGrp.SubGroups) != 0 {
   125  			c.Assert(grp.SubGroups, DeepEquals, expGrp.SubGroups)
   126  		}
   127  	}
   128  }
   129  
   130  func checkSvcAndSliceState(c *C, snapSvc string, slicePath string, sliceMem quantity.Size) {
   131  	slicePath = systemd.EscapeUnitNamePath(slicePath)
   132  	// make sure the service file exists
   133  	svcFileName := filepath.Join(dirs.SnapServicesDir, "snap."+snapSvc+".service")
   134  	c.Assert(svcFileName, testutil.FilePresent)
   135  
   136  	if sliceMem != 0 {
   137  		// the service file should mention this slice
   138  		c.Assert(svcFileName, testutil.FileContains, fmt.Sprintf("\nSlice=snap.%s.slice\n", slicePath))
   139  	} else {
   140  		c.Assert(svcFileName, Not(testutil.FileContains), fmt.Sprintf("Slice=snap.%s.slice", slicePath))
   141  	}
   142  	checkSliceState(c, slicePath, sliceMem)
   143  }
   144  
   145  func checkSliceState(c *C, sliceName string, sliceMem quantity.Size) {
   146  	sliceFileName := filepath.Join(dirs.SnapServicesDir, "snap."+sliceName+".slice")
   147  	if sliceMem != 0 {
   148  		c.Assert(sliceFileName, testutil.FilePresent)
   149  		c.Assert(sliceFileName, testutil.FileContains, fmt.Sprintf("\nMemoryMax=%s\n", sliceMem.String()))
   150  	} else {
   151  		c.Assert(sliceFileName, testutil.FileAbsent)
   152  	}
   153  }
   154  
   155  func systemctlCallsForSliceStart(name string) []expectedSystemctl {
   156  	name = systemd.EscapeUnitNamePath(name)
   157  	slice := "snap." + name + ".slice"
   158  	return []expectedSystemctl{
   159  		{expArgs: []string{"start", slice}},
   160  	}
   161  }
   162  
   163  func systemctlCallsForSliceStop(name string) []expectedSystemctl {
   164  	name = systemd.EscapeUnitNamePath(name)
   165  	slice := "snap." + name + ".slice"
   166  	return []expectedSystemctl{
   167  		{expArgs: []string{"stop", slice}},
   168  		{
   169  			expArgs: []string{"show", "--property=ActiveState", slice},
   170  			output:  "ActiveState=inactive",
   171  		},
   172  	}
   173  }
   174  
   175  func systemctlCallsForServiceRestart(name string) []expectedSystemctl {
   176  	svc := "snap." + name + ".svc1.service"
   177  	return []expectedSystemctl{
   178  		{
   179  			expArgs: []string{"is-enabled", svc},
   180  			output:  "enabled",
   181  		},
   182  		{expArgs: []string{"stop", svc}},
   183  		{
   184  			expArgs: []string{"show", "--property=ActiveState", svc},
   185  			output:  "ActiveState=inactive",
   186  		},
   187  		{expArgs: []string{"start", svc}},
   188  	}
   189  }
   190  
   191  func systemctlCallsForCreateQuota(groupName string, snapNames ...string) []expectedSystemctl {
   192  	calls := join(
   193  		[]expectedSystemctl{{expArgs: []string{"daemon-reload"}}},
   194  		systemctlCallsForSliceStart(groupName),
   195  	)
   196  	for _, snapName := range snapNames {
   197  		calls = join(calls, systemctlCallsForServiceRestart(snapName))
   198  	}
   199  
   200  	return calls
   201  }
   202  
   203  func systemctlCallsVersion(version int) []expectedSystemctl {
   204  	return []expectedSystemctl{
   205  		{
   206  			expArgs: []string{"--version"},
   207  			output:  fmt.Sprintf("systemd %d\n+FOO +BAR\n", version),
   208  		},
   209  	}
   210  }
   211  
   212  func join(calls ...[]expectedSystemctl) []expectedSystemctl {
   213  	fullCall := []expectedSystemctl{}
   214  	for _, call := range calls {
   215  		fullCall = append(fullCall, call...)
   216  	}
   217  
   218  	return fullCall
   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) TestCreateQuotaPreseeding(c *C) {
   251  	// should be no systemctl calls since we are preseeding
   252  	r := snapdenv.MockPreseeding(true)
   253  	defer r()
   254  
   255  	st := s.state
   256  	st.Lock()
   257  	defer st.Unlock()
   258  
   259  	// setup the snap so it exists
   260  	snapstate.Set(s.state, "test-snap", s.testSnapState)
   261  	snaptest.MockSnapCurrent(c, testYaml, s.testSnapSideInfo)
   262  
   263  	// now we can create the quota group
   264  	err := servicestate.CreateQuota(s.state, "foo", "", []string{"test-snap"}, quantity.SizeGiB)
   265  	c.Assert(err, IsNil)
   266  
   267  	// check that the quota groups were created in the state
   268  	checkQuotaState(c, st, map[string]quotaGroupState{
   269  		"foo": {
   270  			MemoryLimit: quantity.SizeGiB,
   271  			Snaps:       []string{"test-snap"},
   272  		},
   273  	})
   274  }
   275  
   276  func (s *quotaControlSuite) TestCreateQuota(c *C) {
   277  	r := s.mockSystemctlCalls(c, join(
   278  		// CreateQuota for non-installed snap - fails
   279  
   280  		// CreateQuota for foo - success
   281  		systemctlCallsForCreateQuota("foo", "test-snap"),
   282  
   283  		// CreateQuota for foo2 with overlapping snap already in foo
   284  
   285  		// CreateQuota for foo again - fails
   286  	))
   287  	defer r()
   288  
   289  	st := s.state
   290  	st.Lock()
   291  	defer st.Unlock()
   292  
   293  	// trying to create a quota with a snap that doesn't exist fails
   294  	err := servicestate.CreateQuota(s.state, "foo", "", []string{"test-snap"}, quantity.SizeGiB)
   295  	c.Assert(err, ErrorMatches, `cannot use snap "test-snap" in group "foo": snap "test-snap" is not installed`)
   296  
   297  	// setup the snap so it exists
   298  	snapstate.Set(s.state, "test-snap", s.testSnapState)
   299  	snaptest.MockSnapCurrent(c, testYaml, s.testSnapSideInfo)
   300  
   301  	// now we can create the quota group
   302  	err = servicestate.CreateQuota(s.state, "foo", "", []string{"test-snap"}, quantity.SizeGiB)
   303  	c.Assert(err, IsNil)
   304  
   305  	// we can't add the same snap to a different group though
   306  	err = servicestate.CreateQuota(s.state, "foo2", "", []string{"test-snap"}, quantity.SizeGiB)
   307  	c.Assert(err, ErrorMatches, `cannot add snap "test-snap" to group "foo2": snap already in quota group "foo"`)
   308  
   309  	// creating the same group again will fail
   310  	err = servicestate.CreateQuota(s.state, "foo", "", []string{"test-snap"}, quantity.SizeGiB)
   311  	c.Assert(err, ErrorMatches, `group "foo" already exists`)
   312  
   313  	// check that the quota groups were created in the state
   314  	checkQuotaState(c, st, map[string]quotaGroupState{
   315  		"foo": {
   316  			MemoryLimit: quantity.SizeGiB,
   317  			Snaps:       []string{"test-snap"},
   318  		},
   319  	})
   320  }
   321  
   322  func (s *quotaControlSuite) TestCreateSubGroupQuota(c *C) {
   323  	r := s.mockSystemctlCalls(c, join(
   324  		// CreateQuota for foo - no systemctl calls since no snaps in it
   325  
   326  		// CreateQuota for foo2 - fails thus no systemctl calls
   327  
   328  		// CreateQuota for foo2 - we don't write anything for the first quota
   329  		// since there are no snaps in the quota to track
   330  		[]expectedSystemctl{{expArgs: []string{"daemon-reload"}}},
   331  		systemctlCallsForSliceStart("foo-group"),
   332  		systemctlCallsForSliceStart("foo-group/foo2"),
   333  		systemctlCallsForServiceRestart("test-snap"),
   334  	))
   335  	defer r()
   336  
   337  	st := s.state
   338  	st.Lock()
   339  	defer st.Unlock()
   340  
   341  	// setup the snap so it exists
   342  	snapstate.Set(s.state, "test-snap", s.testSnapState)
   343  	snaptest.MockSnapCurrent(c, testYaml, s.testSnapSideInfo)
   344  
   345  	// create a quota group with no snaps to be the parent
   346  	err := servicestate.CreateQuota(s.state, "foo-group", "", nil, quantity.SizeGiB)
   347  	c.Assert(err, IsNil)
   348  
   349  	// trying to create a quota group with a non-existent parent group fails
   350  	err = servicestate.CreateQuota(s.state, "foo2", "foo-non-real", []string{"test-snap"}, quantity.SizeGiB)
   351  	c.Assert(err, ErrorMatches, `cannot create group under non-existent parent group "foo-non-real"`)
   352  
   353  	// trying to create a quota group with too big of a limit to fit inside the
   354  	// parent fails
   355  	err = servicestate.CreateQuota(s.state, "foo2", "foo-group", []string{"test-snap"}, 2*quantity.SizeGiB)
   356  	c.Assert(err, ErrorMatches, `sub-group memory limit of 2 GiB is too large to fit inside remaining quota space 1 GiB for parent group foo-group`)
   357  
   358  	// now we can create a sub-quota
   359  	err = servicestate.CreateQuota(s.state, "foo2", "foo-group", []string{"test-snap"}, quantity.SizeGiB)
   360  	c.Assert(err, IsNil)
   361  
   362  	// check that the quota groups were created in the state
   363  	checkQuotaState(c, st, map[string]quotaGroupState{
   364  		"foo-group": {
   365  			MemoryLimit: quantity.SizeGiB,
   366  			SubGroups:   []string{"foo2"},
   367  		},
   368  		"foo2": {
   369  			MemoryLimit: quantity.SizeGiB,
   370  			Snaps:       []string{"test-snap"},
   371  			ParentGroup: "foo-group",
   372  		},
   373  	})
   374  
   375  	// foo-group exists as a slice too, but has no snap services in the slice
   376  	checkSliceState(c, systemd.EscapeUnitNamePath("foo-group"), quantity.SizeGiB)
   377  }
   378  
   379  func (s *quotaControlSuite) TestRemoveQuota(c *C) {
   380  	r := s.mockSystemctlCalls(c, join(
   381  		// CreateQuota for foo
   382  		systemctlCallsForCreateQuota("foo", "test-snap"),
   383  
   384  		// for CreateQuota foo2 - no systemctl calls since there are no snaps
   385  
   386  		// for CreateQuota foo3 - no systemctl calls since there are no snaps
   387  
   388  		// RemoveQuota for foo2 - no daemon reload initially because
   389  		// we didn't modify anything, as there are no snaps in foo2 so we don't
   390  		// create that group on disk
   391  		// TODO: is this bit correct in practice? we are in effect calling
   392  		// systemctl stop <non-existing-slice> ?
   393  		systemctlCallsForSliceStop("foo/foo3"),
   394  
   395  		systemctlCallsForSliceStop("foo/foo2"),
   396  
   397  		// RemoveQuota for foo
   398  		[]expectedSystemctl{{expArgs: []string{"daemon-reload"}}},
   399  		systemctlCallsForSliceStop("foo"),
   400  		[]expectedSystemctl{{expArgs: []string{"daemon-reload"}}},
   401  		systemctlCallsForServiceRestart("test-snap"),
   402  	))
   403  	defer r()
   404  
   405  	st := s.state
   406  	st.Lock()
   407  	defer st.Unlock()
   408  
   409  	// setup the snap so it exists
   410  	snapstate.Set(s.state, "test-snap", s.testSnapState)
   411  	snaptest.MockSnapCurrent(c, testYaml, s.testSnapSideInfo)
   412  
   413  	// trying to remove a group that does not exist fails
   414  	err := servicestate.RemoveQuota(s.state, "not-exists")
   415  	c.Assert(err, ErrorMatches, `cannot remove non-existent quota group "not-exists"`)
   416  
   417  	// create a quota
   418  	err = servicestate.CreateQuota(s.state, "foo", "", []string{"test-snap"}, quantity.SizeGiB)
   419  	c.Assert(err, IsNil)
   420  
   421  	// create 2 quota sub-groups too
   422  	err = servicestate.CreateQuota(s.state, "foo2", "foo", nil, quantity.SizeGiB/2)
   423  	c.Assert(err, IsNil)
   424  
   425  	err = servicestate.CreateQuota(s.state, "foo3", "foo", nil, quantity.SizeGiB/2)
   426  	c.Assert(err, IsNil)
   427  
   428  	// check that the quota groups was created in the state
   429  	checkQuotaState(c, st, map[string]quotaGroupState{
   430  		"foo": {
   431  			MemoryLimit: quantity.SizeGiB,
   432  			Snaps:       []string{"test-snap"},
   433  			SubGroups:   []string{"foo2", "foo3"},
   434  		},
   435  		"foo2": {
   436  			MemoryLimit: quantity.SizeGiB / 2,
   437  			ParentGroup: "foo",
   438  		},
   439  		"foo3": {
   440  			MemoryLimit: quantity.SizeGiB / 2,
   441  			ParentGroup: "foo",
   442  		},
   443  	})
   444  
   445  	// try removing the parent and it fails since it still has a sub-group
   446  	// under it
   447  	err = servicestate.RemoveQuota(s.state, "foo")
   448  	c.Assert(err, ErrorMatches, "cannot remove quota group with sub-groups, remove the sub-groups first")
   449  
   450  	// but we can remove the sub-group successfully first
   451  	err = servicestate.RemoveQuota(s.state, "foo3")
   452  	c.Assert(err, IsNil)
   453  
   454  	checkQuotaState(c, st, map[string]quotaGroupState{
   455  		"foo": {
   456  			MemoryLimit: quantity.SizeGiB,
   457  			Snaps:       []string{"test-snap"},
   458  			SubGroups:   []string{"foo2"},
   459  		},
   460  		"foo2": {
   461  			MemoryLimit: quantity.SizeGiB / 2,
   462  			ParentGroup: "foo",
   463  		},
   464  	})
   465  
   466  	// and we can remove the other sub-group
   467  	err = servicestate.RemoveQuota(s.state, "foo2")
   468  	c.Assert(err, IsNil)
   469  
   470  	checkQuotaState(c, st, map[string]quotaGroupState{
   471  		"foo": {
   472  			MemoryLimit: quantity.SizeGiB,
   473  			Snaps:       []string{"test-snap"},
   474  		},
   475  	})
   476  
   477  	// now we can remove the quota from the state
   478  	err = servicestate.RemoveQuota(s.state, "foo")
   479  	c.Assert(err, IsNil)
   480  
   481  	checkQuotaState(c, st, nil)
   482  
   483  	// foo is not mentioned in the service and doesn't exist
   484  	checkSvcAndSliceState(c, "test-snap.svc1", "foo", 0)
   485  }
   486  
   487  func (s *quotaControlSuite) TestUpdateQuotaGroupNotExist(c *C) {
   488  	s.state.Lock()
   489  	defer s.state.Unlock()
   490  
   491  	opts := servicestate.QuotaGroupUpdate{}
   492  	err := servicestate.UpdateQuota(s.state, "non-existing", opts)
   493  	c.Check(err, ErrorMatches, `group "non-existing" does not exist`)
   494  }
   495  
   496  func (s *quotaControlSuite) TestUpdateQuotaSubGroupTooBig(c *C) {
   497  	r := s.mockSystemctlCalls(c, join(
   498  		// CreateQuota for foo
   499  		systemctlCallsForCreateQuota("foo", "test-snap"),
   500  
   501  		// CreateQuota for foo2
   502  		systemctlCallsForCreateQuota("foo/foo2", "test-snap2"),
   503  
   504  		// UpdateQuota for foo2 - just the slice changes
   505  		[]expectedSystemctl{{expArgs: []string{"daemon-reload"}}},
   506  
   507  		// UpdateQuota for foo2 which fails - no systemctl calls
   508  	))
   509  	defer r()
   510  
   511  	st := s.state
   512  	st.Lock()
   513  	defer st.Unlock()
   514  
   515  	// setup the snap so it exists
   516  	snapstate.Set(s.state, "test-snap", s.testSnapState)
   517  	snaptest.MockSnapCurrent(c, testYaml, s.testSnapSideInfo)
   518  	// and test-snap2
   519  	si2 := &snap.SideInfo{RealName: "test-snap2", Revision: snap.R(42)}
   520  	snapst2 := &snapstate.SnapState{
   521  		Sequence: []*snap.SideInfo{si2},
   522  		Current:  si2.Revision,
   523  		Active:   true,
   524  		SnapType: "app",
   525  	}
   526  	snapstate.Set(s.state, "test-snap2", snapst2)
   527  	snaptest.MockSnapCurrent(c, testYaml2, si2)
   528  
   529  	// create a quota group
   530  	err := servicestate.CreateQuota(s.state, "foo", "", []string{"test-snap"}, quantity.SizeGiB)
   531  	c.Assert(err, IsNil)
   532  
   533  	// ensure mem-limit is 1 GB
   534  	expFooGroupState := quotaGroupState{
   535  		MemoryLimit: quantity.SizeGiB,
   536  		Snaps:       []string{"test-snap"},
   537  	}
   538  	checkQuotaState(c, st, map[string]quotaGroupState{
   539  		"foo": expFooGroupState,
   540  	})
   541  
   542  	// create a sub-group with 0.5 GiB
   543  	err = servicestate.CreateQuota(s.state, "foo2", "foo", []string{"test-snap2"}, quantity.SizeGiB/2)
   544  	c.Assert(err, IsNil)
   545  
   546  	expFooGroupState.SubGroups = []string{"foo2"}
   547  
   548  	expFoo2GroupState := quotaGroupState{
   549  		MemoryLimit: quantity.SizeGiB / 2,
   550  		Snaps:       []string{"test-snap2"},
   551  		ParentGroup: "foo",
   552  	}
   553  
   554  	// verify it was set in state
   555  	checkQuotaState(c, st, map[string]quotaGroupState{
   556  		"foo":  expFooGroupState,
   557  		"foo2": expFoo2GroupState,
   558  	})
   559  
   560  	// now try to increase it to the max size
   561  	err = servicestate.UpdateQuota(s.state, "foo2", servicestate.QuotaGroupUpdate{
   562  		NewMemoryLimit: quantity.SizeGiB,
   563  	})
   564  	c.Assert(err, IsNil)
   565  
   566  	expFoo2GroupState.MemoryLimit = quantity.SizeGiB
   567  	// and check that it got updated in the state
   568  	checkQuotaState(c, st, map[string]quotaGroupState{
   569  		"foo":  expFooGroupState,
   570  		"foo2": expFoo2GroupState,
   571  	})
   572  
   573  	// now try to increase it above the parent limit
   574  	err = servicestate.UpdateQuota(s.state, "foo2", servicestate.QuotaGroupUpdate{
   575  		NewMemoryLimit: 2 * quantity.SizeGiB,
   576  	})
   577  	c.Assert(err, ErrorMatches, `cannot update quota "foo2": group "foo2" is invalid: sub-group memory limit of 2 GiB is too large to fit inside remaining quota space 1 GiB for parent group foo`)
   578  
   579  	// and make sure that the existing memory limit is still in place
   580  	checkQuotaState(c, st, map[string]quotaGroupState{
   581  		"foo":  expFooGroupState,
   582  		"foo2": expFoo2GroupState,
   583  	})
   584  }
   585  
   586  func (s *quotaControlSuite) TestUpdateQuotaGroupNotEnabled(c *C) {
   587  	s.state.Lock()
   588  	defer s.state.Unlock()
   589  	tr := config.NewTransaction(s.state)
   590  	tr.Set("core", "experimental.quota-groups", false)
   591  	tr.Commit()
   592  
   593  	opts := servicestate.QuotaGroupUpdate{}
   594  	err := servicestate.UpdateQuota(s.state, "foo", opts)
   595  	c.Assert(err, ErrorMatches, `experimental feature disabled - test it by setting 'experimental.quota-groups' to true`)
   596  }
   597  
   598  func (s *quotaControlSuite) TestUpdateQuotaChangeMemLimit(c *C) {
   599  	r := s.mockSystemctlCalls(c, join(
   600  		// CreateQuota for foo
   601  		systemctlCallsForCreateQuota("foo", "test-snap"),
   602  
   603  		// UpdateQuota for foo - an existing slice was changed, so all we need
   604  		// to is daemon-reload
   605  		[]expectedSystemctl{{expArgs: []string{"daemon-reload"}}},
   606  	))
   607  	defer r()
   608  
   609  	st := s.state
   610  	st.Lock()
   611  	defer st.Unlock()
   612  
   613  	// setup the snap so it exists
   614  	snapstate.Set(s.state, "test-snap", s.testSnapState)
   615  	snaptest.MockSnapCurrent(c, testYaml, s.testSnapSideInfo)
   616  
   617  	// create a quota group
   618  	err := servicestate.CreateQuota(s.state, "foo", "", []string{"test-snap"}, quantity.SizeGiB)
   619  	c.Assert(err, IsNil)
   620  
   621  	// ensure mem-limit is 1 GB
   622  	checkQuotaState(c, st, map[string]quotaGroupState{
   623  		"foo": {
   624  			MemoryLimit: quantity.SizeGiB,
   625  			Snaps:       []string{"test-snap"},
   626  		},
   627  	})
   628  
   629  	// modify to 2 GB
   630  	opts := servicestate.QuotaGroupUpdate{NewMemoryLimit: 2 * quantity.SizeGiB}
   631  	err = servicestate.UpdateQuota(s.state, "foo", opts)
   632  	c.Assert(err, IsNil)
   633  
   634  	// and check that it got updated in the state
   635  	checkQuotaState(c, st, map[string]quotaGroupState{
   636  		"foo": {
   637  			MemoryLimit: 2 * quantity.SizeGiB,
   638  			Snaps:       []string{"test-snap"},
   639  		},
   640  	})
   641  
   642  	// trying to decrease the memory limit is not yet supported
   643  	opts = servicestate.QuotaGroupUpdate{NewMemoryLimit: quantity.SizeGiB}
   644  	err = servicestate.UpdateQuota(s.state, "foo", opts)
   645  	c.Assert(err, ErrorMatches, "cannot decrease memory limit of existing quota-group, remove and re-create it to decrease the limit")
   646  }
   647  
   648  func (s *quotaControlSuite) TestUpdateQuotaAddSnap(c *C) {
   649  	r := s.mockSystemctlCalls(c, join(
   650  		// CreateQuota for foo
   651  		systemctlCallsForCreateQuota("foo", "test-snap"),
   652  
   653  		// UpdateQuota with just test-snap2 restarted since the group already
   654  		// exists
   655  		[]expectedSystemctl{{expArgs: []string{"daemon-reload"}}},
   656  		systemctlCallsForServiceRestart("test-snap2"),
   657  	))
   658  	defer r()
   659  
   660  	st := s.state
   661  	st.Lock()
   662  	defer st.Unlock()
   663  
   664  	// setup test-snap
   665  	snapstate.Set(s.state, "test-snap", s.testSnapState)
   666  	snaptest.MockSnapCurrent(c, testYaml, s.testSnapSideInfo)
   667  	// and test-snap2
   668  	si2 := &snap.SideInfo{RealName: "test-snap2", Revision: snap.R(42)}
   669  	snapst2 := &snapstate.SnapState{
   670  		Sequence: []*snap.SideInfo{si2},
   671  		Current:  si2.Revision,
   672  		Active:   true,
   673  		SnapType: "app",
   674  	}
   675  	snapstate.Set(s.state, "test-snap2", snapst2)
   676  	snaptest.MockSnapCurrent(c, testYaml2, si2)
   677  
   678  	// create a quota group
   679  	err := servicestate.CreateQuota(s.state, "foo", "", []string{"test-snap"}, quantity.SizeGiB)
   680  	c.Assert(err, IsNil)
   681  
   682  	checkQuotaState(c, st, map[string]quotaGroupState{
   683  		"foo": {
   684  			MemoryLimit: quantity.SizeGiB,
   685  			Snaps:       []string{"test-snap"},
   686  		},
   687  	})
   688  
   689  	// add a snap
   690  	opts := servicestate.QuotaGroupUpdate{AddSnaps: []string{"test-snap2"}}
   691  	err = servicestate.UpdateQuota(s.state, "foo", opts)
   692  	c.Assert(err, IsNil)
   693  
   694  	// and check that it got updated in the state
   695  	checkQuotaState(c, st, map[string]quotaGroupState{
   696  		"foo": {
   697  			MemoryLimit: quantity.SizeGiB,
   698  			Snaps:       []string{"test-snap", "test-snap2"},
   699  		},
   700  	})
   701  }
   702  
   703  func (s *quotaControlSuite) TestUpdateQuotaAddSnapAlreadyInOtherGroup(c *C) {
   704  	r := s.mockSystemctlCalls(c, join(
   705  		// CreateQuota for foo
   706  		systemctlCallsForCreateQuota("foo", "test-snap"),
   707  
   708  		// CreateQuota for foo2
   709  		systemctlCallsForCreateQuota("foo2", "test-snap2"),
   710  
   711  		// UpdateQuota for foo which fails - no systemctl calls
   712  	))
   713  	defer r()
   714  
   715  	st := s.state
   716  	st.Lock()
   717  	defer st.Unlock()
   718  
   719  	// setup test-snap
   720  	snapstate.Set(s.state, "test-snap", s.testSnapState)
   721  	snaptest.MockSnapCurrent(c, testYaml, s.testSnapSideInfo)
   722  	// and test-snap2
   723  	si2 := &snap.SideInfo{RealName: "test-snap2", Revision: snap.R(42)}
   724  	snapst2 := &snapstate.SnapState{
   725  		Sequence: []*snap.SideInfo{si2},
   726  		Current:  si2.Revision,
   727  		Active:   true,
   728  		SnapType: "app",
   729  	}
   730  	snapstate.Set(s.state, "test-snap2", snapst2)
   731  	snaptest.MockSnapCurrent(c, testYaml2, si2)
   732  
   733  	// create a quota group
   734  	err := servicestate.CreateQuota(s.state, "foo", "", []string{"test-snap"}, quantity.SizeGiB)
   735  	c.Assert(err, IsNil)
   736  
   737  	checkQuotaState(c, st, map[string]quotaGroupState{
   738  		"foo": {
   739  			MemoryLimit: quantity.SizeGiB,
   740  			Snaps:       []string{"test-snap"},
   741  		},
   742  	})
   743  
   744  	// create another quota group with the second snap
   745  	err = servicestate.CreateQuota(s.state, "foo2", "", []string{"test-snap2"}, quantity.SizeGiB)
   746  	c.Assert(err, IsNil)
   747  
   748  	// verify state
   749  	checkQuotaState(c, st, map[string]quotaGroupState{
   750  		"foo": {
   751  			MemoryLimit: quantity.SizeGiB,
   752  			Snaps:       []string{"test-snap"},
   753  		},
   754  		"foo2": {
   755  			MemoryLimit: quantity.SizeGiB,
   756  			Snaps:       []string{"test-snap2"},
   757  		},
   758  	})
   759  
   760  	// try to add test-snap2 to foo
   761  	err = servicestate.UpdateQuota(st, "foo", servicestate.QuotaGroupUpdate{
   762  		AddSnaps: []string{"test-snap2"},
   763  	})
   764  	c.Assert(err, ErrorMatches, `cannot add snap "test-snap2" to group "foo": snap already in quota group "foo2"`)
   765  
   766  	// nothing changed in the state
   767  	checkQuotaState(c, st, map[string]quotaGroupState{
   768  		"foo": {
   769  			MemoryLimit: quantity.SizeGiB,
   770  			Snaps:       []string{"test-snap"},
   771  		},
   772  		"foo2": {
   773  			MemoryLimit: quantity.SizeGiB,
   774  			Snaps:       []string{"test-snap2"},
   775  		},
   776  	})
   777  }
   778  
   779  func (s *quotaControlSuite) TestEnsureSnapAbsentFromQuotaGroup(c *C) {
   780  	r := s.mockSystemctlCalls(c, join(
   781  		// CreateQuota for foo
   782  		systemctlCallsForCreateQuota("foo", "test-snap", "test-snap2"),
   783  
   784  		// EnsureSnapAbsentFromQuota with just test-snap restarted since it is
   785  		// no longer in the group
   786  		[]expectedSystemctl{{expArgs: []string{"daemon-reload"}}},
   787  		systemctlCallsForServiceRestart("test-snap"),
   788  
   789  		// another identical call to EnsureSnapAbsentFromQuota does nothing
   790  		// since the function is idempotent
   791  
   792  		// EnsureSnapAbsentFromQuota with just test-snap2 restarted since it is no
   793  		// longer in the group
   794  		[]expectedSystemctl{{expArgs: []string{"daemon-reload"}}},
   795  		systemctlCallsForServiceRestart("test-snap2"),
   796  	))
   797  	defer r()
   798  
   799  	st := s.state
   800  	st.Lock()
   801  	defer st.Unlock()
   802  	// setup test-snap
   803  	snapstate.Set(s.state, "test-snap", s.testSnapState)
   804  	snaptest.MockSnapCurrent(c, testYaml, s.testSnapSideInfo)
   805  	// and test-snap2
   806  	si2 := &snap.SideInfo{RealName: "test-snap2", Revision: snap.R(42)}
   807  	snapst2 := &snapstate.SnapState{
   808  		Sequence: []*snap.SideInfo{si2},
   809  		Current:  si2.Revision,
   810  		Active:   true,
   811  		SnapType: "app",
   812  	}
   813  	snapstate.Set(s.state, "test-snap2", snapst2)
   814  	snaptest.MockSnapCurrent(c, testYaml2, si2)
   815  
   816  	// create a quota group
   817  	err := servicestate.CreateQuota(s.state, "foo", "", []string{"test-snap", "test-snap2"}, quantity.SizeGiB)
   818  	c.Assert(err, IsNil)
   819  
   820  	checkQuotaState(c, st, map[string]quotaGroupState{
   821  		"foo": {
   822  			MemoryLimit: quantity.SizeGiB,
   823  			Snaps:       []string{"test-snap", "test-snap2"},
   824  		},
   825  	})
   826  
   827  	// remove test-snap from the group
   828  	err = servicestate.EnsureSnapAbsentFromQuota(s.state, "test-snap")
   829  	c.Assert(err, IsNil)
   830  
   831  	checkQuotaState(c, st, map[string]quotaGroupState{
   832  		"foo": {
   833  			MemoryLimit: quantity.SizeGiB,
   834  			Snaps:       []string{"test-snap2"},
   835  		},
   836  	})
   837  
   838  	// removing the same snap twice works as well but does nothing
   839  	err = servicestate.EnsureSnapAbsentFromQuota(s.state, "test-snap")
   840  	c.Assert(err, IsNil)
   841  
   842  	// now remove test-snap2 too
   843  	err = servicestate.EnsureSnapAbsentFromQuota(s.state, "test-snap2")
   844  	c.Assert(err, IsNil)
   845  
   846  	// and check that it got updated in the state
   847  	checkQuotaState(c, st, map[string]quotaGroupState{
   848  		"foo": {
   849  			MemoryLimit: quantity.SizeGiB,
   850  		},
   851  	})
   852  
   853  	// it's not an error to call EnsureSnapAbsentFromQuotaGroup on a snap that
   854  	// is not in any quota group
   855  	err = servicestate.EnsureSnapAbsentFromQuota(s.state, "test-snap33333")
   856  	c.Assert(err, IsNil)
   857  }