github.com/ubuntu-core/snappy@v0.0.0-20210827154228-9e584df982bb/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  }