github.com/meulengracht/snapd@v0.0.0-20210719210640-8bde69bcc84e/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/snaptest"
    32  	"github.com/snapcore/snapd/snapdenv"
    33  	"github.com/snapcore/snapd/systemd"
    34  )
    35  
    36  type quotaHandlersSuite struct {
    37  	baseServiceMgrTestSuite
    38  }
    39  
    40  var _ = Suite(&quotaHandlersSuite{})
    41  
    42  func (s *quotaHandlersSuite) SetUpTest(c *C) {
    43  	s.baseServiceMgrTestSuite.SetUpTest(c)
    44  
    45  	// we don't need the EnsureSnapServices ensure loop to run by default
    46  	servicestate.MockEnsuredSnapServices(s.mgr, true)
    47  
    48  	// we enable quota-groups by default
    49  	s.state.Lock()
    50  	defer s.state.Unlock()
    51  	tr := config.NewTransaction(s.state)
    52  	tr.Set("core", "experimental.quota-groups", true)
    53  	tr.Commit()
    54  
    55  	// mock that we have a new enough version of systemd by default
    56  	r := servicestate.MockSystemdVersion(248)
    57  	s.AddCleanup(r)
    58  }
    59  
    60  func (s *quotaHandlersSuite) TestDoQuotaControlCreate(c *C) {
    61  	r := s.mockSystemctlCalls(c, join(
    62  		// doQuotaControl handler to create the group
    63  		systemctlCallsForCreateQuota("foo-group", "test-snap"),
    64  	))
    65  	defer r()
    66  
    67  	st := s.state
    68  	st.Lock()
    69  	defer st.Unlock()
    70  
    71  	// setup the snap so it exists
    72  	snapstate.Set(s.state, "test-snap", s.testSnapState)
    73  	snaptest.MockSnapCurrent(c, testYaml, s.testSnapSideInfo)
    74  
    75  	// make a fake task
    76  	t := st.NewTask("create-quota", "...")
    77  
    78  	qcs := []servicestate.QuotaControlAction{
    79  		{
    80  			Action:      "create",
    81  			QuotaName:   "foo-group",
    82  			MemoryLimit: quantity.SizeGiB,
    83  			AddSnaps:    []string{"test-snap"},
    84  		},
    85  	}
    86  
    87  	t.Set("quota-control-actions", &qcs)
    88  
    89  	st.Unlock()
    90  	err := s.o.ServiceManager().DoQuotaControl(t, nil)
    91  	st.Lock()
    92  
    93  	c.Assert(err, IsNil)
    94  	c.Assert(t.Status(), Equals, state.DoneStatus)
    95  
    96  	checkQuotaState(c, st, map[string]quotaGroupState{
    97  		"foo-group": {
    98  			MemoryLimit: quantity.SizeGiB,
    99  			Snaps:       []string{"test-snap"},
   100  		},
   101  	})
   102  }
   103  
   104  func (s *quotaHandlersSuite) TestDoQuotaControlCreateRestartOK(c *C) {
   105  	// test a situation where because of restart the task is reentered
   106  	r := s.mockSystemctlCalls(c, join(
   107  		// doQuotaControl handler to create the group
   108  		systemctlCallsForCreateQuota("foo-group", "test-snap"),
   109  		// after task restart
   110  		systemctlCallsForServiceRestart("test-snap"),
   111  	))
   112  	defer r()
   113  
   114  	st := s.state
   115  	st.Lock()
   116  	defer st.Unlock()
   117  
   118  	// setup the snap so it exists
   119  	snapstate.Set(s.state, "test-snap", s.testSnapState)
   120  	snaptest.MockSnapCurrent(c, testYaml, s.testSnapSideInfo)
   121  
   122  	// make a fake task
   123  	t := st.NewTask("create-quota", "...")
   124  
   125  	qcs := []servicestate.QuotaControlAction{
   126  		{
   127  			Action:      "create",
   128  			QuotaName:   "foo-group",
   129  			MemoryLimit: quantity.SizeGiB,
   130  			AddSnaps:    []string{"test-snap"},
   131  		},
   132  	}
   133  
   134  	t.Set("quota-control-actions", &qcs)
   135  
   136  	expectedQuotaState := map[string]quotaGroupState{
   137  		"foo-group": {
   138  			MemoryLimit: quantity.SizeGiB,
   139  			Snaps:       []string{"test-snap"},
   140  		},
   141  	}
   142  
   143  	st.Unlock()
   144  	err := s.o.ServiceManager().DoQuotaControl(t, nil)
   145  	st.Lock()
   146  	c.Assert(err, IsNil)
   147  
   148  	c.Assert(t.Status(), Equals, state.DoneStatus)
   149  
   150  	checkQuotaState(c, st, expectedQuotaState)
   151  
   152  	t.SetStatus(state.DoingStatus)
   153  
   154  	st.Unlock()
   155  	err = s.o.ServiceManager().DoQuotaControl(t, nil)
   156  	st.Lock()
   157  	c.Assert(err, IsNil)
   158  
   159  	c.Assert(t.Status(), Equals, state.DoneStatus)
   160  
   161  	checkQuotaState(c, st, expectedQuotaState)
   162  }
   163  
   164  func (s *quotaHandlersSuite) TestQuotaStateAlreadyUpdatedBehavior(c *C) {
   165  	// test a situation where because of restart the task is reentered
   166  	r := s.mockSystemctlCalls(c, join(
   167  		// doQuotaControl handler to create the group
   168  		systemctlCallsForCreateQuota("foo-group", "test-snap"),
   169  	))
   170  	defer r()
   171  
   172  	st := s.state
   173  	st.Lock()
   174  	defer st.Unlock()
   175  
   176  	// setup the snap so it exists
   177  	snapstate.Set(s.state, "test-snap", s.testSnapState)
   178  	snaptest.MockSnapCurrent(c, testYaml, s.testSnapSideInfo)
   179  
   180  	// make a fake task
   181  	t := st.NewTask("create-quota", "...")
   182  
   183  	qcs := []servicestate.QuotaControlAction{
   184  		{
   185  			Action:      "create",
   186  			QuotaName:   "foo-group",
   187  			MemoryLimit: quantity.SizeGiB,
   188  			AddSnaps:    []string{"test-snap"},
   189  		},
   190  	}
   191  
   192  	t.Set("quota-control-actions", &qcs)
   193  
   194  	st.Unlock()
   195  	err := s.o.ServiceManager().DoQuotaControl(t, nil)
   196  	st.Lock()
   197  	c.Assert(err, IsNil)
   198  
   199  	c.Assert(t.Status(), Equals, state.DoneStatus)
   200  	t.SetStatus(state.DoingStatus)
   201  
   202  	updated, appsToRestart, err := servicestate.QuotaStateAlreadyUpdated(t)
   203  	c.Assert(err, IsNil)
   204  	c.Check(updated, Equals, true)
   205  	c.Assert(appsToRestart, HasLen, 1)
   206  	for info, apps := range appsToRestart {
   207  		c.Check(info.InstanceName(), Equals, "test-snap")
   208  		c.Assert(apps, HasLen, 1)
   209  		c.Check(apps[0], Equals, info.Apps["svc1"])
   210  	}
   211  
   212  	// rebooted
   213  	r = servicestate.MockOsutilBootID("other-boot")
   214  	defer r()
   215  
   216  	updated, appsToRestart, err = servicestate.QuotaStateAlreadyUpdated(t)
   217  	c.Assert(err, IsNil)
   218  	c.Check(updated, Equals, true)
   219  	c.Check(appsToRestart, HasLen, 0)
   220  	r()
   221  
   222  	// restored
   223  	_, appsToRestart, err = servicestate.QuotaStateAlreadyUpdated(t)
   224  	c.Assert(err, IsNil)
   225  	c.Check(appsToRestart, HasLen, 1)
   226  
   227  	// snap went missing
   228  	snapstate.Set(s.state, "test-snap", nil)
   229  	updated, appsToRestart, err = servicestate.QuotaStateAlreadyUpdated(t)
   230  	c.Assert(err, IsNil)
   231  	c.Check(updated, Equals, true)
   232  	c.Check(appsToRestart, HasLen, 0)
   233  }
   234  
   235  func (s *quotaHandlersSuite) TestDoQuotaControlUpdate(c *C) {
   236  	r := s.mockSystemctlCalls(c, join(
   237  		// CreateQuota for foo-group
   238  		systemctlCallsForCreateQuota("foo-group", "test-snap"),
   239  
   240  		// doQuotaControl handler which updates the group
   241  		[]expectedSystemctl{{expArgs: []string{"daemon-reload"}}},
   242  	))
   243  	defer r()
   244  
   245  	st := s.state
   246  	st.Lock()
   247  	defer st.Unlock()
   248  
   249  	// setup the snap so it exists
   250  	snapstate.Set(s.state, "test-snap", s.testSnapState)
   251  	snaptest.MockSnapCurrent(c, testYaml, s.testSnapSideInfo)
   252  
   253  	// create a quota group
   254  	t := st.NewTask("create-quota", "...")
   255  
   256  	qcs := []servicestate.QuotaControlAction{
   257  		{
   258  			Action:      "create",
   259  			QuotaName:   "foo-group",
   260  			MemoryLimit: quantity.SizeGiB,
   261  			AddSnaps:    []string{"test-snap"},
   262  		},
   263  	}
   264  
   265  	t.Set("quota-control-actions", &qcs)
   266  
   267  	st.Unlock()
   268  	err := s.o.ServiceManager().DoQuotaControl(t, nil)
   269  	st.Lock()
   270  	c.Assert(err, IsNil)
   271  
   272  	// create a task for updating the quota group
   273  	t = st.NewTask("update-quota", "...")
   274  
   275  	// update the memory limit to be double
   276  	qcs = []servicestate.QuotaControlAction{
   277  		{
   278  			Action:      "update",
   279  			QuotaName:   "foo-group",
   280  			MemoryLimit: 2 * quantity.SizeGiB,
   281  		},
   282  	}
   283  
   284  	t.Set("quota-control-actions", &qcs)
   285  
   286  	st.Unlock()
   287  	err = s.o.ServiceManager().DoQuotaControl(t, nil)
   288  	st.Lock()
   289  
   290  	c.Assert(err, IsNil)
   291  	c.Assert(t.Status(), Equals, state.DoneStatus)
   292  
   293  	checkQuotaState(c, st, map[string]quotaGroupState{
   294  		"foo-group": {
   295  			MemoryLimit: 2 * quantity.SizeGiB,
   296  			Snaps:       []string{"test-snap"},
   297  		},
   298  	})
   299  }
   300  
   301  func (s *quotaHandlersSuite) TestDoQuotaControlUpdateRestartOK(c *C) {
   302  	// test a situation where because of restart the task is reentered
   303  	r := s.mockSystemctlCalls(c, join(
   304  		// CreateQuota for foo-group
   305  		systemctlCallsForCreateQuota("foo-group", "test-snap"),
   306  
   307  		// doQuotaControl handler which updates the group
   308  		[]expectedSystemctl{{expArgs: []string{"daemon-reload"}}},
   309  	))
   310  	defer r()
   311  
   312  	st := s.state
   313  	st.Lock()
   314  	defer st.Unlock()
   315  
   316  	// setup the snap so it exists
   317  	snapstate.Set(s.state, "test-snap", s.testSnapState)
   318  	snaptest.MockSnapCurrent(c, testYaml, s.testSnapSideInfo)
   319  
   320  	// create a quota group
   321  	t := st.NewTask("create-quota", "...")
   322  
   323  	qcs := []servicestate.QuotaControlAction{
   324  		{
   325  			Action:      "create",
   326  			QuotaName:   "foo-group",
   327  			MemoryLimit: quantity.SizeGiB,
   328  			AddSnaps:    []string{"test-snap"},
   329  		},
   330  	}
   331  
   332  	t.Set("quota-control-actions", &qcs)
   333  
   334  	st.Unlock()
   335  	err := s.o.ServiceManager().DoQuotaControl(t, nil)
   336  	st.Lock()
   337  	c.Assert(err, IsNil)
   338  
   339  	// create a task for updating the quota group
   340  	t = st.NewTask("update-quota", "...")
   341  
   342  	// update the memory limit to be double
   343  	qcs = []servicestate.QuotaControlAction{
   344  		{
   345  			Action:      "update",
   346  			QuotaName:   "foo-group",
   347  			MemoryLimit: 2 * quantity.SizeGiB,
   348  		},
   349  	}
   350  
   351  	t.Set("quota-control-actions", &qcs)
   352  
   353  	expectedQuotaState := map[string]quotaGroupState{
   354  		"foo-group": {
   355  			MemoryLimit: 2 * quantity.SizeGiB,
   356  			Snaps:       []string{"test-snap"},
   357  		},
   358  	}
   359  
   360  	st.Unlock()
   361  	err = s.o.ServiceManager().DoQuotaControl(t, nil)
   362  	st.Lock()
   363  	c.Assert(err, IsNil)
   364  
   365  	c.Assert(t.Status(), Equals, state.DoneStatus)
   366  
   367  	checkQuotaState(c, st, expectedQuotaState)
   368  
   369  	t.SetStatus(state.DoingStatus)
   370  
   371  	st.Unlock()
   372  	err = s.o.ServiceManager().DoQuotaControl(t, nil)
   373  	st.Lock()
   374  	c.Assert(err, IsNil)
   375  
   376  	c.Assert(t.Status(), Equals, state.DoneStatus)
   377  
   378  	checkQuotaState(c, st, expectedQuotaState)
   379  }
   380  
   381  func (s *quotaHandlersSuite) TestDoQuotaControlRemove(c *C) {
   382  	r := s.mockSystemctlCalls(c, join(
   383  		// CreateQuota for foo-group
   384  		systemctlCallsForCreateQuota("foo-group", "test-snap"),
   385  
   386  		// doQuotaControl handler which removes the group
   387  		[]expectedSystemctl{{expArgs: []string{"daemon-reload"}}},
   388  		systemctlCallsForSliceStop("foo-group"),
   389  		[]expectedSystemctl{{expArgs: []string{"daemon-reload"}}},
   390  		systemctlCallsForServiceRestart("test-snap"),
   391  	))
   392  	defer r()
   393  
   394  	st := s.state
   395  	st.Lock()
   396  	defer st.Unlock()
   397  
   398  	// setup the snap so it exists
   399  	snapstate.Set(s.state, "test-snap", s.testSnapState)
   400  	snaptest.MockSnapCurrent(c, testYaml, s.testSnapSideInfo)
   401  
   402  	// create a quota group
   403  	t := st.NewTask("create-quota", "...")
   404  
   405  	qcs := []servicestate.QuotaControlAction{
   406  		{
   407  			Action:      "create",
   408  			QuotaName:   "foo-group",
   409  			MemoryLimit: quantity.SizeGiB,
   410  			AddSnaps:    []string{"test-snap"},
   411  		},
   412  	}
   413  
   414  	t.Set("quota-control-actions", &qcs)
   415  
   416  	st.Unlock()
   417  	err := s.o.ServiceManager().DoQuotaControl(t, nil)
   418  	st.Lock()
   419  	c.Assert(err, IsNil)
   420  
   421  	// create a task for removing the quota group
   422  	t = st.NewTask("remove-quota", "...")
   423  
   424  	// remove quota group
   425  	qcs = []servicestate.QuotaControlAction{
   426  		{
   427  			Action:    "remove",
   428  			QuotaName: "foo-group",
   429  		},
   430  	}
   431  
   432  	t.Set("quota-control-actions", &qcs)
   433  
   434  	st.Unlock()
   435  	err = s.o.ServiceManager().DoQuotaControl(t, nil)
   436  	st.Lock()
   437  
   438  	c.Assert(err, IsNil)
   439  	c.Assert(t.Status(), Equals, state.DoneStatus)
   440  
   441  	checkQuotaState(c, st, nil)
   442  }
   443  
   444  func (s *quotaHandlersSuite) TestDoQuotaControlRemoveRestartOK(c *C) {
   445  	// test a situation where because of restart the task is reentered
   446  	r := s.mockSystemctlCalls(c, join(
   447  		// CreateQuota for foo-group
   448  		systemctlCallsForCreateQuota("foo-group", "test-snap"),
   449  
   450  		// doQuotaControl handler which removes the group
   451  		[]expectedSystemctl{{expArgs: []string{"daemon-reload"}}},
   452  		systemctlCallsForSliceStop("foo-group"),
   453  		[]expectedSystemctl{{expArgs: []string{"daemon-reload"}}},
   454  		systemctlCallsForServiceRestart("test-snap"),
   455  		// after task restart
   456  		systemctlCallsForServiceRestart("test-snap"),
   457  	))
   458  	defer r()
   459  
   460  	st := s.state
   461  	st.Lock()
   462  	defer st.Unlock()
   463  
   464  	// setup the snap so it exists
   465  	snapstate.Set(s.state, "test-snap", s.testSnapState)
   466  	snaptest.MockSnapCurrent(c, testYaml, s.testSnapSideInfo)
   467  
   468  	// create a quota group
   469  	t := st.NewTask("create-quota", "...")
   470  
   471  	qcs := []servicestate.QuotaControlAction{
   472  		{
   473  			Action:      "create",
   474  			QuotaName:   "foo-group",
   475  			MemoryLimit: quantity.SizeGiB,
   476  			AddSnaps:    []string{"test-snap"},
   477  		},
   478  	}
   479  
   480  	t.Set("quota-control-actions", &qcs)
   481  
   482  	st.Unlock()
   483  	err := s.o.ServiceManager().DoQuotaControl(t, nil)
   484  	st.Lock()
   485  	c.Assert(err, IsNil)
   486  
   487  	// create a task for removing the quota group
   488  	t = st.NewTask("remove-quota", "...")
   489  
   490  	// remove quota group
   491  	qcs = []servicestate.QuotaControlAction{
   492  		{
   493  			Action:    "remove",
   494  			QuotaName: "foo-group",
   495  		},
   496  	}
   497  
   498  	t.Set("quota-control-actions", &qcs)
   499  
   500  	st.Unlock()
   501  	err = s.o.ServiceManager().DoQuotaControl(t, nil)
   502  	st.Lock()
   503  	c.Assert(err, IsNil)
   504  
   505  	c.Assert(t.Status(), Equals, state.DoneStatus)
   506  
   507  	checkQuotaState(c, st, nil)
   508  
   509  	t.SetStatus(state.DoingStatus)
   510  
   511  	st.Unlock()
   512  	err = s.o.ServiceManager().DoQuotaControl(t, nil)
   513  	st.Lock()
   514  	c.Assert(err, IsNil)
   515  
   516  	c.Assert(t.Status(), Equals, state.DoneStatus)
   517  
   518  	checkQuotaState(c, st, nil)
   519  }
   520  
   521  func (s *quotaHandlersSuite) callDoQuotaControl(action *servicestate.QuotaControlAction) error {
   522  	st := s.state
   523  	qcs := []*servicestate.QuotaControlAction{action}
   524  	t := st.NewTask("quota-task", "...")
   525  	t.Set("quota-control-actions", &qcs)
   526  
   527  	st.Unlock()
   528  	err := s.o.ServiceManager().DoQuotaControl(t, nil)
   529  	st.Lock()
   530  
   531  	return err
   532  }
   533  
   534  func (s *quotaHandlersSuite) TestQuotaCreatePreseeding(c *C) {
   535  	// should be no systemctl calls since we are preseeding
   536  	r := snapdenv.MockPreseeding(true)
   537  	defer r()
   538  
   539  	st := s.state
   540  	st.Lock()
   541  	defer st.Unlock()
   542  
   543  	// setup the snap so it exists
   544  	snapstate.Set(s.state, "test-snap", s.testSnapState)
   545  	snaptest.MockSnapCurrent(c, testYaml, s.testSnapSideInfo)
   546  
   547  	// now we can create the quota group
   548  	qc := servicestate.QuotaControlAction{
   549  		Action:      "create",
   550  		QuotaName:   "foo",
   551  		MemoryLimit: quantity.SizeGiB,
   552  		AddSnaps:    []string{"test-snap"},
   553  	}
   554  
   555  	err := s.callDoQuotaControl(&qc)
   556  	c.Assert(err, IsNil)
   557  
   558  	// check that the quota groups were created in the state
   559  	checkQuotaState(c, st, map[string]quotaGroupState{
   560  		"foo": {
   561  			MemoryLimit: quantity.SizeGiB,
   562  			Snaps:       []string{"test-snap"},
   563  		},
   564  	})
   565  }
   566  
   567  func (s *quotaHandlersSuite) TestQuotaCreate(c *C) {
   568  	r := s.mockSystemctlCalls(c, join(
   569  		// CreateQuota for non-installed snap - fails
   570  
   571  		// CreateQuota for foo - success
   572  		systemctlCallsForCreateQuota("foo", "test-snap"),
   573  
   574  		// CreateQuota for foo2 with overlapping snap already in foo
   575  
   576  		// CreateQuota for foo again - fails
   577  	))
   578  	defer r()
   579  
   580  	st := s.state
   581  	st.Lock()
   582  	defer st.Unlock()
   583  
   584  	// trying to create a quota with a snap that doesn't exist fails
   585  	qc := servicestate.QuotaControlAction{
   586  		Action:      "create",
   587  		QuotaName:   "foo",
   588  		MemoryLimit: quantity.SizeGiB,
   589  		AddSnaps:    []string{"test-snap"},
   590  	}
   591  
   592  	err := s.callDoQuotaControl(&qc)
   593  	c.Assert(err, ErrorMatches, `cannot use snap "test-snap" in group "foo": snap "test-snap" is not installed`)
   594  
   595  	// setup the snap so it exists
   596  	snapstate.Set(s.state, "test-snap", s.testSnapState)
   597  	snaptest.MockSnapCurrent(c, testYaml, s.testSnapSideInfo)
   598  
   599  	qc2 := servicestate.QuotaControlAction{
   600  		Action:      "create",
   601  		QuotaName:   "foo",
   602  		MemoryLimit: 4 * quantity.SizeKiB,
   603  		AddSnaps:    []string{"test-snap"},
   604  	}
   605  
   606  	// trying to create a quota with too low of a memory limit fails
   607  	err = s.callDoQuotaControl(&qc2)
   608  	c.Assert(err, ErrorMatches, `memory limit for group "foo" is too small: size must be larger than 4KB`)
   609  
   610  	// but with an adequately sized memory limit, and a snap that exists, we can
   611  	// create it
   612  	qc3 := servicestate.QuotaControlAction{
   613  		Action:      "create",
   614  		QuotaName:   "foo",
   615  		MemoryLimit: 4*quantity.SizeKiB + 1,
   616  		AddSnaps:    []string{"test-snap"},
   617  	}
   618  	err = s.callDoQuotaControl(&qc3)
   619  	c.Assert(err, IsNil)
   620  
   621  	// check that the quota groups were created in the state
   622  	checkQuotaState(c, st, map[string]quotaGroupState{
   623  		"foo": {
   624  			MemoryLimit: 4*quantity.SizeKiB + 1,
   625  			Snaps:       []string{"test-snap"},
   626  		},
   627  	})
   628  }
   629  
   630  func (s *quotaHandlersSuite) TestDoCreateSubGroupQuota(c *C) {
   631  	r := s.mockSystemctlCalls(c, join(
   632  		// CreateQuota for foo - no systemctl calls since no snaps in it
   633  
   634  		// CreateQuota for foo2 - fails thus no systemctl calls
   635  
   636  		// CreateQuota for foo2 - we don't write anything for the first quota
   637  		// since there are no snaps in the quota to track
   638  		[]expectedSystemctl{{expArgs: []string{"daemon-reload"}}},
   639  		systemctlCallsForSliceStart("foo-group"),
   640  		systemctlCallsForSliceStart("foo-group/foo2"),
   641  		systemctlCallsForServiceRestart("test-snap"),
   642  	))
   643  	defer r()
   644  
   645  	st := s.state
   646  	st.Lock()
   647  	defer st.Unlock()
   648  
   649  	// setup the snap so it exists
   650  	snapstate.Set(s.state, "test-snap", s.testSnapState)
   651  	snaptest.MockSnapCurrent(c, testYaml, s.testSnapSideInfo)
   652  
   653  	// create a quota group with no snaps to be the parent
   654  	qc := servicestate.QuotaControlAction{
   655  		Action:      "create",
   656  		QuotaName:   "foo-group",
   657  		MemoryLimit: quantity.SizeGiB,
   658  	}
   659  
   660  	err := s.callDoQuotaControl(&qc)
   661  	c.Assert(err, IsNil)
   662  
   663  	// trying to create a quota group with a non-existent parent group fails
   664  	qc2 := servicestate.QuotaControlAction{
   665  		Action:      "create",
   666  		QuotaName:   "foo2",
   667  		MemoryLimit: quantity.SizeGiB,
   668  		ParentName:  "foo-non-real",
   669  		AddSnaps:    []string{"test-snap"},
   670  	}
   671  
   672  	err = s.callDoQuotaControl(&qc2)
   673  	c.Assert(err, ErrorMatches, `cannot create group under non-existent parent group "foo-non-real"`)
   674  
   675  	// trying to create a quota group with too big of a limit to fit inside the
   676  	// parent fails
   677  	qc3 := servicestate.QuotaControlAction{
   678  		Action:      "create",
   679  		QuotaName:   "foo2",
   680  		MemoryLimit: 2 * quantity.SizeGiB,
   681  		ParentName:  "foo-group",
   682  		AddSnaps:    []string{"test-snap"},
   683  	}
   684  
   685  	err = s.callDoQuotaControl(&qc3)
   686  	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`)
   687  
   688  	// now we can create a sub-quota
   689  	qc4 := servicestate.QuotaControlAction{
   690  		Action:      "create",
   691  		QuotaName:   "foo2",
   692  		MemoryLimit: quantity.SizeGiB,
   693  		ParentName:  "foo-group",
   694  		AddSnaps:    []string{"test-snap"},
   695  	}
   696  
   697  	err = s.callDoQuotaControl(&qc4)
   698  	c.Assert(err, IsNil)
   699  
   700  	// check that the quota groups were created in the state
   701  	checkQuotaState(c, st, map[string]quotaGroupState{
   702  		"foo-group": {
   703  			MemoryLimit: quantity.SizeGiB,
   704  			SubGroups:   []string{"foo2"},
   705  		},
   706  		"foo2": {
   707  			MemoryLimit: quantity.SizeGiB,
   708  			Snaps:       []string{"test-snap"},
   709  			ParentGroup: "foo-group",
   710  		},
   711  	})
   712  
   713  	// foo-group exists as a slice too, but has no snap services in the slice
   714  	checkSliceState(c, systemd.EscapeUnitNamePath("foo-group"), quantity.SizeGiB)
   715  }
   716  
   717  func (s *quotaHandlersSuite) TestQuotaRemove(c *C) {
   718  	r := s.mockSystemctlCalls(c, join(
   719  		// CreateQuota for foo
   720  		systemctlCallsForCreateQuota("foo", "test-snap"),
   721  
   722  		// for CreateQuota foo2 - no systemctl calls since there are no snaps
   723  
   724  		// for CreateQuota foo3 - no systemctl calls since there are no snaps
   725  
   726  		// RemoveQuota for foo2 - no daemon reload initially because
   727  		// we didn't modify anything, as there are no snaps in foo2 so we don't
   728  		// create that group on disk
   729  		// TODO: is this bit correct in practice? we are in effect calling
   730  		// systemctl stop <non-existing-slice> ?
   731  		systemctlCallsForSliceStop("foo/foo3"),
   732  
   733  		systemctlCallsForSliceStop("foo/foo2"),
   734  
   735  		// RemoveQuota for foo
   736  		[]expectedSystemctl{{expArgs: []string{"daemon-reload"}}},
   737  		systemctlCallsForSliceStop("foo"),
   738  		[]expectedSystemctl{{expArgs: []string{"daemon-reload"}}},
   739  		systemctlCallsForServiceRestart("test-snap"),
   740  	))
   741  	defer r()
   742  
   743  	st := s.state
   744  	st.Lock()
   745  	defer st.Unlock()
   746  
   747  	// setup the snap so it exists
   748  	snapstate.Set(s.state, "test-snap", s.testSnapState)
   749  	snaptest.MockSnapCurrent(c, testYaml, s.testSnapSideInfo)
   750  
   751  	// trying to remove a group that does not exist fails
   752  	qc := servicestate.QuotaControlAction{
   753  		Action:    "remove",
   754  		QuotaName: "not-exists",
   755  	}
   756  
   757  	err := s.callDoQuotaControl(&qc)
   758  	c.Assert(err, ErrorMatches, `cannot remove non-existent quota group "not-exists"`)
   759  
   760  	qc2 := servicestate.QuotaControlAction{
   761  		Action:      "create",
   762  		QuotaName:   "foo",
   763  		MemoryLimit: quantity.SizeGiB,
   764  		AddSnaps:    []string{"test-snap"},
   765  	}
   766  
   767  	err = s.callDoQuotaControl(&qc2)
   768  	c.Assert(err, IsNil)
   769  
   770  	// create 2 quota sub-groups too
   771  	qc3 := servicestate.QuotaControlAction{
   772  		Action:      "create",
   773  		QuotaName:   "foo2",
   774  		MemoryLimit: quantity.SizeGiB / 2,
   775  		ParentName:  "foo",
   776  	}
   777  
   778  	err = s.callDoQuotaControl(&qc3)
   779  	c.Assert(err, IsNil)
   780  
   781  	qc4 := servicestate.QuotaControlAction{
   782  		Action:      "create",
   783  		QuotaName:   "foo3",
   784  		MemoryLimit: quantity.SizeGiB / 2,
   785  		ParentName:  "foo",
   786  	}
   787  
   788  	err = s.callDoQuotaControl(&qc4)
   789  	c.Assert(err, IsNil)
   790  
   791  	// check that the quota groups was created in the state
   792  	checkQuotaState(c, st, map[string]quotaGroupState{
   793  		"foo": {
   794  			MemoryLimit: quantity.SizeGiB,
   795  			Snaps:       []string{"test-snap"},
   796  			SubGroups:   []string{"foo2", "foo3"},
   797  		},
   798  		"foo2": {
   799  			MemoryLimit: quantity.SizeGiB / 2,
   800  			ParentGroup: "foo",
   801  		},
   802  		"foo3": {
   803  			MemoryLimit: quantity.SizeGiB / 2,
   804  			ParentGroup: "foo",
   805  		},
   806  	})
   807  
   808  	// try removing the parent and it fails since it still has a sub-group
   809  	// under it
   810  	qc5 := servicestate.QuotaControlAction{
   811  		Action:    "remove",
   812  		QuotaName: "foo",
   813  	}
   814  
   815  	err = s.callDoQuotaControl(&qc5)
   816  	c.Assert(err, ErrorMatches, "cannot remove quota group with sub-groups, remove the sub-groups first")
   817  
   818  	// but we can remove the sub-group successfully first
   819  	qc6 := servicestate.QuotaControlAction{
   820  		Action:    "remove",
   821  		QuotaName: "foo3",
   822  	}
   823  
   824  	err = s.callDoQuotaControl(&qc6)
   825  	c.Assert(err, IsNil)
   826  
   827  	checkQuotaState(c, st, map[string]quotaGroupState{
   828  		"foo": {
   829  			MemoryLimit: quantity.SizeGiB,
   830  			Snaps:       []string{"test-snap"},
   831  			SubGroups:   []string{"foo2"},
   832  		},
   833  		"foo2": {
   834  			MemoryLimit: quantity.SizeGiB / 2,
   835  			ParentGroup: "foo",
   836  		},
   837  	})
   838  
   839  	// and we can remove the other sub-group
   840  	qc7 := servicestate.QuotaControlAction{
   841  		Action:    "remove",
   842  		QuotaName: "foo2",
   843  	}
   844  
   845  	err = s.callDoQuotaControl(&qc7)
   846  	c.Assert(err, IsNil)
   847  
   848  	checkQuotaState(c, st, map[string]quotaGroupState{
   849  		"foo": {
   850  			MemoryLimit: quantity.SizeGiB,
   851  			Snaps:       []string{"test-snap"},
   852  		},
   853  	})
   854  
   855  	// now we can remove the quota from the state
   856  	qc8 := servicestate.QuotaControlAction{
   857  		Action:    "remove",
   858  		QuotaName: "foo",
   859  	}
   860  
   861  	err = s.callDoQuotaControl(&qc8)
   862  	c.Assert(err, IsNil)
   863  
   864  	checkQuotaState(c, st, nil)
   865  
   866  	// foo is not mentioned in the service and doesn't exist
   867  	checkSvcAndSliceState(c, "test-snap.svc1", "foo", 0)
   868  }
   869  
   870  func (s *quotaHandlersSuite) TestQuotaUpdateGroupNotExist(c *C) {
   871  	st := s.state
   872  	st.Lock()
   873  	defer st.Unlock()
   874  
   875  	// non-existent quota group
   876  	qc := servicestate.QuotaControlAction{
   877  		Action:    "update",
   878  		QuotaName: "non-existing",
   879  	}
   880  
   881  	err := s.callDoQuotaControl(&qc)
   882  	c.Check(err, ErrorMatches, `group "non-existing" does not exist`)
   883  }
   884  
   885  func (s *quotaHandlersSuite) TestQuotaUpdateSubGroupTooBig(c *C) {
   886  	r := s.mockSystemctlCalls(c, join(
   887  		// CreateQuota for foo
   888  		systemctlCallsForCreateQuota("foo", "test-snap"),
   889  
   890  		// CreateQuota for foo2
   891  		systemctlCallsForCreateQuota("foo/foo2", "test-snap2"),
   892  
   893  		// UpdateQuota for foo2 - just the slice changes
   894  		[]expectedSystemctl{{expArgs: []string{"daemon-reload"}}},
   895  
   896  		// UpdateQuota for foo2 which fails - no systemctl calls
   897  	))
   898  	defer r()
   899  
   900  	st := s.state
   901  	st.Lock()
   902  	defer st.Unlock()
   903  
   904  	// setup the snap so it exists
   905  	snapstate.Set(s.state, "test-snap", s.testSnapState)
   906  	snaptest.MockSnapCurrent(c, testYaml, s.testSnapSideInfo)
   907  	// and test-snap2
   908  	si2 := &snap.SideInfo{RealName: "test-snap2", Revision: snap.R(42)}
   909  	snapst2 := &snapstate.SnapState{
   910  		Sequence: []*snap.SideInfo{si2},
   911  		Current:  si2.Revision,
   912  		Active:   true,
   913  		SnapType: "app",
   914  	}
   915  	snapstate.Set(s.state, "test-snap2", snapst2)
   916  	snaptest.MockSnapCurrent(c, testYaml2, si2)
   917  
   918  	// create a quota group
   919  	qc := servicestate.QuotaControlAction{
   920  		Action:      "create",
   921  		QuotaName:   "foo",
   922  		MemoryLimit: quantity.SizeGiB,
   923  		AddSnaps:    []string{"test-snap"},
   924  	}
   925  
   926  	err := s.callDoQuotaControl(&qc)
   927  	c.Assert(err, IsNil)
   928  
   929  	// ensure mem-limit is 1 GB
   930  	expFooGroupState := quotaGroupState{
   931  		MemoryLimit: quantity.SizeGiB,
   932  		Snaps:       []string{"test-snap"},
   933  	}
   934  	checkQuotaState(c, st, map[string]quotaGroupState{
   935  		"foo": expFooGroupState,
   936  	})
   937  
   938  	// create a sub-group with 0.5 GiB
   939  	qc2 := servicestate.QuotaControlAction{
   940  		Action:      "create",
   941  		QuotaName:   "foo2",
   942  		MemoryLimit: quantity.SizeGiB / 2,
   943  		AddSnaps:    []string{"test-snap2"},
   944  		ParentName:  "foo",
   945  	}
   946  
   947  	err = s.callDoQuotaControl(&qc2)
   948  	c.Assert(err, IsNil)
   949  
   950  	expFooGroupState.SubGroups = []string{"foo2"}
   951  
   952  	expFoo2GroupState := quotaGroupState{
   953  		MemoryLimit: quantity.SizeGiB / 2,
   954  		Snaps:       []string{"test-snap2"},
   955  		ParentGroup: "foo",
   956  	}
   957  
   958  	// verify it was set in state
   959  	checkQuotaState(c, st, map[string]quotaGroupState{
   960  		"foo":  expFooGroupState,
   961  		"foo2": expFoo2GroupState,
   962  	})
   963  
   964  	// now try to increase it to the max size
   965  	qc3 := servicestate.QuotaControlAction{
   966  		Action:      "update",
   967  		QuotaName:   "foo2",
   968  		MemoryLimit: quantity.SizeGiB,
   969  	}
   970  
   971  	err = s.callDoQuotaControl(&qc3)
   972  	c.Assert(err, IsNil)
   973  
   974  	expFoo2GroupState.MemoryLimit = quantity.SizeGiB
   975  	// and check that it got updated in the state
   976  	checkQuotaState(c, st, map[string]quotaGroupState{
   977  		"foo":  expFooGroupState,
   978  		"foo2": expFoo2GroupState,
   979  	})
   980  
   981  	// now try to increase it above the parent limit
   982  	qc4 := servicestate.QuotaControlAction{
   983  		Action:      "update",
   984  		QuotaName:   "foo2",
   985  		MemoryLimit: 2 * quantity.SizeGiB,
   986  	}
   987  
   988  	err = s.callDoQuotaControl(&qc4)
   989  	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`)
   990  
   991  	// and make sure that the existing memory limit is still in place
   992  	checkQuotaState(c, st, map[string]quotaGroupState{
   993  		"foo":  expFooGroupState,
   994  		"foo2": expFoo2GroupState,
   995  	})
   996  }
   997  
   998  func (s *quotaHandlersSuite) TestQuotaUpdateChangeMemLimit(c *C) {
   999  	r := s.mockSystemctlCalls(c, join(
  1000  		// CreateQuota for foo
  1001  		systemctlCallsForCreateQuota("foo", "test-snap"),
  1002  
  1003  		// UpdateQuota for foo - an existing slice was changed, so all we need
  1004  		// to is daemon-reload
  1005  		[]expectedSystemctl{{expArgs: []string{"daemon-reload"}}},
  1006  	))
  1007  	defer r()
  1008  
  1009  	st := s.state
  1010  	st.Lock()
  1011  	defer st.Unlock()
  1012  
  1013  	// setup the snap so it exists
  1014  	snapstate.Set(s.state, "test-snap", s.testSnapState)
  1015  	snaptest.MockSnapCurrent(c, testYaml, s.testSnapSideInfo)
  1016  
  1017  	// create a quota group
  1018  	qc := servicestate.QuotaControlAction{
  1019  		Action:      "create",
  1020  		QuotaName:   "foo",
  1021  		MemoryLimit: quantity.SizeGiB,
  1022  		AddSnaps:    []string{"test-snap"},
  1023  	}
  1024  
  1025  	err := s.callDoQuotaControl(&qc)
  1026  	c.Assert(err, IsNil)
  1027  
  1028  	// ensure mem-limit is 1 GB
  1029  	checkQuotaState(c, st, map[string]quotaGroupState{
  1030  		"foo": {
  1031  			MemoryLimit: quantity.SizeGiB,
  1032  			Snaps:       []string{"test-snap"},
  1033  		},
  1034  	})
  1035  
  1036  	// modify to 2 GB
  1037  	qc2 := servicestate.QuotaControlAction{
  1038  		Action:      "update",
  1039  		QuotaName:   "foo",
  1040  		MemoryLimit: 2 * quantity.SizeGiB,
  1041  	}
  1042  	err = s.callDoQuotaControl(&qc2)
  1043  	c.Assert(err, IsNil)
  1044  
  1045  	// and check that it got updated in the state
  1046  	checkQuotaState(c, st, map[string]quotaGroupState{
  1047  		"foo": {
  1048  			MemoryLimit: 2 * quantity.SizeGiB,
  1049  			Snaps:       []string{"test-snap"},
  1050  		},
  1051  	})
  1052  
  1053  	// trying to decrease the memory limit is not yet supported
  1054  	qc3 := servicestate.QuotaControlAction{
  1055  		Action:      "update",
  1056  		QuotaName:   "foo",
  1057  		MemoryLimit: quantity.SizeGiB,
  1058  	}
  1059  	err = s.callDoQuotaControl(&qc3)
  1060  	c.Assert(err, ErrorMatches, "cannot decrease memory limit of existing quota-group, remove and re-create it to decrease the limit")
  1061  }
  1062  
  1063  func (s *quotaHandlersSuite) TestQuotaUpdateAddSnap(c *C) {
  1064  	r := s.mockSystemctlCalls(c, join(
  1065  		// CreateQuota for foo
  1066  		systemctlCallsForCreateQuota("foo", "test-snap"),
  1067  
  1068  		// UpdateQuota with just test-snap2 restarted since the group already
  1069  		// exists
  1070  		[]expectedSystemctl{{expArgs: []string{"daemon-reload"}}},
  1071  		systemctlCallsForServiceRestart("test-snap2"),
  1072  	))
  1073  	defer r()
  1074  
  1075  	st := s.state
  1076  	st.Lock()
  1077  	defer st.Unlock()
  1078  
  1079  	// setup test-snap
  1080  	snapstate.Set(s.state, "test-snap", s.testSnapState)
  1081  	snaptest.MockSnapCurrent(c, testYaml, s.testSnapSideInfo)
  1082  	// and test-snap2
  1083  	si2 := &snap.SideInfo{RealName: "test-snap2", Revision: snap.R(42)}
  1084  	snapst2 := &snapstate.SnapState{
  1085  		Sequence: []*snap.SideInfo{si2},
  1086  		Current:  si2.Revision,
  1087  		Active:   true,
  1088  		SnapType: "app",
  1089  	}
  1090  	snapstate.Set(s.state, "test-snap2", snapst2)
  1091  	snaptest.MockSnapCurrent(c, testYaml2, si2)
  1092  
  1093  	// create a quota group
  1094  	qc := servicestate.QuotaControlAction{
  1095  		Action:      "create",
  1096  		QuotaName:   "foo",
  1097  		MemoryLimit: quantity.SizeGiB,
  1098  		AddSnaps:    []string{"test-snap"},
  1099  	}
  1100  
  1101  	err := s.callDoQuotaControl(&qc)
  1102  	c.Assert(err, IsNil)
  1103  
  1104  	checkQuotaState(c, st, map[string]quotaGroupState{
  1105  		"foo": {
  1106  			MemoryLimit: quantity.SizeGiB,
  1107  			Snaps:       []string{"test-snap"},
  1108  		},
  1109  	})
  1110  
  1111  	// add a snap
  1112  	qc2 := servicestate.QuotaControlAction{
  1113  		Action:    "update",
  1114  		QuotaName: "foo",
  1115  		AddSnaps:  []string{"test-snap2"},
  1116  	}
  1117  	err = s.callDoQuotaControl(&qc2)
  1118  	c.Assert(err, IsNil)
  1119  
  1120  	// and check that it got updated in the state
  1121  	checkQuotaState(c, st, map[string]quotaGroupState{
  1122  		"foo": {
  1123  			MemoryLimit: quantity.SizeGiB,
  1124  			Snaps:       []string{"test-snap", "test-snap2"},
  1125  		},
  1126  	})
  1127  }
  1128  
  1129  func (s *quotaHandlersSuite) TestQuotaUpdateAddSnapAlreadyInOtherGroup(c *C) {
  1130  	r := s.mockSystemctlCalls(c, join(
  1131  		// CreateQuota for foo
  1132  		systemctlCallsForCreateQuota("foo", "test-snap"),
  1133  
  1134  		// CreateQuota for foo2
  1135  		systemctlCallsForCreateQuota("foo2", "test-snap2"),
  1136  
  1137  		// UpdateQuota for foo which fails - no systemctl calls
  1138  	))
  1139  	defer r()
  1140  
  1141  	st := s.state
  1142  	st.Lock()
  1143  	defer st.Unlock()
  1144  
  1145  	// setup test-snap
  1146  	snapstate.Set(s.state, "test-snap", s.testSnapState)
  1147  	snaptest.MockSnapCurrent(c, testYaml, s.testSnapSideInfo)
  1148  	// and test-snap2
  1149  	si2 := &snap.SideInfo{RealName: "test-snap2", Revision: snap.R(42)}
  1150  	snapst2 := &snapstate.SnapState{
  1151  		Sequence: []*snap.SideInfo{si2},
  1152  		Current:  si2.Revision,
  1153  		Active:   true,
  1154  		SnapType: "app",
  1155  	}
  1156  	snapstate.Set(s.state, "test-snap2", snapst2)
  1157  	snaptest.MockSnapCurrent(c, testYaml2, si2)
  1158  
  1159  	// create a quota group
  1160  	qc := servicestate.QuotaControlAction{
  1161  		Action:      "create",
  1162  		QuotaName:   "foo",
  1163  		MemoryLimit: quantity.SizeGiB,
  1164  		AddSnaps:    []string{"test-snap"},
  1165  	}
  1166  
  1167  	err := s.callDoQuotaControl(&qc)
  1168  	c.Assert(err, IsNil)
  1169  
  1170  	checkQuotaState(c, st, map[string]quotaGroupState{
  1171  		"foo": {
  1172  			MemoryLimit: quantity.SizeGiB,
  1173  			Snaps:       []string{"test-snap"},
  1174  		},
  1175  	})
  1176  
  1177  	// create another quota group with the second snap
  1178  	qc2 := servicestate.QuotaControlAction{
  1179  		Action:      "create",
  1180  		QuotaName:   "foo2",
  1181  		MemoryLimit: quantity.SizeGiB,
  1182  		AddSnaps:    []string{"test-snap2"},
  1183  	}
  1184  
  1185  	err = s.callDoQuotaControl(&qc2)
  1186  	c.Assert(err, IsNil)
  1187  
  1188  	// verify state
  1189  	checkQuotaState(c, st, map[string]quotaGroupState{
  1190  		"foo": {
  1191  			MemoryLimit: quantity.SizeGiB,
  1192  			Snaps:       []string{"test-snap"},
  1193  		},
  1194  		"foo2": {
  1195  			MemoryLimit: quantity.SizeGiB,
  1196  			Snaps:       []string{"test-snap2"},
  1197  		},
  1198  	})
  1199  
  1200  	// try to add test-snap2 to foo
  1201  	qc3 := servicestate.QuotaControlAction{
  1202  		Action:    "update",
  1203  		QuotaName: "foo",
  1204  		AddSnaps:  []string{"test-snap2"},
  1205  	}
  1206  
  1207  	err = s.callDoQuotaControl(&qc3)
  1208  	c.Assert(err, ErrorMatches, `cannot add snap "test-snap2" to group "foo": snap already in quota group "foo2"`)
  1209  
  1210  	// nothing changed in the state
  1211  	checkQuotaState(c, st, map[string]quotaGroupState{
  1212  		"foo": {
  1213  			MemoryLimit: quantity.SizeGiB,
  1214  			Snaps:       []string{"test-snap"},
  1215  		},
  1216  		"foo2": {
  1217  			MemoryLimit: quantity.SizeGiB,
  1218  			Snaps:       []string{"test-snap2"},
  1219  		},
  1220  	})
  1221  }