github.com/meulengracht/snapd@v0.0.0-20210719210640-8bde69bcc84e/overlord/configstate/configcore/vitality.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  // +build !nomanagers
     3  
     4  /*
     5   * Copyright (C) 2020 Canonical Ltd
     6   *
     7   * This program is free software: you can redistribute it and/or modify
     8   * it under the terms of the GNU General Public License version 3 as
     9   * published by the Free Software Foundation.
    10   *
    11   * This program is distributed in the hope that it will be useful,
    12   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    13   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    14   * GNU General Public License for more details.
    15   *
    16   * You should have received a copy of the GNU General Public License
    17   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    18   *
    19   */
    20  
    21  package configcore
    22  
    23  import (
    24  	"fmt"
    25  	"strings"
    26  
    27  	"github.com/snapcore/snapd/overlord/configstate/config"
    28  	"github.com/snapcore/snapd/overlord/servicestate"
    29  	"github.com/snapcore/snapd/overlord/snapstate"
    30  	"github.com/snapcore/snapd/overlord/state"
    31  	"github.com/snapcore/snapd/progress"
    32  	"github.com/snapcore/snapd/snap"
    33  	"github.com/snapcore/snapd/snap/naming"
    34  	"github.com/snapcore/snapd/timings"
    35  	"github.com/snapcore/snapd/wrappers"
    36  )
    37  
    38  const vitalityOpt = "resilience.vitality-hint"
    39  
    40  func init() {
    41  	// add supported configuration of this module
    42  	supportedConfigurations["core."+vitalityOpt] = true
    43  }
    44  
    45  func handleVitalityConfiguration(tr config.Conf, opts *fsOnlyContext) error {
    46  	var pristineVitalityStr, newVitalityStr string
    47  
    48  	if err := tr.GetPristine("core", vitalityOpt, &pristineVitalityStr); err != nil && !config.IsNoOption(err) {
    49  		return err
    50  	}
    51  	if err := tr.Get("core", vitalityOpt, &newVitalityStr); err != nil && !config.IsNoOption(err) {
    52  		return err
    53  	}
    54  	if pristineVitalityStr == newVitalityStr {
    55  		return nil
    56  	}
    57  
    58  	st := tr.State()
    59  	st.Lock()
    60  	defer st.Unlock()
    61  
    62  	// TODO: Reimplement most of this as a servicestate.UpdateVitalityRank
    63  
    64  	oldVitalityMap := map[string]int{}
    65  	newVitalityMap := map[string]int{}
    66  	// assign "0" (delete) rank to all old entries
    67  	for i, instanceName := range strings.Split(pristineVitalityStr, ",") {
    68  		oldVitalityMap[instanceName] = i + 1
    69  		newVitalityMap[instanceName] = 0
    70  	}
    71  	// build rank of the new entries
    72  	for i, instanceName := range strings.Split(newVitalityStr, ",") {
    73  		newVitalityMap[instanceName] = i + 1
    74  	}
    75  
    76  	// use a single cache of the quota groups for calculating the quota groups
    77  	// that services should be in
    78  	grps, err := servicestate.AllQuotas(st)
    79  	if err != nil {
    80  		return err
    81  	}
    82  
    83  	for instanceName, rank := range newVitalityMap {
    84  		var snapst snapstate.SnapState
    85  		err := snapstate.Get(st, instanceName, &snapst)
    86  		// not installed, vitality-score will be applied when the snap
    87  		// gets installed
    88  		if err == state.ErrNoState {
    89  			continue
    90  		}
    91  		if err != nil {
    92  			return err
    93  		}
    94  		// not active, vitality-score will be applied when the snap
    95  		// becomes active
    96  		if !snapst.Active {
    97  			continue
    98  		}
    99  		info, err := snapst.CurrentInfo()
   100  		if err != nil {
   101  			return err
   102  		}
   103  
   104  		// nothing to do if rank is unchanged
   105  		if oldVitalityMap[instanceName] == newVitalityMap[instanceName] {
   106  			continue
   107  		}
   108  
   109  		// rank changed, rewrite/restart services
   110  
   111  		// first get the device context to decide if we need to set
   112  		// RequireMountedSnapdSnap
   113  		// TODO: use sysconfig.Device instead
   114  		deviceCtx, err := snapstate.DeviceCtx(st, nil, nil)
   115  		if err != nil {
   116  			return err
   117  		}
   118  
   119  		ensureOpts := &wrappers.EnsureSnapServicesOptions{}
   120  
   121  		// we need the snapd snap mounted whenever in order for services to
   122  		// start for all services on UC18+
   123  		if !deviceCtx.Classic() && deviceCtx.Model().Base() != "" {
   124  			ensureOpts.RequireMountedSnapdSnap = true
   125  		}
   126  
   127  		// get the options for this snap service
   128  		snapSvcOpts, err := servicestate.SnapServiceOptions(st, instanceName, grps)
   129  		if err != nil {
   130  			return err
   131  		}
   132  
   133  		m := map[*snap.Info]*wrappers.SnapServiceOptions{
   134  			info: snapSvcOpts,
   135  		}
   136  
   137  		// overwrite the VitalityRank we got from SnapServiceOptions to use the
   138  		// rank we calculated as part of this transaction
   139  		m[info].VitalityRank = rank
   140  
   141  		// ensure that the snap services are re-written with these units
   142  		if err := wrappers.EnsureSnapServices(m, ensureOpts, nil, progress.Null); err != nil {
   143  			return err
   144  		}
   145  
   146  		// and then restart the services
   147  
   148  		// TODO: this doesn't actually restart the services, meaning that the
   149  		// OOMScoreAdjust vitality-hint ranking doesn't take effect until the
   150  		// service is restarted by a refresh or a reboot, etc.
   151  		// TODO: this option also doesn't work with services that use
   152  		// Delegate=true, i.e. docker, greengrass, kubernetes, so we should do
   153  		// something about that combination because currently we jus silently
   154  		// apply the setting which never does anything
   155  
   156  		// XXX: copied from handlers.go:startSnapServices()
   157  
   158  		disabledSvcs, err := wrappers.QueryDisabledServices(info, progress.Null)
   159  		if err != nil {
   160  			return err
   161  		}
   162  
   163  		svcs := info.Services()
   164  		startupOrdered, err := snap.SortServices(svcs)
   165  		if err != nil {
   166  			return err
   167  		}
   168  		flags := &wrappers.StartServicesFlags{Enable: true}
   169  		tm := timings.New(nil)
   170  		if err = wrappers.StartServices(startupOrdered, disabledSvcs, flags, progress.Null, tm); err != nil {
   171  			return err
   172  		}
   173  	}
   174  
   175  	return nil
   176  }
   177  
   178  func validateVitalitySettings(tr config.Conf) error {
   179  	option, err := coreCfg(tr, vitalityOpt)
   180  	if err != nil {
   181  		return err
   182  	}
   183  	if option == "" {
   184  		return nil
   185  	}
   186  	vitalityHints := strings.Split(option, ",")
   187  	if len(vitalityHints) > 100 {
   188  		return fmt.Errorf("cannot set more than 100 snaps in %q: got %v", vitalityOpt, len(vitalityHints))
   189  	}
   190  	for _, instanceName := range vitalityHints {
   191  		if err := naming.ValidateInstance(instanceName); err != nil {
   192  			return fmt.Errorf("cannot set %q: %v", vitalityOpt, err)
   193  		}
   194  		// The "snapd" snap is always at OOMScoreAdjust=-900.
   195  		if instanceName == "snapd" {
   196  			return fmt.Errorf("cannot set %q: snapd snap vitality cannot be changed", vitalityOpt)
   197  		}
   198  	}
   199  
   200  	return nil
   201  }