github.com/david-imola/snapd@v0.0.0-20210611180407-2de8ddeece6d/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  		deviceCtx, err := snapstate.DeviceCtx(st, nil, nil)
   114  		if err != nil {
   115  			return err
   116  		}
   117  
   118  		ensureOpts := &wrappers.EnsureSnapServicesOptions{}
   119  
   120  		// we need the snapd snap mounted whenever in order for services to
   121  		// start for all services on UC18+
   122  		if !deviceCtx.Classic() && deviceCtx.Model().Base() != "" {
   123  			ensureOpts.RequireMountedSnapdSnap = true
   124  		}
   125  
   126  		// get the options for this snap service
   127  		snapSvcOpts, err := servicestate.SnapServiceOptions(st, instanceName, grps)
   128  		if err != nil {
   129  			return err
   130  		}
   131  
   132  		m := map[*snap.Info]*wrappers.SnapServiceOptions{
   133  			info: snapSvcOpts,
   134  		}
   135  
   136  		// overwrite the VitalityRank we got from SnapServiceOptions to use the
   137  		// rank we calculated as part of this transaction
   138  		m[info].VitalityRank = rank
   139  
   140  		// ensure that the snap services are re-written with these units
   141  		if err := wrappers.EnsureSnapServices(m, ensureOpts, nil, progress.Null); err != nil {
   142  			return err
   143  		}
   144  
   145  		// and then restart the services
   146  
   147  		// TODO: this doesn't actually restart the services, meaning that the
   148  		// OOMScoreAdjust vitality-hint ranking doesn't take effect until the
   149  		// service is restarted by a refresh or a reboot, etc.
   150  		// TODO: this option also doesn't work with services that use
   151  		// Delegate=true, i.e. docker, greengrass, kubernetes, so we should do
   152  		// something about that combination because currently we jus silently
   153  		// apply the setting which never does anything
   154  
   155  		// XXX: copied from handlers.go:startSnapServices()
   156  
   157  		disabledSvcs, err := wrappers.QueryDisabledServices(info, progress.Null)
   158  		if err != nil {
   159  			return err
   160  		}
   161  
   162  		svcs := info.Services()
   163  		startupOrdered, err := snap.SortServices(svcs)
   164  		if err != nil {
   165  			return err
   166  		}
   167  		flags := &wrappers.StartServicesFlags{Enable: true}
   168  		tm := timings.New(nil)
   169  		if err = wrappers.StartServices(startupOrdered, disabledSvcs, flags, progress.Null, tm); err != nil {
   170  			return err
   171  		}
   172  	}
   173  
   174  	return nil
   175  }
   176  
   177  func validateVitalitySettings(tr config.Conf) error {
   178  	option, err := coreCfg(tr, vitalityOpt)
   179  	if err != nil {
   180  		return err
   181  	}
   182  	if option == "" {
   183  		return nil
   184  	}
   185  	vitalityHints := strings.Split(option, ",")
   186  	if len(vitalityHints) > 100 {
   187  		return fmt.Errorf("cannot set more than 100 snaps in %q: got %v", vitalityOpt, len(vitalityHints))
   188  	}
   189  	for _, instanceName := range vitalityHints {
   190  		if err := naming.ValidateInstance(instanceName); err != nil {
   191  			return fmt.Errorf("cannot set %q: %v", vitalityOpt, err)
   192  		}
   193  		// The "snapd" snap is always at OOMScoreAdjust=-900.
   194  		if instanceName == "snapd" {
   195  			return fmt.Errorf("cannot set %q: snapd snap vitality cannot be changed", vitalityOpt)
   196  		}
   197  	}
   198  
   199  	return nil
   200  }