github.com/freetocompute/snapd@v0.0.0-20210618182524-2fb355d72fd9/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/snapdenv"
    32  	"github.com/snapcore/snapd/systemd"
    33  )
    34  
    35  var (
    36  	systemdVersion int
    37  )
    38  
    39  // TODO: move to a systemd.AtLeast() ?
    40  func checkSystemdVersion() error {
    41  	vers, err := systemd.Version()
    42  	if err != nil {
    43  		return err
    44  	}
    45  	systemdVersion = vers
    46  	return nil
    47  }
    48  
    49  func init() {
    50  	if err := checkSystemdVersion(); err != nil {
    51  		logger.Noticef("failed to check systemd version: %v", err)
    52  	}
    53  }
    54  
    55  // MockSystemdVersion mocks the systemd version to the given version. This is
    56  // only available for unit tests and will panic when run in production.
    57  func MockSystemdVersion(vers int) (restore func()) {
    58  	osutil.MustBeTestBinary("cannot mock systemd version outside of tests")
    59  	old := systemdVersion
    60  	systemdVersion = vers
    61  	return func() {
    62  		systemdVersion = old
    63  	}
    64  }
    65  
    66  func quotaGroupsAvailable(st *state.State) error {
    67  	// check if the systemd version is too old
    68  	if systemdVersion < 205 {
    69  		return fmt.Errorf("systemd version too old: snap quotas requires systemd 205 and newer (currently have %d)", systemdVersion)
    70  	}
    71  
    72  	tr := config.NewTransaction(st)
    73  	enableQuotaGroups, err := features.Flag(tr, features.QuotaGroups)
    74  	if err != nil && !config.IsNoOption(err) {
    75  		return err
    76  	}
    77  	if !enableQuotaGroups {
    78  		return fmt.Errorf("experimental feature disabled - test it by setting 'experimental.quota-groups' to true")
    79  	}
    80  
    81  	return nil
    82  }
    83  
    84  // CreateQuota attempts to create the specified quota group with the specified
    85  // snaps in it.
    86  // TODO: should this use something like QuotaGroupUpdate with fewer fields?
    87  func CreateQuota(st *state.State, name string, parentName string, snaps []string, memoryLimit quantity.Size) error {
    88  	if err := quotaGroupsAvailable(st); err != nil {
    89  		return err
    90  	}
    91  
    92  	allGrps, err := AllQuotas(st)
    93  	if err != nil {
    94  		return err
    95  	}
    96  
    97  	// TODO: switch to returning a taskset with the right handler instead of
    98  	// executing this directly
    99  	qc := QuotaControlAction{
   100  		Action:      "create",
   101  		QuotaName:   name,
   102  		MemoryLimit: memoryLimit,
   103  		AddSnaps:    snaps,
   104  		ParentName:  parentName,
   105  	}
   106  
   107  	return quotaCreate(st, nil, qc, allGrps, nil, nil)
   108  }
   109  
   110  // RemoveQuota deletes the specific quota group. Any snaps currently in the
   111  // quota will no longer be in any quota group, even if the quota group being
   112  // removed is a sub-group.
   113  // TODO: currently this only supports removing leaf sub-group groups, it doesn't
   114  // support removing parent quotas, but probably it makes sense to allow that too
   115  func RemoveQuota(st *state.State, name string) error {
   116  	if snapdenv.Preseeding() {
   117  		return fmt.Errorf("removing quota groups not supported while preseeding")
   118  	}
   119  
   120  	allGrps, err := AllQuotas(st)
   121  	if err != nil {
   122  		return err
   123  	}
   124  
   125  	// TODO: switch to returning a taskset with the right handler instead of
   126  	// executing this directly
   127  	qc := QuotaControlAction{
   128  		Action:    "remove",
   129  		QuotaName: name,
   130  	}
   131  
   132  	return quotaRemove(st, nil, qc, allGrps, nil, nil)
   133  }
   134  
   135  // QuotaGroupUpdate reflects all of the modifications that can be performed on
   136  // a quota group in one operation.
   137  type QuotaGroupUpdate struct {
   138  	// AddSnaps is the set of snaps to add to the quota group. These are
   139  	// instance names of snaps, and are appended to the existing snaps in
   140  	// the quota group
   141  	AddSnaps []string
   142  
   143  	// NewMemoryLimit is the new memory limit to be used for the quota group. If
   144  	// zero, then the quota group's memory limit is not changed.
   145  	NewMemoryLimit quantity.Size
   146  }
   147  
   148  // UpdateQuota updates the quota as per the options.
   149  // TODO: this should support more kinds of updates such as moving groups between
   150  // parents, removing sub-groups from their parents, and removing snaps from
   151  // the group.
   152  func UpdateQuota(st *state.State, name string, updateOpts QuotaGroupUpdate) error {
   153  	if err := quotaGroupsAvailable(st); err != nil {
   154  		return err
   155  	}
   156  
   157  	allGrps, err := AllQuotas(st)
   158  	if err != nil {
   159  		return err
   160  	}
   161  
   162  	// TODO: switch to returning a taskset with the right handler instead of
   163  	// executing this directly
   164  	qc := QuotaControlAction{
   165  		Action:      "update",
   166  		QuotaName:   name,
   167  		MemoryLimit: updateOpts.NewMemoryLimit,
   168  		AddSnaps:    updateOpts.AddSnaps,
   169  	}
   170  
   171  	return quotaUpdate(st, nil, qc, allGrps, nil, nil)
   172  }
   173  
   174  // EnsureSnapAbsentFromQuota ensures that the specified snap is not present
   175  // in any quota group, usually in preparation for removing that snap from the
   176  // system to keep the quota group itself consistent.
   177  // This function is idempotent, since if it was interrupted after unlocking the
   178  // state inside ensureSnapServicesForGroup it will not re-execute since the
   179  // specified snap will not be present inside the group reference in the state.
   180  func EnsureSnapAbsentFromQuota(st *state.State, snap string) error {
   181  	allGrps, err := AllQuotas(st)
   182  	if err != nil {
   183  		return err
   184  	}
   185  
   186  	// try to find the snap in any group
   187  	for _, grp := range allGrps {
   188  		for idx, sn := range grp.Snaps {
   189  			if sn == snap {
   190  				// drop this snap from the list of Snaps by swapping it with the
   191  				// last snap in the list, and then dropping the last snap from
   192  				// the list
   193  				grp.Snaps[idx] = grp.Snaps[len(grp.Snaps)-1]
   194  				grp.Snaps = grp.Snaps[:len(grp.Snaps)-1]
   195  
   196  				// update the quota group state
   197  				allGrps, err = patchQuotas(st, grp)
   198  				if err != nil {
   199  					return err
   200  				}
   201  
   202  				// ensure service states are updated - note we have to add the
   203  				// snap as an extra snap to ensure since it was removed from the
   204  				// group and thus won't be considered just by looking at the
   205  				// group pointer directly
   206  				opts := &ensureSnapServicesForGroupOptions{
   207  					allGrps:    allGrps,
   208  					extraSnaps: []string{snap},
   209  				}
   210  				// TODO: we could pass timing and progress here from the task we
   211  				// are executing as eventually
   212  				return ensureSnapServicesForGroup(st, nil, grp, opts, nil, nil)
   213  			}
   214  		}
   215  	}
   216  
   217  	// the snap wasn't in any group, nothing to do
   218  	return nil
   219  }