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