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