github.com/stulluk/snapd@v0.0.0-20210611110309-f6d5d5bd24b0/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/state" 31 "github.com/snapcore/snapd/snap/quota" 32 "github.com/snapcore/snapd/snapdenv" 33 "github.com/snapcore/snapd/systemd" 34 ) 35 36 var ( 37 systemdVersion int 38 ) 39 40 // TODO: move to a systemd.AtLeast() ? 41 func checkSystemdVersion() error { 42 vers, err := systemd.Version() 43 if err != nil { 44 return err 45 } 46 systemdVersion = vers 47 return nil 48 } 49 50 func init() { 51 if err := checkSystemdVersion(); err != nil { 52 logger.Noticef("failed to check systemd version: %v", err) 53 } 54 } 55 56 // MockSystemdVersion mocks the systemd version to the given version. This is 57 // only available for unit tests and will panic when run in production. 58 func MockSystemdVersion(vers int) (restore func()) { 59 osutil.MustBeTestBinary("cannot mock systemd version outside of tests") 60 old := systemdVersion 61 systemdVersion = vers 62 return func() { 63 systemdVersion = old 64 } 65 } 66 67 func quotaGroupsAvailable(st *state.State) error { 68 // check if the systemd version is too old 69 if systemdVersion < 205 { 70 return fmt.Errorf("systemd version too old: snap quotas requires systemd 205 and newer (currently have %d)", systemdVersion) 71 } 72 73 tr := config.NewTransaction(st) 74 enableQuotaGroups, err := features.Flag(tr, features.QuotaGroups) 75 if err != nil && !config.IsNoOption(err) { 76 return err 77 } 78 if !enableQuotaGroups { 79 return fmt.Errorf("experimental feature disabled - test it by setting 'experimental.quota-groups' to true") 80 } 81 82 return nil 83 } 84 85 // CreateQuota attempts to create the specified quota group with the specified 86 // snaps in it. 87 // TODO: should this use something like QuotaGroupUpdate with fewer fields? 88 func CreateQuota(st *state.State, name string, parentName string, snaps []string, memoryLimit quantity.Size) error { 89 if err := quotaGroupsAvailable(st); err != nil { 90 return err 91 } 92 93 allGrps, err := AllQuotas(st) 94 if err != nil { 95 return err 96 } 97 98 // ensure that the quota group does not exist yet 99 if _, ok := allGrps[name]; ok { 100 return fmt.Errorf("group %q already exists", name) 101 } 102 103 // make sure the specified snaps exist and aren't currently in another group 104 if err := validateSnapForAddingToGroup(st, snaps, name, allGrps); err != nil { 105 return err 106 } 107 108 // make sure that the parent group exists if we are creating a sub-group 109 var grp *quota.Group 110 updatedGrps := []*quota.Group{} 111 if parentName != "" { 112 parentGrp, ok := allGrps[parentName] 113 if !ok { 114 return fmt.Errorf("cannot create group under non-existent parent group %q", parentName) 115 } 116 117 grp, err = parentGrp.NewSubGroup(name, memoryLimit) 118 if err != nil { 119 return err 120 } 121 122 updatedGrps = append(updatedGrps, parentGrp) 123 } else { 124 // make a new group 125 grp, err = quota.NewGroup(name, memoryLimit) 126 if err != nil { 127 return err 128 } 129 } 130 updatedGrps = append(updatedGrps, grp) 131 132 // put the snaps in the group 133 grp.Snaps = snaps 134 135 // update the modified groups in state 136 allGrps, err = patchQuotas(st, updatedGrps...) 137 if err != nil { 138 return err 139 } 140 141 // ensure the snap services with the group 142 opts := &ensureSnapServicesForGroupOptions{ 143 allGrps: allGrps, 144 } 145 if err := ensureSnapServicesForGroup(st, grp, opts, nil, nil); err != nil { 146 return err 147 } 148 149 return nil 150 } 151 152 // RemoveQuota deletes the specific quota group. Any snaps currently in the 153 // quota will no longer be in any quota group, even if the quota group being 154 // removed is a sub-group. 155 // TODO: currently this only supports removing leaf sub-group groups, it doesn't 156 // support removing parent quotas, but probably it makes sense to allow that too 157 func RemoveQuota(st *state.State, name string) error { 158 if snapdenv.Preseeding() { 159 return fmt.Errorf("removing quota groups not supported while preseeding") 160 } 161 162 allGrps, err := AllQuotas(st) 163 if err != nil { 164 return err 165 } 166 167 // first get the group for later before it is deleted from state 168 grp, ok := allGrps[name] 169 if !ok { 170 return fmt.Errorf("cannot remove non-existent quota group %q", name) 171 } 172 173 // XXX: remove this limitation eventually 174 if len(grp.SubGroups) != 0 { 175 return fmt.Errorf("cannot remove quota group with sub-groups, remove the sub-groups first") 176 } 177 178 // if this group has a parent, we need to remove the linkage to this 179 // sub-group from the parent first 180 if grp.ParentGroup != "" { 181 // the parent here must exist otherwise AllQuotas would have failed 182 // because state would have been inconsistent 183 parent := allGrps[grp.ParentGroup] 184 185 // ensure that the parent group of this group no longer mentions this 186 // group as a sub-group - we know that it must since AllQuotas validated 187 // the state for us 188 if len(parent.SubGroups) == 1 { 189 // this group was an only child, so clear the whole list 190 parent.SubGroups = nil 191 } else { 192 // we have to delete the child but keep the other children 193 newSubgroups := make([]string, 0, len(parent.SubGroups)-1) 194 for _, sub := range parent.SubGroups { 195 if sub != name { 196 newSubgroups = append(newSubgroups, sub) 197 } 198 } 199 200 parent.SubGroups = newSubgroups 201 } 202 203 allGrps[grp.ParentGroup] = parent 204 } 205 206 // now delete the group from state - do this first for convenience to ensure 207 // that we can just use SnapServiceOptions below and since it operates via 208 // state, it will immediately reflect the deletion 209 delete(allGrps, name) 210 211 // make sure that the group set is consistent before saving it - we may need 212 // to delete old links from this group's parent to the child 213 if err := quota.ResolveCrossReferences(allGrps); err != nil { 214 return fmt.Errorf("cannot remove quota %q: %v", name, err) 215 } 216 217 // now set it in state 218 st.Set("quotas", allGrps) 219 220 // update snap service units that may need to be re-written because they are 221 // not in a slice anymore 222 opts := &ensureSnapServicesForGroupOptions{ 223 allGrps: allGrps, 224 } 225 if err := ensureSnapServicesForGroup(st, grp, opts, nil, nil); err != nil { 226 return err 227 } 228 229 return nil 230 } 231 232 // QuotaGroupUpdate reflects all of the modifications that can be performed on 233 // a quota group in one operation. 234 type QuotaGroupUpdate struct { 235 // AddSnaps is the set of snaps to add to the quota group. These are 236 // instance names of snaps, and are appended to the existing snaps in 237 // the quota group 238 AddSnaps []string 239 240 // NewMemoryLimit is the new memory limit to be used for the quota group. If 241 // zero, then the quota group's memory limit is not changed. 242 NewMemoryLimit quantity.Size 243 } 244 245 // UpdateQuota updates the quota as per the options. 246 // TODO: this should support more kinds of updates such as moving groups between 247 // parents, removing sub-groups from their parents, and removing snaps from 248 // the group. 249 func UpdateQuota(st *state.State, name string, updateOpts QuotaGroupUpdate) error { 250 if err := quotaGroupsAvailable(st); err != nil { 251 return err 252 } 253 254 // ensure that the quota group exists 255 allGrps, err := AllQuotas(st) 256 if err != nil { 257 return err 258 } 259 260 grp, ok := allGrps[name] 261 if !ok { 262 return fmt.Errorf("group %q does not exist", name) 263 } 264 265 modifiedGrps := []*quota.Group{grp} 266 267 // now ensure that all of the snaps mentioned in AddSnaps exist as snaps and 268 // that they aren't already in an existing quota group 269 if err := validateSnapForAddingToGroup(st, updateOpts.AddSnaps, name, allGrps); err != nil { 270 return err 271 } 272 273 // append the snaps list in the group 274 grp.Snaps = append(grp.Snaps, updateOpts.AddSnaps...) 275 276 // if the memory limit is not zero then change it too 277 if updateOpts.NewMemoryLimit != 0 { 278 // we disallow decreasing the memory limit because it is difficult to do 279 // so correctly with the current state of our code in 280 // EnsureSnapServices, see comment in ensureSnapServicesForGroup for 281 // full details 282 if updateOpts.NewMemoryLimit < grp.MemoryLimit { 283 return fmt.Errorf("cannot decrease memory limit of existing quota-group, remove and re-create it to decrease the limit") 284 } 285 grp.MemoryLimit = updateOpts.NewMemoryLimit 286 } 287 288 // update the quota group state 289 allGrps, err = patchQuotas(st, modifiedGrps...) 290 if err != nil { 291 return err 292 } 293 294 // ensure service states are updated 295 opts := &ensureSnapServicesForGroupOptions{ 296 allGrps: allGrps, 297 } 298 return ensureSnapServicesForGroup(st, grp, opts, nil, nil) 299 } 300 301 // EnsureSnapAbsentFromQuota ensures that the specified snap is not present 302 // in any quota group, usually in preparation for removing that snap from the 303 // system to keep the quota group itself consistent. 304 func EnsureSnapAbsentFromQuota(st *state.State, snap string) error { 305 allGrps, err := AllQuotas(st) 306 if err != nil { 307 return err 308 } 309 310 // try to find the snap in any group 311 for _, grp := range allGrps { 312 for idx, sn := range grp.Snaps { 313 if sn == snap { 314 // drop this snap from the list of Snaps by swapping it with the 315 // last snap in the list, and then dropping the last snap from 316 // the list 317 grp.Snaps[idx] = grp.Snaps[len(grp.Snaps)-1] 318 grp.Snaps = grp.Snaps[:len(grp.Snaps)-1] 319 320 // update the quota group state 321 allGrps, err = patchQuotas(st, grp) 322 if err != nil { 323 return err 324 } 325 326 // ensure service states are updated - note we have to add the 327 // snap as an extra snap to ensure since it was removed from the 328 // group and thus won't be considered just by looking at the 329 // group pointer directly 330 opts := &ensureSnapServicesForGroupOptions{ 331 allGrps: allGrps, 332 extraSnaps: []string{snap}, 333 } 334 return ensureSnapServicesForGroup(st, grp, opts, nil, nil) 335 } 336 } 337 } 338 339 // the snap wasn't in any group, nothing to do 340 return nil 341 }