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