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