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