github.com/chipaca/snappy@v0.0.0-20210104084008-1f06296fe8ad/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 "time" 27 28 "github.com/snapcore/snapd/client" 29 "github.com/snapcore/snapd/overlord/cmdstate" 30 "github.com/snapcore/snapd/overlord/hookstate" 31 "github.com/snapcore/snapd/overlord/snapstate" 32 "github.com/snapcore/snapd/overlord/state" 33 "github.com/snapcore/snapd/snap" 34 "github.com/snapcore/snapd/systemd" 35 ) 36 37 type Instruction struct { 38 Action string `json:"action"` 39 Names []string `json:"names"` 40 client.StartOptions 41 client.StopOptions 42 client.RestartOptions 43 } 44 45 type ServiceActionConflictError struct{ error } 46 47 // serviceControlTs creates "service-control" task for every snap derived from appInfos. 48 func serviceControlTs(st *state.State, appInfos []*snap.AppInfo, inst *Instruction) (*state.TaskSet, error) { 49 servicesBySnap := make(map[string][]string, len(appInfos)) 50 sortedNames := make([]string, 0, len(appInfos)) 51 52 // group services by snap, we need to create one task for every affected snap 53 for _, app := range appInfos { 54 snapName := app.Snap.InstanceName() 55 if _, ok := servicesBySnap[snapName]; !ok { 56 sortedNames = append(sortedNames, snapName) 57 } 58 servicesBySnap[snapName] = append(servicesBySnap[snapName], app.Name) 59 } 60 sort.Strings(sortedNames) 61 62 ts := state.NewTaskSet() 63 var prev *state.Task 64 for _, snapName := range sortedNames { 65 var snapst snapstate.SnapState 66 if err := snapstate.Get(st, snapName, &snapst); err != nil { 67 if err == state.ErrNoState { 68 return nil, fmt.Errorf("snap not found: %s", snapName) 69 } 70 return nil, err 71 } 72 73 cmd := &ServiceAction{SnapName: snapName} 74 switch { 75 case inst.Action == "start": 76 cmd.Action = "start" 77 if inst.Enable { 78 cmd.ActionModifier = "enable" 79 } 80 case inst.Action == "stop": 81 cmd.Action = "stop" 82 if inst.Disable { 83 cmd.ActionModifier = "disable" 84 } 85 case inst.Action == "restart": 86 if inst.Reload { 87 cmd.Action = "reload-or-restart" 88 } else { 89 cmd.Action = "restart" 90 } 91 default: 92 return nil, fmt.Errorf("unknown action %q", inst.Action) 93 } 94 95 svcs := servicesBySnap[snapName] 96 sort.Strings(svcs) 97 cmd.Services = svcs 98 99 summary := fmt.Sprintf("Run service command %q for services %q of snap %q", cmd.Action, svcs, cmd.SnapName) 100 task := st.NewTask("service-control", summary) 101 task.Set("service-action", cmd) 102 if prev != nil { 103 task.WaitFor(prev) 104 } 105 prev = task 106 ts.AddTask(task) 107 } 108 return ts, nil 109 } 110 111 // Flags carries extra flags for Control 112 type Flags struct { 113 // CreateExecCommandTasks tells Control method to create exec-command tasks 114 // (alongside service-control tasks) for compatibility with old snapd. 115 CreateExecCommandTasks bool 116 } 117 118 // Control creates a taskset for starting/stopping/restarting services via systemctl. 119 // The appInfos and inst define the services and the command to execute. 120 // Context is used to determine change conflicts - we will not conflict with 121 // tasks from same change as that of context's. 122 func Control(st *state.State, appInfos []*snap.AppInfo, inst *Instruction, flags *Flags, context *hookstate.Context) ([]*state.TaskSet, error) { 123 var tts []*state.TaskSet 124 var ctlcmds []string 125 126 // create exec-command tasks for compatibility with old snapd 127 if flags != nil && flags.CreateExecCommandTasks { 128 switch { 129 case inst.Action == "start": 130 if inst.Enable { 131 ctlcmds = []string{"enable"} 132 } 133 ctlcmds = append(ctlcmds, "start") 134 case inst.Action == "stop": 135 if inst.Disable { 136 ctlcmds = []string{"disable"} 137 } 138 ctlcmds = append(ctlcmds, "stop") 139 case inst.Action == "restart": 140 if inst.Reload { 141 ctlcmds = []string{"reload-or-restart"} 142 } else { 143 ctlcmds = []string{"restart"} 144 } 145 default: 146 return nil, fmt.Errorf("unknown action %q", inst.Action) 147 } 148 } 149 150 svcs := make([]string, 0, len(appInfos)) 151 snapNames := make([]string, 0, len(appInfos)) 152 lastName := "" 153 names := make([]string, len(appInfos)) 154 for i, svc := range appInfos { 155 svcs = append(svcs, svc.ServiceName()) 156 snapName := svc.Snap.InstanceName() 157 names[i] = snapName + "." + svc.Name 158 if snapName != lastName { 159 snapNames = append(snapNames, snapName) 160 lastName = snapName 161 } 162 } 163 164 var ignoreChangeID string 165 if context != nil { 166 ignoreChangeID = context.ChangeID() 167 } 168 if err := snapstate.CheckChangeConflictMany(st, snapNames, ignoreChangeID); err != nil { 169 return nil, &ServiceActionConflictError{err} 170 } 171 172 for _, cmd := range ctlcmds { 173 argv := append([]string{"systemctl", cmd}, svcs...) 174 desc := fmt.Sprintf("%s of %v", cmd, names) 175 // Give the systemctl a maximum time of 61 for now. 176 // 177 // Longer term we need to refactor this code and 178 // reuse the snapd/systemd and snapd/wrapper packages 179 // to control the timeout in a single place. 180 ts := cmdstate.ExecWithTimeout(st, desc, argv, 61*time.Second) 181 182 // set ignore flag on the tasks, new snapd uses service-control tasks. 183 ignore := true 184 for _, t := range ts.Tasks() { 185 t.Set("ignore", ignore) 186 } 187 tts = append(tts, ts) 188 } 189 190 // XXX: serviceControlTs could be merged with above logic at the cost of 191 // slightly more complicated logic. 192 ts, err := serviceControlTs(st, appInfos, inst) 193 if err != nil { 194 return nil, err 195 } 196 tts = append(tts, ts) 197 198 // make a taskset wait for its predecessor 199 for i := 1; i < len(tts); i++ { 200 tts[i].WaitAll(tts[i-1]) 201 } 202 203 return tts, nil 204 } 205 206 // StatusDecorator supports decorating client.AppInfos with service status. 207 type StatusDecorator struct { 208 sysd systemd.Systemd 209 } 210 211 // NewStatusDecorator returns a new StatusDecorator. 212 func NewStatusDecorator(rep interface { 213 Notify(string) 214 }) *StatusDecorator { 215 return &StatusDecorator{ 216 sysd: systemd.New(systemd.SystemMode, rep), 217 } 218 } 219 220 // DecorateWithStatus adds service status information to the given 221 // client.AppInfo associated with the given snap.AppInfo. 222 // If the snap is inactive or the app is not service it does nothing. 223 func (sd *StatusDecorator) DecorateWithStatus(appInfo *client.AppInfo, snapApp *snap.AppInfo) error { 224 if appInfo.Snap != snapApp.Snap.InstanceName() || appInfo.Name != snapApp.Name { 225 return fmt.Errorf("internal error: misassociated app info %v and client app info %s.%s", snapApp, appInfo.Snap, appInfo.Name) 226 } 227 if !snapApp.Snap.IsActive() || !snapApp.IsService() { 228 // nothing to do 229 return nil 230 } 231 232 // collect all services for a single call to systemctl 233 extra := len(snapApp.Sockets) 234 if snapApp.Timer != nil { 235 extra++ 236 } 237 serviceNames := make([]string, 0, 1+extra) 238 serviceNames = append(serviceNames, snapApp.ServiceName()) 239 240 sockSvcFileToName := make(map[string]string, len(snapApp.Sockets)) 241 for _, sock := range snapApp.Sockets { 242 sockUnit := filepath.Base(sock.File()) 243 sockSvcFileToName[sockUnit] = sock.Name 244 serviceNames = append(serviceNames, sockUnit) 245 } 246 if snapApp.Timer != nil { 247 timerUnit := filepath.Base(snapApp.Timer.File()) 248 serviceNames = append(serviceNames, timerUnit) 249 } 250 251 // sysd.Status() makes sure that we get only the units we asked 252 // for and raises an error otherwise 253 sts, err := sd.sysd.Status(serviceNames...) 254 if err != nil { 255 return fmt.Errorf("cannot get status of services of app %q: %v", appInfo.Name, err) 256 } 257 if len(sts) != len(serviceNames) { 258 return fmt.Errorf("cannot get status of services of app %q: expected %d results, got %d", appInfo.Name, len(serviceNames), len(sts)) 259 } 260 for _, st := range sts { 261 switch filepath.Ext(st.UnitName) { 262 case ".service": 263 appInfo.Enabled = st.Enabled 264 appInfo.Active = st.Active 265 case ".timer": 266 appInfo.Activators = append(appInfo.Activators, client.AppActivator{ 267 Name: snapApp.Name, 268 Enabled: st.Enabled, 269 Active: st.Active, 270 Type: "timer", 271 }) 272 case ".socket": 273 appInfo.Activators = append(appInfo.Activators, client.AppActivator{ 274 Name: sockSvcFileToName[st.UnitName], 275 Enabled: st.Enabled, 276 Active: st.Active, 277 Type: "socket", 278 }) 279 } 280 } 281 282 return nil 283 }