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 := &quotaGroup{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  }