gopkg.in/ubuntu-core/snappy.v0@v0.0.0-20210902073436-25a8614f10a6/overlord/servicestate/quota_control_test.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2021 Canonical Ltd
     5   *
     6   * This program is free software: you can redistribute it and/or modify
     7   * it under the terms of the GNU General Public License version 3 as
     8   * published by the Free Software Foundation.
     9   *
    10   * This program is distributed in the hope that it will be useful,
    11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13   * GNU General Public License for more details.
    14   *
    15   * You should have received a copy of the GNU General Public License
    16   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17   *
    18   */
    19  
    20  package servicestate_test
    21  
    22  import (
    23  	"fmt"
    24  	"path/filepath"
    25  	"time"
    26  
    27  	. "gopkg.in/check.v1"
    28  
    29  	"github.com/snapcore/snapd/dirs"
    30  	"github.com/snapcore/snapd/gadget/quantity"
    31  	"github.com/snapcore/snapd/overlord/configstate/config"
    32  	"github.com/snapcore/snapd/overlord/servicestate"
    33  	"github.com/snapcore/snapd/overlord/servicestate/servicestatetest"
    34  	"github.com/snapcore/snapd/overlord/snapstate"
    35  	"github.com/snapcore/snapd/overlord/state"
    36  	"github.com/snapcore/snapd/snap"
    37  	"github.com/snapcore/snapd/snap/snaptest"
    38  	"github.com/snapcore/snapd/snapdenv"
    39  	"github.com/snapcore/snapd/systemd"
    40  	"github.com/snapcore/snapd/testutil"
    41  )
    42  
    43  type quotaControlSuite struct {
    44  	baseServiceMgrTestSuite
    45  }
    46  
    47  var _ = Suite(&quotaControlSuite{})
    48  
    49  func (s *quotaControlSuite) SetUpTest(c *C) {
    50  	s.baseServiceMgrTestSuite.SetUpTest(c)
    51  
    52  	// we don't need the EnsureSnapServices ensure loop to run by default
    53  	servicestate.MockEnsuredSnapServices(s.mgr, true)
    54  
    55  	// we enable quota-groups by default
    56  	s.state.Lock()
    57  	defer s.state.Unlock()
    58  	tr := config.NewTransaction(s.state)
    59  	tr.Set("core", "experimental.quota-groups", true)
    60  	tr.Commit()
    61  
    62  	// mock that we have a new enough version of systemd by default
    63  	r := servicestate.MockSystemdVersion(248)
    64  	s.AddCleanup(r)
    65  }
    66  
    67  type quotaGroupState struct {
    68  	MemoryLimit quantity.Size
    69  	SubGroups   []string
    70  	ParentGroup string
    71  	Snaps       []string
    72  }
    73  
    74  func checkQuotaState(c *C, st *state.State, exp map[string]quotaGroupState) {
    75  	m, err := servicestate.AllQuotas(st)
    76  	c.Assert(err, IsNil)
    77  	c.Assert(m, HasLen, len(exp))
    78  	for name, grp := range m {
    79  		expGrp, ok := exp[name]
    80  		c.Assert(ok, Equals, true, Commentf("unexpected group %q in state", name))
    81  		c.Assert(grp.MemoryLimit, Equals, expGrp.MemoryLimit)
    82  		c.Assert(grp.ParentGroup, Equals, expGrp.ParentGroup)
    83  
    84  		c.Assert(grp.Snaps, HasLen, len(expGrp.Snaps))
    85  		if len(expGrp.Snaps) != 0 {
    86  			c.Assert(grp.Snaps, DeepEquals, expGrp.Snaps)
    87  
    88  			// also check on the service file states
    89  			for _, sn := range expGrp.Snaps {
    90  				// meh assume all services are named svc1
    91  				slicePath := name
    92  				if grp.ParentGroup != "" {
    93  					slicePath = grp.ParentGroup + "/" + name
    94  				}
    95  				checkSvcAndSliceState(c, sn+".svc1", slicePath, grp.MemoryLimit)
    96  			}
    97  		}
    98  
    99  		c.Assert(grp.SubGroups, HasLen, len(expGrp.SubGroups))
   100  		if len(expGrp.SubGroups) != 0 {
   101  			c.Assert(grp.SubGroups, DeepEquals, expGrp.SubGroups)
   102  		}
   103  	}
   104  }
   105  
   106  func checkSvcAndSliceState(c *C, snapSvc string, slicePath string, sliceMem quantity.Size) {
   107  	slicePath = systemd.EscapeUnitNamePath(slicePath)
   108  	// make sure the service file exists
   109  	svcFileName := filepath.Join(dirs.SnapServicesDir, "snap."+snapSvc+".service")
   110  	c.Assert(svcFileName, testutil.FilePresent)
   111  
   112  	if sliceMem != 0 {
   113  		// the service file should mention this slice
   114  		c.Assert(svcFileName, testutil.FileContains, fmt.Sprintf("\nSlice=snap.%s.slice\n", slicePath))
   115  	} else {
   116  		c.Assert(svcFileName, Not(testutil.FileContains), fmt.Sprintf("Slice=snap.%s.slice", slicePath))
   117  	}
   118  	checkSliceState(c, slicePath, sliceMem)
   119  }
   120  
   121  func checkSliceState(c *C, sliceName string, sliceMem quantity.Size) {
   122  	sliceFileName := filepath.Join(dirs.SnapServicesDir, "snap."+sliceName+".slice")
   123  	if sliceMem != 0 {
   124  		c.Assert(sliceFileName, testutil.FilePresent)
   125  		c.Assert(sliceFileName, testutil.FileContains, fmt.Sprintf("\nMemoryMax=%s\n", sliceMem.String()))
   126  	} else {
   127  		c.Assert(sliceFileName, testutil.FileAbsent)
   128  	}
   129  }
   130  
   131  func systemctlCallsForSliceStart(name string) []expectedSystemctl {
   132  	name = systemd.EscapeUnitNamePath(name)
   133  	slice := "snap." + name + ".slice"
   134  	return []expectedSystemctl{
   135  		{expArgs: []string{"start", slice}},
   136  	}
   137  }
   138  
   139  func systemctlCallsForSliceStop(name string) []expectedSystemctl {
   140  	name = systemd.EscapeUnitNamePath(name)
   141  	slice := "snap." + name + ".slice"
   142  	return []expectedSystemctl{
   143  		{expArgs: []string{"stop", slice}},
   144  		{
   145  			expArgs: []string{"show", "--property=ActiveState", slice},
   146  			output:  "ActiveState=inactive",
   147  		},
   148  	}
   149  }
   150  
   151  func systemctlCallsForServiceRestart(name string) []expectedSystemctl {
   152  	svc := "snap." + name + ".svc1.service"
   153  	return []expectedSystemctl{
   154  		{
   155  			expArgs: []string{"show", "--property=Id,ActiveState,UnitFileState,Type", svc},
   156  			output:  fmt.Sprintf("Id=%s\nActiveState=active\nUnitFileState=enabled\nType=simple\n", svc),
   157  		},
   158  		{expArgs: []string{"stop", svc}},
   159  		{
   160  			expArgs: []string{"show", "--property=ActiveState", svc},
   161  			output:  "ActiveState=inactive",
   162  		},
   163  		{expArgs: []string{"start", svc}},
   164  	}
   165  }
   166  
   167  func systemctlCallsForCreateQuota(groupName string, snapNames ...string) []expectedSystemctl {
   168  	calls := join(
   169  		[]expectedSystemctl{{expArgs: []string{"daemon-reload"}}},
   170  		systemctlCallsForSliceStart(groupName),
   171  	)
   172  	for _, snapName := range snapNames {
   173  		calls = join(calls, systemctlCallsForServiceRestart(snapName))
   174  	}
   175  
   176  	return calls
   177  }
   178  
   179  func systemctlCallsVersion(version int) []expectedSystemctl {
   180  	return []expectedSystemctl{
   181  		{
   182  			expArgs: []string{"--version"},
   183  			output:  fmt.Sprintf("systemd %d\n+FOO +BAR\n", version),
   184  		},
   185  	}
   186  }
   187  
   188  func join(calls ...[]expectedSystemctl) []expectedSystemctl {
   189  	fullCall := []expectedSystemctl{}
   190  	for _, call := range calls {
   191  		fullCall = append(fullCall, call...)
   192  	}
   193  
   194  	return fullCall
   195  }
   196  
   197  func checkQuotaControlTasks(c *C, tasks []*state.Task, expAction *servicestate.QuotaControlAction) {
   198  	c.Assert(tasks, HasLen, 1)
   199  	t := tasks[0]
   200  
   201  	c.Assert(t.Kind(), Equals, "quota-control")
   202  	qcs := []*servicestate.QuotaControlAction{}
   203  
   204  	err := t.Get("quota-control-actions", &qcs)
   205  	c.Assert(err, IsNil)
   206  	c.Assert(qcs, HasLen, 1)
   207  
   208  	c.Assert(qcs[0], DeepEquals, expAction)
   209  }
   210  
   211  func (s *quotaControlSuite) TestCreateQuotaNotEnabled(c *C) {
   212  	s.state.Lock()
   213  	defer s.state.Unlock()
   214  	tr := config.NewTransaction(s.state)
   215  	tr.Set("core", "experimental.quota-groups", false)
   216  	tr.Commit()
   217  
   218  	// try to create an empty quota group
   219  	_, err := servicestate.CreateQuota(s.state, "foo", "", nil, quantity.SizeGiB)
   220  	c.Assert(err, ErrorMatches, `experimental feature disabled - test it by setting 'experimental.quota-groups' to true`)
   221  }
   222  
   223  func (s *quotaControlSuite) TestCreateQuotaSystemdTooOld(c *C) {
   224  	s.state.Lock()
   225  	defer s.state.Unlock()
   226  
   227  	r := s.mockSystemctlCalls(c, systemctlCallsVersion(229))
   228  	defer r()
   229  
   230  	err := servicestate.CheckSystemdVersion()
   231  	c.Assert(err, IsNil)
   232  
   233  	_, err = servicestate.CreateQuota(s.state, "foo", "", nil, quantity.SizeGiB)
   234  	c.Assert(err, ErrorMatches, `systemd version too old: snap quotas requires systemd 230 and newer \(currently have 229\)`)
   235  }
   236  
   237  func (s *quotaControlSuite) TestCreateQuotaPrecond(c *C) {
   238  	st := s.state
   239  	st.Lock()
   240  	defer st.Unlock()
   241  
   242  	err := servicestatetest.MockQuotaInState(st, "foo", "", nil, 2*quantity.SizeGiB)
   243  	c.Assert(err, IsNil)
   244  
   245  	tests := []struct {
   246  		name  string
   247  		mem   quantity.Size
   248  		snaps []string
   249  		err   string
   250  	}{
   251  		{"foo", 16 * quantity.SizeKiB, nil, `group "foo" already exists`},
   252  		{"new", 0, nil, `cannot create quota group with no memory limit set`},
   253  		{"new", quantity.SizeKiB, nil, `memory limit for group "new" is too small: size must be larger than 4KB`},
   254  		{"new", 16 * quantity.SizeKiB, []string{"baz"}, `cannot use snap "baz" in group "new": snap "baz" is not installed`},
   255  	}
   256  
   257  	for _, t := range tests {
   258  		_, err := servicestate.CreateQuota(st, t.name, "", t.snaps, t.mem)
   259  		c.Check(err, ErrorMatches, t.err)
   260  	}
   261  }
   262  
   263  func (s *quotaControlSuite) TestRemoveQuotaPreseeding(c *C) {
   264  	r := snapdenv.MockPreseeding(true)
   265  	defer r()
   266  
   267  	st := s.state
   268  	st.Lock()
   269  	defer st.Unlock()
   270  
   271  	snapstate.Set(s.state, "test-snap", s.testSnapState)
   272  	snaptest.MockSnapCurrent(c, testYaml, s.testSnapSideInfo)
   273  
   274  	// create a quota group
   275  	ts, err := servicestate.CreateQuota(s.state, "foo", "", []string{"test-snap"}, quantity.SizeGiB)
   276  	c.Assert(err, IsNil)
   277  
   278  	chg := st.NewChange("quota-control", "...")
   279  	chg.AddAll(ts)
   280  
   281  	exp := &servicestate.QuotaControlAction{
   282  		Action:      "create",
   283  		QuotaName:   "foo",
   284  		AddSnaps:    []string{"test-snap"},
   285  		MemoryLimit: quantity.SizeGiB,
   286  	}
   287  
   288  	checkQuotaControlTasks(c, chg.Tasks(), exp)
   289  
   290  	// run the change
   291  	st.Unlock()
   292  	defer s.se.Stop()
   293  	err = s.o.Settle(5 * time.Second)
   294  	st.Lock()
   295  	c.Assert(err, IsNil)
   296  
   297  	// check that the quota groups were created in the state
   298  	checkQuotaState(c, st, map[string]quotaGroupState{
   299  		"foo": {
   300  			MemoryLimit: quantity.SizeGiB,
   301  			Snaps:       []string{"test-snap"},
   302  		},
   303  	})
   304  
   305  	// but removing a quota doesn't work, since it just doesn't make sense to be
   306  	// able to remove a quota group while preseeding, so we purposely fail
   307  	_, err = servicestate.RemoveQuota(st, "foo")
   308  	c.Assert(err, ErrorMatches, `removing quota groups not supported while preseeding`)
   309  }
   310  
   311  func (s *quotaControlSuite) TestCreateUpdateRemoveQuotaHappy(c *C) {
   312  	r := s.mockSystemctlCalls(c, join(
   313  		// CreateQuota for foo - success
   314  		systemctlCallsForCreateQuota("foo", "test-snap"),
   315  
   316  		// UpdateQuota for foo
   317  		[]expectedSystemctl{{expArgs: []string{"daemon-reload"}}},
   318  
   319  		// RemoveQuota for foo
   320  		[]expectedSystemctl{{expArgs: []string{"daemon-reload"}}},
   321  		systemctlCallsForSliceStop("foo"),
   322  		[]expectedSystemctl{{expArgs: []string{"daemon-reload"}}},
   323  		systemctlCallsForServiceRestart("test-snap"),
   324  	))
   325  	defer r()
   326  
   327  	st := s.state
   328  	st.Lock()
   329  	defer st.Unlock()
   330  
   331  	// setup the snap so it exists
   332  	snapstate.Set(s.state, "test-snap", s.testSnapState)
   333  	snaptest.MockSnapCurrent(c, testYaml, s.testSnapSideInfo)
   334  
   335  	// create the quota group
   336  	ts, err := servicestate.CreateQuota(st, "foo", "", []string{"test-snap"}, quantity.SizeGiB)
   337  	c.Assert(err, IsNil)
   338  
   339  	chg := st.NewChange("quota-control", "...")
   340  	chg.AddAll(ts)
   341  
   342  	exp := &servicestate.QuotaControlAction{
   343  		Action:      "create",
   344  		QuotaName:   "foo",
   345  		AddSnaps:    []string{"test-snap"},
   346  		MemoryLimit: quantity.SizeGiB,
   347  	}
   348  
   349  	checkQuotaControlTasks(c, chg.Tasks(), exp)
   350  
   351  	// run the change
   352  	st.Unlock()
   353  	defer s.se.Stop()
   354  	err = s.o.Settle(5 * time.Second)
   355  	st.Lock()
   356  	c.Assert(err, IsNil)
   357  
   358  	// check that the quota groups were created in the state
   359  	checkQuotaState(c, st, map[string]quotaGroupState{
   360  		"foo": {
   361  			MemoryLimit: quantity.SizeGiB,
   362  			Snaps:       []string{"test-snap"},
   363  		},
   364  	})
   365  
   366  	// increase the memory limit
   367  	ts, err = servicestate.UpdateQuota(st, "foo", servicestate.QuotaGroupUpdate{NewMemoryLimit: 2 * quantity.SizeGiB})
   368  	c.Assert(err, IsNil)
   369  
   370  	chg = st.NewChange("quota-control", "...")
   371  	chg.AddAll(ts)
   372  
   373  	exp2 := &servicestate.QuotaControlAction{
   374  		Action:      "update",
   375  		QuotaName:   "foo",
   376  		MemoryLimit: 2 * quantity.SizeGiB,
   377  	}
   378  
   379  	checkQuotaControlTasks(c, chg.Tasks(), exp2)
   380  
   381  	// run the change
   382  	st.Unlock()
   383  	defer s.se.Stop()
   384  	err = s.o.Settle(5 * time.Second)
   385  	st.Lock()
   386  	c.Assert(err, IsNil)
   387  
   388  	checkQuotaState(c, st, map[string]quotaGroupState{
   389  		"foo": {
   390  			MemoryLimit: 2 * quantity.SizeGiB,
   391  			Snaps:       []string{"test-snap"},
   392  		},
   393  	})
   394  
   395  	// remove the quota
   396  	ts, err = servicestate.RemoveQuota(st, "foo")
   397  	c.Assert(err, IsNil)
   398  
   399  	chg = st.NewChange("quota-control", "...")
   400  	chg.AddAll(ts)
   401  
   402  	exp3 := &servicestate.QuotaControlAction{
   403  		Action:    "remove",
   404  		QuotaName: "foo",
   405  	}
   406  
   407  	checkQuotaControlTasks(c, chg.Tasks(), exp3)
   408  
   409  	// run the change
   410  	st.Unlock()
   411  	defer s.se.Stop()
   412  	err = s.o.Settle(5 * time.Second)
   413  	st.Lock()
   414  	c.Assert(err, IsNil)
   415  
   416  	checkQuotaState(c, st, nil)
   417  }
   418  
   419  func (s *quotaControlSuite) TestEnsureSnapAbsentFromQuotaGroup(c *C) {
   420  	r := s.mockSystemctlCalls(c, join(
   421  		// CreateQuota for foo
   422  		systemctlCallsForCreateQuota("foo", "test-snap", "test-snap2"),
   423  
   424  		// EnsureSnapAbsentFromQuota with just test-snap restarted since it is
   425  		// no longer in the group
   426  		[]expectedSystemctl{{expArgs: []string{"daemon-reload"}}},
   427  		systemctlCallsForServiceRestart("test-snap"),
   428  
   429  		// another identical call to EnsureSnapAbsentFromQuota does nothing
   430  		// since the function is idempotent
   431  
   432  		// EnsureSnapAbsentFromQuota with just test-snap2 restarted since it is no
   433  		// longer in the group
   434  		[]expectedSystemctl{{expArgs: []string{"daemon-reload"}}},
   435  		systemctlCallsForServiceRestart("test-snap2"),
   436  	))
   437  	defer r()
   438  
   439  	st := s.state
   440  	st.Lock()
   441  	defer st.Unlock()
   442  	// setup test-snap
   443  	snapstate.Set(s.state, "test-snap", s.testSnapState)
   444  	snaptest.MockSnapCurrent(c, testYaml, s.testSnapSideInfo)
   445  	// and test-snap2
   446  	si2 := &snap.SideInfo{RealName: "test-snap2", Revision: snap.R(42)}
   447  	snapst2 := &snapstate.SnapState{
   448  		Sequence: []*snap.SideInfo{si2},
   449  		Current:  si2.Revision,
   450  		Active:   true,
   451  		SnapType: "app",
   452  	}
   453  	snapstate.Set(s.state, "test-snap2", snapst2)
   454  	snaptest.MockSnapCurrent(c, testYaml2, si2)
   455  
   456  	// create a quota group
   457  	ts, err := servicestate.CreateQuota(s.state, "foo", "", []string{"test-snap", "test-snap2"}, quantity.SizeGiB)
   458  	c.Assert(err, IsNil)
   459  
   460  	chg := st.NewChange("quota-control", "...")
   461  	chg.AddAll(ts)
   462  
   463  	exp := &servicestate.QuotaControlAction{
   464  		Action:      "create",
   465  		QuotaName:   "foo",
   466  		AddSnaps:    []string{"test-snap", "test-snap2"},
   467  		MemoryLimit: quantity.SizeGiB,
   468  	}
   469  
   470  	checkQuotaControlTasks(c, chg.Tasks(), exp)
   471  
   472  	// run the change
   473  	st.Unlock()
   474  	defer s.se.Stop()
   475  	err = s.o.Settle(5 * time.Second)
   476  	st.Lock()
   477  	c.Assert(err, IsNil)
   478  
   479  	checkQuotaState(c, st, map[string]quotaGroupState{
   480  		"foo": {
   481  			MemoryLimit: quantity.SizeGiB,
   482  			Snaps:       []string{"test-snap", "test-snap2"},
   483  		},
   484  	})
   485  
   486  	// remove test-snap from the group
   487  	err = servicestate.EnsureSnapAbsentFromQuota(s.state, "test-snap")
   488  	c.Assert(err, IsNil)
   489  
   490  	checkQuotaState(c, st, map[string]quotaGroupState{
   491  		"foo": {
   492  			MemoryLimit: quantity.SizeGiB,
   493  			Snaps:       []string{"test-snap2"},
   494  		},
   495  	})
   496  
   497  	// removing the same snap twice works as well but does nothing
   498  	err = servicestate.EnsureSnapAbsentFromQuota(s.state, "test-snap")
   499  	c.Assert(err, IsNil)
   500  
   501  	// now remove test-snap2 too
   502  	err = servicestate.EnsureSnapAbsentFromQuota(s.state, "test-snap2")
   503  	c.Assert(err, IsNil)
   504  
   505  	// and check that it got updated in the state
   506  	checkQuotaState(c, st, map[string]quotaGroupState{
   507  		"foo": {
   508  			MemoryLimit: quantity.SizeGiB,
   509  		},
   510  	})
   511  
   512  	// it's not an error to call EnsureSnapAbsentFromQuotaGroup on a snap that
   513  	// is not in any quota group
   514  	err = servicestate.EnsureSnapAbsentFromQuota(s.state, "test-snap33333")
   515  	c.Assert(err, IsNil)
   516  }
   517  
   518  func (s *quotaControlSuite) TestUpdateQuotaGroupNotEnabled(c *C) {
   519  	s.state.Lock()
   520  	defer s.state.Unlock()
   521  	tr := config.NewTransaction(s.state)
   522  	tr.Set("core", "experimental.quota-groups", false)
   523  	tr.Commit()
   524  
   525  	opts := servicestate.QuotaGroupUpdate{}
   526  	_, err := servicestate.UpdateQuota(s.state, "foo", opts)
   527  	c.Assert(err, ErrorMatches, `experimental feature disabled - test it by setting 'experimental.quota-groups' to true`)
   528  }
   529  
   530  func (s *quotaControlSuite) TestUpdateQuotaPrecond(c *C) {
   531  	st := s.state
   532  	st.Lock()
   533  	defer st.Unlock()
   534  
   535  	err := servicestatetest.MockQuotaInState(st, "foo", "", nil, 2*quantity.SizeGiB)
   536  	c.Assert(err, IsNil)
   537  
   538  	tests := []struct {
   539  		name string
   540  		opts servicestate.QuotaGroupUpdate
   541  		err  string
   542  	}{
   543  		{"what", servicestate.QuotaGroupUpdate{}, `group "what" does not exist`},
   544  		{"foo", servicestate.QuotaGroupUpdate{NewMemoryLimit: quantity.SizeGiB}, `cannot decrease memory limit of existing quota-group, remove and re-create it to decrease the limit`},
   545  		{"foo", servicestate.QuotaGroupUpdate{AddSnaps: []string{"baz"}}, `cannot use snap "baz" in group "foo": snap "baz" is not installed`},
   546  	}
   547  
   548  	for _, t := range tests {
   549  		_, err := servicestate.UpdateQuota(st, t.name, t.opts)
   550  		c.Check(err, ErrorMatches, t.err)
   551  	}
   552  }
   553  
   554  func (s *quotaControlSuite) TestRemoveQuotaPrecond(c *C) {
   555  	st := s.state
   556  	st.Lock()
   557  	defer st.Unlock()
   558  
   559  	err := servicestatetest.MockQuotaInState(st, "foo", "", nil, 2*quantity.SizeGiB)
   560  	c.Assert(err, IsNil)
   561  	err = servicestatetest.MockQuotaInState(st, "bar", "foo", nil, quantity.SizeGiB)
   562  	c.Assert(err, IsNil)
   563  
   564  	_, err = servicestate.RemoveQuota(st, "what")
   565  	c.Check(err, ErrorMatches, `cannot remove non-existent quota group "what"`)
   566  
   567  	_, err = servicestate.RemoveQuota(st, "foo")
   568  	c.Check(err, ErrorMatches, `cannot remove quota group "foo" with sub-groups, remove the sub-groups first`)
   569  }
   570  
   571  func (s *quotaControlSuite) createQuota(c *C, name string, limit quantity.Size, snaps ...string) {
   572  	ts, err := servicestate.CreateQuota(s.state, name, "", snaps, limit)
   573  	c.Assert(err, IsNil)
   574  
   575  	chg := s.state.NewChange("quota-control", "...")
   576  	chg.AddAll(ts)
   577  
   578  	// run the change
   579  	s.state.Unlock()
   580  	err = s.o.Settle(5 * time.Second)
   581  	s.state.Lock()
   582  	c.Assert(err, IsNil)
   583  }
   584  
   585  func (s *quotaControlSuite) TestSnapOpUpdateQuotaConflict(c *C) {
   586  	r := s.mockSystemctlCalls(c, join(
   587  		// CreateQuota for foo
   588  		systemctlCallsForCreateQuota("foo", "test-snap"),
   589  	))
   590  	defer r()
   591  
   592  	st := s.state
   593  	st.Lock()
   594  	defer st.Unlock()
   595  
   596  	// setup test-snap
   597  	snapstate.Set(s.state, "test-snap", s.testSnapState)
   598  	snaptest.MockSnapCurrent(c, testYaml, s.testSnapSideInfo)
   599  	// and test-snap2
   600  	si2 := &snap.SideInfo{RealName: "test-snap2", Revision: snap.R(42)}
   601  	snapst2 := &snapstate.SnapState{
   602  		Sequence: []*snap.SideInfo{si2},
   603  		Current:  si2.Revision,
   604  		Active:   true,
   605  		SnapType: "app",
   606  	}
   607  	snapstate.Set(s.state, "test-snap2", snapst2)
   608  	snaptest.MockSnapCurrent(c, testYaml2, si2)
   609  
   610  	// create a quota group
   611  	defer s.se.Stop()
   612  	s.createQuota(c, "foo", quantity.SizeGiB, "test-snap")
   613  
   614  	ts, err := snapstate.Disable(st, "test-snap2")
   615  	c.Assert(err, IsNil)
   616  	chg1 := s.state.NewChange("disable", "...")
   617  	chg1.AddAll(ts)
   618  
   619  	_, err = servicestate.UpdateQuota(st, "foo", servicestate.QuotaGroupUpdate{AddSnaps: []string{"test-snap2"}})
   620  	c.Assert(err, ErrorMatches, `snap "test-snap2" has "disable" change in progress`)
   621  }
   622  
   623  func (s *quotaControlSuite) TestSnapOpCreateQuotaConflict(c *C) {
   624  	st := s.state
   625  	st.Lock()
   626  	defer st.Unlock()
   627  
   628  	// setup test-snap
   629  	snapstate.Set(s.state, "test-snap", s.testSnapState)
   630  	snaptest.MockSnapCurrent(c, testYaml, s.testSnapSideInfo)
   631  
   632  	ts, err := snapstate.Disable(st, "test-snap")
   633  	c.Assert(err, IsNil)
   634  	chg1 := s.state.NewChange("disable", "...")
   635  	chg1.AddAll(ts)
   636  
   637  	_, err = servicestate.CreateQuota(s.state, "foo", "", []string{"test-snap"}, quantity.SizeGiB)
   638  	c.Assert(err, ErrorMatches, `snap "test-snap" has "disable" change in progress`)
   639  }
   640  
   641  func (s *quotaControlSuite) TestSnapOpRemoveQuotaConflict(c *C) {
   642  	r := s.mockSystemctlCalls(c, join(
   643  		// CreateQuota for foo
   644  		systemctlCallsForCreateQuota("foo", "test-snap"),
   645  	))
   646  	defer r()
   647  
   648  	st := s.state
   649  	st.Lock()
   650  	defer st.Unlock()
   651  
   652  	// setup test-snap
   653  	snapstate.Set(s.state, "test-snap", s.testSnapState)
   654  	snaptest.MockSnapCurrent(c, testYaml, s.testSnapSideInfo)
   655  
   656  	// create a quota group
   657  	defer s.se.Stop()
   658  	s.createQuota(c, "foo", quantity.SizeGiB, "test-snap")
   659  
   660  	ts, err := snapstate.Disable(st, "test-snap")
   661  	c.Assert(err, IsNil)
   662  	chg1 := s.state.NewChange("disable", "...")
   663  	chg1.AddAll(ts)
   664  
   665  	_, err = servicestate.RemoveQuota(st, "foo")
   666  	c.Assert(err, ErrorMatches, `snap "test-snap" has "disable" change in progress`)
   667  }
   668  
   669  func (s *quotaControlSuite) TestCreateQuotaSnapOpConflict(c *C) {
   670  	st := s.state
   671  	st.Lock()
   672  	defer st.Unlock()
   673  
   674  	// setup test-snap
   675  	snapstate.Set(s.state, "test-snap", s.testSnapState)
   676  	snaptest.MockSnapCurrent(c, testYaml, s.testSnapSideInfo)
   677  
   678  	ts, err := servicestate.CreateQuota(s.state, "foo", "", []string{"test-snap"}, quantity.SizeGiB)
   679  	c.Assert(err, IsNil)
   680  	chg1 := s.state.NewChange("quota-control", "...")
   681  	chg1.AddAll(ts)
   682  
   683  	_, err = snapstate.Disable(st, "test-snap")
   684  	c.Assert(err, ErrorMatches, `snap "test-snap" has "quota-control" change in progress`)
   685  }
   686  
   687  func (s *quotaControlSuite) TestUpdateQuotaSnapOpConflict(c *C) {
   688  	r := s.mockSystemctlCalls(c, join(
   689  		// CreateQuota for foo
   690  		systemctlCallsForCreateQuota("foo", "test-snap"),
   691  	))
   692  	defer r()
   693  
   694  	st := s.state
   695  	st.Lock()
   696  	defer st.Unlock()
   697  
   698  	// setup test-snap
   699  	snapstate.Set(s.state, "test-snap", s.testSnapState)
   700  	snaptest.MockSnapCurrent(c, testYaml, s.testSnapSideInfo)
   701  	// and test-snap2
   702  	si2 := &snap.SideInfo{RealName: "test-snap2", Revision: snap.R(42)}
   703  	snapst2 := &snapstate.SnapState{
   704  		Sequence: []*snap.SideInfo{si2},
   705  		Current:  si2.Revision,
   706  		Active:   true,
   707  		SnapType: "app",
   708  	}
   709  	snapstate.Set(s.state, "test-snap2", snapst2)
   710  	snaptest.MockSnapCurrent(c, testYaml2, si2)
   711  
   712  	// create a quota group
   713  	defer s.se.Stop()
   714  	s.createQuota(c, "foo", quantity.SizeGiB, "test-snap")
   715  
   716  	ts, err := servicestate.UpdateQuota(st, "foo", servicestate.QuotaGroupUpdate{AddSnaps: []string{"test-snap2"}})
   717  	c.Assert(err, IsNil)
   718  	chg1 := s.state.NewChange("quota-control", "...")
   719  	chg1.AddAll(ts)
   720  
   721  	_, err = snapstate.Disable(st, "test-snap2")
   722  	c.Assert(err, ErrorMatches, `snap "test-snap2" has "quota-control" change in progress`)
   723  }
   724  
   725  func (s *quotaControlSuite) TestRemoveQuotaSnapOpConflict(c *C) {
   726  	r := s.mockSystemctlCalls(c, join(
   727  		// CreateQuota for foo
   728  		systemctlCallsForCreateQuota("foo", "test-snap"),
   729  	))
   730  	defer r()
   731  
   732  	st := s.state
   733  	st.Lock()
   734  	defer st.Unlock()
   735  
   736  	// setup test-snap
   737  	snapstate.Set(s.state, "test-snap", s.testSnapState)
   738  	snaptest.MockSnapCurrent(c, testYaml, s.testSnapSideInfo)
   739  
   740  	// create a quota group
   741  	defer s.se.Stop()
   742  	s.createQuota(c, "foo", quantity.SizeGiB, "test-snap")
   743  
   744  	ts, err := servicestate.RemoveQuota(st, "foo")
   745  	c.Assert(err, IsNil)
   746  	chg1 := s.state.NewChange("quota-control", "...")
   747  	chg1.AddAll(ts)
   748  
   749  	_, err = snapstate.Disable(st, "test-snap")
   750  	c.Assert(err, ErrorMatches, `snap "test-snap" has "quota-control" change in progress`)
   751  }
   752  
   753  func (s *quotaControlSuite) TestRemoveQuotaLateSnapOpConflict(c *C) {
   754  	r := s.mockSystemctlCalls(c, join(
   755  		// CreateQuota for foo
   756  		systemctlCallsForCreateQuota("foo", "test-snap"),
   757  	))
   758  	defer r()
   759  
   760  	st := s.state
   761  	st.Lock()
   762  	defer st.Unlock()
   763  
   764  	// setup test-snap
   765  	snapstate.Set(s.state, "test-snap", s.testSnapState)
   766  	snaptest.MockSnapCurrent(c, testYaml, s.testSnapSideInfo)
   767  
   768  	// create a quota group
   769  	defer s.se.Stop()
   770  	s.createQuota(c, "foo", quantity.SizeGiB, "test-snap")
   771  
   772  	ts, err := servicestate.RemoveQuota(st, "foo")
   773  	c.Assert(err, IsNil)
   774  	c.Assert(ts.Tasks(), HasLen, 1)
   775  	chg1 := s.state.NewChange("quota-control", "...")
   776  	chg1.AddAll(ts)
   777  
   778  	// the group is already gone, but the task is not finished
   779  	s.state.Set("quotas", nil)
   780  	task := ts.Tasks()[0]
   781  	task.Set("state-updated", servicestate.QuotaStateUpdated{
   782  		BootID: "boot-id",
   783  		AppsToRestartBySnap: map[string][]string{
   784  			"test-snap": {"svc1"},
   785  		},
   786  	})
   787  
   788  	_, err = snapstate.Disable(st, "test-snap")
   789  	c.Assert(err, ErrorMatches, `snap "test-snap" has "quota-control" change in progress`)
   790  }
   791  
   792  func (s *quotaControlSuite) TestUpdateQuotaUpdateQuotaConflict(c *C) {
   793  	r := s.mockSystemctlCalls(c, join(
   794  		// CreateQuota for foo
   795  		systemctlCallsForCreateQuota("foo", "test-snap"),
   796  	))
   797  	defer r()
   798  
   799  	st := s.state
   800  	st.Lock()
   801  	defer st.Unlock()
   802  
   803  	// setup test-snap
   804  	snapstate.Set(s.state, "test-snap", s.testSnapState)
   805  	snaptest.MockSnapCurrent(c, testYaml, s.testSnapSideInfo)
   806  	// and test-snap2
   807  	si2 := &snap.SideInfo{RealName: "test-snap2", Revision: snap.R(42)}
   808  	snapst2 := &snapstate.SnapState{
   809  		Sequence: []*snap.SideInfo{si2},
   810  		Current:  si2.Revision,
   811  		Active:   true,
   812  		SnapType: "app",
   813  	}
   814  	snapstate.Set(s.state, "test-snap2", snapst2)
   815  	snaptest.MockSnapCurrent(c, testYaml2, si2)
   816  
   817  	// create a quota group
   818  	defer s.se.Stop()
   819  	s.createQuota(c, "foo", quantity.SizeGiB, "test-snap")
   820  
   821  	ts, err := servicestate.UpdateQuota(st, "foo", servicestate.QuotaGroupUpdate{AddSnaps: []string{"test-snap2"}})
   822  	c.Assert(err, IsNil)
   823  	chg1 := s.state.NewChange("quota-control", "...")
   824  	chg1.AddAll(ts)
   825  
   826  	_, err = servicestate.UpdateQuota(st, "foo", servicestate.QuotaGroupUpdate{NewMemoryLimit: 2 * quantity.SizeGiB})
   827  	c.Assert(err, ErrorMatches, `quota group "foo" has "quota-control" change in progress`)
   828  }
   829  
   830  func (s *quotaControlSuite) TestUpdateQuotaRemoveQuotaConflict(c *C) {
   831  	r := s.mockSystemctlCalls(c, join(
   832  		// CreateQuota for foo
   833  		systemctlCallsForCreateQuota("foo", "test-snap"),
   834  	))
   835  	defer r()
   836  
   837  	st := s.state
   838  	st.Lock()
   839  	defer st.Unlock()
   840  
   841  	// setup test-snap
   842  	snapstate.Set(s.state, "test-snap", s.testSnapState)
   843  	snaptest.MockSnapCurrent(c, testYaml, s.testSnapSideInfo)
   844  	// and test-snap2
   845  	si2 := &snap.SideInfo{RealName: "test-snap2", Revision: snap.R(42)}
   846  	snapst2 := &snapstate.SnapState{
   847  		Sequence: []*snap.SideInfo{si2},
   848  		Current:  si2.Revision,
   849  		Active:   true,
   850  		SnapType: "app",
   851  	}
   852  	snapstate.Set(s.state, "test-snap2", snapst2)
   853  	snaptest.MockSnapCurrent(c, testYaml2, si2)
   854  
   855  	// create a quota group
   856  	defer s.se.Stop()
   857  	s.createQuota(c, "foo", quantity.SizeGiB, "test-snap")
   858  
   859  	ts, err := servicestate.UpdateQuota(st, "foo", servicestate.QuotaGroupUpdate{AddSnaps: []string{"test-snap2"}})
   860  	c.Assert(err, IsNil)
   861  	chg1 := s.state.NewChange("quota-control", "...")
   862  	chg1.AddAll(ts)
   863  
   864  	_, err = servicestate.RemoveQuota(st, "foo")
   865  	c.Assert(err, ErrorMatches, `quota group "foo" has "quota-control" change in progress`)
   866  }
   867  
   868  func (s *quotaControlSuite) TestRemoveQuotaUpdateQuotaConflict(c *C) {
   869  	r := s.mockSystemctlCalls(c, join(
   870  		// CreateQuota for foo
   871  		systemctlCallsForCreateQuota("foo", "test-snap"),
   872  	))
   873  	defer r()
   874  
   875  	st := s.state
   876  	st.Lock()
   877  	defer st.Unlock()
   878  
   879  	// setup test-snap
   880  	snapstate.Set(s.state, "test-snap", s.testSnapState)
   881  	snaptest.MockSnapCurrent(c, testYaml, s.testSnapSideInfo)
   882  	// and test-snap2
   883  	si2 := &snap.SideInfo{RealName: "test-snap2", Revision: snap.R(42)}
   884  	snapst2 := &snapstate.SnapState{
   885  		Sequence: []*snap.SideInfo{si2},
   886  		Current:  si2.Revision,
   887  		Active:   true,
   888  		SnapType: "app",
   889  	}
   890  	snapstate.Set(s.state, "test-snap2", snapst2)
   891  	snaptest.MockSnapCurrent(c, testYaml2, si2)
   892  
   893  	// create a quota group
   894  	defer s.se.Stop()
   895  	s.createQuota(c, "foo", quantity.SizeGiB, "test-snap")
   896  
   897  	ts, err := servicestate.RemoveQuota(st, "foo")
   898  	c.Assert(err, IsNil)
   899  	chg1 := s.state.NewChange("quota-control", "...")
   900  	chg1.AddAll(ts)
   901  
   902  	_, err = servicestate.UpdateQuota(st, "foo", servicestate.QuotaGroupUpdate{AddSnaps: []string{"test-snap2"}})
   903  	c.Assert(err, ErrorMatches, `quota group "foo" has "quota-control" change in progress`)
   904  }
   905  
   906  func (s *quotaControlSuite) TestCreateQuotaCreateQuotaConflict(c *C) {
   907  	st := s.state
   908  	st.Lock()
   909  	defer st.Unlock()
   910  
   911  	// setup test-snap
   912  	snapstate.Set(s.state, "test-snap", s.testSnapState)
   913  	snaptest.MockSnapCurrent(c, testYaml, s.testSnapSideInfo)
   914  	// and test-snap2
   915  	si2 := &snap.SideInfo{RealName: "test-snap2", Revision: snap.R(42)}
   916  	snapst2 := &snapstate.SnapState{
   917  		Sequence: []*snap.SideInfo{si2},
   918  		Current:  si2.Revision,
   919  		Active:   true,
   920  		SnapType: "app",
   921  	}
   922  	snapstate.Set(s.state, "test-snap2", snapst2)
   923  	snaptest.MockSnapCurrent(c, testYaml2, si2)
   924  
   925  	ts, err := servicestate.CreateQuota(st, "foo", "", []string{"test-snap"}, quantity.SizeGiB)
   926  	c.Assert(err, IsNil)
   927  	chg1 := s.state.NewChange("quota-control", "...")
   928  	chg1.AddAll(ts)
   929  
   930  	_, err = servicestate.CreateQuota(st, "foo", "", []string{"test-snap2"}, 2*quantity.SizeGiB)
   931  	c.Assert(err, ErrorMatches, `quota group "foo" has "quota-control" change in progress`)
   932  }