github.com/sl1pm4t/consul@v1.4.5-0.20190325224627-74c31c540f9c/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  }