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 }