github.com/Lephar/snapd@v0.0.0-20210825215435-c7fba9cef4d2/overlord/servicestate/servicestate.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2015-2020 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 "path/filepath" 25 "sort" 26 "strings" 27 "time" 28 29 "github.com/snapcore/snapd/client" 30 "github.com/snapcore/snapd/overlord/cmdstate" 31 "github.com/snapcore/snapd/overlord/configstate/config" 32 "github.com/snapcore/snapd/overlord/hookstate" 33 "github.com/snapcore/snapd/overlord/snapstate" 34 "github.com/snapcore/snapd/overlord/state" 35 "github.com/snapcore/snapd/snap" 36 "github.com/snapcore/snapd/snap/quota" 37 "github.com/snapcore/snapd/strutil" 38 "github.com/snapcore/snapd/systemd" 39 "github.com/snapcore/snapd/wrappers" 40 ) 41 42 type Instruction struct { 43 Action string `json:"action"` 44 Names []string `json:"names"` 45 client.StartOptions 46 client.StopOptions 47 client.RestartOptions 48 } 49 50 type ServiceActionConflictError struct{ error } 51 52 func computeExplicitServices(appInfos []*snap.AppInfo, names []string) map[string][]string { 53 explicitServices := make(map[string][]string, len(appInfos)) 54 // requested maps "snapname.appname" to app name. 55 requested := make(map[string]bool, len(names)) 56 for _, name := range names { 57 // Name might also be a snap name (or other strings the user wrote on 58 // the command line), but the loop below ensures that this function 59 // considers only application names. 60 requested[name] = true 61 } 62 63 for _, app := range appInfos { 64 snapName := app.Snap.InstanceName() 65 // app.String() gives "snapname.appname" 66 if requested[app.String()] { 67 explicitServices[snapName] = append(explicitServices[snapName], app.Name) 68 } 69 } 70 71 return explicitServices 72 } 73 74 // serviceControlTs creates "service-control" task for every snap derived from appInfos. 75 func serviceControlTs(st *state.State, appInfos []*snap.AppInfo, inst *Instruction) (*state.TaskSet, error) { 76 servicesBySnap := make(map[string][]string, len(appInfos)) 77 explicitServices := computeExplicitServices(appInfos, inst.Names) 78 sortedNames := make([]string, 0, len(appInfos)) 79 80 // group services by snap, we need to create one task for every affected snap 81 for _, app := range appInfos { 82 snapName := app.Snap.InstanceName() 83 if _, ok := servicesBySnap[snapName]; !ok { 84 sortedNames = append(sortedNames, snapName) 85 } 86 servicesBySnap[snapName] = append(servicesBySnap[snapName], app.Name) 87 } 88 sort.Strings(sortedNames) 89 90 ts := state.NewTaskSet() 91 var prev *state.Task 92 for _, snapName := range sortedNames { 93 var snapst snapstate.SnapState 94 if err := snapstate.Get(st, snapName, &snapst); err != nil { 95 if err == state.ErrNoState { 96 return nil, fmt.Errorf("snap not found: %s", snapName) 97 } 98 return nil, err 99 } 100 101 cmd := &ServiceAction{SnapName: snapName} 102 switch { 103 case inst.Action == "start": 104 cmd.Action = "start" 105 if inst.Enable { 106 cmd.ActionModifier = "enable" 107 } 108 case inst.Action == "stop": 109 cmd.Action = "stop" 110 if inst.Disable { 111 cmd.ActionModifier = "disable" 112 } 113 case inst.Action == "restart": 114 if inst.Reload { 115 cmd.Action = "reload-or-restart" 116 } else { 117 cmd.Action = "restart" 118 } 119 default: 120 return nil, fmt.Errorf("unknown action %q", inst.Action) 121 } 122 123 svcs := servicesBySnap[snapName] 124 sort.Strings(svcs) 125 cmd.Services = svcs 126 explicitSvcs := explicitServices[snapName] 127 sort.Strings(explicitSvcs) 128 cmd.ExplicitServices = explicitSvcs 129 130 summary := fmt.Sprintf("Run service command %q for services %q of snap %q", cmd.Action, svcs, cmd.SnapName) 131 task := st.NewTask("service-control", summary) 132 task.Set("service-action", cmd) 133 if prev != nil { 134 task.WaitFor(prev) 135 } 136 prev = task 137 ts.AddTask(task) 138 } 139 return ts, nil 140 } 141 142 // Flags carries extra flags for Control 143 type Flags struct { 144 // CreateExecCommandTasks tells Control method to create exec-command tasks 145 // (alongside service-control tasks) for compatibility with old snapd. 146 CreateExecCommandTasks bool 147 } 148 149 // Control creates a taskset for starting/stopping/restarting services via systemctl. 150 // The appInfos and inst define the services and the command to execute. 151 // Context is used to determine change conflicts - we will not conflict with 152 // tasks from same change as that of context's. 153 func Control(st *state.State, appInfos []*snap.AppInfo, inst *Instruction, flags *Flags, context *hookstate.Context) ([]*state.TaskSet, error) { 154 var tts []*state.TaskSet 155 var ctlcmds []string 156 157 // create exec-command tasks for compatibility with old snapd 158 if flags != nil && flags.CreateExecCommandTasks { 159 switch { 160 case inst.Action == "start": 161 if inst.Enable { 162 ctlcmds = []string{"enable"} 163 } 164 ctlcmds = append(ctlcmds, "start") 165 case inst.Action == "stop": 166 if inst.Disable { 167 ctlcmds = []string{"disable"} 168 } 169 ctlcmds = append(ctlcmds, "stop") 170 case inst.Action == "restart": 171 if inst.Reload { 172 ctlcmds = []string{"reload-or-restart"} 173 } else { 174 ctlcmds = []string{"restart"} 175 } 176 default: 177 return nil, fmt.Errorf("unknown action %q", inst.Action) 178 } 179 } 180 181 svcs := make([]string, 0, len(appInfos)) 182 snapNames := make([]string, 0, len(appInfos)) 183 lastName := "" 184 names := make([]string, len(appInfos)) 185 for i, svc := range appInfos { 186 svcs = append(svcs, svc.ServiceName()) 187 snapName := svc.Snap.InstanceName() 188 names[i] = snapName + "." + svc.Name 189 if snapName != lastName { 190 snapNames = append(snapNames, snapName) 191 lastName = snapName 192 } 193 } 194 195 var ignoreChangeID string 196 if context != nil { 197 ignoreChangeID = context.ChangeID() 198 } 199 if err := snapstate.CheckChangeConflictMany(st, snapNames, ignoreChangeID); err != nil { 200 return nil, &ServiceActionConflictError{err} 201 } 202 203 for _, cmd := range ctlcmds { 204 argv := append([]string{"systemctl", cmd}, svcs...) 205 desc := fmt.Sprintf("%s of %v", cmd, names) 206 // Give the systemctl a maximum time of 61 for now. 207 // 208 // Longer term we need to refactor this code and 209 // reuse the snapd/systemd and snapd/wrapper packages 210 // to control the timeout in a single place. 211 ts := cmdstate.ExecWithTimeout(st, desc, argv, 61*time.Second) 212 213 // set ignore flag on the tasks, new snapd uses service-control tasks. 214 ignore := true 215 for _, t := range ts.Tasks() { 216 t.Set("ignore", ignore) 217 } 218 tts = append(tts, ts) 219 } 220 221 // XXX: serviceControlTs could be merged with above logic at the cost of 222 // slightly more complicated logic. 223 ts, err := serviceControlTs(st, appInfos, inst) 224 if err != nil { 225 return nil, err 226 } 227 tts = append(tts, ts) 228 229 // make a taskset wait for its predecessor 230 for i := 1; i < len(tts); i++ { 231 tts[i].WaitAll(tts[i-1]) 232 } 233 234 return tts, nil 235 } 236 237 // StatusDecorator supports decorating client.AppInfos with service status. 238 type StatusDecorator struct { 239 sysd systemd.Systemd 240 globalUserSysd systemd.Systemd 241 } 242 243 // NewStatusDecorator returns a new StatusDecorator. 244 func NewStatusDecorator(rep interface { 245 Notify(string) 246 }) *StatusDecorator { 247 return &StatusDecorator{ 248 sysd: systemd.New(systemd.SystemMode, rep), 249 globalUserSysd: systemd.New(systemd.GlobalUserMode, rep), 250 } 251 } 252 253 // DecorateWithStatus adds service status information to the given 254 // client.AppInfo associated with the given snap.AppInfo. 255 // If the snap is inactive or the app is not service it does nothing. 256 func (sd *StatusDecorator) DecorateWithStatus(appInfo *client.AppInfo, snapApp *snap.AppInfo) error { 257 if appInfo.Snap != snapApp.Snap.InstanceName() || appInfo.Name != snapApp.Name { 258 return fmt.Errorf("internal error: misassociated app info %v and client app info %s.%s", snapApp, appInfo.Snap, appInfo.Name) 259 } 260 if !snapApp.Snap.IsActive() || !snapApp.IsService() { 261 // nothing to do 262 return nil 263 } 264 var sysd systemd.Systemd 265 switch snapApp.DaemonScope { 266 case snap.SystemDaemon: 267 sysd = sd.sysd 268 case snap.UserDaemon: 269 sysd = sd.globalUserSysd 270 default: 271 return fmt.Errorf("internal error: unknown daemon-scope %q", snapApp.DaemonScope) 272 } 273 274 // collect all services for a single call to systemctl 275 extra := len(snapApp.Sockets) 276 if snapApp.Timer != nil { 277 extra++ 278 } 279 serviceNames := make([]string, 0, 1+extra) 280 serviceNames = append(serviceNames, snapApp.ServiceName()) 281 282 sockSvcFileToName := make(map[string]string, len(snapApp.Sockets)) 283 for _, sock := range snapApp.Sockets { 284 sockUnit := filepath.Base(sock.File()) 285 sockSvcFileToName[sockUnit] = sock.Name 286 serviceNames = append(serviceNames, sockUnit) 287 } 288 if snapApp.Timer != nil { 289 timerUnit := filepath.Base(snapApp.Timer.File()) 290 serviceNames = append(serviceNames, timerUnit) 291 } 292 293 // sysd.Status() makes sure that we get only the units we asked 294 // for and raises an error otherwise 295 sts, err := sysd.Status(serviceNames...) 296 if err != nil { 297 return fmt.Errorf("cannot get status of services of app %q: %v", appInfo.Name, err) 298 } 299 if len(sts) != len(serviceNames) { 300 return fmt.Errorf("cannot get status of services of app %q: expected %d results, got %d", appInfo.Name, len(serviceNames), len(sts)) 301 } 302 for _, st := range sts { 303 switch filepath.Ext(st.UnitName) { 304 case ".service": 305 appInfo.Enabled = st.Enabled 306 appInfo.Active = st.Active 307 case ".timer": 308 appInfo.Activators = append(appInfo.Activators, client.AppActivator{ 309 Name: snapApp.Name, 310 Enabled: st.Enabled, 311 Active: st.Active, 312 Type: "timer", 313 }) 314 case ".socket": 315 appInfo.Activators = append(appInfo.Activators, client.AppActivator{ 316 Name: sockSvcFileToName[st.UnitName], 317 Enabled: st.Enabled, 318 Active: st.Active, 319 Type: "socket", 320 }) 321 } 322 } 323 // Decorate with D-Bus names that activate this service 324 for _, slot := range snapApp.ActivatesOn { 325 var busName string 326 if err := slot.Attr("name", &busName); err != nil { 327 return fmt.Errorf("cannot get D-Bus bus name of slot %q: %v", slot.Name, err) 328 } 329 // D-Bus activators do not correspond to systemd 330 // units, so don't have the concept of being disabled 331 // or deactivated. As the service activation file is 332 // created when the snap is installed, report as 333 // enabled/active. 334 appInfo.Activators = append(appInfo.Activators, client.AppActivator{ 335 Name: busName, 336 Enabled: true, 337 Active: true, 338 Type: "dbus", 339 }) 340 } 341 342 return nil 343 } 344 345 // SnapServiceOptions computes the options to configure services for 346 // the given snap. This function might not check for the existence 347 // of instanceName. It also takes as argument a map of all quota groups as an 348 // optimization, the map if non-nil is used in place of checking state for 349 // whether or not the specified snap is in a quota group or not. If nil, state 350 // is consulted directly instead. 351 func SnapServiceOptions(st *state.State, instanceName string, quotaGroups map[string]*quota.Group) (opts *wrappers.SnapServiceOptions, err error) { 352 // if quotaGroups was not provided to us, then go get that 353 if quotaGroups == nil { 354 allGrps, err := AllQuotas(st) 355 if err != nil && err != state.ErrNoState { 356 return nil, err 357 } 358 quotaGroups = allGrps 359 } 360 361 opts = &wrappers.SnapServiceOptions{} 362 363 tr := config.NewTransaction(st) 364 var vitalityStr string 365 err = tr.GetMaybe("core", "resilience.vitality-hint", &vitalityStr) 366 if err != nil { 367 return nil, err 368 } 369 for i, s := range strings.Split(vitalityStr, ",") { 370 if s == instanceName { 371 opts.VitalityRank = i + 1 372 break 373 } 374 } 375 376 // also check for quota group for this instance name 377 for _, grp := range quotaGroups { 378 if strutil.ListContains(grp.Snaps, instanceName) { 379 opts.QuotaGroup = grp 380 break 381 } 382 } 383 384 return opts, nil 385 }