github.com/freetocompute/snapd@v0.0.0-20210618182524-2fb355d72fd9/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  	qc2 := servicestate.QuotaControlAction{
   277  		Action:      "create",
   278  		QuotaName:   "foo",
   279  		MemoryLimit: 4 * quantity.SizeKiB,
   280  		AddSnaps:    []string{"test-snap"},
   281  	}
   282  
   283  	// trying to create a quota with too low of a memory limit fails
   284  	err = servicestate.QuotaCreate(st, nil, qc2, allGrps(c, st), nil, nil)
   285  	c.Assert(err, ErrorMatches, `memory limit for group "foo" is too small: size must be larger than 4KB`)
   286  
   287  	// but with an adequately sized memory limit, and a snap that exists, we can
   288  	// create it
   289  	qc3 := servicestate.QuotaControlAction{
   290  		Action:      "create",
   291  		QuotaName:   "foo",
   292  		MemoryLimit: 4*quantity.SizeKiB + 1,
   293  		AddSnaps:    []string{"test-snap"},
   294  	}
   295  	err = servicestate.QuotaCreate(st, nil, qc3, allGrps(c, st), nil, nil)
   296  	c.Assert(err, IsNil)
   297  
   298  	// creating the same group again will fail
   299  	err = servicestate.CreateQuota(s.state, "foo", "", []string{"test-snap"}, 4*quantity.SizeKiB+1)
   300  	c.Assert(err, ErrorMatches, `group "foo" already exists`)
   301  
   302  	// check that the quota groups were created in the state
   303  	checkQuotaState(c, st, map[string]quotaGroupState{
   304  		"foo": {
   305  			MemoryLimit: 4*quantity.SizeKiB + 1,
   306  			Snaps:       []string{"test-snap"},
   307  		},
   308  	})
   309  }
   310  
   311  func (s *quotaHandlersSuite) TestDoCreateSubGroupQuota(c *C) {
   312  	r := s.mockSystemctlCalls(c, join(
   313  		// CreateQuota for foo - no systemctl calls since no snaps in it
   314  
   315  		// CreateQuota for foo2 - fails thus no systemctl calls
   316  
   317  		// CreateQuota for foo2 - we don't write anything for the first quota
   318  		// since there are no snaps in the quota to track
   319  		[]expectedSystemctl{{expArgs: []string{"daemon-reload"}}},
   320  		systemctlCallsForSliceStart("foo-group"),
   321  		systemctlCallsForSliceStart("foo-group/foo2"),
   322  		systemctlCallsForServiceRestart("test-snap"),
   323  	))
   324  	defer r()
   325  
   326  	st := s.state
   327  	st.Lock()
   328  	defer st.Unlock()
   329  
   330  	// setup the snap so it exists
   331  	snapstate.Set(s.state, "test-snap", s.testSnapState)
   332  	snaptest.MockSnapCurrent(c, testYaml, s.testSnapSideInfo)
   333  
   334  	// create a quota group with no snaps to be the parent
   335  	qc := servicestate.QuotaControlAction{
   336  		Action:      "create",
   337  		QuotaName:   "foo-group",
   338  		MemoryLimit: quantity.SizeGiB,
   339  	}
   340  
   341  	err := servicestate.QuotaCreate(st, nil, qc, allGrps(c, st), nil, nil)
   342  	c.Assert(err, IsNil)
   343  
   344  	// trying to create a quota group with a non-existent parent group fails
   345  	qc2 := servicestate.QuotaControlAction{
   346  		Action:      "create",
   347  		QuotaName:   "foo2",
   348  		MemoryLimit: quantity.SizeGiB,
   349  		ParentName:  "foo-non-real",
   350  		AddSnaps:    []string{"test-snap"},
   351  	}
   352  
   353  	err = servicestate.QuotaCreate(st, nil, qc2, allGrps(c, st), nil, nil)
   354  	c.Assert(err, ErrorMatches, `cannot create group under non-existent parent group "foo-non-real"`)
   355  
   356  	// trying to create a quota group with too big of a limit to fit inside the
   357  	// parent fails
   358  	qc3 := servicestate.QuotaControlAction{
   359  		Action:      "create",
   360  		QuotaName:   "foo2",
   361  		MemoryLimit: 2 * quantity.SizeGiB,
   362  		ParentName:  "foo-group",
   363  		AddSnaps:    []string{"test-snap"},
   364  	}
   365  
   366  	err = servicestate.QuotaCreate(st, nil, qc3, allGrps(c, st), nil, nil)
   367  	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`)
   368  
   369  	// now we can create a sub-quota
   370  	qc4 := servicestate.QuotaControlAction{
   371  		Action:      "create",
   372  		QuotaName:   "foo2",
   373  		MemoryLimit: quantity.SizeGiB,
   374  		ParentName:  "foo-group",
   375  		AddSnaps:    []string{"test-snap"},
   376  	}
   377  
   378  	err = servicestate.QuotaCreate(st, nil, qc4, allGrps(c, st), nil, nil)
   379  	c.Assert(err, IsNil)
   380  
   381  	// check that the quota groups were created in the state
   382  	checkQuotaState(c, st, map[string]quotaGroupState{
   383  		"foo-group": {
   384  			MemoryLimit: quantity.SizeGiB,
   385  			SubGroups:   []string{"foo2"},
   386  		},
   387  		"foo2": {
   388  			MemoryLimit: quantity.SizeGiB,
   389  			Snaps:       []string{"test-snap"},
   390  			ParentGroup: "foo-group",
   391  		},
   392  	})
   393  
   394  	// foo-group exists as a slice too, but has no snap services in the slice
   395  	checkSliceState(c, systemd.EscapeUnitNamePath("foo-group"), quantity.SizeGiB)
   396  }
   397  
   398  func (s *quotaHandlersSuite) TestQuotaRemove(c *C) {
   399  	r := s.mockSystemctlCalls(c, join(
   400  		// CreateQuota for foo
   401  		systemctlCallsForCreateQuota("foo", "test-snap"),
   402  
   403  		// for CreateQuota foo2 - no systemctl calls since there are no snaps
   404  
   405  		// for CreateQuota foo3 - no systemctl calls since there are no snaps
   406  
   407  		// RemoveQuota for foo2 - no daemon reload initially because
   408  		// we didn't modify anything, as there are no snaps in foo2 so we don't
   409  		// create that group on disk
   410  		// TODO: is this bit correct in practice? we are in effect calling
   411  		// systemctl stop <non-existing-slice> ?
   412  		systemctlCallsForSliceStop("foo/foo3"),
   413  
   414  		systemctlCallsForSliceStop("foo/foo2"),
   415  
   416  		// RemoveQuota for foo
   417  		[]expectedSystemctl{{expArgs: []string{"daemon-reload"}}},
   418  		systemctlCallsForSliceStop("foo"),
   419  		[]expectedSystemctl{{expArgs: []string{"daemon-reload"}}},
   420  		systemctlCallsForServiceRestart("test-snap"),
   421  	))
   422  	defer r()
   423  
   424  	st := s.state
   425  	st.Lock()
   426  	defer st.Unlock()
   427  
   428  	// setup the snap so it exists
   429  	snapstate.Set(s.state, "test-snap", s.testSnapState)
   430  	snaptest.MockSnapCurrent(c, testYaml, s.testSnapSideInfo)
   431  
   432  	// trying to remove a group that does not exist fails
   433  	qc := servicestate.QuotaControlAction{
   434  		Action:    "remove",
   435  		QuotaName: "not-exists",
   436  	}
   437  
   438  	err := servicestate.QuotaRemove(st, nil, qc, allGrps(c, st), nil, nil)
   439  	c.Assert(err, ErrorMatches, `cannot remove non-existent quota group "not-exists"`)
   440  
   441  	qc2 := servicestate.QuotaControlAction{
   442  		Action:      "create",
   443  		QuotaName:   "foo",
   444  		MemoryLimit: quantity.SizeGiB,
   445  		AddSnaps:    []string{"test-snap"},
   446  	}
   447  
   448  	err = servicestate.QuotaCreate(st, nil, qc2, allGrps(c, st), nil, nil)
   449  	c.Assert(err, IsNil)
   450  
   451  	// create 2 quota sub-groups too
   452  	qc3 := servicestate.QuotaControlAction{
   453  		Action:      "create",
   454  		QuotaName:   "foo2",
   455  		MemoryLimit: quantity.SizeGiB / 2,
   456  		ParentName:  "foo",
   457  	}
   458  
   459  	err = servicestate.QuotaCreate(st, nil, qc3, allGrps(c, st), nil, nil)
   460  	c.Assert(err, IsNil)
   461  
   462  	qc4 := servicestate.QuotaControlAction{
   463  		Action:      "create",
   464  		QuotaName:   "foo3",
   465  		MemoryLimit: quantity.SizeGiB / 2,
   466  		ParentName:  "foo",
   467  	}
   468  
   469  	err = servicestate.QuotaCreate(st, nil, qc4, allGrps(c, st), nil, nil)
   470  	c.Assert(err, IsNil)
   471  
   472  	// check that the quota groups was created in the state
   473  	checkQuotaState(c, st, map[string]quotaGroupState{
   474  		"foo": {
   475  			MemoryLimit: quantity.SizeGiB,
   476  			Snaps:       []string{"test-snap"},
   477  			SubGroups:   []string{"foo2", "foo3"},
   478  		},
   479  		"foo2": {
   480  			MemoryLimit: quantity.SizeGiB / 2,
   481  			ParentGroup: "foo",
   482  		},
   483  		"foo3": {
   484  			MemoryLimit: quantity.SizeGiB / 2,
   485  			ParentGroup: "foo",
   486  		},
   487  	})
   488  
   489  	// try removing the parent and it fails since it still has a sub-group
   490  	// under it
   491  	qc5 := servicestate.QuotaControlAction{
   492  		Action:    "remove",
   493  		QuotaName: "foo",
   494  	}
   495  
   496  	err = servicestate.QuotaRemove(st, nil, qc5, allGrps(c, st), nil, nil)
   497  	c.Assert(err, ErrorMatches, "cannot remove quota group with sub-groups, remove the sub-groups first")
   498  
   499  	// but we can remove the sub-group successfully first
   500  	qc6 := servicestate.QuotaControlAction{
   501  		Action:    "remove",
   502  		QuotaName: "foo3",
   503  	}
   504  
   505  	err = servicestate.QuotaRemove(st, nil, qc6, allGrps(c, st), nil, nil)
   506  	c.Assert(err, IsNil)
   507  
   508  	checkQuotaState(c, st, map[string]quotaGroupState{
   509  		"foo": {
   510  			MemoryLimit: quantity.SizeGiB,
   511  			Snaps:       []string{"test-snap"},
   512  			SubGroups:   []string{"foo2"},
   513  		},
   514  		"foo2": {
   515  			MemoryLimit: quantity.SizeGiB / 2,
   516  			ParentGroup: "foo",
   517  		},
   518  	})
   519  
   520  	// and we can remove the other sub-group
   521  	qc7 := servicestate.QuotaControlAction{
   522  		Action:    "remove",
   523  		QuotaName: "foo2",
   524  	}
   525  
   526  	err = servicestate.QuotaRemove(st, nil, qc7, allGrps(c, st), nil, nil)
   527  	c.Assert(err, IsNil)
   528  
   529  	checkQuotaState(c, st, map[string]quotaGroupState{
   530  		"foo": {
   531  			MemoryLimit: quantity.SizeGiB,
   532  			Snaps:       []string{"test-snap"},
   533  		},
   534  	})
   535  
   536  	// now we can remove the quota from the state
   537  	qc8 := servicestate.QuotaControlAction{
   538  		Action:    "remove",
   539  		QuotaName: "foo",
   540  	}
   541  
   542  	err = servicestate.QuotaRemove(st, nil, qc8, allGrps(c, st), nil, nil)
   543  	c.Assert(err, IsNil)
   544  
   545  	checkQuotaState(c, st, nil)
   546  
   547  	// foo is not mentioned in the service and doesn't exist
   548  	checkSvcAndSliceState(c, "test-snap.svc1", "foo", 0)
   549  }
   550  
   551  func (s *quotaHandlersSuite) TestQuotaUpdateGroupNotExist(c *C) {
   552  	st := s.state
   553  	st.Lock()
   554  	defer st.Unlock()
   555  
   556  	// non-existent quota group
   557  	qc := servicestate.QuotaControlAction{
   558  		Action:    "update",
   559  		QuotaName: "non-existing",
   560  	}
   561  
   562  	err := servicestate.QuotaUpdate(st, nil, qc, allGrps(c, st), nil, nil)
   563  	c.Check(err, ErrorMatches, `group "non-existing" does not exist`)
   564  }
   565  
   566  func (s *quotaHandlersSuite) TestQuotaUpdateSubGroupTooBig(c *C) {
   567  	r := s.mockSystemctlCalls(c, join(
   568  		// CreateQuota for foo
   569  		systemctlCallsForCreateQuota("foo", "test-snap"),
   570  
   571  		// CreateQuota for foo2
   572  		systemctlCallsForCreateQuota("foo/foo2", "test-snap2"),
   573  
   574  		// UpdateQuota for foo2 - just the slice changes
   575  		[]expectedSystemctl{{expArgs: []string{"daemon-reload"}}},
   576  
   577  		// UpdateQuota for foo2 which fails - no systemctl calls
   578  	))
   579  	defer r()
   580  
   581  	st := s.state
   582  	st.Lock()
   583  	defer st.Unlock()
   584  
   585  	// setup the snap so it exists
   586  	snapstate.Set(s.state, "test-snap", s.testSnapState)
   587  	snaptest.MockSnapCurrent(c, testYaml, s.testSnapSideInfo)
   588  	// and test-snap2
   589  	si2 := &snap.SideInfo{RealName: "test-snap2", Revision: snap.R(42)}
   590  	snapst2 := &snapstate.SnapState{
   591  		Sequence: []*snap.SideInfo{si2},
   592  		Current:  si2.Revision,
   593  		Active:   true,
   594  		SnapType: "app",
   595  	}
   596  	snapstate.Set(s.state, "test-snap2", snapst2)
   597  	snaptest.MockSnapCurrent(c, testYaml2, si2)
   598  
   599  	// create a quota group
   600  	qc := servicestate.QuotaControlAction{
   601  		Action:      "create",
   602  		QuotaName:   "foo",
   603  		MemoryLimit: quantity.SizeGiB,
   604  		AddSnaps:    []string{"test-snap"},
   605  	}
   606  
   607  	err := servicestate.QuotaCreate(st, nil, qc, allGrps(c, st), nil, nil)
   608  	c.Assert(err, IsNil)
   609  
   610  	// ensure mem-limit is 1 GB
   611  	expFooGroupState := quotaGroupState{
   612  		MemoryLimit: quantity.SizeGiB,
   613  		Snaps:       []string{"test-snap"},
   614  	}
   615  	checkQuotaState(c, st, map[string]quotaGroupState{
   616  		"foo": expFooGroupState,
   617  	})
   618  
   619  	// create a sub-group with 0.5 GiB
   620  	qc2 := servicestate.QuotaControlAction{
   621  		Action:      "create",
   622  		QuotaName:   "foo2",
   623  		MemoryLimit: quantity.SizeGiB / 2,
   624  		AddSnaps:    []string{"test-snap2"},
   625  		ParentName:  "foo",
   626  	}
   627  
   628  	err = servicestate.QuotaCreate(st, nil, qc2, allGrps(c, st), nil, nil)
   629  	c.Assert(err, IsNil)
   630  
   631  	expFooGroupState.SubGroups = []string{"foo2"}
   632  
   633  	expFoo2GroupState := quotaGroupState{
   634  		MemoryLimit: quantity.SizeGiB / 2,
   635  		Snaps:       []string{"test-snap2"},
   636  		ParentGroup: "foo",
   637  	}
   638  
   639  	// verify it was set in state
   640  	checkQuotaState(c, st, map[string]quotaGroupState{
   641  		"foo":  expFooGroupState,
   642  		"foo2": expFoo2GroupState,
   643  	})
   644  
   645  	// now try to increase it to the max size
   646  	qc3 := servicestate.QuotaControlAction{
   647  		Action:      "update",
   648  		QuotaName:   "foo2",
   649  		MemoryLimit: quantity.SizeGiB,
   650  	}
   651  
   652  	err = servicestate.QuotaUpdate(st, nil, qc3, allGrps(c, st), nil, nil)
   653  	c.Assert(err, IsNil)
   654  
   655  	expFoo2GroupState.MemoryLimit = quantity.SizeGiB
   656  	// and check that it got updated in the state
   657  	checkQuotaState(c, st, map[string]quotaGroupState{
   658  		"foo":  expFooGroupState,
   659  		"foo2": expFoo2GroupState,
   660  	})
   661  
   662  	// now try to increase it above the parent limit
   663  	qc4 := servicestate.QuotaControlAction{
   664  		Action:      "update",
   665  		QuotaName:   "foo2",
   666  		MemoryLimit: 2 * quantity.SizeGiB,
   667  	}
   668  
   669  	err = servicestate.QuotaUpdate(st, nil, qc4, allGrps(c, st), nil, nil)
   670  	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`)
   671  
   672  	// and make sure that the existing memory limit is still in place
   673  	checkQuotaState(c, st, map[string]quotaGroupState{
   674  		"foo":  expFooGroupState,
   675  		"foo2": expFoo2GroupState,
   676  	})
   677  }
   678  
   679  func (s *quotaHandlersSuite) TestUpdateQuotaGroupNotEnabled(c *C) {
   680  	s.state.Lock()
   681  	defer s.state.Unlock()
   682  	tr := config.NewTransaction(s.state)
   683  	tr.Set("core", "experimental.quota-groups", false)
   684  	tr.Commit()
   685  
   686  	opts := servicestate.QuotaGroupUpdate{}
   687  	err := servicestate.UpdateQuota(s.state, "foo", opts)
   688  	c.Assert(err, ErrorMatches, `experimental feature disabled - test it by setting 'experimental.quota-groups' to true`)
   689  }
   690  
   691  func (s *quotaHandlersSuite) TestQuotaUpdateChangeMemLimit(c *C) {
   692  	r := s.mockSystemctlCalls(c, join(
   693  		// CreateQuota for foo
   694  		systemctlCallsForCreateQuota("foo", "test-snap"),
   695  
   696  		// UpdateQuota for foo - an existing slice was changed, so all we need
   697  		// to is daemon-reload
   698  		[]expectedSystemctl{{expArgs: []string{"daemon-reload"}}},
   699  	))
   700  	defer r()
   701  
   702  	st := s.state
   703  	st.Lock()
   704  	defer st.Unlock()
   705  
   706  	// setup the snap so it exists
   707  	snapstate.Set(s.state, "test-snap", s.testSnapState)
   708  	snaptest.MockSnapCurrent(c, testYaml, s.testSnapSideInfo)
   709  
   710  	// create a quota group
   711  	qc := servicestate.QuotaControlAction{
   712  		Action:      "create",
   713  		QuotaName:   "foo",
   714  		MemoryLimit: quantity.SizeGiB,
   715  		AddSnaps:    []string{"test-snap"},
   716  	}
   717  
   718  	err := servicestate.QuotaCreate(st, nil, qc, allGrps(c, st), nil, nil)
   719  	c.Assert(err, IsNil)
   720  
   721  	// ensure mem-limit is 1 GB
   722  	checkQuotaState(c, st, map[string]quotaGroupState{
   723  		"foo": {
   724  			MemoryLimit: quantity.SizeGiB,
   725  			Snaps:       []string{"test-snap"},
   726  		},
   727  	})
   728  
   729  	// modify to 2 GB
   730  	qc2 := servicestate.QuotaControlAction{
   731  		Action:      "update",
   732  		QuotaName:   "foo",
   733  		MemoryLimit: 2 * quantity.SizeGiB,
   734  	}
   735  	err = servicestate.QuotaUpdate(st, nil, qc2, allGrps(c, st), nil, nil)
   736  	c.Assert(err, IsNil)
   737  
   738  	// and check that it got updated in the state
   739  	checkQuotaState(c, st, map[string]quotaGroupState{
   740  		"foo": {
   741  			MemoryLimit: 2 * quantity.SizeGiB,
   742  			Snaps:       []string{"test-snap"},
   743  		},
   744  	})
   745  
   746  	// trying to decrease the memory limit is not yet supported
   747  	qc3 := servicestate.QuotaControlAction{
   748  		Action:      "update",
   749  		QuotaName:   "foo",
   750  		MemoryLimit: quantity.SizeGiB,
   751  	}
   752  	err = servicestate.QuotaUpdate(st, nil, qc3, allGrps(c, st), nil, nil)
   753  	c.Assert(err, ErrorMatches, "cannot decrease memory limit of existing quota-group, remove and re-create it to decrease the limit")
   754  }
   755  
   756  func (s *quotaHandlersSuite) TestQuotaUpdateAddSnap(c *C) {
   757  	r := s.mockSystemctlCalls(c, join(
   758  		// CreateQuota for foo
   759  		systemctlCallsForCreateQuota("foo", "test-snap"),
   760  
   761  		// UpdateQuota with just test-snap2 restarted since the group already
   762  		// exists
   763  		[]expectedSystemctl{{expArgs: []string{"daemon-reload"}}},
   764  		systemctlCallsForServiceRestart("test-snap2"),
   765  	))
   766  	defer r()
   767  
   768  	st := s.state
   769  	st.Lock()
   770  	defer st.Unlock()
   771  
   772  	// setup test-snap
   773  	snapstate.Set(s.state, "test-snap", s.testSnapState)
   774  	snaptest.MockSnapCurrent(c, testYaml, s.testSnapSideInfo)
   775  	// and test-snap2
   776  	si2 := &snap.SideInfo{RealName: "test-snap2", Revision: snap.R(42)}
   777  	snapst2 := &snapstate.SnapState{
   778  		Sequence: []*snap.SideInfo{si2},
   779  		Current:  si2.Revision,
   780  		Active:   true,
   781  		SnapType: "app",
   782  	}
   783  	snapstate.Set(s.state, "test-snap2", snapst2)
   784  	snaptest.MockSnapCurrent(c, testYaml2, si2)
   785  
   786  	// create a quota group
   787  	qc := servicestate.QuotaControlAction{
   788  		Action:      "create",
   789  		QuotaName:   "foo",
   790  		MemoryLimit: quantity.SizeGiB,
   791  		AddSnaps:    []string{"test-snap"},
   792  	}
   793  
   794  	err := servicestate.QuotaCreate(st, nil, qc, allGrps(c, st), nil, nil)
   795  	c.Assert(err, IsNil)
   796  
   797  	checkQuotaState(c, st, map[string]quotaGroupState{
   798  		"foo": {
   799  			MemoryLimit: quantity.SizeGiB,
   800  			Snaps:       []string{"test-snap"},
   801  		},
   802  	})
   803  
   804  	// add a snap
   805  	qc2 := servicestate.QuotaControlAction{
   806  		Action:    "update",
   807  		QuotaName: "foo",
   808  		AddSnaps:  []string{"test-snap2"},
   809  	}
   810  	err = servicestate.QuotaUpdate(st, nil, qc2, allGrps(c, st), nil, nil)
   811  	c.Assert(err, IsNil)
   812  
   813  	// and check that it got updated in the state
   814  	checkQuotaState(c, st, map[string]quotaGroupState{
   815  		"foo": {
   816  			MemoryLimit: quantity.SizeGiB,
   817  			Snaps:       []string{"test-snap", "test-snap2"},
   818  		},
   819  	})
   820  }
   821  
   822  func (s *quotaHandlersSuite) TestQuotaUpdateAddSnapAlreadyInOtherGroup(c *C) {
   823  	r := s.mockSystemctlCalls(c, join(
   824  		// CreateQuota for foo
   825  		systemctlCallsForCreateQuota("foo", "test-snap"),
   826  
   827  		// CreateQuota for foo2
   828  		systemctlCallsForCreateQuota("foo2", "test-snap2"),
   829  
   830  		// UpdateQuota for foo which fails - no systemctl calls
   831  	))
   832  	defer r()
   833  
   834  	st := s.state
   835  	st.Lock()
   836  	defer st.Unlock()
   837  
   838  	// setup test-snap
   839  	snapstate.Set(s.state, "test-snap", s.testSnapState)
   840  	snaptest.MockSnapCurrent(c, testYaml, s.testSnapSideInfo)
   841  	// and test-snap2
   842  	si2 := &snap.SideInfo{RealName: "test-snap2", Revision: snap.R(42)}
   843  	snapst2 := &snapstate.SnapState{
   844  		Sequence: []*snap.SideInfo{si2},
   845  		Current:  si2.Revision,
   846  		Active:   true,
   847  		SnapType: "app",
   848  	}
   849  	snapstate.Set(s.state, "test-snap2", snapst2)
   850  	snaptest.MockSnapCurrent(c, testYaml2, si2)
   851  
   852  	// create a quota group
   853  	qc := servicestate.QuotaControlAction{
   854  		Action:      "create",
   855  		QuotaName:   "foo",
   856  		MemoryLimit: quantity.SizeGiB,
   857  		AddSnaps:    []string{"test-snap"},
   858  	}
   859  
   860  	err := servicestate.QuotaCreate(st, nil, qc, allGrps(c, st), nil, nil)
   861  	c.Assert(err, IsNil)
   862  
   863  	checkQuotaState(c, st, map[string]quotaGroupState{
   864  		"foo": {
   865  			MemoryLimit: quantity.SizeGiB,
   866  			Snaps:       []string{"test-snap"},
   867  		},
   868  	})
   869  
   870  	// create another quota group with the second snap
   871  	qc2 := servicestate.QuotaControlAction{
   872  		Action:      "create",
   873  		QuotaName:   "foo2",
   874  		MemoryLimit: quantity.SizeGiB,
   875  		AddSnaps:    []string{"test-snap2"},
   876  	}
   877  
   878  	err = servicestate.QuotaCreate(st, nil, qc2, allGrps(c, st), nil, nil)
   879  	c.Assert(err, IsNil)
   880  
   881  	// verify state
   882  	checkQuotaState(c, st, map[string]quotaGroupState{
   883  		"foo": {
   884  			MemoryLimit: quantity.SizeGiB,
   885  			Snaps:       []string{"test-snap"},
   886  		},
   887  		"foo2": {
   888  			MemoryLimit: quantity.SizeGiB,
   889  			Snaps:       []string{"test-snap2"},
   890  		},
   891  	})
   892  
   893  	// try to add test-snap2 to foo
   894  	qc3 := servicestate.QuotaControlAction{
   895  		Action:    "update",
   896  		QuotaName: "foo",
   897  		AddSnaps:  []string{"test-snap2"},
   898  	}
   899  
   900  	err = servicestate.QuotaUpdate(st, nil, qc3, allGrps(c, st), nil, nil)
   901  	c.Assert(err, ErrorMatches, `cannot add snap "test-snap2" to group "foo": snap already in quota group "foo2"`)
   902  
   903  	// nothing changed in the state
   904  	checkQuotaState(c, st, map[string]quotaGroupState{
   905  		"foo": {
   906  			MemoryLimit: quantity.SizeGiB,
   907  			Snaps:       []string{"test-snap"},
   908  		},
   909  		"foo2": {
   910  			MemoryLimit: quantity.SizeGiB,
   911  			Snaps:       []string{"test-snap2"},
   912  		},
   913  	})
   914  }