github.com/mhilton/juju-juju@v0.0.0-20150901100907-a94dd2c73455/cmd/juju/commands/run.go (about) 1 // Copyright 2013 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package commands 5 6 import ( 7 "encoding/base64" 8 "fmt" 9 "strings" 10 "time" 11 "unicode/utf8" 12 13 "github.com/juju/cmd" 14 "github.com/juju/names" 15 "launchpad.net/gnuflag" 16 17 "github.com/juju/juju/apiserver/params" 18 "github.com/juju/juju/cmd/envcmd" 19 "github.com/juju/juju/cmd/juju/block" 20 ) 21 22 // RunCommand is responsible for running arbitrary commands on remote machines. 23 type RunCommand struct { 24 envcmd.EnvCommandBase 25 out cmd.Output 26 all bool 27 timeout time.Duration 28 machines []string 29 services []string 30 units []string 31 commands string 32 } 33 34 const runDoc = ` 35 Run the commands on the specified targets. 36 37 Targets are specified using either machine ids, service names or unit 38 names. At least one target specifier is needed. 39 40 Multiple values can be set for --machine, --service, and --unit by using 41 comma separated values. 42 43 If the target is a machine, the command is run as the "ubuntu" user on 44 the remote machine. 45 46 If the target is a service, the command is run on all units for that 47 service. For example, if there was a service "mysql" and that service 48 had two units, "mysql/0" and "mysql/1", then 49 --service mysql 50 is equivalent to 51 --unit mysql/0,mysql/1 52 53 Commands run for services or units are executed in a 'hook context' for 54 the unit. 55 56 --all is provided as a simple way to run the command on all the machines 57 in the environment. If you specify --all you cannot provide additional 58 targets. 59 60 ` 61 62 func (c *RunCommand) Info() *cmd.Info { 63 return &cmd.Info{ 64 Name: "run", 65 Args: "<commands>", 66 Purpose: "run the commands on the remote targets specified", 67 Doc: runDoc, 68 } 69 } 70 71 func (c *RunCommand) SetFlags(f *gnuflag.FlagSet) { 72 c.out.AddFlags(f, "smart", cmd.DefaultFormatters) 73 f.BoolVar(&c.all, "all", false, "run the commands on all the machines") 74 f.DurationVar(&c.timeout, "timeout", 5*time.Minute, "how long to wait before the remote command is considered to have failed") 75 f.Var(cmd.NewStringsValue(nil, &c.machines), "machine", "one or more machine ids") 76 f.Var(cmd.NewStringsValue(nil, &c.services), "service", "one or more service names") 77 f.Var(cmd.NewStringsValue(nil, &c.units), "unit", "one or more unit ids") 78 } 79 80 func (c *RunCommand) Init(args []string) error { 81 if len(args) == 0 { 82 return fmt.Errorf("no commands specified") 83 } 84 c.commands, args = args[0], args[1:] 85 86 if c.all { 87 if len(c.machines) != 0 { 88 return fmt.Errorf("You cannot specify --all and individual machines") 89 } 90 if len(c.services) != 0 { 91 return fmt.Errorf("You cannot specify --all and individual services") 92 } 93 if len(c.units) != 0 { 94 return fmt.Errorf("You cannot specify --all and individual units") 95 } 96 } else { 97 if len(c.machines) == 0 && len(c.services) == 0 && len(c.units) == 0 { 98 return fmt.Errorf("You must specify a target, either through --all, --machine, --service or --unit") 99 } 100 } 101 102 var nameErrors []string 103 for _, machineId := range c.machines { 104 if !names.IsValidMachine(machineId) { 105 nameErrors = append(nameErrors, fmt.Sprintf(" %q is not a valid machine id", machineId)) 106 } 107 } 108 for _, service := range c.services { 109 if !names.IsValidService(service) { 110 nameErrors = append(nameErrors, fmt.Sprintf(" %q is not a valid service name", service)) 111 } 112 } 113 for _, unit := range c.units { 114 if !names.IsValidUnit(unit) { 115 nameErrors = append(nameErrors, fmt.Sprintf(" %q is not a valid unit name", unit)) 116 } 117 } 118 if len(nameErrors) > 0 { 119 return fmt.Errorf("The following run targets are not valid:\n%s", 120 strings.Join(nameErrors, "\n")) 121 } 122 123 return cmd.CheckEmpty(args) 124 } 125 126 func encodeBytes(input []byte) (value string, encoding string) { 127 if utf8.Valid(input) { 128 value = string(input) 129 encoding = "utf8" 130 } else { 131 value = base64.StdEncoding.EncodeToString(input) 132 encoding = "base64" 133 } 134 return value, encoding 135 } 136 137 func storeOutput(values map[string]interface{}, key string, input []byte) { 138 value, encoding := encodeBytes(input) 139 values[key] = value 140 if encoding != "utf8" { 141 values[key+".encoding"] = encoding 142 } 143 } 144 145 // ConvertRunResults takes the results from the api and creates a map 146 // suitable for format converstion to YAML or JSON. 147 func ConvertRunResults(runResults []params.RunResult) interface{} { 148 var results = make([]interface{}, len(runResults)) 149 150 for i, result := range runResults { 151 // We always want to have a string for stdout, but only show stderr, 152 // code and error if they are there. 153 values := make(map[string]interface{}) 154 values["MachineId"] = result.MachineId 155 if result.UnitId != "" { 156 values["UnitId"] = result.UnitId 157 158 } 159 storeOutput(values, "Stdout", result.Stdout) 160 if len(result.Stderr) > 0 { 161 storeOutput(values, "Stderr", result.Stderr) 162 } 163 if result.Code != 0 { 164 values["ReturnCode"] = result.Code 165 } 166 if result.Error != "" { 167 values["Error"] = result.Error 168 } 169 results[i] = values 170 } 171 172 return results 173 } 174 175 func (c *RunCommand) Run(ctx *cmd.Context) error { 176 client, err := getRunAPIClient(c) 177 if err != nil { 178 return err 179 } 180 defer client.Close() 181 182 var runResults []params.RunResult 183 if c.all { 184 runResults, err = client.RunOnAllMachines(c.commands, c.timeout) 185 } else { 186 params := params.RunParams{ 187 Commands: c.commands, 188 Timeout: c.timeout, 189 Machines: c.machines, 190 Services: c.services, 191 Units: c.units, 192 } 193 runResults, err = client.Run(params) 194 } 195 196 if err != nil { 197 return block.ProcessBlockedError(err, block.BlockChange) 198 } 199 200 // If we are just dealing with one result, AND we are using the smart 201 // format, then pretend we were running it locally. 202 if len(runResults) == 1 && c.out.Name() == "smart" { 203 result := runResults[0] 204 ctx.Stdout.Write(result.Stdout) 205 ctx.Stderr.Write(result.Stderr) 206 if result.Error != "" { 207 // Convert the error string back into an error object. 208 return fmt.Errorf("%s", result.Error) 209 } 210 if result.Code != 0 { 211 return cmd.NewRcPassthroughError(result.Code) 212 } 213 return nil 214 } 215 216 c.out.Write(ctx, ConvertRunResults(runResults)) 217 return nil 218 } 219 220 // In order to be able to easily mock out the API side for testing, 221 // the API client is got using a function. 222 223 type RunClient interface { 224 Close() error 225 RunOnAllMachines(commands string, timeout time.Duration) ([]params.RunResult, error) 226 Run(run params.RunParams) ([]params.RunResult, error) 227 } 228 229 // Here we need the signature to be correct for the interface. 230 var getRunAPIClient = func(c *RunCommand) (RunClient, error) { 231 return c.NewAPIClient() 232 }