/
quota_control.go
381 lines (324 loc) · 11.4 KB
/
quota_control.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
// -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2021 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package servicestate
import (
"fmt"
"github.com/snapcore/snapd/features"
"github.com/snapcore/snapd/gadget/quantity"
"github.com/snapcore/snapd/logger"
"github.com/snapcore/snapd/osutil"
"github.com/snapcore/snapd/overlord/configstate/config"
"github.com/snapcore/snapd/overlord/servicestate/internal"
"github.com/snapcore/snapd/overlord/snapstate"
"github.com/snapcore/snapd/overlord/state"
"github.com/snapcore/snapd/snapdenv"
"github.com/snapcore/snapd/systemd"
)
var (
systemdVersion int
)
// TODO: move to a systemd.AtLeast() ?
func checkSystemdVersion() error {
vers, err := systemd.Version()
if err != nil {
return err
}
systemdVersion = vers
return nil
}
func init() {
if err := checkSystemdVersion(); err != nil {
logger.Noticef("failed to check systemd version: %v", err)
}
}
// MockSystemdVersion mocks the systemd version to the given version. This is
// only available for unit tests and will panic when run in production.
func MockSystemdVersion(vers int) (restore func()) {
osutil.MustBeTestBinary("cannot mock systemd version outside of tests")
old := systemdVersion
systemdVersion = vers
return func() {
systemdVersion = old
}
}
func quotaGroupsAvailable(st *state.State) error {
// check if the systemd version is too old
if systemdVersion < 230 {
return fmt.Errorf("systemd version too old: snap quotas requires systemd 230 and newer (currently have %d)", systemdVersion)
}
tr := config.NewTransaction(st)
enableQuotaGroups, err := features.Flag(tr, features.QuotaGroups)
if err != nil && !config.IsNoOption(err) {
return err
}
if !enableQuotaGroups {
return fmt.Errorf("experimental feature disabled - test it by setting 'experimental.quota-groups' to true")
}
return nil
}
// CreateQuota attempts to create the specified quota group with the specified
// snaps in it.
// TODO: should this use something like QuotaGroupUpdate with fewer fields?
func CreateQuota(st *state.State, name string, parentName string, snaps []string, memoryLimit quantity.Size) (*state.TaskSet, error) {
if err := quotaGroupsAvailable(st); err != nil {
return nil, err
}
allGrps, err := AllQuotas(st)
if err != nil {
return nil, err
}
// make sure the group does not exist yet
if _, ok := allGrps[name]; ok {
return nil, fmt.Errorf("group %q already exists", name)
}
if memoryLimit == 0 {
return nil, fmt.Errorf("cannot create quota group with no memory limit set")
}
// make sure the memory limit is at least 4K, that is the minimum size
// to allow nesting, otherwise groups with less than 4K will trigger the
// oom killer to be invoked when a new group is added as a sub-group to the
// larger group.
if memoryLimit <= 4*quantity.SizeKiB {
return nil, fmt.Errorf("memory limit for group %q is too small: size must be larger than 4KB", name)
}
// make sure the specified snaps exist and aren't currently in another group
if err := validateSnapForAddingToGroup(st, snaps, name, allGrps); err != nil {
return nil, err
}
if err := CheckQuotaChangeConflictMany(st, []string{name}); err != nil {
return nil, err
}
if err := snapstate.CheckChangeConflictMany(st, snaps, ""); err != nil {
return nil, err
}
// create the task with the action in it
qc := QuotaControlAction{
Action: "create",
QuotaName: name,
MemoryLimit: memoryLimit,
AddSnaps: snaps,
ParentName: parentName,
}
ts := state.NewTaskSet()
summary := fmt.Sprintf("Create quota group %q", name)
task := st.NewTask("quota-control", summary)
task.Set("quota-control-actions", []QuotaControlAction{qc})
ts.AddTask(task)
return ts, nil
}
// RemoveQuota deletes the specific quota group. Any snaps currently in the
// quota will no longer be in any quota group, even if the quota group being
// removed is a sub-group.
// TODO: currently this only supports removing leaf sub-group groups, it doesn't
// support removing parent quotas, but probably it makes sense to allow that too
func RemoveQuota(st *state.State, name string) (*state.TaskSet, error) {
if snapdenv.Preseeding() {
return nil, fmt.Errorf("removing quota groups not supported while preseeding")
}
allGrps, err := AllQuotas(st)
if err != nil {
return nil, err
}
// make sure the group exists
grp, ok := allGrps[name]
if !ok {
return nil, fmt.Errorf("cannot remove non-existent quota group %q", name)
}
// XXX: remove this limitation eventually
if len(grp.SubGroups) != 0 {
return nil, fmt.Errorf("cannot remove quota group %q with sub-groups, remove the sub-groups first", name)
}
if err := CheckQuotaChangeConflictMany(st, []string{name}); err != nil {
return nil, err
}
if err := snapstate.CheckChangeConflictMany(st, grp.Snaps, ""); err != nil {
return nil, err
}
qc := QuotaControlAction{
Action: "remove",
QuotaName: name,
}
ts := state.NewTaskSet()
summary := fmt.Sprintf("Remove quota group %q", name)
task := st.NewTask("quota-control", summary)
task.Set("quota-control-actions", []QuotaControlAction{qc})
ts.AddTask(task)
return ts, nil
}
// QuotaGroupUpdate reflects all of the modifications that can be performed on
// a quota group in one operation.
type QuotaGroupUpdate struct {
// AddSnaps is the set of snaps to add to the quota group. These are
// instance names of snaps, and are appended to the existing snaps in
// the quota group
AddSnaps []string
// NewMemoryLimit is the new memory limit to be used for the quota group. If
// zero, then the quota group's memory limit is not changed.
NewMemoryLimit quantity.Size
}
// UpdateQuota updates the quota as per the options.
// TODO: this should support more kinds of updates such as moving groups between
// parents, removing sub-groups from their parents, and removing snaps from
// the group.
func UpdateQuota(st *state.State, name string, updateOpts QuotaGroupUpdate) (*state.TaskSet, error) {
if err := quotaGroupsAvailable(st); err != nil {
return nil, err
}
allGrps, err := AllQuotas(st)
if err != nil {
return nil, err
}
grp, ok := allGrps[name]
if !ok {
return nil, fmt.Errorf("group %q does not exist", name)
}
// check that the memory limit is not being decreased
if updateOpts.NewMemoryLimit != 0 {
// we disallow decreasing the memory limit because it is difficult to do
// so correctly with the current state of our code in
// EnsureSnapServices, see comment in ensureSnapServicesForGroup for
// full details
if updateOpts.NewMemoryLimit < grp.MemoryLimit {
return nil, fmt.Errorf("cannot decrease memory limit of existing quota-group, remove and re-create it to decrease the limit")
}
}
// now ensure that all of the snaps mentioned in AddSnaps exist as snaps and
// that they aren't already in an existing quota group
if err := validateSnapForAddingToGroup(st, updateOpts.AddSnaps, name, allGrps); err != nil {
return nil, err
}
if err := CheckQuotaChangeConflictMany(st, []string{name}); err != nil {
return nil, err
}
if err := snapstate.CheckChangeConflictMany(st, updateOpts.AddSnaps, ""); err != nil {
return nil, err
}
// create the action and the correspoding task set
qc := QuotaControlAction{
Action: "update",
QuotaName: name,
MemoryLimit: updateOpts.NewMemoryLimit,
AddSnaps: updateOpts.AddSnaps,
}
ts := state.NewTaskSet()
summary := fmt.Sprintf("Update quota group %q", name)
task := st.NewTask("quota-control", summary)
task.Set("quota-control-actions", []QuotaControlAction{qc})
ts.AddTask(task)
return ts, nil
}
// EnsureSnapAbsentFromQuota ensures that the specified snap is not present
// in any quota group, usually in preparation for removing that snap from the
// system to keep the quota group itself consistent.
// This function is idempotent, since if it was interrupted after unlocking the
// state inside ensureSnapServicesForGroup it will not re-execute since the
// specified snap will not be present inside the group reference in the state.
func EnsureSnapAbsentFromQuota(st *state.State, snap string) error {
allGrps, err := AllQuotas(st)
if err != nil {
return err
}
// try to find the snap in any group
for _, grp := range allGrps {
for idx, sn := range grp.Snaps {
if sn == snap {
// drop this snap from the list of Snaps by swapping it with the
// last snap in the list, and then dropping the last snap from
// the list
grp.Snaps[idx] = grp.Snaps[len(grp.Snaps)-1]
grp.Snaps = grp.Snaps[:len(grp.Snaps)-1]
// update the quota group state
allGrps, err = internal.PatchQuotas(st, grp)
if err != nil {
return err
}
// ensure service states are updated - note we have to add the
// snap as an extra snap to ensure since it was removed from the
// group and thus won't be considered just by looking at the
// group pointer directly
opts := &ensureSnapServicesForGroupOptions{
allGrps: allGrps,
extraSnaps: []string{snap},
}
// TODO: we could pass timing and progress here from the task we
// are executing as eventually
return ensureSnapServicesStateForGroup(st, grp, opts)
}
}
}
// the snap wasn't in any group, nothing to do
return nil
}
// QuotaChangeConflictError represents an error because of quota group conflicts between changes.
type QuotaChangeConflictError struct {
Quota string
ChangeKind string
// a Message is optional, otherwise one is composed from the other information
Message string
}
func (e *QuotaChangeConflictError) Error() string {
if e.Message != "" {
return e.Message
}
if e.ChangeKind != "" {
return fmt.Sprintf("quota group %q has %q change in progress", e.Quota, e.ChangeKind)
}
return fmt.Sprintf("quota group %q has changes in progress", e.Quota)
}
// CheckQuotaChangeConflictMany ensures that for the given quota groups no other
// changes that alters them (like create, update, remove) are in
// progress. If a conflict is detected an error is returned.
func CheckQuotaChangeConflictMany(st *state.State, quotaNames []string) error {
quotaMap := make(map[string]bool, len(quotaNames))
for _, k := range quotaNames {
quotaMap[k] = true
}
for _, task := range st.Tasks() {
chg := task.Change()
if chg == nil || chg.IsReady() {
continue
}
quotas, err := affectedQuotas(task)
if err != nil {
return err
}
for _, quota := range quotas {
if quotaMap[quota] {
return &QuotaChangeConflictError{Quota: quota, ChangeKind: chg.Kind()}
}
}
}
return nil
}
func affectedQuotas(task *state.Task) ([]string, error) {
// so far only quota-control is relevant
if task.Kind() != "quota-control" {
return nil, nil
}
qcs := []QuotaControlAction{}
if err := task.Get("quota-control-actions", &qcs); err != nil {
return nil, fmt.Errorf("internal error: cannot get quota-control-actions: %v", err)
}
quotas := make([]string, 0, len(qcs))
for _, qc := range qcs {
// TODO: the affected quotas will expand beyond this
// if we support reparenting or orphaning
quotas = append(quotas, qc.QuotaName)
}
return quotas, nil
}