gopkg.in/ubuntu-core/snappy.v0@v0.0.0-20210902073436-25a8614f10a6/snap/quota/quota.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 quota defines state structures for resource quota groups 21 // for snaps. 22 package quota 23 24 import ( 25 "bytes" 26 "fmt" 27 "sort" 28 29 // TODO: move this to snap/quantity? or similar 30 "github.com/snapcore/snapd/gadget/quantity" 31 "github.com/snapcore/snapd/progress" 32 "github.com/snapcore/snapd/snap/naming" 33 "github.com/snapcore/snapd/systemd" 34 ) 35 36 // Group is a quota group of snaps, services or sub-groups that are all subject 37 // to specific resource quotas. The only quota resource types currently 38 // supported is memory, but this can be expanded in the future. 39 type Group struct { 40 // Name is the name of the quota group. This name is used the 41 // name of the systemd slice underlying the quota group. 42 // Certain names are reserved for future use: system, snapd, root, user. 43 // Otherwise names following the same rules as snap names can be used. 44 Name string `json:"name,omitempty"` 45 46 // SubGroups is the set of sub-groups that are subject to this quota. 47 // Sub-groups have their own limits, subject to the requirement that the 48 // highest quota for a sub-group is that of the parent group. 49 SubGroups []string `json:"sub-groups,omitempty"` 50 51 // subGroups is the set of actual sub-group objects, needed for tracking and 52 // calculations 53 subGroups []*Group 54 55 // MemoryLimit is the limit of memory available to the processes in the 56 // group where if the total used memory of all the processes exceeds the 57 // limit, oom-killer is invoked which will start killing processes. The 58 // specific behavior of which processes are killed is subject to the 59 // ExhaustionBehavior. MemoryLimit is expressed in bytes. 60 MemoryLimit quantity.Size `json:"memory-limit,omitempty"` 61 62 // ParentGroup is the the parent group that this group is a child of. If it 63 // is empty, then this is a "root" quota group. 64 ParentGroup string `json:"parent-group,omitempty"` 65 66 // parentGroup is the actual parent group object, needed for tracking and 67 // calculations 68 parentGroup *Group 69 70 // Snaps is the set of snaps that is part of this quota group. If this is 71 // empty then the underlying slice may not exist on the system. 72 Snaps []string `json:"snaps,omitempty"` 73 } 74 75 // NewGroup creates a new top quota group with the given name and memory limit. 76 func NewGroup(name string, memLimit quantity.Size) (*Group, error) { 77 grp := &Group{ 78 Name: name, 79 MemoryLimit: memLimit, 80 } 81 82 if err := grp.validate(); err != nil { 83 return nil, err 84 } 85 86 return grp, nil 87 } 88 89 // CurrentMemoryUsage returns the current memory usage of the quota group. For 90 // quota groups which do not yet have a backing systemd slice on the system ( 91 // i.e. quota groups without any snaps in them), the memory usage is reported as 92 // 0. 93 func (grp *Group) CurrentMemoryUsage() (quantity.Size, error) { 94 sysd := systemd.New(systemd.SystemMode, progress.Null) 95 96 // check if this group is actually active, it could not physically exist yet 97 // since it has no snaps in it 98 isActive, err := sysd.IsActive(grp.SliceFileName()) 99 if err != nil { 100 return 0, err 101 } 102 if !isActive { 103 return 0, nil 104 } 105 106 mem, err := sysd.CurrentMemoryUsage(grp.SliceFileName()) 107 if err != nil { 108 return 0, err 109 } 110 111 return mem, nil 112 } 113 114 // SliceFileName returns the name of the slice file that should be used for this 115 // quota group. This name will include all of the group's parents in the name. 116 // For example, a group named "bar" that is a child of the "foo" group will have 117 // a systemd slice name as "snap.foo-bar.slice". Note that the slice name may 118 // differ from the snapd friendly group name, mainly in the case that the group 119 // is a sub group. 120 func (grp *Group) SliceFileName() string { 121 escapedGrpName := systemd.EscapeUnitNamePath(grp.Name) 122 if grp.ParentGroup == "" { 123 // root group name, then the slice unit is just "<name>.slice" 124 return fmt.Sprintf("snap.%s.slice", escapedGrpName) 125 } 126 127 // otherwise we need to track back to get all of the parent elements 128 grpNames := []string{} 129 parentGrp := grp.parentGroup 130 for parentGrp != nil { 131 grpNames = append([]string{parentGrp.Name}, grpNames...) 132 parentGrp = parentGrp.parentGroup 133 } 134 135 buf := &bytes.Buffer{} 136 fmt.Fprintf(buf, "snap.") 137 for _, parentGrpName := range grpNames { 138 fmt.Fprintf(buf, "%s-", systemd.EscapeUnitNamePath(parentGrpName)) 139 } 140 fmt.Fprintf(buf, "%s.slice", escapedGrpName) 141 return buf.String() 142 } 143 144 func (grp *Group) validate() error { 145 if err := naming.ValidateQuotaGroup(grp.Name); err != nil { 146 return err 147 } 148 149 // check if the name is reserved for future usage 150 switch grp.Name { 151 case "root", "system", "snapd", "user": 152 return fmt.Errorf("group name %q reserved", grp.Name) 153 } 154 155 if grp.MemoryLimit == 0 { 156 return fmt.Errorf("group memory limit must be non-zero") 157 } 158 159 // TODO: probably there is a minimum amount of bytes here that is 160 // technically usable/enforcable, should we check that too? 161 162 if grp.ParentGroup != "" && grp.Name == grp.ParentGroup { 163 return fmt.Errorf("group has circular parent reference to itself") 164 } 165 166 if len(grp.SubGroups) != 0 { 167 for _, subGrp := range grp.SubGroups { 168 if subGrp == grp.Name { 169 return fmt.Errorf("group has circular sub-group reference to itself") 170 } 171 } 172 } 173 174 // check that if this is a sub-group, then the parent group has enough space 175 // to accommodate this new group (we assume that other existing sub-groups 176 // in the parent group have already been validated) 177 if grp.parentGroup != nil { 178 alreadyUsed := quantity.Size(0) 179 for _, child := range grp.parentGroup.subGroups { 180 if child.Name == grp.Name { 181 continue 182 } 183 alreadyUsed += child.MemoryLimit 184 } 185 // careful arithmetic here in case we somehow overflow the max size of 186 // quantity.Size 187 if grp.parentGroup.MemoryLimit-alreadyUsed < grp.MemoryLimit { 188 remaining := grp.parentGroup.MemoryLimit - alreadyUsed 189 return fmt.Errorf("sub-group memory limit of %s is too large to fit inside remaining quota space %s for parent group %s", grp.MemoryLimit.IECString(), remaining.IECString(), grp.parentGroup.Name) 190 } 191 } 192 193 return nil 194 } 195 196 // NewSubGroup creates a new sub group under the current group. 197 func (grp *Group) NewSubGroup(name string, memLimit quantity.Size) (*Group, error) { 198 // TODO: implement a maximum sub-group depth 199 200 subGrp := &Group{ 201 Name: name, 202 MemoryLimit: memLimit, 203 ParentGroup: grp.Name, 204 parentGroup: grp, 205 } 206 207 // check early that the sub group name is not the same as that of the 208 // parent, this is fine in systemd world, but in snapd we want unique quota 209 // groups 210 if name == grp.Name { 211 return nil, fmt.Errorf("cannot use same name %q for sub group as parent group", name) 212 } 213 214 if err := subGrp.validate(); err != nil { 215 return nil, err 216 } 217 218 // save the details of this new sub-group in the parent group 219 grp.subGroups = append(grp.subGroups, subGrp) 220 grp.SubGroups = append(grp.SubGroups, name) 221 222 return subGrp, nil 223 } 224 225 // ResolveCrossReferences takes a set of deserialized groups and sets all 226 // cross references amongst them using the unexported fields which are not 227 // serialized. 228 func ResolveCrossReferences(grps map[string]*Group) error { 229 // TODO: consider returning a form of multi-error instead? 230 231 // iterate over all groups, looking for sub-groups which need to be threaded 232 // together with their respective parent groups from the set 233 234 for name, grp := range grps { 235 if name != grp.Name { 236 return fmt.Errorf("group has name %q, but is referenced as %q", grp.Name, name) 237 } 238 239 // validate the group, assuming it is unresolved 240 if err := grp.validate(); err != nil { 241 return fmt.Errorf("group %q is invalid: %v", name, err) 242 } 243 244 // first thread the parent link 245 if grp.ParentGroup != "" { 246 parent, ok := grps[grp.ParentGroup] 247 if !ok { 248 return fmt.Errorf("missing group %q referenced as the parent of group %q", grp.ParentGroup, grp.Name) 249 } 250 grp.parentGroup = parent 251 252 // make sure that the parent group references this group 253 found := false 254 for _, parentChildName := range parent.SubGroups { 255 if parentChildName == grp.Name { 256 found = true 257 break 258 } 259 } 260 if !found { 261 return fmt.Errorf("group %q does not reference necessary child group %q", parent.Name, grp.Name) 262 } 263 } 264 265 // now thread any child links from this group to any children 266 if len(grp.SubGroups) != 0 { 267 // re-build the internal sub group list 268 grp.subGroups = make([]*Group, len(grp.SubGroups)) 269 for i, subName := range grp.SubGroups { 270 sub, ok := grps[subName] 271 if !ok { 272 return fmt.Errorf("missing group %q referenced as the sub-group of group %q", subName, grp.Name) 273 } 274 275 // check that this sub-group references this group as it's 276 // parent 277 if sub.ParentGroup != grp.Name { 278 return fmt.Errorf("group %q does not reference necessary parent group %q", sub.Name, grp.Name) 279 } 280 281 grp.subGroups[i] = sub 282 } 283 } 284 } 285 286 return nil 287 } 288 289 // tree recursively returns all of the sub-groups of the group and the group 290 // itself. 291 func (grp *Group) visitTree(visited map[*Group]bool) error { 292 // TODO: limit the depth of the tree we traverse 293 294 // be paranoid about cycles here and check that none of the sub-groups here 295 // has already been seen before recursing 296 for _, sub := range grp.subGroups { 297 // check if this sub-group is actually the same group 298 if sub == grp { 299 return fmt.Errorf("internal error: circular reference found") 300 } 301 302 // check if we have already seen this sub-group 303 if visited[sub] { 304 return fmt.Errorf("internal error: circular reference found") 305 } 306 307 // add it to the map 308 visited[sub] = true 309 } 310 311 for _, sub := range grp.subGroups { 312 if err := sub.visitTree(visited); err != nil { 313 return err 314 } 315 } 316 317 // add this group too to get the full tree flattened 318 visited[grp] = true 319 320 return nil 321 } 322 323 // QuotaGroupSet is a set of quota groups, it is used for tracking a set of 324 // necessary quota groups using AddAllNecessaryGroups to add groups (and their 325 // implicit dependencies), and AllQuotaGroups to enumerate all the quota groups 326 // in the set. 327 type QuotaGroupSet struct { 328 grps map[*Group]bool 329 } 330 331 // AddAllNecessaryGroups adds all groups that are required for the specified 332 // group to be effective to the set. This means all sub-groups of this group, 333 // all parent groups of this group, and all sub-trees of any parent groups. This 334 // set is the set of quota groups that must exist for this quota group to be 335 // fully realized on a system, since all sub-branches of the full tree must 336 // exist since this group may share some quota resources with the other 337 // branches. There is no support for manipulating group trees while 338 // accumulating to a QuotaGroupSet using this. 339 func (s *QuotaGroupSet) AddAllNecessaryGroups(grp *Group) error { 340 if s.grps == nil { 341 s.grps = make(map[*Group]bool) 342 } 343 344 // the easy way to find all the quotas necessary for any arbitrary sub-group 345 // is to walk up all the way to the root parent group, then get the full 346 // tree beneath that and add all groups 347 prevParentGrp := grp 348 nextParentGrp := grp.parentGroup 349 for nextParentGrp != nil { 350 prevParentGrp = nextParentGrp 351 nextParentGrp = nextParentGrp.parentGroup 352 } 353 354 if s.grps[prevParentGrp] { 355 // nothing to do 356 return nil 357 } 358 359 // use a different map to prevent any accumulations to the quota group set 360 // that happen before a cycle is detected, we only want to add the groups 361 treeGroupMap := make(map[*Group]bool) 362 if err := prevParentGrp.visitTree(treeGroupMap); err != nil { 363 return err 364 } 365 366 // add all the groups in the tree to the quota group set 367 for g := range treeGroupMap { 368 s.grps[g] = true 369 } 370 371 return nil 372 } 373 374 // AllQuotaGroups returns a flattend list of all quota groups and necessary 375 // quota groups that have been added to the set. 376 func (s *QuotaGroupSet) AllQuotaGroups() []*Group { 377 grps := make([]*Group, 0, len(s.grps)) 378 for grp := range s.grps { 379 grps = append(grps, grp) 380 } 381 382 // sort the groups by their name for easier testing 383 sort.SliceStable(grps, func(i, j int) bool { 384 return grps[i].Name < grps[j].Name 385 }) 386 387 return grps 388 }