github.com/anonymouse64/snapd@v0.0.0-20210824153203-04c4c42d842d/daemon/api_quotas_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 daemon_test
    21  
    22  import (
    23  	"bytes"
    24  	"encoding/json"
    25  	"fmt"
    26  	"net/http"
    27  	"net/http/httptest"
    28  
    29  	"gopkg.in/check.v1"
    30  
    31  	"github.com/snapcore/snapd/client"
    32  	"github.com/snapcore/snapd/daemon"
    33  	"github.com/snapcore/snapd/gadget/quantity"
    34  	"github.com/snapcore/snapd/overlord/configstate/config"
    35  	"github.com/snapcore/snapd/overlord/servicestate"
    36  	"github.com/snapcore/snapd/overlord/servicestate/servicestatetest"
    37  	"github.com/snapcore/snapd/overlord/snapstate"
    38  	"github.com/snapcore/snapd/overlord/state"
    39  	"github.com/snapcore/snapd/snap/quota"
    40  )
    41  
    42  var _ = check.Suite(&apiQuotaSuite{})
    43  
    44  type apiQuotaSuite struct {
    45  	apiBaseSuite
    46  
    47  	ensureSoonCalled int
    48  }
    49  
    50  func (s *apiQuotaSuite) SetUpTest(c *check.C) {
    51  	s.apiBaseSuite.SetUpTest(c)
    52  	s.daemon(c)
    53  
    54  	st := s.d.Overlord().State()
    55  	st.Lock()
    56  	defer st.Unlock()
    57  	tr := config.NewTransaction(st)
    58  	tr.Set("core", "experimental.quota-groups", true)
    59  	tr.Commit()
    60  
    61  	r := servicestate.MockSystemdVersion(248)
    62  	s.AddCleanup(r)
    63  
    64  	// POST requires root
    65  	s.expectedWriteAccess = daemon.RootAccess{}
    66  
    67  	s.ensureSoonCalled = 0
    68  	_, r = daemon.MockEnsureStateSoon(func(st *state.State) {
    69  		s.ensureSoonCalled++
    70  	})
    71  	s.AddCleanup(r)
    72  }
    73  
    74  func mockQuotas(st *state.State, c *check.C) {
    75  	err := servicestatetest.MockQuotaInState(st, "foo", "", nil, 11000)
    76  	c.Assert(err, check.IsNil)
    77  	err = servicestatetest.MockQuotaInState(st, "bar", "foo", nil, 6000)
    78  	c.Assert(err, check.IsNil)
    79  	err = servicestatetest.MockQuotaInState(st, "baz", "foo", nil, 5000)
    80  	c.Assert(err, check.IsNil)
    81  }
    82  
    83  func (s *apiQuotaSuite) TestPostQuotaUnknownAction(c *check.C) {
    84  	data, err := json.Marshal(daemon.PostQuotaGroupData{Action: "foo", GroupName: "bar"})
    85  	c.Assert(err, check.IsNil)
    86  
    87  	req, err := http.NewRequest("POST", "/v2/quotas", bytes.NewBuffer(data))
    88  	c.Assert(err, check.IsNil)
    89  	rspe := s.errorReq(c, req, nil)
    90  	c.Assert(rspe.Status, check.Equals, 400)
    91  	c.Check(rspe.Message, check.Equals, `unknown quota action "foo"`)
    92  }
    93  
    94  func (s *apiQuotaSuite) TestPostQuotaInvalidGroupName(c *check.C) {
    95  	data, err := json.Marshal(daemon.PostQuotaGroupData{Action: "ensure", GroupName: "$$$"})
    96  	c.Assert(err, check.IsNil)
    97  
    98  	req, err := http.NewRequest("POST", "/v2/quotas", bytes.NewBuffer(data))
    99  	c.Assert(err, check.IsNil)
   100  	rspe := s.errorReq(c, req, nil)
   101  	c.Assert(rspe.Status, check.Equals, 400)
   102  	c.Check(rspe.Message, check.Matches, `invalid quota group name: .*`)
   103  }
   104  
   105  func (s *apiQuotaSuite) TestPostEnsureQuotaUnhappy(c *check.C) {
   106  	r := daemon.MockServicestateCreateQuota(func(st *state.State, name string, parentName string, snaps []string, memoryLimit quantity.Size) (*state.TaskSet, error) {
   107  		c.Check(name, check.Equals, "booze")
   108  		c.Check(parentName, check.Equals, "foo")
   109  		c.Check(snaps, check.DeepEquals, []string{"bar"})
   110  		c.Check(memoryLimit, check.DeepEquals, quantity.Size(1000))
   111  		return nil, fmt.Errorf("boom")
   112  	})
   113  	defer r()
   114  
   115  	data, err := json.Marshal(daemon.PostQuotaGroupData{
   116  		Action:      "ensure",
   117  		GroupName:   "booze",
   118  		Parent:      "foo",
   119  		Snaps:       []string{"bar"},
   120  		Constraints: client.QuotaValues{Memory: quantity.Size(1000)},
   121  	})
   122  	c.Assert(err, check.IsNil)
   123  
   124  	req, err := http.NewRequest("POST", "/v2/quotas", bytes.NewBuffer(data))
   125  	c.Assert(err, check.IsNil)
   126  	rspe := s.errorReq(c, req, nil)
   127  	c.Check(rspe.Status, check.Equals, 400)
   128  	c.Check(rspe.Message, check.Matches, `cannot create quota group: boom`)
   129  	c.Assert(s.ensureSoonCalled, check.Equals, 0)
   130  }
   131  
   132  func (s *apiQuotaSuite) TestPostEnsureQuotaCreateHappy(c *check.C) {
   133  	var createCalled int
   134  	r := daemon.MockServicestateCreateQuota(func(st *state.State, name string, parentName string, snaps []string, memoryLimit quantity.Size) (*state.TaskSet, error) {
   135  		createCalled++
   136  		c.Check(name, check.Equals, "booze")
   137  		c.Check(parentName, check.Equals, "foo")
   138  		c.Check(snaps, check.DeepEquals, []string{"some-snap"})
   139  		c.Check(memoryLimit, check.DeepEquals, quantity.Size(1000))
   140  		ts := state.NewTaskSet(st.NewTask("foo-quota", "..."))
   141  		return ts, nil
   142  	})
   143  	defer r()
   144  
   145  	data, err := json.Marshal(daemon.PostQuotaGroupData{
   146  		Action:      "ensure",
   147  		GroupName:   "booze",
   148  		Parent:      "foo",
   149  		Snaps:       []string{"some-snap"},
   150  		Constraints: client.QuotaValues{Memory: quantity.Size(1000)},
   151  	})
   152  	c.Assert(err, check.IsNil)
   153  
   154  	req, err := http.NewRequest("POST", "/v2/quotas", bytes.NewBuffer(data))
   155  	c.Assert(err, check.IsNil)
   156  	rsp := s.asyncReq(c, req, nil)
   157  	c.Assert(rsp.Status, check.Equals, 202)
   158  	c.Assert(createCalled, check.Equals, 1)
   159  	c.Assert(s.ensureSoonCalled, check.Equals, 1)
   160  }
   161  
   162  func (s *apiQuotaSuite) TestPostEnsureQuotaCreateQuotaConflicts(c *check.C) {
   163  	var createCalled int
   164  	r := daemon.MockServicestateCreateQuota(func(st *state.State, name string, parentName string, snaps []string, memoryLimit quantity.Size) (*state.TaskSet, error) {
   165  		c.Check(name, check.Equals, "booze")
   166  		c.Check(parentName, check.Equals, "foo")
   167  		c.Check(snaps, check.DeepEquals, []string{"some-snap"})
   168  		c.Check(memoryLimit, check.DeepEquals, quantity.Size(1000))
   169  
   170  		createCalled++
   171  		switch createCalled {
   172  		case 1:
   173  			// return a quota conflict as if we were trying to create this quota in
   174  			// another task
   175  			return nil, &servicestate.QuotaChangeConflictError{Quota: "booze", ChangeKind: "quota-control"}
   176  		case 2:
   177  			// return a snap conflict as if we were trying to disable the
   178  			// some-snap in the quota group to be created
   179  			return nil, &snapstate.ChangeConflictError{Snap: "some-snap", ChangeKind: "disable"}
   180  		default:
   181  			c.Errorf("test broken")
   182  			return nil, fmt.Errorf("test broken")
   183  		}
   184  	})
   185  	defer r()
   186  
   187  	data, err := json.Marshal(daemon.PostQuotaGroupData{
   188  		Action:      "ensure",
   189  		GroupName:   "booze",
   190  		Parent:      "foo",
   191  		Snaps:       []string{"some-snap"},
   192  		Constraints: client.QuotaValues{Memory: 1000},
   193  	})
   194  	c.Assert(err, check.IsNil)
   195  
   196  	req, err := http.NewRequest("POST", "/v2/quotas", bytes.NewBuffer(data))
   197  	c.Assert(err, check.IsNil)
   198  	rspe := s.errorReq(c, req, nil)
   199  	c.Assert(rspe.Status, check.Equals, 409)
   200  	c.Check(rspe.Message, check.Equals, `quota group "booze" has "quota-control" change in progress`)
   201  	c.Check(rspe.Value, check.DeepEquals, map[string]interface{}{
   202  		"change-kind": "quota-control",
   203  		"quota-name":  "booze",
   204  	})
   205  
   206  	req, err = http.NewRequest("POST", "/v2/quotas", bytes.NewBuffer(data))
   207  	c.Assert(err, check.IsNil)
   208  
   209  	rspe = s.errorReq(c, req, nil)
   210  	c.Assert(rspe.Status, check.Equals, 409)
   211  	c.Check(rspe.Message, check.Equals, `snap "some-snap" has "disable" change in progress`)
   212  	c.Check(rspe.Value, check.DeepEquals, map[string]interface{}{
   213  		"change-kind": "disable",
   214  		"snap-name":   "some-snap",
   215  	})
   216  
   217  	c.Assert(createCalled, check.Equals, 2)
   218  }
   219  
   220  func (s *apiQuotaSuite) TestPostEnsureQuotaUpdateHappy(c *check.C) {
   221  	st := s.d.Overlord().State()
   222  	st.Lock()
   223  	err := servicestatetest.MockQuotaInState(st, "ginger-ale", "", nil, 5000)
   224  	st.Unlock()
   225  	c.Assert(err, check.IsNil)
   226  
   227  	r := daemon.MockServicestateCreateQuota(func(st *state.State, name string, parentName string, snaps []string, memoryLimit quantity.Size) (*state.TaskSet, error) {
   228  		c.Errorf("should not have called create quota")
   229  		return nil, fmt.Errorf("broken test")
   230  	})
   231  	defer r()
   232  
   233  	updateCalled := 0
   234  	r = daemon.MockServicestateUpdateQuota(func(st *state.State, name string, opts servicestate.QuotaGroupUpdate) (*state.TaskSet, error) {
   235  		updateCalled++
   236  		c.Assert(name, check.Equals, "ginger-ale")
   237  		c.Assert(opts, check.DeepEquals, servicestate.QuotaGroupUpdate{
   238  			AddSnaps:       []string{"some-snap"},
   239  			NewMemoryLimit: 9000,
   240  		})
   241  		ts := state.NewTaskSet(st.NewTask("foo-quota", "..."))
   242  		return ts, nil
   243  	})
   244  	defer r()
   245  
   246  	data, err := json.Marshal(daemon.PostQuotaGroupData{
   247  		Action:      "ensure",
   248  		GroupName:   "ginger-ale",
   249  		Snaps:       []string{"some-snap"},
   250  		Constraints: client.QuotaValues{Memory: quantity.Size(9000)},
   251  	})
   252  	c.Assert(err, check.IsNil)
   253  
   254  	req, err := http.NewRequest("POST", "/v2/quotas", bytes.NewBuffer(data))
   255  	c.Assert(err, check.IsNil)
   256  	rsp := s.asyncReq(c, req, nil)
   257  	c.Assert(rsp.Status, check.Equals, 202)
   258  	c.Assert(updateCalled, check.Equals, 1)
   259  	c.Assert(s.ensureSoonCalled, check.Equals, 1)
   260  }
   261  
   262  func (s *apiQuotaSuite) TestPostEnsureQuotaUpdateConflicts(c *check.C) {
   263  	st := s.d.Overlord().State()
   264  	st.Lock()
   265  	err := servicestatetest.MockQuotaInState(st, "ginger-ale", "", nil, 5000)
   266  	st.Unlock()
   267  	c.Assert(err, check.IsNil)
   268  
   269  	r := daemon.MockServicestateCreateQuota(func(st *state.State, name string, parentName string, snaps []string, memoryLimit quantity.Size) (*state.TaskSet, error) {
   270  		c.Errorf("should not have called create quota")
   271  		return nil, fmt.Errorf("broken test")
   272  	})
   273  	defer r()
   274  
   275  	updateCalled := 0
   276  	r = daemon.MockServicestateUpdateQuota(func(st *state.State, name string, opts servicestate.QuotaGroupUpdate) (*state.TaskSet, error) {
   277  		updateCalled++
   278  		c.Assert(name, check.Equals, "ginger-ale")
   279  		c.Assert(opts, check.DeepEquals, servicestate.QuotaGroupUpdate{
   280  			AddSnaps:       []string{"some-snap"},
   281  			NewMemoryLimit: 9000,
   282  		})
   283  		switch updateCalled {
   284  		case 1:
   285  			// return a quota conflict as if we were trying to update this quota
   286  			// in another task
   287  			return nil, &servicestate.QuotaChangeConflictError{Quota: "ginger-ale", ChangeKind: "quota-control"}
   288  		case 2:
   289  			// return a snap conflict as if we were trying to disable the
   290  			// some-snap in the quota group to be added to the group
   291  			return nil, &snapstate.ChangeConflictError{Snap: "some-snap", ChangeKind: "disable"}
   292  		default:
   293  			c.Errorf("test broken")
   294  			return nil, fmt.Errorf("test broken")
   295  		}
   296  	})
   297  	defer r()
   298  
   299  	data, err := json.Marshal(daemon.PostQuotaGroupData{
   300  		Action:      "ensure",
   301  		GroupName:   "ginger-ale",
   302  		Snaps:       []string{"some-snap"},
   303  		Constraints: client.QuotaValues{Memory: 9000},
   304  	})
   305  	c.Assert(err, check.IsNil)
   306  
   307  	req, err := http.NewRequest("POST", "/v2/quotas", bytes.NewBuffer(data))
   308  	c.Assert(err, check.IsNil)
   309  	rspe := s.errorReq(c, req, nil)
   310  	c.Assert(rspe.Status, check.Equals, 409)
   311  	c.Check(rspe.Message, check.Equals, `quota group "ginger-ale" has "quota-control" change in progress`)
   312  	c.Check(rspe.Value, check.DeepEquals, map[string]interface{}{
   313  		"change-kind": "quota-control",
   314  		"quota-name":  "ginger-ale",
   315  	})
   316  
   317  	req, err = http.NewRequest("POST", "/v2/quotas", bytes.NewBuffer(data))
   318  	c.Assert(err, check.IsNil)
   319  
   320  	rspe = s.errorReq(c, req, nil)
   321  	c.Assert(rspe.Status, check.Equals, 409)
   322  	c.Check(rspe.Message, check.Equals, `snap "some-snap" has "disable" change in progress`)
   323  	c.Check(rspe.Value, check.DeepEquals, map[string]interface{}{
   324  		"change-kind": "disable",
   325  		"snap-name":   "some-snap",
   326  	})
   327  
   328  	c.Assert(updateCalled, check.Equals, 2)
   329  }
   330  
   331  func (s *apiQuotaSuite) TestPostRemoveQuotaHappy(c *check.C) {
   332  	var removeCalled int
   333  	r := daemon.MockServicestateRemoveQuota(func(st *state.State, name string) (*state.TaskSet, error) {
   334  		removeCalled++
   335  		c.Check(name, check.Equals, "booze")
   336  		ts := state.NewTaskSet(st.NewTask("foo-quota", "..."))
   337  		return ts, nil
   338  	})
   339  	defer r()
   340  
   341  	data, err := json.Marshal(daemon.PostQuotaGroupData{
   342  		Action:    "remove",
   343  		GroupName: "booze",
   344  	})
   345  	c.Assert(err, check.IsNil)
   346  
   347  	req, err := http.NewRequest("POST", "/v2/quotas", bytes.NewBuffer(data))
   348  	c.Assert(err, check.IsNil)
   349  	s.asRootAuth(req)
   350  
   351  	rec := httptest.NewRecorder()
   352  	s.serveHTTP(c, rec, req)
   353  	c.Assert(rec.Code, check.Equals, 202)
   354  	c.Assert(removeCalled, check.Equals, 1)
   355  	c.Assert(s.ensureSoonCalled, check.Equals, 1)
   356  }
   357  
   358  func (s *apiQuotaSuite) TestPostRemoveQuotaConflict(c *check.C) {
   359  	st := s.d.Overlord().State()
   360  	st.Lock()
   361  	err := servicestatetest.MockQuotaInState(st, "ginger-ale", "", []string{"some-snap"}, 5000)
   362  	st.Unlock()
   363  	c.Assert(err, check.IsNil)
   364  
   365  	var removeCalled int
   366  	r := daemon.MockServicestateRemoveQuota(func(st *state.State, name string) (*state.TaskSet, error) {
   367  		removeCalled++
   368  		c.Check(name, check.Equals, "booze")
   369  		switch removeCalled {
   370  		case 1:
   371  			// return a quota conflict as if we were trying to update this quota
   372  			// in another task
   373  			return nil, &servicestate.QuotaChangeConflictError{Quota: "booze", ChangeKind: "quota-control"}
   374  		case 2:
   375  			// return a snap conflict as if we were trying to disable the
   376  			// some-snap in the quota group to be added to the group
   377  			return nil, &snapstate.ChangeConflictError{Snap: "some-snap", ChangeKind: "disable"}
   378  		default:
   379  			c.Errorf("test broken")
   380  			return nil, fmt.Errorf("test broken")
   381  		}
   382  	})
   383  	defer r()
   384  
   385  	data, err := json.Marshal(daemon.PostQuotaGroupData{
   386  		Action:    "remove",
   387  		GroupName: "booze",
   388  	})
   389  	c.Assert(err, check.IsNil)
   390  
   391  	req, err := http.NewRequest("POST", "/v2/quotas", bytes.NewBuffer(data))
   392  	c.Assert(err, check.IsNil)
   393  	rspe := s.errorReq(c, req, nil)
   394  	c.Assert(rspe.Status, check.Equals, 409)
   395  	c.Check(rspe.Message, check.Equals, `quota group "booze" has "quota-control" change in progress`)
   396  	c.Check(rspe.Value, check.DeepEquals, map[string]interface{}{
   397  		"change-kind": "quota-control",
   398  		"quota-name":  "booze",
   399  	})
   400  
   401  	req, err = http.NewRequest("POST", "/v2/quotas", bytes.NewBuffer(data))
   402  	c.Assert(err, check.IsNil)
   403  
   404  	rspe = s.errorReq(c, req, nil)
   405  	c.Assert(rspe.Status, check.Equals, 409)
   406  	c.Check(rspe.Message, check.Equals, `snap "some-snap" has "disable" change in progress`)
   407  	c.Check(rspe.Value, check.DeepEquals, map[string]interface{}{
   408  		"change-kind": "disable",
   409  		"snap-name":   "some-snap",
   410  	})
   411  
   412  	c.Assert(removeCalled, check.Equals, 2)
   413  }
   414  
   415  func (s *apiQuotaSuite) TestPostRemoveQuotaUnhappy(c *check.C) {
   416  	r := daemon.MockServicestateRemoveQuota(func(st *state.State, name string) (*state.TaskSet, error) {
   417  		c.Check(name, check.Equals, "booze")
   418  		return nil, fmt.Errorf("boom")
   419  	})
   420  	defer r()
   421  
   422  	data, err := json.Marshal(daemon.PostQuotaGroupData{
   423  		Action:    "remove",
   424  		GroupName: "booze",
   425  	})
   426  	c.Assert(err, check.IsNil)
   427  
   428  	req, err := http.NewRequest("POST", "/v2/quotas", bytes.NewBuffer(data))
   429  	c.Assert(err, check.IsNil)
   430  	rspe := s.errorReq(c, req, nil)
   431  	c.Check(rspe.Status, check.Equals, 400)
   432  	c.Check(rspe.Message, check.Matches, `cannot remove quota group: boom`)
   433  	c.Check(s.ensureSoonCalled, check.Equals, 0)
   434  }
   435  
   436  func (s *apiQuotaSuite) TestPostQuotaRequiresRoot(c *check.C) {
   437  	r := daemon.MockServicestateRemoveQuota(func(st *state.State, name string) (*state.TaskSet, error) {
   438  		c.Fatalf("remove quota should not get called")
   439  		return nil, fmt.Errorf("broken test")
   440  	})
   441  	defer r()
   442  
   443  	data, err := json.Marshal(daemon.PostQuotaGroupData{
   444  		Action:    "remove",
   445  		GroupName: "booze",
   446  	})
   447  	c.Assert(err, check.IsNil)
   448  
   449  	req, err := http.NewRequest("POST", "/v2/quotas", bytes.NewBuffer(data))
   450  	c.Assert(err, check.IsNil)
   451  	s.asUserAuth(c, req)
   452  
   453  	rec := httptest.NewRecorder()
   454  	s.serveHTTP(c, rec, req)
   455  	c.Check(rec.Code, check.Equals, 403)
   456  	c.Check(s.ensureSoonCalled, check.Equals, 0)
   457  }
   458  
   459  func (s *apiQuotaSuite) TestListQuotas(c *check.C) {
   460  	st := s.d.Overlord().State()
   461  	st.Lock()
   462  	mockQuotas(st, c)
   463  	st.Unlock()
   464  
   465  	calls := 0
   466  	r := daemon.MockGetQuotaMemUsage(func(grp *quota.Group) (quantity.Size, error) {
   467  		calls++
   468  		switch grp.Name {
   469  		case "bar":
   470  			return quantity.Size(500), nil
   471  		case "baz":
   472  			return quantity.Size(1000), nil
   473  		case "foo":
   474  			return quantity.Size(5000), nil
   475  		default:
   476  			c.Errorf("unexpected call to get group memory usage for group %q", grp.Name)
   477  			return 0, fmt.Errorf("broken test")
   478  		}
   479  	})
   480  	defer r()
   481  	defer func() {
   482  		c.Assert(calls, check.Equals, 3)
   483  	}()
   484  
   485  	req, err := http.NewRequest("GET", "/v2/quotas", nil)
   486  	c.Assert(err, check.IsNil)
   487  	rsp := s.syncReq(c, req, nil)
   488  	c.Assert(rsp.Status, check.Equals, 200)
   489  	c.Assert(rsp.Result, check.FitsTypeOf, []client.QuotaGroupResult{})
   490  	res := rsp.Result.([]client.QuotaGroupResult)
   491  	c.Check(res, check.DeepEquals, []client.QuotaGroupResult{
   492  		{
   493  			GroupName:   "bar",
   494  			Parent:      "foo",
   495  			Constraints: &client.QuotaValues{Memory: quantity.Size(6000)},
   496  			Current:     &client.QuotaValues{Memory: quantity.Size(500)},
   497  		},
   498  		{
   499  			GroupName:   "baz",
   500  			Parent:      "foo",
   501  			Constraints: &client.QuotaValues{Memory: quantity.Size(5000)},
   502  			Current:     &client.QuotaValues{Memory: quantity.Size(1000)},
   503  		},
   504  		{
   505  			GroupName:   "foo",
   506  			Subgroups:   []string{"bar", "baz"},
   507  			Constraints: &client.QuotaValues{Memory: quantity.Size(11000)},
   508  			Current:     &client.QuotaValues{Memory: quantity.Size(5000)},
   509  		},
   510  	})
   511  	c.Check(s.ensureSoonCalled, check.Equals, 0)
   512  }
   513  
   514  func (s *apiQuotaSuite) TestGetQuota(c *check.C) {
   515  	st := s.d.Overlord().State()
   516  	st.Lock()
   517  	mockQuotas(st, c)
   518  	st.Unlock()
   519  
   520  	calls := 0
   521  	r := daemon.MockGetQuotaMemUsage(func(grp *quota.Group) (quantity.Size, error) {
   522  		calls++
   523  		c.Assert(grp.Name, check.Equals, "bar")
   524  		return quantity.Size(500), nil
   525  	})
   526  	defer r()
   527  	defer func() {
   528  		c.Assert(calls, check.Equals, 1)
   529  	}()
   530  
   531  	req, err := http.NewRequest("GET", "/v2/quotas/bar", nil)
   532  	c.Assert(err, check.IsNil)
   533  	rsp := s.syncReq(c, req, nil)
   534  	c.Assert(rsp.Status, check.Equals, 200)
   535  	c.Assert(rsp.Result, check.FitsTypeOf, client.QuotaGroupResult{})
   536  	res := rsp.Result.(client.QuotaGroupResult)
   537  	c.Check(res, check.DeepEquals, client.QuotaGroupResult{
   538  		GroupName:   "bar",
   539  		Parent:      "foo",
   540  		Constraints: &client.QuotaValues{Memory: quantity.Size(6000)},
   541  		Current:     &client.QuotaValues{Memory: quantity.Size(500)},
   542  	})
   543  
   544  	c.Check(s.ensureSoonCalled, check.Equals, 0)
   545  }
   546  
   547  func (s *apiQuotaSuite) TestGetQuotaInvalidName(c *check.C) {
   548  	st := s.d.Overlord().State()
   549  	st.Lock()
   550  	mockQuotas(st, c)
   551  	st.Unlock()
   552  
   553  	req, err := http.NewRequest("GET", "/v2/quotas/000", nil)
   554  	c.Assert(err, check.IsNil)
   555  	rspe := s.errorReq(c, req, nil)
   556  	c.Check(rspe.Status, check.Equals, 400)
   557  	c.Check(rspe.Message, check.Matches, `invalid quota group name: .*`)
   558  	c.Check(s.ensureSoonCalled, check.Equals, 0)
   559  }
   560  
   561  func (s *apiQuotaSuite) TestGetQuotaNotFound(c *check.C) {
   562  	req, err := http.NewRequest("GET", "/v2/quotas/unknown", nil)
   563  	c.Assert(err, check.IsNil)
   564  	rspe := s.errorReq(c, req, nil)
   565  	c.Check(rspe.Status, check.Equals, 404)
   566  	c.Check(rspe.Message, check.Matches, `cannot find quota group "unknown"`)
   567  	c.Check(s.ensureSoonCalled, check.Equals, 0)
   568  }