github.com/kubiko/snapd@v0.0.0-20201013125620-d4f3094d9ddf/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 "time" 26 27 "github.com/snapcore/snapd/client" 28 "github.com/snapcore/snapd/overlord/cmdstate" 29 "github.com/snapcore/snapd/overlord/hookstate" 30 "github.com/snapcore/snapd/overlord/snapstate" 31 "github.com/snapcore/snapd/overlord/state" 32 "github.com/snapcore/snapd/snap" 33 "github.com/snapcore/snapd/systemd" 34 ) 35 36 type Instruction struct { 37 Action string `json:"action"` 38 Names []string `json:"names"` 39 client.StartOptions 40 client.StopOptions 41 client.RestartOptions 42 } 43 44 type ServiceActionConflictError struct{ error } 45 46 // Control creates a taskset for starting/stopping/restarting services via systemctl. 47 // The appInfos and inst define the services and the command to execute. 48 // Context is used to determine change conflicts - we will not conflict with 49 // tasks from same change as that of context's. 50 func Control(st *state.State, appInfos []*snap.AppInfo, inst *Instruction, context *hookstate.Context) ([]*state.TaskSet, error) { 51 var tts []*state.TaskSet 52 53 var ctlcmds []string 54 switch { 55 case inst.Action == "start": 56 if inst.Enable { 57 ctlcmds = []string{"enable"} 58 } 59 ctlcmds = append(ctlcmds, "start") 60 case inst.Action == "stop": 61 if inst.Disable { 62 ctlcmds = []string{"disable"} 63 } 64 ctlcmds = append(ctlcmds, "stop") 65 case inst.Action == "restart": 66 if inst.Reload { 67 ctlcmds = []string{"reload-or-restart"} 68 } else { 69 ctlcmds = []string{"restart"} 70 } 71 default: 72 return nil, fmt.Errorf("unknown action %q", inst.Action) 73 } 74 75 st.Lock() 76 defer st.Unlock() 77 78 svcs := make([]string, 0, len(appInfos)) 79 snapNames := make([]string, 0, len(appInfos)) 80 lastName := "" 81 names := make([]string, len(appInfos)) 82 for i, svc := range appInfos { 83 svcs = append(svcs, svc.ServiceName()) 84 snapName := svc.Snap.InstanceName() 85 names[i] = snapName + "." + svc.Name 86 if snapName != lastName { 87 snapNames = append(snapNames, snapName) 88 lastName = snapName 89 } 90 } 91 92 var ignoreChangeID string 93 if context != nil && !context.IsEphemeral() { 94 if task, ok := context.Task(); ok { 95 if chg := task.Change(); chg != nil { 96 ignoreChangeID = chg.ID() 97 } 98 } 99 } 100 101 if err := snapstate.CheckChangeConflictMany(st, snapNames, ignoreChangeID); err != nil { 102 return nil, &ServiceActionConflictError{err} 103 } 104 105 for _, cmd := range ctlcmds { 106 argv := append([]string{"systemctl", cmd}, svcs...) 107 desc := fmt.Sprintf("%s of %v", cmd, names) 108 // Give the systemctl a maximum time of 61 for now. 109 // 110 // Longer term we need to refactor this code and 111 // reuse the snapd/systemd and snapd/wrapper packages 112 // to control the timeout in a single place. 113 ts := cmdstate.ExecWithTimeout(st, desc, argv, 61*time.Second) 114 tts = append(tts, ts) 115 } 116 117 // make a taskset wait for its predecessor 118 for i := 1; i < len(tts); i++ { 119 tts[i].WaitAll(tts[i-1]) 120 } 121 122 return tts, nil 123 } 124 125 // StatusDecorator supports decorating client.AppInfos with service status. 126 type StatusDecorator struct { 127 sysd systemd.Systemd 128 } 129 130 // NewStatusDecorator returns a new StatusDecorator. 131 func NewStatusDecorator(rep interface { 132 Notify(string) 133 }) *StatusDecorator { 134 return &StatusDecorator{ 135 sysd: systemd.New(systemd.SystemMode, rep), 136 } 137 } 138 139 // DecorateWithStatus adds service status information to the given 140 // client.AppInfo associated with the given snap.AppInfo. 141 // If the snap is inactive or the app is not service it does nothing. 142 func (sd *StatusDecorator) DecorateWithStatus(appInfo *client.AppInfo, snapApp *snap.AppInfo) error { 143 if appInfo.Snap != snapApp.Snap.InstanceName() || appInfo.Name != snapApp.Name { 144 return fmt.Errorf("internal error: misassociated app info %v and client app info %s.%s", snapApp, appInfo.Snap, appInfo.Name) 145 } 146 if !snapApp.Snap.IsActive() || !snapApp.IsService() { 147 // nothing to do 148 return nil 149 } 150 151 // collect all services for a single call to systemctl 152 extra := len(snapApp.Sockets) 153 if snapApp.Timer != nil { 154 extra++ 155 } 156 serviceNames := make([]string, 0, 1+extra) 157 serviceNames = append(serviceNames, snapApp.ServiceName()) 158 159 sockSvcFileToName := make(map[string]string, len(snapApp.Sockets)) 160 for _, sock := range snapApp.Sockets { 161 sockUnit := filepath.Base(sock.File()) 162 sockSvcFileToName[sockUnit] = sock.Name 163 serviceNames = append(serviceNames, sockUnit) 164 } 165 if snapApp.Timer != nil { 166 timerUnit := filepath.Base(snapApp.Timer.File()) 167 serviceNames = append(serviceNames, timerUnit) 168 } 169 170 // sysd.Status() makes sure that we get only the units we asked 171 // for and raises an error otherwise 172 sts, err := sd.sysd.Status(serviceNames...) 173 if err != nil { 174 return fmt.Errorf("cannot get status of services of app %q: %v", appInfo.Name, err) 175 } 176 if len(sts) != len(serviceNames) { 177 return fmt.Errorf("cannot get status of services of app %q: expected %d results, got %d", appInfo.Name, len(serviceNames), len(sts)) 178 } 179 for _, st := range sts { 180 switch filepath.Ext(st.UnitName) { 181 case ".service": 182 appInfo.Enabled = st.Enabled 183 appInfo.Active = st.Active 184 case ".timer": 185 appInfo.Activators = append(appInfo.Activators, client.AppActivator{ 186 Name: snapApp.Name, 187 Enabled: st.Enabled, 188 Active: st.Active, 189 Type: "timer", 190 }) 191 case ".socket": 192 appInfo.Activators = append(appInfo.Activators, client.AppActivator{ 193 Name: sockSvcFileToName[st.UnitName], 194 Enabled: st.Enabled, 195 Active: st.Active, 196 Type: "socket", 197 }) 198 } 199 } 200 201 return nil 202 }