github.com/stulluk/snapd@v0.0.0-20210611110309-f6d5d5bd24b0/overlord/servicestate/quota_control.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 servicestate
    21  
    22  import (
    23  	"fmt"
    24  
    25  	"github.com/snapcore/snapd/features"
    26  	"github.com/snapcore/snapd/gadget/quantity"
    27  	"github.com/snapcore/snapd/logger"
    28  	"github.com/snapcore/snapd/osutil"
    29  	"github.com/snapcore/snapd/overlord/configstate/config"
    30  	"github.com/snapcore/snapd/overlord/state"
    31  	"github.com/snapcore/snapd/snap/quota"
    32  	"github.com/snapcore/snapd/snapdenv"
    33  	"github.com/snapcore/snapd/systemd"
    34  )
    35  
    36  var (
    37  	systemdVersion int
    38  )
    39  
    40  // TODO: move to a systemd.AtLeast() ?
    41  func checkSystemdVersion() error {
    42  	vers, err := systemd.Version()
    43  	if err != nil {
    44  		return err
    45  	}
    46  	systemdVersion = vers
    47  	return nil
    48  }
    49  
    50  func init() {
    51  	if err := checkSystemdVersion(); err != nil {
    52  		logger.Noticef("failed to check systemd version: %v", err)
    53  	}
    54  }
    55  
    56  // MockSystemdVersion mocks the systemd version to the given version. This is
    57  // only available for unit tests and will panic when run in production.
    58  func MockSystemdVersion(vers int) (restore func()) {
    59  	osutil.MustBeTestBinary("cannot mock systemd version outside of tests")
    60  	old := systemdVersion
    61  	systemdVersion = vers
    62  	return func() {
    63  		systemdVersion = old
    64  	}
    65  }
    66  
    67  func quotaGroupsAvailable(st *state.State) error {
    68  	// check if the systemd version is too old
    69  	if systemdVersion < 205 {
    70  		return fmt.Errorf("systemd version too old: snap quotas requires systemd 205 and newer (currently have %d)", systemdVersion)
    71  	}
    72  
    73  	tr := config.NewTransaction(st)
    74  	enableQuotaGroups, err := features.Flag(tr, features.QuotaGroups)
    75  	if err != nil && !config.IsNoOption(err) {
    76  		return err
    77  	}
    78  	if !enableQuotaGroups {
    79  		return fmt.Errorf("experimental feature disabled - test it by setting 'experimental.quota-groups' to true")
    80  	}
    81  
    82  	return nil
    83  }
    84  
    85  // CreateQuota attempts to create the specified quota group with the specified
    86  // snaps in it.
    87  // TODO: should this use something like QuotaGroupUpdate with fewer fields?
    88  func CreateQuota(st *state.State, name string, parentName string, snaps []string, memoryLimit quantity.Size) error {
    89  	if err := quotaGroupsAvailable(st); err != nil {
    90  		return err
    91  	}
    92  
    93  	allGrps, err := AllQuotas(st)
    94  	if err != nil {
    95  		return err
    96  	}
    97  
    98  	// ensure that the quota group does not exist yet
    99  	if _, ok := allGrps[name]; ok {
   100  		return fmt.Errorf("group %q already exists", name)
   101  	}
   102  
   103  	// make sure the specified snaps exist and aren't currently in another group
   104  	if err := validateSnapForAddingToGroup(st, snaps, name, allGrps); err != nil {
   105  		return err
   106  	}
   107  
   108  	// make sure that the parent group exists if we are creating a sub-group
   109  	var grp *quota.Group
   110  	updatedGrps := []*quota.Group{}
   111  	if parentName != "" {
   112  		parentGrp, ok := allGrps[parentName]
   113  		if !ok {
   114  			return fmt.Errorf("cannot create group under non-existent parent group %q", parentName)
   115  		}
   116  
   117  		grp, err = parentGrp.NewSubGroup(name, memoryLimit)
   118  		if err != nil {
   119  			return err
   120  		}
   121  
   122  		updatedGrps = append(updatedGrps, parentGrp)
   123  	} else {
   124  		// make a new group
   125  		grp, err = quota.NewGroup(name, memoryLimit)
   126  		if err != nil {
   127  			return err
   128  		}
   129  	}
   130  	updatedGrps = append(updatedGrps, grp)
   131  
   132  	// put the snaps in the group
   133  	grp.Snaps = snaps
   134  
   135  	// update the modified groups in state
   136  	allGrps, err = patchQuotas(st, updatedGrps...)
   137  	if err != nil {
   138  		return err
   139  	}
   140  
   141  	// ensure the snap services with the group
   142  	opts := &ensureSnapServicesForGroupOptions{
   143  		allGrps: allGrps,
   144  	}
   145  	if err := ensureSnapServicesForGroup(st, grp, opts, nil, nil); err != nil {
   146  		return err
   147  	}
   148  
   149  	return nil
   150  }
   151  
   152  // RemoveQuota deletes the specific quota group. Any snaps currently in the
   153  // quota will no longer be in any quota group, even if the quota group being
   154  // removed is a sub-group.
   155  // TODO: currently this only supports removing leaf sub-group groups, it doesn't
   156  // support removing parent quotas, but probably it makes sense to allow that too
   157  func RemoveQuota(st *state.State, name string) error {
   158  	if snapdenv.Preseeding() {
   159  		return fmt.Errorf("removing quota groups not supported while preseeding")
   160  	}
   161  
   162  	allGrps, err := AllQuotas(st)
   163  	if err != nil {
   164  		return err
   165  	}
   166  
   167  	// first get the group for later before it is deleted from state
   168  	grp, ok := allGrps[name]
   169  	if !ok {
   170  		return fmt.Errorf("cannot remove non-existent quota group %q", name)
   171  	}
   172  
   173  	// XXX: remove this limitation eventually
   174  	if len(grp.SubGroups) != 0 {
   175  		return fmt.Errorf("cannot remove quota group with sub-groups, remove the sub-groups first")
   176  	}
   177  
   178  	// if this group has a parent, we need to remove the linkage to this
   179  	// sub-group from the parent first
   180  	if grp.ParentGroup != "" {
   181  		// the parent here must exist otherwise AllQuotas would have failed
   182  		// because state would have been inconsistent
   183  		parent := allGrps[grp.ParentGroup]
   184  
   185  		// ensure that the parent group of this group no longer mentions this
   186  		// group as a sub-group - we know that it must since AllQuotas validated
   187  		// the state for us
   188  		if len(parent.SubGroups) == 1 {
   189  			// this group was an only child, so clear the whole list
   190  			parent.SubGroups = nil
   191  		} else {
   192  			// we have to delete the child but keep the other children
   193  			newSubgroups := make([]string, 0, len(parent.SubGroups)-1)
   194  			for _, sub := range parent.SubGroups {
   195  				if sub != name {
   196  					newSubgroups = append(newSubgroups, sub)
   197  				}
   198  			}
   199  
   200  			parent.SubGroups = newSubgroups
   201  		}
   202  
   203  		allGrps[grp.ParentGroup] = parent
   204  	}
   205  
   206  	// now delete the group from state - do this first for convenience to ensure
   207  	// that we can just use SnapServiceOptions below and since it operates via
   208  	// state, it will immediately reflect the deletion
   209  	delete(allGrps, name)
   210  
   211  	// make sure that the group set is consistent before saving it - we may need
   212  	// to delete old links from this group's parent to the child
   213  	if err := quota.ResolveCrossReferences(allGrps); err != nil {
   214  		return fmt.Errorf("cannot remove quota %q: %v", name, err)
   215  	}
   216  
   217  	// now set it in state
   218  	st.Set("quotas", allGrps)
   219  
   220  	// update snap service units that may need to be re-written because they are
   221  	// not in a slice anymore
   222  	opts := &ensureSnapServicesForGroupOptions{
   223  		allGrps: allGrps,
   224  	}
   225  	if err := ensureSnapServicesForGroup(st, grp, opts, nil, nil); err != nil {
   226  		return err
   227  	}
   228  
   229  	return nil
   230  }
   231  
   232  // QuotaGroupUpdate reflects all of the modifications that can be performed on
   233  // a quota group in one operation.
   234  type QuotaGroupUpdate struct {
   235  	// AddSnaps is the set of snaps to add to the quota group. These are
   236  	// instance names of snaps, and are appended to the existing snaps in
   237  	// the quota group
   238  	AddSnaps []string
   239  
   240  	// NewMemoryLimit is the new memory limit to be used for the quota group. If
   241  	// zero, then the quota group's memory limit is not changed.
   242  	NewMemoryLimit quantity.Size
   243  }
   244  
   245  // UpdateQuota updates the quota as per the options.
   246  // TODO: this should support more kinds of updates such as moving groups between
   247  // parents, removing sub-groups from their parents, and removing snaps from
   248  // the group.
   249  func UpdateQuota(st *state.State, name string, updateOpts QuotaGroupUpdate) error {
   250  	if err := quotaGroupsAvailable(st); err != nil {
   251  		return err
   252  	}
   253  
   254  	// ensure that the quota group exists
   255  	allGrps, err := AllQuotas(st)
   256  	if err != nil {
   257  		return err
   258  	}
   259  
   260  	grp, ok := allGrps[name]
   261  	if !ok {
   262  		return fmt.Errorf("group %q does not exist", name)
   263  	}
   264  
   265  	modifiedGrps := []*quota.Group{grp}
   266  
   267  	// now ensure that all of the snaps mentioned in AddSnaps exist as snaps and
   268  	// that they aren't already in an existing quota group
   269  	if err := validateSnapForAddingToGroup(st, updateOpts.AddSnaps, name, allGrps); err != nil {
   270  		return err
   271  	}
   272  
   273  	//  append the snaps list in the group
   274  	grp.Snaps = append(grp.Snaps, updateOpts.AddSnaps...)
   275  
   276  	// if the memory limit is not zero then change it too
   277  	if updateOpts.NewMemoryLimit != 0 {
   278  		// we disallow decreasing the memory limit because it is difficult to do
   279  		// so correctly with the current state of our code in
   280  		// EnsureSnapServices, see comment in ensureSnapServicesForGroup for
   281  		// full details
   282  		if updateOpts.NewMemoryLimit < grp.MemoryLimit {
   283  			return fmt.Errorf("cannot decrease memory limit of existing quota-group, remove and re-create it to decrease the limit")
   284  		}
   285  		grp.MemoryLimit = updateOpts.NewMemoryLimit
   286  	}
   287  
   288  	// update the quota group state
   289  	allGrps, err = patchQuotas(st, modifiedGrps...)
   290  	if err != nil {
   291  		return err
   292  	}
   293  
   294  	// ensure service states are updated
   295  	opts := &ensureSnapServicesForGroupOptions{
   296  		allGrps: allGrps,
   297  	}
   298  	return ensureSnapServicesForGroup(st, grp, opts, nil, nil)
   299  }
   300  
   301  // EnsureSnapAbsentFromQuota ensures that the specified snap is not present
   302  // in any quota group, usually in preparation for removing that snap from the
   303  // system to keep the quota group itself consistent.
   304  func EnsureSnapAbsentFromQuota(st *state.State, snap string) error {
   305  	allGrps, err := AllQuotas(st)
   306  	if err != nil {
   307  		return err
   308  	}
   309  
   310  	// try to find the snap in any group
   311  	for _, grp := range allGrps {
   312  		for idx, sn := range grp.Snaps {
   313  			if sn == snap {
   314  				// drop this snap from the list of Snaps by swapping it with the
   315  				// last snap in the list, and then dropping the last snap from
   316  				// the list
   317  				grp.Snaps[idx] = grp.Snaps[len(grp.Snaps)-1]
   318  				grp.Snaps = grp.Snaps[:len(grp.Snaps)-1]
   319  
   320  				// update the quota group state
   321  				allGrps, err = patchQuotas(st, grp)
   322  				if err != nil {
   323  					return err
   324  				}
   325  
   326  				// ensure service states are updated - note we have to add the
   327  				// snap as an extra snap to ensure since it was removed from the
   328  				// group and thus won't be considered just by looking at the
   329  				// group pointer directly
   330  				opts := &ensureSnapServicesForGroupOptions{
   331  					allGrps:    allGrps,
   332  					extraSnaps: []string{snap},
   333  				}
   334  				return ensureSnapServicesForGroup(st, grp, opts, nil, nil)
   335  			}
   336  		}
   337  	}
   338  
   339  	// the snap wasn't in any group, nothing to do
   340  	return nil
   341  }