github.com/juju/juju@v0.0.0-20240327075706-a90865de2538/worker/machineactions/handleactions.go (about)

     1  // Copyright 2016 Canonical Ltd.
     2  // Copyright 2016 Cloudbase Solutions
     3  // Licensed under the AGPLv3, see LICENCE file for details.
     4  
     5  package machineactions
     6  
     7  import (
     8  	"encoding/base64"
     9  	"os"
    10  	"time"
    11  	"unicode/utf8"
    12  
    13  	"github.com/juju/clock"
    14  	"github.com/juju/errors"
    15  	"github.com/juju/utils/v3/exec"
    16  
    17  	"github.com/juju/juju/core/actions"
    18  )
    19  
    20  // RunAsUser is the user that the machine juju-exec action is executed as.
    21  var RunAsUser = "ubuntu"
    22  
    23  // HandleAction receives a name and a map of parameters for a given machine action.
    24  // It will handle that action in a specific way and return a results map suitable for ActionFinish.
    25  func HandleAction(name string, params map[string]interface{}) (results map[string]interface{}, err error) {
    26  	spec, ok := actions.PredefinedActionsSpec[name]
    27  	if !ok {
    28  		return nil, errors.Errorf("unexpected action %s", name)
    29  	}
    30  	if err := spec.ValidateParams(params); err != nil {
    31  		return nil, errors.Errorf("invalid action parameters")
    32  	}
    33  
    34  	if actions.IsJujuExecAction(name) {
    35  		return handleJujuExecAction(params)
    36  	} else {
    37  		return nil, errors.Errorf("unexpected action %s", name)
    38  	}
    39  }
    40  
    41  func handleJujuExecAction(params map[string]interface{}) (results map[string]interface{}, err error) {
    42  	// The spec checks that the parameters are available so we don't need to check again here
    43  	command, _ := params["command"].(string)
    44  	logger.Tracef("juju run %q", command)
    45  
    46  	// The timeout is passed in in nanoseconds(which are represented in go as int64)
    47  	// But due to serialization it comes out as float64
    48  	timeout, _ := params["timeout"].(float64)
    49  
    50  	res, err := runCommandWithTimeout(command, time.Duration(timeout), clock.WallClock)
    51  	if err != nil {
    52  		return nil, errors.Trace(err)
    53  	}
    54  
    55  	actionResults := map[string]interface{}{}
    56  	actionResults["return-code"] = res.Code
    57  	storeOutput(actionResults, "stdout", res.Stdout)
    58  	storeOutput(actionResults, "stderr", res.Stderr)
    59  
    60  	return actionResults, nil
    61  }
    62  
    63  func runCommandWithTimeout(command string, timeout time.Duration, clock clock.Clock) (*exec.ExecResponse, error) {
    64  	cmd := exec.RunParams{
    65  		Commands:    command,
    66  		Environment: os.Environ(),
    67  		Clock:       clock,
    68  		User:        RunAsUser,
    69  	}
    70  
    71  	err := cmd.Run()
    72  	if err != nil {
    73  		return nil, errors.Trace(err)
    74  	}
    75  
    76  	var cancel chan struct{}
    77  	if timeout != 0 {
    78  		cancel = make(chan struct{})
    79  		go func() {
    80  			<-clock.After(timeout)
    81  			close(cancel)
    82  		}()
    83  	}
    84  
    85  	return cmd.WaitWithCancel(cancel)
    86  }
    87  
    88  func encodeBytes(input []byte) (value string, encoding string) {
    89  	if utf8.Valid(input) {
    90  		value = string(input)
    91  		encoding = "utf8"
    92  	} else {
    93  		value = base64.StdEncoding.EncodeToString(input)
    94  		encoding = "base64"
    95  	}
    96  	return value, encoding
    97  }
    98  
    99  func storeOutput(values map[string]interface{}, key string, input []byte) {
   100  	value, encoding := encodeBytes(input)
   101  	values[key] = value
   102  	if encoding != "utf8" {
   103  		values[key+"Encoding"] = encoding
   104  	}
   105  }