github.com/ethanhsieh/snapd@v0.0.0-20210615102523-3db9b8e4edc5/overlord/servicestate/quota_handlers_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  	. "gopkg.in/check.v1"
    24  
    25  	"github.com/snapcore/snapd/gadget/quantity"
    26  	"github.com/snapcore/snapd/overlord/configstate/config"
    27  	"github.com/snapcore/snapd/overlord/servicestate"
    28  	"github.com/snapcore/snapd/overlord/snapstate"
    29  	"github.com/snapcore/snapd/overlord/state"
    30  	"github.com/snapcore/snapd/snap"
    31  	"github.com/snapcore/snapd/snap/quota"
    32  	"github.com/snapcore/snapd/snap/snaptest"
    33  	"github.com/snapcore/snapd/snapdenv"
    34  	"github.com/snapcore/snapd/systemd"
    35  )
    36  
    37  type quotaHandlersSuite struct {
    38  	baseServiceMgrTestSuite
    39  }
    40  
    41  var _ = Suite(&quotaHandlersSuite{})
    42  
    43  func allGrps(c *C, st *state.State) map[string]*quota.Group {
    44  	allGrps, err := servicestate.AllQuotas(st)
    45  	c.Assert(err, IsNil)
    46  
    47  	return allGrps
    48  }
    49  
    50  func (s *quotaHandlersSuite) SetUpTest(c *C) {
    51  	s.baseServiceMgrTestSuite.SetUpTest(c)
    52  
    53  	// we don't need the EnsureSnapServices ensure loop to run by default
    54  	servicestate.MockEnsuredSnapServices(s.mgr, true)
    55  
    56  	// we enable quota-groups by default
    57  	s.state.Lock()
    58  	defer s.state.Unlock()
    59  	tr := config.NewTransaction(s.state)
    60  	tr.Set("core", "experimental.quota-groups", true)
    61  	tr.Commit()
    62  
    63  	// mock that we have a new enough version of systemd by default
    64  	r := servicestate.MockSystemdVersion(248)
    65  	s.AddCleanup(r)
    66  }
    67  
    68  func (s *quotaHandlersSuite) TestDoQuotaControlCreate(c *C) {
    69  	r := s.mockSystemctlCalls(c, join(
    70  		// doQuotaControl handler to create the group
    71  		systemctlCallsForCreateQuota("foo-group", "test-snap"),
    72  	))
    73  	defer r()
    74  
    75  	st := s.state
    76  	st.Lock()
    77  	defer st.Unlock()
    78  
    79  	// setup the snap so it exists
    80  	snapstate.Set(s.state, "test-snap", s.testSnapState)
    81  	snaptest.MockSnapCurrent(c, testYaml, s.testSnapSideInfo)
    82  
    83  	// make a fake task
    84  	t := st.NewTask("create-quota", "...")
    85  
    86  	qcs := []servicestate.QuotaControlAction{
    87  		{
    88  			Action:      "create",
    89  			QuotaName:   "foo-group",
    90  			MemoryLimit: quantity.SizeGiB,
    91  			AddSnaps:    []string{"test-snap"},
    92  		},
    93  	}
    94  
    95  	t.Set("quota-control-actions", &qcs)
    96  
    97  	st.Unlock()
    98  	err := s.o.ServiceManager().DoQuotaControl(t, nil)
    99  	st.Lock()
   100  
   101  	c.Assert(err, IsNil)
   102  	c.Assert(t.Status(), Equals, state.DoneStatus)
   103  
   104  	checkQuotaState(c, st, map[string]quotaGroupState{
   105  		"foo-group": {
   106  			MemoryLimit: quantity.SizeGiB,
   107  			Snaps:       []string{"test-snap"},
   108  		},
   109  	})
   110  }
   111  
   112  func (s *quotaHandlersSuite) TestDoQuotaControlUpdate(c *C) {
   113  	r := s.mockSystemctlCalls(c, join(
   114  		// CreateQuota for foo-group
   115  		systemctlCallsForCreateQuota("foo-group", "test-snap"),
   116  
   117  		// doQuotaControl handler which updates the group
   118  		[]expectedSystemctl{{expArgs: []string{"daemon-reload"}}},
   119  	))
   120  	defer r()
   121  
   122  	st := s.state
   123  	st.Lock()
   124  	defer st.Unlock()
   125  
   126  	// setup the snap so it exists
   127  	snapstate.Set(s.state, "test-snap", s.testSnapState)
   128  	snaptest.MockSnapCurrent(c, testYaml, s.testSnapSideInfo)
   129  
   130  	// create a quota group
   131  	err := servicestate.CreateQuota(st, "foo-group", "", []string{"test-snap"}, quantity.SizeGiB)
   132  	c.Assert(err, IsNil)
   133  
   134  	// create a task for updating the quota group
   135  	t := st.NewTask("update-quota", "...")
   136  
   137  	// update the memory limit to be double
   138  	qcs := []servicestate.QuotaControlAction{
   139  		{
   140  			Action:      "update",
   141  			QuotaName:   "foo-group",
   142  			MemoryLimit: 2 * quantity.SizeGiB,
   143  		},
   144  	}
   145  
   146  	t.Set("quota-control-actions", &qcs)
   147  
   148  	st.Unlock()
   149  	err = s.o.ServiceManager().DoQuotaControl(t, nil)
   150  	st.Lock()
   151  
   152  	c.Assert(err, IsNil)
   153  	c.Assert(t.Status(), Equals, state.DoneStatus)
   154  
   155  	checkQuotaState(c, st, map[string]quotaGroupState{
   156  		"foo-group": {
   157  			MemoryLimit: 2 * quantity.SizeGiB,
   158  			Snaps:       []string{"test-snap"},
   159  		},
   160  	})
   161  }
   162  
   163  func (s *quotaHandlersSuite) TestDoQuotaControlRemove(c *C) {
   164  	r := s.mockSystemctlCalls(c, join(
   165  		// CreateQuota for foo-group
   166  		systemctlCallsForCreateQuota("foo-group", "test-snap"),
   167  
   168  		// doQuotaControl handler which removes the group
   169  		[]expectedSystemctl{{expArgs: []string{"daemon-reload"}}},
   170  		systemctlCallsForSliceStop("foo-group"),
   171  		[]expectedSystemctl{{expArgs: []string{"daemon-reload"}}},
   172  		systemctlCallsForServiceRestart("test-snap"),
   173  	))
   174  	defer r()
   175  
   176  	st := s.state
   177  	st.Lock()
   178  	defer st.Unlock()
   179  
   180  	// setup the snap so it exists
   181  	snapstate.Set(s.state, "test-snap", s.testSnapState)
   182  	snaptest.MockSnapCurrent(c, testYaml, s.testSnapSideInfo)
   183  
   184  	// create a quota group
   185  	err := servicestate.CreateQuota(st, "foo-group", "", []string{"test-snap"}, quantity.SizeGiB)
   186  	c.Assert(err, IsNil)
   187  
   188  	// create a task for removing the quota group
   189  	t := st.NewTask("remove-quota", "...")
   190  
   191  	// update the memory limit to be double
   192  	qcs := []servicestate.QuotaControlAction{
   193  		{
   194  			Action:    "remove",
   195  			QuotaName: "foo-group",
   196  		},
   197  	}
   198  
   199  	t.Set("quota-control-actions", &qcs)
   200  
   201  	st.Unlock()
   202  	err = s.o.ServiceManager().DoQuotaControl(t, nil)
   203  	st.Lock()
   204  
   205  	c.Assert(err, IsNil)
   206  	c.Assert(t.Status(), Equals, state.DoneStatus)
   207  
   208  	checkQuotaState(c, st, nil)
   209  }
   210  
   211  func (s *quotaHandlersSuite) TestQuotaCreatePreseeding(c *C) {
   212  	// should be no systemctl calls since we are preseeding
   213  	r := snapdenv.MockPreseeding(true)
   214  	defer r()
   215  
   216  	st := s.state
   217  	st.Lock()
   218  	defer st.Unlock()
   219  
   220  	// setup the snap so it exists
   221  	snapstate.Set(s.state, "test-snap", s.testSnapState)
   222  	snaptest.MockSnapCurrent(c, testYaml, s.testSnapSideInfo)
   223  
   224  	// now we can create the quota group
   225  	qc := servicestate.QuotaControlAction{
   226  		Action:      "create",
   227  		QuotaName:   "foo",
   228  		MemoryLimit: quantity.SizeGiB,
   229  		AddSnaps:    []string{"test-snap"},
   230  	}
   231  
   232  	err := servicestate.QuotaCreate(st, nil, qc, allGrps(c, st), nil, nil)
   233  	c.Assert(err, IsNil)
   234  
   235  	// check that the quota groups were created in the state
   236  	checkQuotaState(c, st, map[string]quotaGroupState{
   237  		"foo": {
   238  			MemoryLimit: quantity.SizeGiB,
   239  			Snaps:       []string{"test-snap"},
   240  		},
   241  	})
   242  }
   243  
   244  func (s *quotaHandlersSuite) TestQuotaCreate(c *C) {
   245  	r := s.mockSystemctlCalls(c, join(
   246  		// CreateQuota for non-installed snap - fails
   247  
   248  		// CreateQuota for foo - success
   249  		systemctlCallsForCreateQuota("foo", "test-snap"),
   250  
   251  		// CreateQuota for foo2 with overlapping snap already in foo
   252  
   253  		// CreateQuota for foo again - fails
   254  	))
   255  	defer r()
   256  
   257  	st := s.state
   258  	st.Lock()
   259  	defer st.Unlock()
   260  
   261  	// trying to create a quota with a snap that doesn't exist fails
   262  	qc := servicestate.QuotaControlAction{
   263  		Action:      "create",
   264  		QuotaName:   "foo",
   265  		MemoryLimit: quantity.SizeGiB,
   266  		AddSnaps:    []string{"test-snap"},
   267  	}
   268  
   269  	err := servicestate.QuotaCreate(st, nil, qc, allGrps(c, st), nil, nil)
   270  	c.Assert(err, ErrorMatches, `cannot use snap "test-snap" in group "foo": snap "test-snap" is not installed`)
   271  
   272  	// setup the snap so it exists
   273  	snapstate.Set(s.state, "test-snap", s.testSnapState)
   274  	snaptest.MockSnapCurrent(c, testYaml, s.testSnapSideInfo)
   275  
   276  	// now we can create the quota group
   277  	err = servicestate.QuotaCreate(st, nil, qc, allGrps(c, st), nil, nil)
   278  	c.Assert(err, IsNil)
   279  
   280  	// creating the same group again will fail
   281  	err = servicestate.QuotaCreate(st, nil, qc, allGrps(c, st), nil, nil)
   282  	c.Assert(err, ErrorMatches, `group "foo" already exists`)
   283  
   284  	// we can't add the same snap to a different group
   285  	qc2 := servicestate.QuotaControlAction{
   286  		Action:      "create",
   287  		QuotaName:   "foo2",
   288  		MemoryLimit: quantity.SizeGiB,
   289  		AddSnaps:    []string{"test-snap"},
   290  	}
   291  
   292  	err = servicestate.QuotaCreate(st, nil, qc2, allGrps(c, st), nil, nil)
   293  	c.Assert(err, ErrorMatches, `cannot add snap "test-snap" to group "foo2": snap already in quota group "foo"`)
   294  
   295  	// check that the quota groups were created in the state
   296  	checkQuotaState(c, st, map[string]quotaGroupState{
   297  		"foo": {
   298  			MemoryLimit: quantity.SizeGiB,
   299  			Snaps:       []string{"test-snap"},
   300  		},
   301  	})
   302  }
   303  
   304  func (s *quotaHandlersSuite) TestDoCreateSubGroupQuota(c *C) {
   305  	r := s.mockSystemctlCalls(c, join(
   306  		// CreateQuota for foo - no systemctl calls since no snaps in it
   307  
   308  		// CreateQuota for foo2 - fails thus no systemctl calls
   309  
   310  		// CreateQuota for foo2 - we don't write anything for the first quota
   311  		// since there are no snaps in the quota to track
   312  		[]expectedSystemctl{{expArgs: []string{"daemon-reload"}}},
   313  		systemctlCallsForSliceStart("foo-group"),
   314  		systemctlCallsForSliceStart("foo-group/foo2"),
   315  		systemctlCallsForServiceRestart("test-snap"),
   316  	))
   317  	defer r()
   318  
   319  	st := s.state
   320  	st.Lock()
   321  	defer st.Unlock()
   322  
   323  	// setup the snap so it exists
   324  	snapstate.Set(s.state, "test-snap", s.testSnapState)
   325  	snaptest.MockSnapCurrent(c, testYaml, s.testSnapSideInfo)
   326  
   327  	// create a quota group with no snaps to be the parent
   328  	qc := servicestate.QuotaControlAction{
   329  		Action:      "create",
   330  		QuotaName:   "foo-group",
   331  		MemoryLimit: quantity.SizeGiB,
   332  	}
   333  
   334  	err := servicestate.QuotaCreate(st, nil, qc, allGrps(c, st), nil, nil)
   335  	c.Assert(err, IsNil)
   336  
   337  	// trying to create a quota group with a non-existent parent group fails
   338  	qc2 := servicestate.QuotaControlAction{
   339  		Action:      "create",
   340  		QuotaName:   "foo2",
   341  		MemoryLimit: quantity.SizeGiB,
   342  		ParentName:  "foo-non-real",
   343  		AddSnaps:    []string{"test-snap"},
   344  	}
   345  
   346  	err = servicestate.QuotaCreate(st, nil, qc2, allGrps(c, st), nil, nil)
   347  	c.Assert(err, ErrorMatches, `cannot create group under non-existent parent group "foo-non-real"`)
   348  
   349  	// trying to create a quota group with too big of a limit to fit inside the
   350  	// parent fails
   351  	qc3 := servicestate.QuotaControlAction{
   352  		Action:      "create",
   353  		QuotaName:   "foo2",
   354  		MemoryLimit: 2 * quantity.SizeGiB,
   355  		ParentName:  "foo-group",
   356  		AddSnaps:    []string{"test-snap"},
   357  	}
   358  
   359  	err = servicestate.QuotaCreate(st, nil, qc3, allGrps(c, st), nil, nil)
   360  	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`)
   361  
   362  	// now we can create a sub-quota
   363  	qc4 := servicestate.QuotaControlAction{
   364  		Action:      "create",
   365  		QuotaName:   "foo2",
   366  		MemoryLimit: quantity.SizeGiB,
   367  		ParentName:  "foo-group",
   368  		AddSnaps:    []string{"test-snap"},
   369  	}
   370  
   371  	err = servicestate.QuotaCreate(st, nil, qc4, allGrps(c, st), nil, nil)
   372  	c.Assert(err, IsNil)
   373  
   374  	// check that the quota groups were created in the state
   375  	checkQuotaState(c, st, map[string]quotaGroupState{
   376  		"foo-group": {
   377  			MemoryLimit: quantity.SizeGiB,
   378  			SubGroups:   []string{"foo2"},
   379  		},
   380  		"foo2": {
   381  			MemoryLimit: quantity.SizeGiB,
   382  			Snaps:       []string{"test-snap"},
   383  			ParentGroup: "foo-group",
   384  		},
   385  	})
   386  
   387  	// foo-group exists as a slice too, but has no snap services in the slice
   388  	checkSliceState(c, systemd.EscapeUnitNamePath("foo-group"), quantity.SizeGiB)
   389  }
   390  
   391  func (s *quotaHandlersSuite) TestQuotaRemove(c *C) {
   392  	r := s.mockSystemctlCalls(c, join(
   393  		// CreateQuota for foo
   394  		systemctlCallsForCreateQuota("foo", "test-snap"),
   395  
   396  		// for CreateQuota foo2 - no systemctl calls since there are no snaps
   397  
   398  		// for CreateQuota foo3 - no systemctl calls since there are no snaps
   399  
   400  		// RemoveQuota for foo2 - no daemon reload initially because
   401  		// we didn't modify anything, as there are no snaps in foo2 so we don't
   402  		// create that group on disk
   403  		// TODO: is this bit correct in practice? we are in effect calling
   404  		// systemctl stop <non-existing-slice> ?
   405  		systemctlCallsForSliceStop("foo/foo3"),
   406  
   407  		systemctlCallsForSliceStop("foo/foo2"),
   408  
   409  		// RemoveQuota for foo
   410  		[]expectedSystemctl{{expArgs: []string{"daemon-reload"}}},
   411  		systemctlCallsForSliceStop("foo"),
   412  		[]expectedSystemctl{{expArgs: []string{"daemon-reload"}}},
   413  		systemctlCallsForServiceRestart("test-snap"),
   414  	))
   415  	defer r()
   416  
   417  	st := s.state
   418  	st.Lock()
   419  	defer st.Unlock()
   420  
   421  	// setup the snap so it exists
   422  	snapstate.Set(s.state, "test-snap", s.testSnapState)
   423  	snaptest.MockSnapCurrent(c, testYaml, s.testSnapSideInfo)
   424  
   425  	// trying to remove a group that does not exist fails
   426  	qc := servicestate.QuotaControlAction{
   427  		Action:    "remove",
   428  		QuotaName: "not-exists",
   429  	}
   430  
   431  	err := servicestate.QuotaRemove(st, nil, qc, allGrps(c, st), nil, nil)
   432  	c.Assert(err, ErrorMatches, `cannot remove non-existent quota group "not-exists"`)
   433  
   434  	qc2 := servicestate.QuotaControlAction{
   435  		Action:      "create",
   436  		QuotaName:   "foo",
   437  		MemoryLimit: quantity.SizeGiB,
   438  		AddSnaps:    []string{"test-snap"},
   439  	}
   440  
   441  	err = servicestate.QuotaCreate(st, nil, qc2, allGrps(c, st), nil, nil)
   442  	c.Assert(err, IsNil)
   443  
   444  	// create 2 quota sub-groups too
   445  	qc3 := servicestate.QuotaControlAction{
   446  		Action:      "create",
   447  		QuotaName:   "foo2",
   448  		MemoryLimit: quantity.SizeGiB / 2,
   449  		ParentName:  "foo",
   450  	}
   451  
   452  	err = servicestate.QuotaCreate(st, nil, qc3, allGrps(c, st), nil, nil)
   453  	c.Assert(err, IsNil)
   454  
   455  	qc4 := servicestate.QuotaControlAction{
   456  		Action:      "create",
   457  		QuotaName:   "foo3",
   458  		MemoryLimit: quantity.SizeGiB / 2,
   459  		ParentName:  "foo",
   460  	}
   461  
   462  	err = servicestate.QuotaCreate(st, nil, qc4, allGrps(c, st), nil, nil)
   463  	c.Assert(err, IsNil)
   464  
   465  	// check that the quota groups was created in the state
   466  	checkQuotaState(c, st, map[string]quotaGroupState{
   467  		"foo": {
   468  			MemoryLimit: quantity.SizeGiB,
   469  			Snaps:       []string{"test-snap"},
   470  			SubGroups:   []string{"foo2", "foo3"},
   471  		},
   472  		"foo2": {
   473  			MemoryLimit: quantity.SizeGiB / 2,
   474  			ParentGroup: "foo",
   475  		},
   476  		"foo3": {
   477  			MemoryLimit: quantity.SizeGiB / 2,
   478  			ParentGroup: "foo",
   479  		},
   480  	})
   481  
   482  	// try removing the parent and it fails since it still has a sub-group
   483  	// under it
   484  	qc5 := servicestate.QuotaControlAction{
   485  		Action:    "remove",
   486  		QuotaName: "foo",
   487  	}
   488  
   489  	err = servicestate.QuotaRemove(st, nil, qc5, allGrps(c, st), nil, nil)
   490  	c.Assert(err, ErrorMatches, "cannot remove quota group with sub-groups, remove the sub-groups first")
   491  
   492  	// but we can remove the sub-group successfully first
   493  	qc6 := servicestate.QuotaControlAction{
   494  		Action:    "remove",
   495  		QuotaName: "foo3",
   496  	}
   497  
   498  	err = servicestate.QuotaRemove(st, nil, qc6, allGrps(c, st), nil, nil)
   499  	c.Assert(err, IsNil)
   500  
   501  	checkQuotaState(c, st, map[string]quotaGroupState{
   502  		"foo": {
   503  			MemoryLimit: quantity.SizeGiB,
   504  			Snaps:       []string{"test-snap"},
   505  			SubGroups:   []string{"foo2"},
   506  		},
   507  		"foo2": {
   508  			MemoryLimit: quantity.SizeGiB / 2,
   509  			ParentGroup: "foo",
   510  		},
   511  	})
   512  
   513  	// and we can remove the other sub-group
   514  	qc7 := servicestate.QuotaControlAction{
   515  		Action:    "remove",
   516  		QuotaName: "foo2",
   517  	}
   518  
   519  	err = servicestate.QuotaRemove(st, nil, qc7, allGrps(c, st), nil, nil)
   520  	c.Assert(err, IsNil)
   521  
   522  	checkQuotaState(c, st, map[string]quotaGroupState{
   523  		"foo": {
   524  			MemoryLimit: quantity.SizeGiB,
   525  			Snaps:       []string{"test-snap"},
   526  		},
   527  	})
   528  
   529  	// now we can remove the quota from the state
   530  	qc8 := servicestate.QuotaControlAction{
   531  		Action:    "remove",
   532  		QuotaName: "foo",
   533  	}
   534  
   535  	err = servicestate.QuotaRemove(st, nil, qc8, allGrps(c, st), nil, nil)
   536  	c.Assert(err, IsNil)
   537  
   538  	checkQuotaState(c, st, nil)
   539  
   540  	// foo is not mentioned in the service and doesn't exist
   541  	checkSvcAndSliceState(c, "test-snap.svc1", "foo", 0)
   542  }
   543  
   544  func (s *quotaHandlersSuite) TestQuotaUpdateGroupNotExist(c *C) {
   545  	st := s.state
   546  	st.Lock()
   547  	defer st.Unlock()
   548  
   549  	// non-existent quota group
   550  	qc := servicestate.QuotaControlAction{
   551  		Action:    "update",
   552  		QuotaName: "non-existing",
   553  	}
   554  
   555  	err := servicestate.QuotaUpdate(st, nil, qc, allGrps(c, st), nil, nil)
   556  	c.Check(err, ErrorMatches, `group "non-existing" does not exist`)
   557  }
   558  
   559  func (s *quotaHandlersSuite) TestQuotaUpdateSubGroupTooBig(c *C) {
   560  	r := s.mockSystemctlCalls(c, join(
   561  		// CreateQuota for foo
   562  		systemctlCallsForCreateQuota("foo", "test-snap"),
   563  
   564  		// CreateQuota for foo2
   565  		systemctlCallsForCreateQuota("foo/foo2", "test-snap2"),
   566  
   567  		// UpdateQuota for foo2 - just the slice changes
   568  		[]expectedSystemctl{{expArgs: []string{"daemon-reload"}}},
   569  
   570  		// UpdateQuota for foo2 which fails - no systemctl calls
   571  	))
   572  	defer r()
   573  
   574  	st := s.state
   575  	st.Lock()
   576  	defer st.Unlock()
   577  
   578  	// setup the snap so it exists
   579  	snapstate.Set(s.state, "test-snap", s.testSnapState)
   580  	snaptest.MockSnapCurrent(c, testYaml, s.testSnapSideInfo)
   581  	// and test-snap2
   582  	si2 := &snap.SideInfo{RealName: "test-snap2", Revision: snap.R(42)}
   583  	snapst2 := &snapstate.SnapState{
   584  		Sequence: []*snap.SideInfo{si2},
   585  		Current:  si2.Revision,
   586  		Active:   true,
   587  		SnapType: "app",
   588  	}
   589  	snapstate.Set(s.state, "test-snap2", snapst2)
   590  	snaptest.MockSnapCurrent(c, testYaml2, si2)
   591  
   592  	// create a quota group
   593  	qc := servicestate.QuotaControlAction{
   594  		Action:      "create",
   595  		QuotaName:   "foo",
   596  		MemoryLimit: quantity.SizeGiB,
   597  		AddSnaps:    []string{"test-snap"},
   598  	}
   599  
   600  	err := servicestate.QuotaCreate(st, nil, qc, allGrps(c, st), nil, nil)
   601  	c.Assert(err, IsNil)
   602  
   603  	// ensure mem-limit is 1 GB
   604  	expFooGroupState := quotaGroupState{
   605  		MemoryLimit: quantity.SizeGiB,
   606  		Snaps:       []string{"test-snap"},
   607  	}
   608  	checkQuotaState(c, st, map[string]quotaGroupState{
   609  		"foo": expFooGroupState,
   610  	})
   611  
   612  	// create a sub-group with 0.5 GiB
   613  	qc2 := servicestate.QuotaControlAction{
   614  		Action:      "create",
   615  		QuotaName:   "foo2",
   616  		MemoryLimit: quantity.SizeGiB / 2,
   617  		AddSnaps:    []string{"test-snap2"},
   618  		ParentName:  "foo",
   619  	}
   620  
   621  	err = servicestate.QuotaCreate(st, nil, qc2, allGrps(c, st), nil, nil)
   622  	c.Assert(err, IsNil)
   623  
   624  	expFooGroupState.SubGroups = []string{"foo2"}
   625  
   626  	expFoo2GroupState := quotaGroupState{
   627  		MemoryLimit: quantity.SizeGiB / 2,
   628  		Snaps:       []string{"test-snap2"},
   629  		ParentGroup: "foo",
   630  	}
   631  
   632  	// verify it was set in state
   633  	checkQuotaState(c, st, map[string]quotaGroupState{
   634  		"foo":  expFooGroupState,
   635  		"foo2": expFoo2GroupState,
   636  	})
   637  
   638  	// now try to increase it to the max size
   639  	qc3 := servicestate.QuotaControlAction{
   640  		Action:      "update",
   641  		QuotaName:   "foo2",
   642  		MemoryLimit: quantity.SizeGiB,
   643  	}
   644  
   645  	err = servicestate.QuotaUpdate(st, nil, qc3, allGrps(c, st), nil, nil)
   646  	c.Assert(err, IsNil)
   647  
   648  	expFoo2GroupState.MemoryLimit = quantity.SizeGiB
   649  	// and check that it got updated in the state
   650  	checkQuotaState(c, st, map[string]quotaGroupState{
   651  		"foo":  expFooGroupState,
   652  		"foo2": expFoo2GroupState,
   653  	})
   654  
   655  	// now try to increase it above the parent limit
   656  	qc4 := servicestate.QuotaControlAction{
   657  		Action:      "update",
   658  		QuotaName:   "foo2",
   659  		MemoryLimit: 2 * quantity.SizeGiB,
   660  	}
   661  
   662  	err = servicestate.QuotaUpdate(st, nil, qc4, allGrps(c, st), nil, nil)
   663  	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`)
   664  
   665  	// and make sure that the existing memory limit is still in place
   666  	checkQuotaState(c, st, map[string]quotaGroupState{
   667  		"foo":  expFooGroupState,
   668  		"foo2": expFoo2GroupState,
   669  	})
   670  }
   671  
   672  func (s *quotaHandlersSuite) TestUpdateQuotaGroupNotEnabled(c *C) {
   673  	s.state.Lock()
   674  	defer s.state.Unlock()
   675  	tr := config.NewTransaction(s.state)
   676  	tr.Set("core", "experimental.quota-groups", false)
   677  	tr.Commit()
   678  
   679  	opts := servicestate.QuotaGroupUpdate{}
   680  	err := servicestate.UpdateQuota(s.state, "foo", opts)
   681  	c.Assert(err, ErrorMatches, `experimental feature disabled - test it by setting 'experimental.quota-groups' to true`)
   682  }
   683  
   684  func (s *quotaHandlersSuite) TestQuotaUpdateChangeMemLimit(c *C) {
   685  	r := s.mockSystemctlCalls(c, join(
   686  		// CreateQuota for foo
   687  		systemctlCallsForCreateQuota("foo", "test-snap"),
   688  
   689  		// UpdateQuota for foo - an existing slice was changed, so all we need
   690  		// to is daemon-reload
   691  		[]expectedSystemctl{{expArgs: []string{"daemon-reload"}}},
   692  	))
   693  	defer r()
   694  
   695  	st := s.state
   696  	st.Lock()
   697  	defer st.Unlock()
   698  
   699  	// setup the snap so it exists
   700  	snapstate.Set(s.state, "test-snap", s.testSnapState)
   701  	snaptest.MockSnapCurrent(c, testYaml, s.testSnapSideInfo)
   702  
   703  	// create a quota group
   704  	qc := servicestate.QuotaControlAction{
   705  		Action:      "create",
   706  		QuotaName:   "foo",
   707  		MemoryLimit: quantity.SizeGiB,
   708  		AddSnaps:    []string{"test-snap"},
   709  	}
   710  
   711  	err := servicestate.QuotaCreate(st, nil, qc, allGrps(c, st), nil, nil)
   712  	c.Assert(err, IsNil)
   713  
   714  	// ensure mem-limit is 1 GB
   715  	checkQuotaState(c, st, map[string]quotaGroupState{
   716  		"foo": {
   717  			MemoryLimit: quantity.SizeGiB,
   718  			Snaps:       []string{"test-snap"},
   719  		},
   720  	})
   721  
   722  	// modify to 2 GB
   723  	qc2 := servicestate.QuotaControlAction{
   724  		Action:      "update",
   725  		QuotaName:   "foo",
   726  		MemoryLimit: 2 * quantity.SizeGiB,
   727  	}
   728  	err = servicestate.QuotaUpdate(st, nil, qc2, allGrps(c, st), nil, nil)
   729  	c.Assert(err, IsNil)
   730  
   731  	// and check that it got updated in the state
   732  	checkQuotaState(c, st, map[string]quotaGroupState{
   733  		"foo": {
   734  			MemoryLimit: 2 * quantity.SizeGiB,
   735  			Snaps:       []string{"test-snap"},
   736  		},
   737  	})
   738  
   739  	// trying to decrease the memory limit is not yet supported
   740  	qc3 := servicestate.QuotaControlAction{
   741  		Action:      "update",
   742  		QuotaName:   "foo",
   743  		MemoryLimit: quantity.SizeGiB,
   744  	}
   745  	err = servicestate.QuotaUpdate(st, nil, qc3, allGrps(c, st), nil, nil)
   746  	c.Assert(err, ErrorMatches, "cannot decrease memory limit of existing quota-group, remove and re-create it to decrease the limit")
   747  }
   748  
   749  func (s *quotaHandlersSuite) TestQuotaUpdateAddSnap(c *C) {
   750  	r := s.mockSystemctlCalls(c, join(
   751  		// CreateQuota for foo
   752  		systemctlCallsForCreateQuota("foo", "test-snap"),
   753  
   754  		// UpdateQuota with just test-snap2 restarted since the group already
   755  		// exists
   756  		[]expectedSystemctl{{expArgs: []string{"daemon-reload"}}},
   757  		systemctlCallsForServiceRestart("test-snap2"),
   758  	))
   759  	defer r()
   760  
   761  	st := s.state
   762  	st.Lock()
   763  	defer st.Unlock()
   764  
   765  	// setup test-snap
   766  	snapstate.Set(s.state, "test-snap", s.testSnapState)
   767  	snaptest.MockSnapCurrent(c, testYaml, s.testSnapSideInfo)
   768  	// and test-snap2
   769  	si2 := &snap.SideInfo{RealName: "test-snap2", Revision: snap.R(42)}
   770  	snapst2 := &snapstate.SnapState{
   771  		Sequence: []*snap.SideInfo{si2},
   772  		Current:  si2.Revision,
   773  		Active:   true,
   774  		SnapType: "app",
   775  	}
   776  	snapstate.Set(s.state, "test-snap2", snapst2)
   777  	snaptest.MockSnapCurrent(c, testYaml2, si2)
   778  
   779  	// create a quota group
   780  	qc := servicestate.QuotaControlAction{
   781  		Action:      "create",
   782  		QuotaName:   "foo",
   783  		MemoryLimit: quantity.SizeGiB,
   784  		AddSnaps:    []string{"test-snap"},
   785  	}
   786  
   787  	err := servicestate.QuotaCreate(st, nil, qc, allGrps(c, st), nil, nil)
   788  	c.Assert(err, IsNil)
   789  
   790  	checkQuotaState(c, st, map[string]quotaGroupState{
   791  		"foo": {
   792  			MemoryLimit: quantity.SizeGiB,
   793  			Snaps:       []string{"test-snap"},
   794  		},
   795  	})
   796  
   797  	// add a snap
   798  	qc2 := servicestate.QuotaControlAction{
   799  		Action:    "update",
   800  		QuotaName: "foo",
   801  		AddSnaps:  []string{"test-snap2"},
   802  	}
   803  	err = servicestate.QuotaUpdate(st, nil, qc2, allGrps(c, st), nil, nil)
   804  	c.Assert(err, IsNil)
   805  
   806  	// and check that it got updated in the state
   807  	checkQuotaState(c, st, map[string]quotaGroupState{
   808  		"foo": {
   809  			MemoryLimit: quantity.SizeGiB,
   810  			Snaps:       []string{"test-snap", "test-snap2"},
   811  		},
   812  	})
   813  }
   814  
   815  func (s *quotaHandlersSuite) TestQuotaUpdateAddSnapAlreadyInOtherGroup(c *C) {
   816  	r := s.mockSystemctlCalls(c, join(
   817  		// CreateQuota for foo
   818  		systemctlCallsForCreateQuota("foo", "test-snap"),
   819  
   820  		// CreateQuota for foo2
   821  		systemctlCallsForCreateQuota("foo2", "test-snap2"),
   822  
   823  		// UpdateQuota for foo which fails - no systemctl calls
   824  	))
   825  	defer r()
   826  
   827  	st := s.state
   828  	st.Lock()
   829  	defer st.Unlock()
   830  
   831  	// setup test-snap
   832  	snapstate.Set(s.state, "test-snap", s.testSnapState)
   833  	snaptest.MockSnapCurrent(c, testYaml, s.testSnapSideInfo)
   834  	// and test-snap2
   835  	si2 := &snap.SideInfo{RealName: "test-snap2", Revision: snap.R(42)}
   836  	snapst2 := &snapstate.SnapState{
   837  		Sequence: []*snap.SideInfo{si2},
   838  		Current:  si2.Revision,
   839  		Active:   true,
   840  		SnapType: "app",
   841  	}
   842  	snapstate.Set(s.state, "test-snap2", snapst2)
   843  	snaptest.MockSnapCurrent(c, testYaml2, si2)
   844  
   845  	// create a quota group
   846  	qc := servicestate.QuotaControlAction{
   847  		Action:      "create",
   848  		QuotaName:   "foo",
   849  		MemoryLimit: quantity.SizeGiB,
   850  		AddSnaps:    []string{"test-snap"},
   851  	}
   852  
   853  	err := servicestate.QuotaCreate(st, nil, qc, allGrps(c, st), nil, nil)
   854  	c.Assert(err, IsNil)
   855  
   856  	checkQuotaState(c, st, map[string]quotaGroupState{
   857  		"foo": {
   858  			MemoryLimit: quantity.SizeGiB,
   859  			Snaps:       []string{"test-snap"},
   860  		},
   861  	})
   862  
   863  	// create another quota group with the second snap
   864  	qc2 := servicestate.QuotaControlAction{
   865  		Action:      "create",
   866  		QuotaName:   "foo2",
   867  		MemoryLimit: quantity.SizeGiB,
   868  		AddSnaps:    []string{"test-snap2"},
   869  	}
   870  
   871  	err = servicestate.QuotaCreate(st, nil, qc2, allGrps(c, st), nil, nil)
   872  	c.Assert(err, IsNil)
   873  
   874  	// verify state
   875  	checkQuotaState(c, st, map[string]quotaGroupState{
   876  		"foo": {
   877  			MemoryLimit: quantity.SizeGiB,
   878  			Snaps:       []string{"test-snap"},
   879  		},
   880  		"foo2": {
   881  			MemoryLimit: quantity.SizeGiB,
   882  			Snaps:       []string{"test-snap2"},
   883  		},
   884  	})
   885  
   886  	// try to add test-snap2 to foo
   887  	qc3 := servicestate.QuotaControlAction{
   888  		Action:    "update",
   889  		QuotaName: "foo",
   890  		AddSnaps:  []string{"test-snap2"},
   891  	}
   892  
   893  	err = servicestate.QuotaUpdate(st, nil, qc3, allGrps(c, st), nil, nil)
   894  	c.Assert(err, ErrorMatches, `cannot add snap "test-snap2" to group "foo": snap already in quota group "foo2"`)
   895  
   896  	// nothing changed in the state
   897  	checkQuotaState(c, st, map[string]quotaGroupState{
   898  		"foo": {
   899  			MemoryLimit: quantity.SizeGiB,
   900  			Snaps:       []string{"test-snap"},
   901  		},
   902  		"foo2": {
   903  			MemoryLimit: quantity.SizeGiB,
   904  			Snaps:       []string{"test-snap2"},
   905  		},
   906  	})
   907  }