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