github.com/Lephar/snapd@v0.0.0-20210825215435-c7fba9cef4d2/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/servicestate/internal"
    31  	"github.com/snapcore/snapd/overlord/snapstate"
    32  	"github.com/snapcore/snapd/overlord/state"
    33  	"github.com/snapcore/snapd/snapdenv"
    34  	"github.com/snapcore/snapd/systemd"
    35  )
    36  
    37  var (
    38  	systemdVersion int
    39  )
    40  
    41  // TODO: move to a systemd.AtLeast() ?
    42  func checkSystemdVersion() error {
    43  	vers, err := systemd.Version()
    44  	if err != nil {
    45  		return err
    46  	}
    47  	systemdVersion = vers
    48  	return nil
    49  }
    50  
    51  func init() {
    52  	if err := checkSystemdVersion(); err != nil {
    53  		logger.Noticef("failed to check systemd version: %v", err)
    54  	}
    55  }
    56  
    57  // MockSystemdVersion mocks the systemd version to the given version. This is
    58  // only available for unit tests and will panic when run in production.
    59  func MockSystemdVersion(vers int) (restore func()) {
    60  	osutil.MustBeTestBinary("cannot mock systemd version outside of tests")
    61  	old := systemdVersion
    62  	systemdVersion = vers
    63  	return func() {
    64  		systemdVersion = old
    65  	}
    66  }
    67  
    68  func quotaGroupsAvailable(st *state.State) error {
    69  	// check if the systemd version is too old
    70  	if systemdVersion < 230 {
    71  		return fmt.Errorf("systemd version too old: snap quotas requires systemd 230 and newer (currently have %d)", systemdVersion)
    72  	}
    73  
    74  	tr := config.NewTransaction(st)
    75  	enableQuotaGroups, err := features.Flag(tr, features.QuotaGroups)
    76  	if err != nil && !config.IsNoOption(err) {
    77  		return err
    78  	}
    79  	if !enableQuotaGroups {
    80  		return fmt.Errorf("experimental feature disabled - test it by setting 'experimental.quota-groups' to true")
    81  	}
    82  
    83  	return nil
    84  }
    85  
    86  // CreateQuota attempts to create the specified quota group with the specified
    87  // snaps in it.
    88  // TODO: should this use something like QuotaGroupUpdate with fewer fields?
    89  func CreateQuota(st *state.State, name string, parentName string, snaps []string, memoryLimit quantity.Size) (*state.TaskSet, error) {
    90  	if err := quotaGroupsAvailable(st); err != nil {
    91  		return nil, err
    92  	}
    93  
    94  	allGrps, err := AllQuotas(st)
    95  	if err != nil {
    96  		return nil, err
    97  	}
    98  
    99  	// make sure the group does not exist yet
   100  	if _, ok := allGrps[name]; ok {
   101  		return nil, fmt.Errorf("group %q already exists", name)
   102  	}
   103  
   104  	if memoryLimit == 0 {
   105  		return nil, fmt.Errorf("cannot create quota group with no memory limit set")
   106  	}
   107  
   108  	// make sure the memory limit is at least 4K, that is the minimum size
   109  	// to allow nesting, otherwise groups with less than 4K will trigger the
   110  	// oom killer to be invoked when a new group is added as a sub-group to the
   111  	// larger group.
   112  	if memoryLimit <= 4*quantity.SizeKiB {
   113  		return nil, fmt.Errorf("memory limit for group %q is too small: size must be larger than 4KB", name)
   114  	}
   115  
   116  	// make sure the specified snaps exist and aren't currently in another group
   117  	if err := validateSnapForAddingToGroup(st, snaps, name, allGrps); err != nil {
   118  		return nil, err
   119  	}
   120  
   121  	if err := CheckQuotaChangeConflictMany(st, []string{name}); err != nil {
   122  		return nil, err
   123  	}
   124  	if err := snapstate.CheckChangeConflictMany(st, snaps, ""); err != nil {
   125  		return nil, err
   126  	}
   127  
   128  	// create the task with the action in it
   129  	qc := QuotaControlAction{
   130  		Action:      "create",
   131  		QuotaName:   name,
   132  		MemoryLimit: memoryLimit,
   133  		AddSnaps:    snaps,
   134  		ParentName:  parentName,
   135  	}
   136  
   137  	ts := state.NewTaskSet()
   138  
   139  	summary := fmt.Sprintf("Create quota group %q", name)
   140  	task := st.NewTask("quota-control", summary)
   141  	task.Set("quota-control-actions", []QuotaControlAction{qc})
   142  	ts.AddTask(task)
   143  
   144  	return ts, nil
   145  }
   146  
   147  // RemoveQuota deletes the specific quota group. Any snaps currently in the
   148  // quota will no longer be in any quota group, even if the quota group being
   149  // removed is a sub-group.
   150  // TODO: currently this only supports removing leaf sub-group groups, it doesn't
   151  // support removing parent quotas, but probably it makes sense to allow that too
   152  func RemoveQuota(st *state.State, name string) (*state.TaskSet, error) {
   153  	if snapdenv.Preseeding() {
   154  		return nil, fmt.Errorf("removing quota groups not supported while preseeding")
   155  	}
   156  
   157  	allGrps, err := AllQuotas(st)
   158  	if err != nil {
   159  		return nil, err
   160  	}
   161  
   162  	// make sure the group exists
   163  	grp, ok := allGrps[name]
   164  	if !ok {
   165  		return nil, fmt.Errorf("cannot remove non-existent quota group %q", name)
   166  	}
   167  
   168  	// XXX: remove this limitation eventually
   169  	if len(grp.SubGroups) != 0 {
   170  		return nil, fmt.Errorf("cannot remove quota group %q with sub-groups, remove the sub-groups first", name)
   171  	}
   172  
   173  	if err := CheckQuotaChangeConflictMany(st, []string{name}); err != nil {
   174  		return nil, err
   175  	}
   176  	if err := snapstate.CheckChangeConflictMany(st, grp.Snaps, ""); err != nil {
   177  		return nil, err
   178  	}
   179  
   180  	qc := QuotaControlAction{
   181  		Action:    "remove",
   182  		QuotaName: name,
   183  	}
   184  
   185  	ts := state.NewTaskSet()
   186  
   187  	summary := fmt.Sprintf("Remove quota group %q", name)
   188  	task := st.NewTask("quota-control", summary)
   189  	task.Set("quota-control-actions", []QuotaControlAction{qc})
   190  	ts.AddTask(task)
   191  
   192  	return ts, nil
   193  }
   194  
   195  // QuotaGroupUpdate reflects all of the modifications that can be performed on
   196  // a quota group in one operation.
   197  type QuotaGroupUpdate struct {
   198  	// AddSnaps is the set of snaps to add to the quota group. These are
   199  	// instance names of snaps, and are appended to the existing snaps in
   200  	// the quota group
   201  	AddSnaps []string
   202  
   203  	// NewMemoryLimit is the new memory limit to be used for the quota group. If
   204  	// zero, then the quota group's memory limit is not changed.
   205  	NewMemoryLimit quantity.Size
   206  }
   207  
   208  // UpdateQuota updates the quota as per the options.
   209  // TODO: this should support more kinds of updates such as moving groups between
   210  // parents, removing sub-groups from their parents, and removing snaps from
   211  // the group.
   212  func UpdateQuota(st *state.State, name string, updateOpts QuotaGroupUpdate) (*state.TaskSet, error) {
   213  	if err := quotaGroupsAvailable(st); err != nil {
   214  		return nil, err
   215  	}
   216  
   217  	allGrps, err := AllQuotas(st)
   218  	if err != nil {
   219  		return nil, err
   220  	}
   221  
   222  	grp, ok := allGrps[name]
   223  	if !ok {
   224  		return nil, fmt.Errorf("group %q does not exist", name)
   225  	}
   226  
   227  	// check that the memory limit is not being decreased
   228  	if updateOpts.NewMemoryLimit != 0 {
   229  		// we disallow decreasing the memory limit because it is difficult to do
   230  		// so correctly with the current state of our code in
   231  		// EnsureSnapServices, see comment in ensureSnapServicesForGroup for
   232  		// full details
   233  		if updateOpts.NewMemoryLimit < grp.MemoryLimit {
   234  			return nil, fmt.Errorf("cannot decrease memory limit of existing quota-group, remove and re-create it to decrease the limit")
   235  		}
   236  	}
   237  
   238  	// now ensure that all of the snaps mentioned in AddSnaps exist as snaps and
   239  	// that they aren't already in an existing quota group
   240  	if err := validateSnapForAddingToGroup(st, updateOpts.AddSnaps, name, allGrps); err != nil {
   241  		return nil, err
   242  	}
   243  
   244  	if err := CheckQuotaChangeConflictMany(st, []string{name}); err != nil {
   245  		return nil, err
   246  	}
   247  	if err := snapstate.CheckChangeConflictMany(st, updateOpts.AddSnaps, ""); err != nil {
   248  		return nil, err
   249  	}
   250  
   251  	// create the action and the correspoding task set
   252  	qc := QuotaControlAction{
   253  		Action:      "update",
   254  		QuotaName:   name,
   255  		MemoryLimit: updateOpts.NewMemoryLimit,
   256  		AddSnaps:    updateOpts.AddSnaps,
   257  	}
   258  
   259  	ts := state.NewTaskSet()
   260  
   261  	summary := fmt.Sprintf("Update quota group %q", name)
   262  	task := st.NewTask("quota-control", summary)
   263  	task.Set("quota-control-actions", []QuotaControlAction{qc})
   264  	ts.AddTask(task)
   265  
   266  	return ts, nil
   267  }
   268  
   269  // EnsureSnapAbsentFromQuota ensures that the specified snap is not present
   270  // in any quota group, usually in preparation for removing that snap from the
   271  // system to keep the quota group itself consistent.
   272  // This function is idempotent, since if it was interrupted after unlocking the
   273  // state inside ensureSnapServicesForGroup it will not re-execute since the
   274  // specified snap will not be present inside the group reference in the state.
   275  func EnsureSnapAbsentFromQuota(st *state.State, snap string) error {
   276  	allGrps, err := AllQuotas(st)
   277  	if err != nil {
   278  		return err
   279  	}
   280  
   281  	// try to find the snap in any group
   282  	for _, grp := range allGrps {
   283  		for idx, sn := range grp.Snaps {
   284  			if sn == snap {
   285  				// drop this snap from the list of Snaps by swapping it with the
   286  				// last snap in the list, and then dropping the last snap from
   287  				// the list
   288  				grp.Snaps[idx] = grp.Snaps[len(grp.Snaps)-1]
   289  				grp.Snaps = grp.Snaps[:len(grp.Snaps)-1]
   290  
   291  				// update the quota group state
   292  				allGrps, err = internal.PatchQuotas(st, grp)
   293  				if err != nil {
   294  					return err
   295  				}
   296  
   297  				// ensure service states are updated - note we have to add the
   298  				// snap as an extra snap to ensure since it was removed from the
   299  				// group and thus won't be considered just by looking at the
   300  				// group pointer directly
   301  				opts := &ensureSnapServicesForGroupOptions{
   302  					allGrps:    allGrps,
   303  					extraSnaps: []string{snap},
   304  				}
   305  				// TODO: we could pass timing and progress here from the task we
   306  				// are executing as eventually
   307  				return ensureSnapServicesStateForGroup(st, grp, opts)
   308  			}
   309  		}
   310  	}
   311  
   312  	// the snap wasn't in any group, nothing to do
   313  	return nil
   314  }
   315  
   316  // QuotaChangeConflictError represents an error because of quota group conflicts between changes.
   317  type QuotaChangeConflictError struct {
   318  	Quota      string
   319  	ChangeKind string
   320  	// a Message is optional, otherwise one is composed from the other information
   321  	Message string
   322  }
   323  
   324  func (e *QuotaChangeConflictError) Error() string {
   325  	if e.Message != "" {
   326  		return e.Message
   327  	}
   328  	if e.ChangeKind != "" {
   329  		return fmt.Sprintf("quota group %q has %q change in progress", e.Quota, e.ChangeKind)
   330  	}
   331  	return fmt.Sprintf("quota group %q has changes in progress", e.Quota)
   332  }
   333  
   334  // CheckQuotaChangeConflictMany ensures that for the given quota groups no other
   335  // changes that alters them (like create, update, remove) are in
   336  // progress. If a conflict is detected an error is returned.
   337  func CheckQuotaChangeConflictMany(st *state.State, quotaNames []string) error {
   338  	quotaMap := make(map[string]bool, len(quotaNames))
   339  	for _, k := range quotaNames {
   340  		quotaMap[k] = true
   341  	}
   342  
   343  	for _, task := range st.Tasks() {
   344  		chg := task.Change()
   345  		if chg == nil || chg.IsReady() {
   346  			continue
   347  		}
   348  
   349  		quotas, err := affectedQuotas(task)
   350  		if err != nil {
   351  			return err
   352  		}
   353  
   354  		for _, quota := range quotas {
   355  			if quotaMap[quota] {
   356  				return &QuotaChangeConflictError{Quota: quota, ChangeKind: chg.Kind()}
   357  			}
   358  		}
   359  	}
   360  
   361  	return nil
   362  }
   363  
   364  func affectedQuotas(task *state.Task) ([]string, error) {
   365  	// so far only quota-control is relevant
   366  	if task.Kind() != "quota-control" {
   367  		return nil, nil
   368  	}
   369  
   370  	qcs := []QuotaControlAction{}
   371  	if err := task.Get("quota-control-actions", &qcs); err != nil {
   372  		return nil, fmt.Errorf("internal error: cannot get quota-control-actions: %v", err)
   373  	}
   374  	quotas := make([]string, 0, len(qcs))
   375  	for _, qc := range qcs {
   376  		// TODO: the affected quotas will expand beyond this
   377  		// if we support reparenting or orphaning
   378  		quotas = append(quotas, qc.QuotaName)
   379  	}
   380  	return quotas, nil
   381  }