github.com/stulluk/snapd@v0.0.0-20210611110309-f6d5d5bd24b0/overlord/servicestate/quota_handlers.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  	"sort"
    25  	"time"
    26  
    27  	"github.com/snapcore/snapd/logger"
    28  	"github.com/snapcore/snapd/overlord/snapstate"
    29  	"github.com/snapcore/snapd/overlord/state"
    30  	"github.com/snapcore/snapd/progress"
    31  	"github.com/snapcore/snapd/snap"
    32  	"github.com/snapcore/snapd/snap/quota"
    33  	"github.com/snapcore/snapd/snapdenv"
    34  	"github.com/snapcore/snapd/strutil"
    35  	"github.com/snapcore/snapd/systemd"
    36  	"github.com/snapcore/snapd/timings"
    37  	"github.com/snapcore/snapd/wrappers"
    38  )
    39  
    40  type ensureSnapServicesForGroupOptions struct {
    41  	// allGrps is the updated set of quota groups
    42  	allGrps map[string]*quota.Group
    43  
    44  	// extraSnaps is the set of extra snaps to consider when ensuring services,
    45  	// mainly only used when snaps are removed from quota groups
    46  	extraSnaps []string
    47  }
    48  
    49  func ensureSnapServicesForGroup(st *state.State, grp *quota.Group, opts *ensureSnapServicesForGroupOptions, meter progress.Meter, perfTimings *timings.Timings) error {
    50  	if opts == nil {
    51  		return fmt.Errorf("internal error: unset group information for ensuring")
    52  	}
    53  
    54  	allGrps := opts.allGrps
    55  
    56  	if meter == nil {
    57  		meter = progress.Null
    58  	}
    59  
    60  	if perfTimings == nil {
    61  		perfTimings = &timings.Timings{}
    62  	}
    63  
    64  	// extraSnaps []string, meter progress.Meter, perfTimings *timings.Timings
    65  	// build the map of snap infos to options to provide to EnsureSnapServices
    66  	snapSvcMap := map[*snap.Info]*wrappers.SnapServiceOptions{}
    67  	for _, sn := range append(grp.Snaps, opts.extraSnaps...) {
    68  		info, err := snapstate.CurrentInfo(st, sn)
    69  		if err != nil {
    70  			return err
    71  		}
    72  
    73  		opts, err := SnapServiceOptions(st, sn, allGrps)
    74  		if err != nil {
    75  			return err
    76  		}
    77  
    78  		snapSvcMap[info] = opts
    79  	}
    80  
    81  	// TODO: the following lines should maybe be EnsureOptionsForDevice() or
    82  	// something since it is duplicated a few places
    83  	ensureOpts := &wrappers.EnsureSnapServicesOptions{
    84  		Preseeding: snapdenv.Preseeding(),
    85  	}
    86  
    87  	// set RequireMountedSnapdSnap if we are on UC18+ only
    88  	deviceCtx, err := snapstate.DeviceCtx(st, nil, nil)
    89  	if err != nil {
    90  		return err
    91  	}
    92  
    93  	if !deviceCtx.Classic() && deviceCtx.Model().Base() != "" {
    94  		ensureOpts.RequireMountedSnapdSnap = true
    95  	}
    96  
    97  	grpsToStart := []*quota.Group{}
    98  	appsToRestartBySnap := map[*snap.Info][]*snap.AppInfo{}
    99  
   100  	collectModifiedUnits := func(app *snap.AppInfo, grp *quota.Group, unitType string, name, old, new string) {
   101  		switch unitType {
   102  		case "slice":
   103  			// this slice was either modified or written for the first time
   104  
   105  			// There are currently 3 possible cases that have different
   106  			// operations required, but we ignore one of them, so there really
   107  			// are just 2 cases we care about:
   108  			// 1. If this slice was initially written, we just need to systemctl
   109  			//    start it
   110  			// 2. If the slice was modified to be given more resources (i.e. a
   111  			//    higher memory limit), then we just need to do a daemon-reload
   112  			//    which causes systemd to modify the cgroup which will always
   113  			//    work since a cgroup can be atomically given more resources
   114  			//    without issue since the cgroup can't be using more than the
   115  			//    current limit.
   116  			// 3. If the slice was modified to be given _less_ resources (i.e. a
   117  			//    lower memory limit), then we need to stop the services before
   118  			//    issuing the daemon-reload to systemd, then do the
   119  			//    daemon-reload which will succeed in modifying the cgroup, then
   120  			//    start the services we stopped back up again. This is because
   121  			//    otherwise if the services are currently running and using more
   122  			//    resources than they would be allowed after the modification is
   123  			//    applied by systemd to the cgroup, the kernel responds with
   124  			//    EBUSY, and it isn't clear if the modification is then properly
   125  			//    in place or not.
   126  			//
   127  			// We will already have called daemon-reload at the end of
   128  			// EnsureSnapServices directly, so handling case 3 is difficult, and
   129  			// for now we disallow making this sort of change to a quota group,
   130  			// that logic is handled at a higher level than this function.
   131  			// Thus the only decision we really have to make is if the slice was
   132  			// newly written or not, and if it was save it for later
   133  			if old == "" {
   134  				grpsToStart = append(grpsToStart, grp)
   135  			}
   136  
   137  		case "service":
   138  			// in this case, the only way that a service could have been changed
   139  			// was if it was moved into or out of a slice, in both cases we need
   140  			// to restart the service
   141  			sn := app.Snap
   142  			appsToRestartBySnap[sn] = append(appsToRestartBySnap[sn], app)
   143  
   144  			// TODO: what about sockets and timers? activation units just start
   145  			// the full unit, so as long as the full unit is restarted we should
   146  			// be okay?
   147  		}
   148  	}
   149  	if err := wrappers.EnsureSnapServices(snapSvcMap, ensureOpts, collectModifiedUnits, meter); err != nil {
   150  		return err
   151  	}
   152  
   153  	if ensureOpts.Preseeding {
   154  		return nil
   155  	}
   156  
   157  	// TODO: should this logic move to wrappers in wrappers.RestartGroups()?
   158  	systemSysd := systemd.New(systemd.SystemMode, meter)
   159  
   160  	// now start the slices
   161  	for _, grp := range grpsToStart {
   162  		// TODO: what should these timeouts for stopping/restart slices be?
   163  		if err := systemSysd.Start(grp.SliceFileName()); err != nil {
   164  			return err
   165  		}
   166  	}
   167  
   168  	// after starting all the grps that we modified from EnsureSnapServices,
   169  	// we need to handle the case where a quota was removed, this will only
   170  	// happen one at a time and can be identified by the grp provided to us
   171  	// not existing in the state
   172  	if _, ok := allGrps[grp.Name]; !ok {
   173  		// stop the quota group, then remove it
   174  		if !ensureOpts.Preseeding {
   175  			if err := systemSysd.Stop(grp.SliceFileName(), 5*time.Second); err != nil {
   176  				logger.Noticef("unable to stop systemd slice while removing group %q: %v", grp.Name, err)
   177  			}
   178  		}
   179  
   180  		// TODO: this results in a second systemctl daemon-reload which is
   181  		// undesirable, we should figure out how to do this operation with a
   182  		// single daemon-reload
   183  		st.Unlock()
   184  		err := wrappers.RemoveQuotaGroup(grp, meter)
   185  		st.Lock()
   186  		if err != nil {
   187  			return err
   188  		}
   189  	}
   190  
   191  	// now restart the services for each snap that was newly moved into a quota
   192  	// group
   193  
   194  	// iterate in a sorted order over the snaps to restart their apps for easy
   195  	// tests
   196  	snaps := make([]*snap.Info, 0, len(appsToRestartBySnap))
   197  	for sn := range appsToRestartBySnap {
   198  		snaps = append(snaps, sn)
   199  	}
   200  
   201  	sort.Slice(snaps, func(i, j int) bool {
   202  		return snaps[i].InstanceName() < snaps[j].InstanceName()
   203  	})
   204  
   205  	for _, sn := range snaps {
   206  		st.Unlock()
   207  		disabledSvcs, err := wrappers.QueryDisabledServices(sn, meter)
   208  		st.Lock()
   209  		if err != nil {
   210  			return err
   211  		}
   212  
   213  		isDisabledSvc := make(map[string]bool, len(disabledSvcs))
   214  		for _, svc := range disabledSvcs {
   215  			isDisabledSvc[svc] = true
   216  		}
   217  
   218  		startupOrdered, err := snap.SortServices(appsToRestartBySnap[sn])
   219  		if err != nil {
   220  			return err
   221  		}
   222  
   223  		// drop disabled services from the startup ordering
   224  		startupOrderedMinusDisabled := make([]*snap.AppInfo, 0, len(startupOrdered)-len(disabledSvcs))
   225  
   226  		for _, svc := range startupOrdered {
   227  			if !isDisabledSvc[svc.ServiceName()] {
   228  				startupOrderedMinusDisabled = append(startupOrderedMinusDisabled, svc)
   229  			}
   230  		}
   231  
   232  		st.Unlock()
   233  		err = wrappers.RestartServices(startupOrderedMinusDisabled, nil, meter, perfTimings)
   234  		st.Lock()
   235  
   236  		if err != nil {
   237  			return err
   238  		}
   239  	}
   240  	return nil
   241  }
   242  
   243  func validateSnapForAddingToGroup(st *state.State, snaps []string, group string, allGrps map[string]*quota.Group) error {
   244  	for _, name := range snaps {
   245  		// validate that the snap exists
   246  		_, err := snapstate.CurrentInfo(st, name)
   247  		if err != nil {
   248  			return fmt.Errorf("cannot use snap %q in group %q: %v", name, group, err)
   249  		}
   250  
   251  		// check that the snap is not already in a group
   252  		for _, grp := range allGrps {
   253  			if strutil.ListContains(grp.Snaps, name) {
   254  				return fmt.Errorf("cannot add snap %q to group %q: snap already in quota group %q", name, group, grp.Name)
   255  			}
   256  		}
   257  	}
   258  
   259  	return nil
   260  }