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