github.com/ubuntu-core/snappy@v0.0.0-20210827154228-9e584df982bb/cmd/snap/cmd_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 main 21 22 import ( 23 "fmt" 24 "sort" 25 "strings" 26 27 "github.com/jessevdk/go-flags" 28 29 "github.com/snapcore/snapd/client" 30 "github.com/snapcore/snapd/gadget/quantity" 31 "github.com/snapcore/snapd/i18n" 32 "github.com/snapcore/snapd/strutil" 33 ) 34 35 var shortQuotaHelp = i18n.G("Show quota group for a set of snaps") 36 var longQuotaHelp = i18n.G(` 37 The quota command shows information about a quota group, including the set of 38 snaps and any sub-groups it contains, as well as its resource constraints and 39 the current usage of those constrained resources. 40 `) 41 42 var shortQuotasHelp = i18n.G("Show quota groups") 43 var longQuotasHelp = i18n.G(` 44 The quotas command shows all quota groups. 45 `) 46 47 var shortRemoveQuotaHelp = i18n.G("Remove quota group") 48 var longRemoveQuotaHelp = i18n.G(` 49 The remove-quota command removes the given quota group. 50 51 Currently, only quota groups with no sub-groups can be removed. In order to 52 remove a quota group with sub-groups, the sub-groups must first be removed until 53 there are no sub-groups for the group, then the group itself can be removed. 54 `) 55 56 var shortSetQuotaHelp = i18n.G(`Create or update a quota group.`) 57 var longSetQuotaHelp = i18n.G(` 58 The set-quota command updates or creates a quota group with the specified set of 59 snaps. 60 61 A quota group sets resource limits on the set of snaps it contains. Only maximum 62 memory is currently supported. Snaps can be at most in one quota group but quota 63 groups can be nested. Nested quota groups are subject to the restriction that 64 the total sum of maximum memory in sub-groups cannot exceed that of the parent 65 group the nested groups are part of. 66 67 All provided snaps are appended to the group; to remove a snap from a 68 quota group, the entire group must be removed with remove-quota and recreated 69 without the quota group. To remove a sub-group from the quota group, the 70 sub-group must be removed directly with the remove-quota command. 71 72 The memory limit for a quota group can be increased but not decreased. To 73 decrease the memory limit for a quota group, the entire group must be removed 74 with the remove-quota command and recreated with a lower limit. Increasing the 75 memory limit for a quota group does not restart any services associated with 76 snaps in the quota group. 77 78 Adding new snaps to a quota group will result in all non-disabled services in 79 that snap being restarted. 80 81 An existing sub group cannot be moved from one parent to another. 82 `) 83 84 func init() { 85 // TODO: unhide the commands when non-experimental 86 cmd := addCommand("set-quota", shortSetQuotaHelp, longSetQuotaHelp, func() flags.Commander { return &cmdSetQuota{} }, nil, nil) 87 cmd.hidden = true 88 89 cmd = addCommand("quota", shortQuotaHelp, longQuotaHelp, func() flags.Commander { return &cmdQuota{} }, nil, nil) 90 cmd.hidden = true 91 92 cmd = addCommand("quotas", shortQuotasHelp, longQuotasHelp, func() flags.Commander { return &cmdQuotas{} }, nil, nil) 93 cmd.hidden = true 94 95 cmd = addCommand("remove-quota", shortRemoveQuotaHelp, longRemoveQuotaHelp, func() flags.Commander { return &cmdRemoveQuota{} }, nil, nil) 96 cmd.hidden = true 97 } 98 99 type cmdSetQuota struct { 100 waitMixin 101 102 MemoryMax string `long:"memory" optional:"true"` 103 Parent string `long:"parent" optional:"true"` 104 Positional struct { 105 GroupName string `positional-arg-name:"<group-name>" required:"true"` 106 Snaps []installedSnapName `positional-arg-name:"<snap>" optional:"true"` 107 } `positional-args:"yes"` 108 } 109 110 func (x *cmdSetQuota) Execute(args []string) (err error) { 111 var maxMemory string 112 switch { 113 case x.MemoryMax != "": 114 maxMemory = x.MemoryMax 115 } 116 117 names := installedSnapNames(x.Positional.Snaps) 118 119 // figure out if the group exists or not to make error messages more useful 120 groupExists := false 121 if _, err = x.client.GetQuotaGroup(x.Positional.GroupName); err == nil { 122 groupExists = true 123 } 124 125 var chgID string 126 127 switch { 128 case maxMemory == "" && x.Parent == "" && len(x.Positional.Snaps) == 0: 129 // no snaps were specified, no memory limit was specified, and no parent 130 // was specified, so just the group name was provided - this is not 131 // supported since there is nothing to change/create 132 133 if groupExists { 134 return fmt.Errorf("no options set to change quota group") 135 } 136 return fmt.Errorf("cannot create quota group without memory limit") 137 138 case maxMemory == "" && x.Parent != "" && len(x.Positional.Snaps) == 0: 139 // this is either trying to create a new group with a parent and forgot 140 // to specify the memory limit for the new group, or the user is trying 141 // to re-parent a group, i.e. move it from the current parent to a 142 // different one, which is currently unsupported 143 144 if groupExists { 145 // TODO: or this could be setting the parent to the existing parent, 146 // which is effectively no change or update but maybe we allow since 147 // it's a noop? 148 return fmt.Errorf("cannot move a quota group to a new parent") 149 } 150 return fmt.Errorf("cannot create quota group without memory limit") 151 152 case maxMemory != "": 153 // we have a memory limit to set for this group, so specify that along 154 // with whatever snaps may have been provided and whatever parent may 155 // have been specified 156 157 mem, err := strutil.ParseByteSize(maxMemory) 158 if err != nil { 159 return err 160 } 161 162 // note that the group could currently exist with a parent, and we could 163 // be specifying x.Parent as "" here - in the future that may mean to 164 // orphan a sub-group to no longer have a parent, but currently it just 165 // means leave the group with whatever parent it has, or if it doesn't 166 // currently exist, create the group without a parent group 167 chgID, err = x.client.EnsureQuota(x.Positional.GroupName, x.Parent, names, quantity.Size(mem)) 168 if err != nil { 169 return err 170 } 171 case len(x.Positional.Snaps) != 0: 172 // there are snaps specified for this group but no memory limit, so the 173 // group must already exist and we must be adding the specified snaps to 174 // the group 175 176 // TODO: this case may someday also imply overwriting the current set of 177 // snaps with whatever was specified with some option, but we don't 178 // currently support that, so currently all snaps specified here are 179 // just added to the group 180 181 chgID, err = x.client.EnsureQuota(x.Positional.GroupName, x.Parent, names, 0) 182 if err != nil { 183 return err 184 } 185 default: 186 // should be logically impossible to reach here 187 panic("impossible set of options") 188 } 189 190 if _, err := x.wait(chgID); err != nil { 191 if err == noWait { 192 return nil 193 } 194 return err 195 } 196 197 return nil 198 } 199 200 type cmdQuota struct { 201 clientMixin 202 203 Positional struct { 204 GroupName string `positional-arg-name:"<group-name>" required:"true"` 205 } `positional-args:"yes"` 206 } 207 208 func (x *cmdQuota) Execute(args []string) (err error) { 209 if len(args) != 0 { 210 return fmt.Errorf("too many arguments provided") 211 } 212 213 group, err := x.client.GetQuotaGroup(x.Positional.GroupName) 214 if err != nil { 215 return err 216 } 217 218 w := tabWriter() 219 defer w.Flush() 220 221 fmt.Fprintf(w, "name:\t%s\n", group.GroupName) 222 if group.Parent != "" { 223 fmt.Fprintf(w, "parent:\t%s\n", group.Parent) 224 } 225 226 fmt.Fprintf(w, "constraints:\n") 227 228 // Constraints should always be non-nil, since a quota group always needs to 229 // have a memory limit 230 if group.Constraints == nil { 231 return fmt.Errorf("internal error: constraints is missing from daemon response") 232 } 233 val := strings.TrimSpace(fmtSize(int64(group.Constraints.Memory))) 234 fmt.Fprintf(w, " memory:\t%s\n", val) 235 236 fmt.Fprintf(w, "current:\n") 237 if group.Current == nil { 238 // current however may be missing if there is no memory usage 239 val = "0B" 240 } else { 241 // use the value from the response 242 val = strings.TrimSpace(fmtSize(int64(group.Current.Memory))) 243 } 244 245 fmt.Fprintf(w, " memory:\t%s\n", val) 246 247 if len(group.Subgroups) > 0 { 248 fmt.Fprint(w, "subgroups:\n") 249 for _, name := range group.Subgroups { 250 fmt.Fprintf(w, " - %s\n", name) 251 } 252 } 253 if len(group.Snaps) > 0 { 254 fmt.Fprint(w, "snaps:\n") 255 for _, snapName := range group.Snaps { 256 fmt.Fprintf(w, " - %s\n", snapName) 257 } 258 } 259 260 return nil 261 } 262 263 type cmdRemoveQuota struct { 264 waitMixin 265 266 Positional struct { 267 GroupName string `positional-arg-name:"<group-name>" required:"true"` 268 } `positional-args:"yes"` 269 } 270 271 func (x *cmdRemoveQuota) Execute(args []string) (err error) { 272 chgID, err := x.client.RemoveQuotaGroup(x.Positional.GroupName) 273 if err != nil { 274 return err 275 } 276 277 if _, err := x.wait(chgID); err != nil { 278 if err == noWait { 279 return nil 280 } 281 return err 282 } 283 284 return nil 285 } 286 287 type cmdQuotas struct { 288 clientMixin 289 } 290 291 func (x *cmdQuotas) Execute(args []string) (err error) { 292 res, err := x.client.Quotas() 293 if err != nil { 294 return err 295 } 296 if len(res) == 0 { 297 fmt.Fprintln(Stdout, i18n.G("No quota groups defined.")) 298 return nil 299 } 300 301 w := tabWriter() 302 fmt.Fprintf(w, "Quota\tParent\tConstraints\tCurrent\n") 303 err = processQuotaGroupsTree(res, func(q *client.QuotaGroupResult) error { 304 if q.Constraints == nil { 305 return fmt.Errorf("internal error: constraints is missing from daemon response") 306 } 307 308 constraintVal := "memory=" + strings.TrimSpace(fmtSize(int64(q.Constraints.Memory))) 309 currentVal := "" 310 if q.Current != nil && q.Current.Memory != 0 { 311 currentVal = "memory=" + strings.TrimSpace(fmtSize(int64(q.Current.Memory))) 312 } 313 fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", q.GroupName, q.Parent, constraintVal, currentVal) 314 315 return nil 316 }) 317 if err != nil { 318 return err 319 } 320 w.Flush() 321 return nil 322 } 323 324 type quotaGroup struct { 325 res *client.QuotaGroupResult 326 subGroups []*quotaGroup 327 } 328 329 type byQuotaName []*quotaGroup 330 331 func (q byQuotaName) Len() int { return len(q) } 332 func (q byQuotaName) Swap(i, j int) { q[i], q[j] = q[j], q[i] } 333 func (q byQuotaName) Less(i, j int) bool { return q[i].res.GroupName < q[j].res.GroupName } 334 335 // processQuotaGroupsTree recreates the hierarchy of quotas and then visits it 336 // recursively following the hierarchy first, then naming order. 337 func processQuotaGroupsTree(quotas []*client.QuotaGroupResult, handleGroup func(q *client.QuotaGroupResult) error) error { 338 var roots []*quotaGroup 339 groupLookup := make(map[string]*quotaGroup, len(quotas)) 340 341 for _, q := range quotas { 342 grp := "aGroup{res: q} 343 groupLookup[q.GroupName] = grp 344 345 if q.Parent == "" { 346 roots = append(roots, grp) 347 } 348 } 349 350 sort.Sort(byQuotaName(roots)) 351 352 // populate sub-groups 353 for _, g := range groupLookup { 354 sort.Strings(g.res.Subgroups) 355 for _, subgrpName := range g.res.Subgroups { 356 subGroup, ok := groupLookup[subgrpName] 357 if !ok { 358 return fmt.Errorf("internal error: inconsistent groups received, unknown subgroup %q", subgrpName) 359 } 360 g.subGroups = append(g.subGroups, subGroup) 361 } 362 } 363 364 var processGroups func(groups []*quotaGroup) error 365 processGroups = func(groups []*quotaGroup) error { 366 for _, g := range groups { 367 if err := handleGroup(g.res); err != nil { 368 return err 369 } 370 if len(g.subGroups) > 0 { 371 if err := processGroups(g.subGroups); err != nil { 372 return err 373 } 374 } 375 } 376 return nil 377 } 378 return processGroups(roots) 379 }