github.com/outbrain/consul@v1.4.5/agent/remote_exec.go (about) 1 package agent 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "io/ioutil" 7 "os" 8 osexec "os/exec" 9 "path" 10 "strconv" 11 "sync" 12 "syscall" 13 "time" 14 15 "github.com/hashicorp/consul/agent/exec" 16 "github.com/hashicorp/consul/agent/structs" 17 "github.com/hashicorp/consul/api" 18 ) 19 20 const ( 21 // remoteExecFileName is the name of the file we append to 22 // the path, e.g. _rexec/session_id/job 23 remoteExecFileName = "job" 24 25 // rExecAck is the suffix added to an ack path 26 remoteExecAckSuffix = "ack" 27 28 // remoteExecAck is the suffix added to an exit code 29 remoteExecExitSuffix = "exit" 30 31 // remoteExecOutputDivider is used to namespace the output 32 remoteExecOutputDivider = "out" 33 34 // remoteExecOutputSize is the size we chunk output too 35 remoteExecOutputSize = 4 * 1024 36 37 // remoteExecOutputDeadline is how long we wait before uploading 38 // less than the chunk size 39 remoteExecOutputDeadline = 500 * time.Millisecond 40 ) 41 42 // remoteExecEvent is used as the payload of the user event to transmit 43 // what we need to know about the event 44 type remoteExecEvent struct { 45 Prefix string 46 Session string 47 } 48 49 // remoteExecSpec is used as the specification of the remote exec. 50 // It is stored in the KV store 51 type remoteExecSpec struct { 52 Command string 53 Args []string 54 Script []byte 55 Wait time.Duration 56 } 57 58 type rexecWriter struct { 59 BufCh chan []byte 60 BufSize int 61 BufIdle time.Duration 62 CancelCh chan struct{} 63 64 buf []byte 65 bufLen int 66 bufLock sync.Mutex 67 flush *time.Timer 68 } 69 70 func (r *rexecWriter) Write(b []byte) (int, error) { 71 r.bufLock.Lock() 72 defer r.bufLock.Unlock() 73 if r.flush != nil { 74 r.flush.Stop() 75 r.flush = nil 76 } 77 inpLen := len(b) 78 if r.buf == nil { 79 r.buf = make([]byte, r.BufSize) 80 } 81 82 COPY: 83 remain := len(r.buf) - r.bufLen 84 if remain > len(b) { 85 copy(r.buf[r.bufLen:], b) 86 r.bufLen += len(b) 87 } else { 88 copy(r.buf[r.bufLen:], b[:remain]) 89 b = b[remain:] 90 r.bufLen += remain 91 r.bufLock.Unlock() 92 r.Flush() 93 r.bufLock.Lock() 94 goto COPY 95 } 96 97 r.flush = time.AfterFunc(r.BufIdle, r.Flush) 98 return inpLen, nil 99 } 100 101 func (r *rexecWriter) Flush() { 102 r.bufLock.Lock() 103 defer r.bufLock.Unlock() 104 if r.flush != nil { 105 r.flush.Stop() 106 r.flush = nil 107 } 108 if r.bufLen == 0 { 109 return 110 } 111 select { 112 case r.BufCh <- r.buf[:r.bufLen]: 113 r.buf = make([]byte, r.BufSize) 114 r.bufLen = 0 115 case <-r.CancelCh: 116 r.bufLen = 0 117 } 118 } 119 120 // handleRemoteExec is invoked when a new remote exec request is received 121 func (a *Agent) handleRemoteExec(msg *UserEvent) { 122 a.logger.Printf("[DEBUG] agent: received remote exec event (ID: %s)", msg.ID) 123 // Decode the event payload 124 var event remoteExecEvent 125 if err := json.Unmarshal(msg.Payload, &event); err != nil { 126 a.logger.Printf("[ERR] agent: failed to decode remote exec event: %v", err) 127 return 128 } 129 130 // Read the job specification 131 var spec remoteExecSpec 132 if !a.remoteExecGetSpec(&event, &spec) { 133 return 134 } 135 136 // Write the acknowledgement 137 if !a.remoteExecWriteAck(&event) { 138 return 139 } 140 141 // Ensure we write out an exit code 142 exitCode := 0 143 defer a.remoteExecWriteExitCode(&event, &exitCode) 144 145 // Check if this is a script, we may need to spill to disk 146 var script string 147 if len(spec.Script) != 0 { 148 tmpFile, err := ioutil.TempFile("", "rexec") 149 if err != nil { 150 a.logger.Printf("[DEBUG] agent: failed to make tmp file: %v", err) 151 exitCode = 255 152 return 153 } 154 defer os.Remove(tmpFile.Name()) 155 os.Chmod(tmpFile.Name(), 0750) 156 tmpFile.Write(spec.Script) 157 tmpFile.Close() 158 script = tmpFile.Name() 159 } else { 160 script = spec.Command 161 } 162 163 // Create the exec.Cmd 164 a.logger.Printf("[INFO] agent: remote exec '%s'", script) 165 var cmd *osexec.Cmd 166 var err error 167 if len(spec.Args) > 0 { 168 cmd, err = exec.Subprocess(spec.Args) 169 } else { 170 cmd, err = exec.Script(script) 171 } 172 if err != nil { 173 a.logger.Printf("[DEBUG] agent: failed to start remote exec: %v", err) 174 exitCode = 255 175 return 176 } 177 178 // Setup the output streaming 179 writer := &rexecWriter{ 180 BufCh: make(chan []byte, 16), 181 BufSize: remoteExecOutputSize, 182 BufIdle: remoteExecOutputDeadline, 183 CancelCh: make(chan struct{}), 184 } 185 cmd.Stdout = writer 186 cmd.Stderr = writer 187 188 // Start execution 189 if err := cmd.Start(); err != nil { 190 a.logger.Printf("[DEBUG] agent: failed to start remote exec: %v", err) 191 exitCode = 255 192 return 193 } 194 195 // Wait for the process to exit 196 exitCh := make(chan int, 1) 197 go func() { 198 err := cmd.Wait() 199 writer.Flush() 200 close(writer.BufCh) 201 if err == nil { 202 exitCh <- 0 203 return 204 } 205 206 // Try to determine the exit code 207 if exitErr, ok := err.(*osexec.ExitError); ok { 208 if status, ok := exitErr.Sys().(syscall.WaitStatus); ok { 209 exitCh <- status.ExitStatus() 210 return 211 } 212 } 213 exitCh <- 1 214 }() 215 216 // Wait until we are complete, uploading as we go 217 WAIT: 218 for num := 0; ; num++ { 219 select { 220 case out := <-writer.BufCh: 221 if out == nil { 222 break WAIT 223 } 224 if !a.remoteExecWriteOutput(&event, num, out) { 225 close(writer.CancelCh) 226 exitCode = 255 227 return 228 } 229 case <-time.After(spec.Wait): 230 // Acts like a heartbeat, since there is no output 231 if !a.remoteExecWriteOutput(&event, num, nil) { 232 close(writer.CancelCh) 233 exitCode = 255 234 return 235 } 236 } 237 } 238 239 // Get the exit code 240 exitCode = <-exitCh 241 } 242 243 // remoteExecGetSpec is used to get the exec specification. 244 // Returns if execution should continue 245 func (a *Agent) remoteExecGetSpec(event *remoteExecEvent, spec *remoteExecSpec) bool { 246 get := structs.KeyRequest{ 247 Datacenter: a.config.Datacenter, 248 Key: path.Join(event.Prefix, event.Session, remoteExecFileName), 249 QueryOptions: structs.QueryOptions{ 250 AllowStale: true, // Stale read for scale! Retry on failure. 251 }, 252 } 253 get.Token = a.tokens.AgentToken() 254 var out structs.IndexedDirEntries 255 QUERY: 256 if err := a.RPC("KVS.Get", &get, &out); err != nil { 257 a.logger.Printf("[ERR] agent: failed to get remote exec job: %v", err) 258 return false 259 } 260 if len(out.Entries) == 0 { 261 // If the initial read was stale and had no data, retry as a consistent read 262 if get.QueryOptions.AllowStale { 263 a.logger.Printf("[DEBUG] agent: trying consistent fetch of remote exec job spec") 264 get.QueryOptions.AllowStale = false 265 goto QUERY 266 } else { 267 a.logger.Printf("[DEBUG] agent: remote exec aborted, job spec missing") 268 return false 269 } 270 } 271 if err := json.Unmarshal(out.Entries[0].Value, &spec); err != nil { 272 a.logger.Printf("[ERR] agent: failed to decode remote exec spec: %v", err) 273 return false 274 } 275 return true 276 } 277 278 // remoteExecWriteAck is used to write an ack. Returns if execution should 279 // continue. 280 func (a *Agent) remoteExecWriteAck(event *remoteExecEvent) bool { 281 if err := a.remoteExecWriteKey(event, remoteExecAckSuffix, nil); err != nil { 282 a.logger.Printf("[ERR] agent: failed to ack remote exec job: %v", err) 283 return false 284 } 285 return true 286 } 287 288 // remoteExecWriteOutput is used to write output 289 func (a *Agent) remoteExecWriteOutput(event *remoteExecEvent, num int, output []byte) bool { 290 suffix := path.Join(remoteExecOutputDivider, fmt.Sprintf("%05x", num)) 291 if err := a.remoteExecWriteKey(event, suffix, output); err != nil { 292 a.logger.Printf("[ERR] agent: failed to write output for remote exec job: %v", err) 293 return false 294 } 295 return true 296 } 297 298 // remoteExecWriteExitCode is used to write an exit code 299 func (a *Agent) remoteExecWriteExitCode(event *remoteExecEvent, exitCode *int) bool { 300 val := []byte(strconv.FormatInt(int64(*exitCode), 10)) 301 if err := a.remoteExecWriteKey(event, remoteExecExitSuffix, val); err != nil { 302 a.logger.Printf("[ERR] agent: failed to write exit code for remote exec job: %v", err) 303 return false 304 } 305 return true 306 } 307 308 // remoteExecWriteKey is used to write an output key for a remote exec job 309 func (a *Agent) remoteExecWriteKey(event *remoteExecEvent, suffix string, val []byte) error { 310 key := path.Join(event.Prefix, event.Session, a.config.NodeName, suffix) 311 write := structs.KVSRequest{ 312 Datacenter: a.config.Datacenter, 313 Op: api.KVLock, 314 DirEnt: structs.DirEntry{ 315 Key: key, 316 Value: val, 317 Session: event.Session, 318 }, 319 } 320 write.Token = a.tokens.AgentToken() 321 var success bool 322 if err := a.RPC("KVS.Apply", &write, &success); err != nil { 323 return err 324 } 325 if !success { 326 return fmt.Errorf("write failed") 327 } 328 return nil 329 }