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 }