github.com/freetocompute/snapd@v0.0.0-20210618182524-2fb355d72fd9/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 type quotaGroupState struct { 66 MemoryLimit quantity.Size 67 SubGroups []string 68 ParentGroup string 69 Snaps []string 70 } 71 72 func checkQuotaState(c *C, st *state.State, exp map[string]quotaGroupState) { 73 m, err := servicestate.AllQuotas(st) 74 c.Assert(err, IsNil) 75 c.Assert(m, HasLen, len(exp)) 76 for name, grp := range m { 77 expGrp, ok := exp[name] 78 c.Assert(ok, Equals, true, Commentf("unexpected group %q in state", name)) 79 c.Assert(grp.MemoryLimit, Equals, expGrp.MemoryLimit) 80 c.Assert(grp.ParentGroup, Equals, expGrp.ParentGroup) 81 82 c.Assert(grp.Snaps, HasLen, len(expGrp.Snaps)) 83 if len(expGrp.Snaps) != 0 { 84 c.Assert(grp.Snaps, DeepEquals, expGrp.Snaps) 85 86 // also check on the service file states 87 for _, sn := range expGrp.Snaps { 88 // meh assume all services are named svc1 89 slicePath := name 90 if grp.ParentGroup != "" { 91 slicePath = grp.ParentGroup + "/" + name 92 } 93 checkSvcAndSliceState(c, sn+".svc1", slicePath, grp.MemoryLimit) 94 } 95 } 96 97 c.Assert(grp.SubGroups, HasLen, len(expGrp.SubGroups)) 98 if len(expGrp.SubGroups) != 0 { 99 c.Assert(grp.SubGroups, DeepEquals, expGrp.SubGroups) 100 } 101 } 102 } 103 104 func checkSvcAndSliceState(c *C, snapSvc string, slicePath string, sliceMem quantity.Size) { 105 slicePath = systemd.EscapeUnitNamePath(slicePath) 106 // make sure the service file exists 107 svcFileName := filepath.Join(dirs.SnapServicesDir, "snap."+snapSvc+".service") 108 c.Assert(svcFileName, testutil.FilePresent) 109 110 if sliceMem != 0 { 111 // the service file should mention this slice 112 c.Assert(svcFileName, testutil.FileContains, fmt.Sprintf("\nSlice=snap.%s.slice\n", slicePath)) 113 } else { 114 c.Assert(svcFileName, Not(testutil.FileContains), fmt.Sprintf("Slice=snap.%s.slice", slicePath)) 115 } 116 checkSliceState(c, slicePath, sliceMem) 117 } 118 119 func checkSliceState(c *C, sliceName string, sliceMem quantity.Size) { 120 sliceFileName := filepath.Join(dirs.SnapServicesDir, "snap."+sliceName+".slice") 121 if sliceMem != 0 { 122 c.Assert(sliceFileName, testutil.FilePresent) 123 c.Assert(sliceFileName, testutil.FileContains, fmt.Sprintf("\nMemoryMax=%s\n", sliceMem.String())) 124 } else { 125 c.Assert(sliceFileName, testutil.FileAbsent) 126 } 127 } 128 129 func systemctlCallsForSliceStart(name string) []expectedSystemctl { 130 name = systemd.EscapeUnitNamePath(name) 131 slice := "snap." + name + ".slice" 132 return []expectedSystemctl{ 133 {expArgs: []string{"start", slice}}, 134 } 135 } 136 137 func systemctlCallsForSliceStop(name string) []expectedSystemctl { 138 name = systemd.EscapeUnitNamePath(name) 139 slice := "snap." + name + ".slice" 140 return []expectedSystemctl{ 141 {expArgs: []string{"stop", slice}}, 142 { 143 expArgs: []string{"show", "--property=ActiveState", slice}, 144 output: "ActiveState=inactive", 145 }, 146 } 147 } 148 149 func systemctlCallsForServiceRestart(name string) []expectedSystemctl { 150 svc := "snap." + name + ".svc1.service" 151 return []expectedSystemctl{ 152 { 153 expArgs: []string{"show", "--property=Id,ActiveState,UnitFileState,Type", svc}, 154 output: fmt.Sprintf("Id=%s\nActiveState=active\nUnitFileState=enabled\nType=simple\n", svc), 155 }, 156 {expArgs: []string{"stop", svc}}, 157 { 158 expArgs: []string{"show", "--property=ActiveState", svc}, 159 output: "ActiveState=inactive", 160 }, 161 {expArgs: []string{"start", svc}}, 162 } 163 } 164 165 func systemctlCallsForCreateQuota(groupName string, snapNames ...string) []expectedSystemctl { 166 calls := join( 167 []expectedSystemctl{{expArgs: []string{"daemon-reload"}}}, 168 systemctlCallsForSliceStart(groupName), 169 ) 170 for _, snapName := range snapNames { 171 calls = join(calls, systemctlCallsForServiceRestart(snapName)) 172 } 173 174 return calls 175 } 176 177 func systemctlCallsVersion(version int) []expectedSystemctl { 178 return []expectedSystemctl{ 179 { 180 expArgs: []string{"--version"}, 181 output: fmt.Sprintf("systemd %d\n+FOO +BAR\n", version), 182 }, 183 } 184 } 185 186 func join(calls ...[]expectedSystemctl) []expectedSystemctl { 187 fullCall := []expectedSystemctl{} 188 for _, call := range calls { 189 fullCall = append(fullCall, call...) 190 } 191 192 return fullCall 193 } 194 195 func (s *quotaControlSuite) TestCreateQuotaNotEnabled(c *C) { 196 s.state.Lock() 197 defer s.state.Unlock() 198 tr := config.NewTransaction(s.state) 199 tr.Set("core", "experimental.quota-groups", false) 200 tr.Commit() 201 202 // try to create an empty quota group 203 err := servicestate.CreateQuota(s.state, "foo", "", nil, quantity.SizeGiB) 204 c.Assert(err, ErrorMatches, `experimental feature disabled - test it by setting 'experimental.quota-groups' to true`) 205 } 206 207 func (s *quotaControlSuite) TestCreateQuotaSystemdTooOld(c *C) { 208 s.state.Lock() 209 defer s.state.Unlock() 210 211 r := s.mockSystemctlCalls(c, systemctlCallsVersion(204)) 212 defer r() 213 214 err := servicestate.CheckSystemdVersion() 215 c.Assert(err, IsNil) 216 217 err = servicestate.CreateQuota(s.state, "foo", "", nil, quantity.SizeGiB) 218 c.Assert(err, ErrorMatches, `systemd version too old: snap quotas requires systemd 205 and newer \(currently have 204\)`) 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) TestCreateUpdateRemoveQuotaHappy(c *C) { 251 r := s.mockSystemctlCalls(c, join( 252 // CreateQuota for foo - success 253 systemctlCallsForCreateQuota("foo", "test-snap"), 254 255 // UpdateQuota for foo 256 []expectedSystemctl{{expArgs: []string{"daemon-reload"}}}, 257 258 // RemoveQuota for foo 259 []expectedSystemctl{{expArgs: []string{"daemon-reload"}}}, 260 systemctlCallsForSliceStop("foo"), 261 []expectedSystemctl{{expArgs: []string{"daemon-reload"}}}, 262 systemctlCallsForServiceRestart("test-snap"), 263 )) 264 defer r() 265 266 st := s.state 267 st.Lock() 268 defer st.Unlock() 269 270 // setup the snap so it exists 271 snapstate.Set(s.state, "test-snap", s.testSnapState) 272 snaptest.MockSnapCurrent(c, testYaml, s.testSnapSideInfo) 273 274 // create the quota group 275 err := servicestate.CreateQuota(st, "foo", "", []string{"test-snap"}, quantity.SizeGiB) 276 c.Assert(err, IsNil) 277 278 // check that the quota groups were created in the state 279 checkQuotaState(c, st, map[string]quotaGroupState{ 280 "foo": { 281 MemoryLimit: quantity.SizeGiB, 282 Snaps: []string{"test-snap"}, 283 }, 284 }) 285 286 // increase the memory limit 287 err = servicestate.UpdateQuota(st, "foo", servicestate.QuotaGroupUpdate{NewMemoryLimit: 2 * quantity.SizeGiB}) 288 c.Assert(err, IsNil) 289 290 checkQuotaState(c, st, map[string]quotaGroupState{ 291 "foo": { 292 MemoryLimit: 2 * quantity.SizeGiB, 293 Snaps: []string{"test-snap"}, 294 }, 295 }) 296 297 // remove the quota 298 err = servicestate.RemoveQuota(st, "foo") 299 c.Assert(err, IsNil) 300 checkQuotaState(c, st, nil) 301 } 302 303 func (s *quotaControlSuite) TestEnsureSnapAbsentFromQuotaGroup(c *C) { 304 r := s.mockSystemctlCalls(c, join( 305 // CreateQuota for foo 306 systemctlCallsForCreateQuota("foo", "test-snap", "test-snap2"), 307 308 // EnsureSnapAbsentFromQuota with just test-snap restarted since it is 309 // no longer in the group 310 []expectedSystemctl{{expArgs: []string{"daemon-reload"}}}, 311 systemctlCallsForServiceRestart("test-snap"), 312 313 // another identical call to EnsureSnapAbsentFromQuota does nothing 314 // since the function is idempotent 315 316 // EnsureSnapAbsentFromQuota with just test-snap2 restarted since it is no 317 // longer in the group 318 []expectedSystemctl{{expArgs: []string{"daemon-reload"}}}, 319 systemctlCallsForServiceRestart("test-snap2"), 320 )) 321 defer r() 322 323 st := s.state 324 st.Lock() 325 defer st.Unlock() 326 // setup test-snap 327 snapstate.Set(s.state, "test-snap", s.testSnapState) 328 snaptest.MockSnapCurrent(c, testYaml, s.testSnapSideInfo) 329 // and test-snap2 330 si2 := &snap.SideInfo{RealName: "test-snap2", Revision: snap.R(42)} 331 snapst2 := &snapstate.SnapState{ 332 Sequence: []*snap.SideInfo{si2}, 333 Current: si2.Revision, 334 Active: true, 335 SnapType: "app", 336 } 337 snapstate.Set(s.state, "test-snap2", snapst2) 338 snaptest.MockSnapCurrent(c, testYaml2, si2) 339 340 // create a quota group 341 err := servicestate.CreateQuota(s.state, "foo", "", []string{"test-snap", "test-snap2"}, quantity.SizeGiB) 342 c.Assert(err, IsNil) 343 344 checkQuotaState(c, st, map[string]quotaGroupState{ 345 "foo": { 346 MemoryLimit: quantity.SizeGiB, 347 Snaps: []string{"test-snap", "test-snap2"}, 348 }, 349 }) 350 351 // remove test-snap from the group 352 err = servicestate.EnsureSnapAbsentFromQuota(s.state, "test-snap") 353 c.Assert(err, IsNil) 354 355 checkQuotaState(c, st, map[string]quotaGroupState{ 356 "foo": { 357 MemoryLimit: quantity.SizeGiB, 358 Snaps: []string{"test-snap2"}, 359 }, 360 }) 361 362 // removing the same snap twice works as well but does nothing 363 err = servicestate.EnsureSnapAbsentFromQuota(s.state, "test-snap") 364 c.Assert(err, IsNil) 365 366 // now remove test-snap2 too 367 err = servicestate.EnsureSnapAbsentFromQuota(s.state, "test-snap2") 368 c.Assert(err, IsNil) 369 370 // and check that it got updated in the state 371 checkQuotaState(c, st, map[string]quotaGroupState{ 372 "foo": { 373 MemoryLimit: quantity.SizeGiB, 374 }, 375 }) 376 377 // it's not an error to call EnsureSnapAbsentFromQuotaGroup on a snap that 378 // is not in any quota group 379 err = servicestate.EnsureSnapAbsentFromQuota(s.state, "test-snap33333") 380 c.Assert(err, IsNil) 381 }