github.com/makyo/juju@v0.0.0-20160425123129-2608902037e9/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 "strconv" 10 "strings" 11 "time" 12 13 "github.com/juju/cmd" 14 "github.com/juju/errors" 15 "github.com/juju/names" 16 "launchpad.net/gnuflag" 17 18 actionapi "github.com/juju/juju/api/action" 19 "github.com/juju/juju/apiserver/params" 20 "github.com/juju/juju/cmd/juju/action" 21 "github.com/juju/juju/cmd/juju/block" 22 "github.com/juju/juju/cmd/modelcmd" 23 ) 24 25 func newRunCommand() cmd.Command { 26 return modelcmd.Wrap(&runCommand{}) 27 } 28 29 // runCommand is responsible for running arbitrary commands on remote machines. 30 type runCommand struct { 31 modelcmd.ModelCommandBase 32 out cmd.Output 33 all bool 34 timeout time.Duration 35 machines []string 36 services []string 37 units []string 38 commands string 39 } 40 41 const runDoc = ` 42 Run the commands on the specified targets. 43 44 Targets are specified using either machine ids, service names or unit 45 names. At least one target specifier is needed. 46 47 Multiple values can be set for --machine, --service, and --unit by using 48 comma separated values. 49 50 If the target is a machine, the command is run as the "ubuntu" user on 51 the remote machine. 52 53 If the target is a service, the command is run on all units for that 54 service. For example, if there was a service "mysql" and that service 55 had two units, "mysql/0" and "mysql/1", then 56 --service mysql 57 is equivalent to 58 --unit mysql/0,mysql/1 59 60 Commands run for services or units are executed in a 'hook context' for 61 the unit. 62 63 --all is provided as a simple way to run the command on all the machines 64 in the model. If you specify --all you cannot provide additional 65 targets. 66 67 Since juju run creates actions, you can query for the status of commands 68 started with juju run by calling "juju show-action-status --name juju-run". 69 ` 70 71 func (c *runCommand) Info() *cmd.Info { 72 return &cmd.Info{ 73 Name: "run", 74 Args: "<commands>", 75 Purpose: "run the commands on the remote targets specified", 76 Doc: runDoc, 77 } 78 } 79 80 func (c *runCommand) SetFlags(f *gnuflag.FlagSet) { 81 c.out.AddFlags(f, "smart", cmd.DefaultFormatters) 82 f.BoolVar(&c.all, "all", false, "run the commands on all the machines") 83 f.DurationVar(&c.timeout, "timeout", 5*time.Minute, "how long to wait before the remote command is considered to have failed") 84 f.Var(cmd.NewStringsValue(nil, &c.machines), "machine", "one or more machine ids") 85 f.Var(cmd.NewStringsValue(nil, &c.services), "service", "one or more service names") 86 f.Var(cmd.NewStringsValue(nil, &c.units), "unit", "one or more unit ids") 87 } 88 89 func (c *runCommand) Init(args []string) error { 90 if len(args) == 0 { 91 return fmt.Errorf("no commands specified") 92 } 93 c.commands, args = args[0], args[1:] 94 95 if c.all { 96 if len(c.machines) != 0 { 97 return fmt.Errorf("You cannot specify --all and individual machines") 98 } 99 if len(c.services) != 0 { 100 return fmt.Errorf("You cannot specify --all and individual services") 101 } 102 if len(c.units) != 0 { 103 return fmt.Errorf("You cannot specify --all and individual units") 104 } 105 } else { 106 if len(c.machines) == 0 && len(c.services) == 0 && len(c.units) == 0 { 107 return fmt.Errorf("You must specify a target, either through --all, --machine, --service or --unit") 108 } 109 } 110 111 var nameErrors []string 112 for _, machineId := range c.machines { 113 if !names.IsValidMachine(machineId) { 114 nameErrors = append(nameErrors, fmt.Sprintf(" %q is not a valid machine id", machineId)) 115 } 116 } 117 for _, service := range c.services { 118 if !names.IsValidService(service) { 119 nameErrors = append(nameErrors, fmt.Sprintf(" %q is not a valid service name", service)) 120 } 121 } 122 for _, unit := range c.units { 123 if !names.IsValidUnit(unit) { 124 nameErrors = append(nameErrors, fmt.Sprintf(" %q is not a valid unit name", unit)) 125 } 126 } 127 if len(nameErrors) > 0 { 128 return fmt.Errorf("The following run targets are not valid:\n%s", 129 strings.Join(nameErrors, "\n")) 130 } 131 132 return cmd.CheckEmpty(args) 133 } 134 135 // ConvertActionResults takes the results from the api and creates a map 136 // suitable for format converstion to YAML or JSON. 137 func ConvertActionResults(result params.ActionResult, query actionQuery) map[string]interface{} { 138 values := make(map[string]interface{}) 139 values[query.receiver.receiverType] = query.receiver.tag.Id() 140 if result.Error != nil { 141 values["Error"] = result.Error.Error() 142 values["Action"] = query.actionTag.Id() 143 return values 144 } 145 if result.Action.Tag != query.actionTag.String() { 146 values["Error"] = fmt.Sprintf("expected action tag %q, got %q", query.actionTag.String(), result.Action.Tag) 147 values["Action"] = query.actionTag.Id() 148 return values 149 } 150 if result.Action.Receiver != query.receiver.tag.String() { 151 values["Error"] = fmt.Sprintf("expected action receiver %q, got %q", query.receiver.tag.String(), result.Action.Receiver) 152 values["Action"] = query.actionTag.Id() 153 return values 154 } 155 if result.Message != "" { 156 values["Message"] = result.Message 157 } 158 // We always want to have a string for stdout, but only show stderr, 159 // code and error if they are there. 160 if res, ok := result.Output["Stdout"].(string); ok { 161 values["Stdout"] = strings.Replace(res, "\r\n", "\n", -1) 162 if res, ok := result.Output["StdoutEncoding"].(string); ok && res != "" { 163 values["Stdout.encoding"] = res 164 } 165 } else { 166 values["Stdout"] = "" 167 } 168 if res, ok := result.Output["Stderr"].(string); ok && res != "" { 169 values["Stderr"] = strings.Replace(res, "\r\n", "\n", -1) 170 if res, ok := result.Output["StderrEncoding"].(string); ok && res != "" { 171 values["Stderr.encoding"] = res 172 } 173 } 174 if res, ok := result.Output["Code"].(string); ok { 175 code, err := strconv.Atoi(res) 176 if err == nil && code != 0 { 177 values["ReturnCode"] = code 178 } 179 } 180 return values 181 } 182 183 func (c *runCommand) Run(ctx *cmd.Context) error { 184 client, err := getRunAPIClient(c) 185 if err != nil { 186 return err 187 } 188 defer client.Close() 189 190 var runResults []params.ActionResult 191 if c.all { 192 runResults, err = client.RunOnAllMachines(c.commands, c.timeout) 193 } else { 194 params := params.RunParams{ 195 Commands: c.commands, 196 Timeout: c.timeout, 197 Machines: c.machines, 198 Services: c.services, 199 Units: c.units, 200 } 201 runResults, err = client.Run(params) 202 } 203 204 if err != nil { 205 return block.ProcessBlockedError(err, block.BlockChange) 206 } 207 208 actionsToQuery := []actionQuery{} 209 for _, result := range runResults { 210 if result.Error != nil { 211 fmt.Fprintf(ctx.GetStderr(), "couldn't queue one action: %v", result.Error) 212 continue 213 } 214 actionTag, err := names.ParseActionTag(result.Action.Tag) 215 if err != nil { 216 fmt.Fprintf(ctx.GetStderr(), "got invalid action tag %v for receiver %v", result.Action.Tag, result.Action.Receiver) 217 continue 218 } 219 220 receiverTag, err := names.ActionReceiverFromTag(result.Action.Receiver) 221 if err != nil { 222 fmt.Fprintf(ctx.GetStderr(), "got invalid action receiver tag %v for action %v", result.Action.Receiver, result.Action.Tag) 223 continue 224 } 225 var receiverType string 226 switch receiverTag.(type) { 227 case names.UnitTag: 228 receiverType = "UnitId" 229 case names.MachineTag: 230 receiverType = "MachineId" 231 default: 232 receiverType = "ReceiverId" 233 } 234 actionsToQuery = append(actionsToQuery, actionQuery{ 235 actionTag: actionTag, 236 receiver: actionReceiver{ 237 receiverType: receiverType, 238 tag: receiverTag, 239 }}) 240 } 241 242 if len(actionsToQuery) == 0 { 243 return errors.New("no actions were successfully enqueued, aborting") 244 } 245 246 values := []interface{}{} 247 for len(actionsToQuery) > 0 { 248 actionResults, err := client.Actions(entities(actionsToQuery)) 249 if err != nil { 250 return errors.Trace(err) 251 } 252 253 newActionsToQuery := []actionQuery{} 254 for i, result := range actionResults.Results { 255 if result.Error == nil { 256 switch result.Status { 257 case params.ActionRunning, params.ActionPending: 258 newActionsToQuery = append(newActionsToQuery, actionsToQuery[i]) 259 continue 260 } 261 } 262 263 values = append(values, ConvertActionResults(result, actionsToQuery[i])) 264 } 265 266 actionsToQuery = newActionsToQuery 267 268 // TODO: use a watcher instead of sleeping 269 // this should be easier once we implement action grouping 270 <-afterFunc(1 * time.Second) 271 } 272 273 // If we are just dealing with one result, AND we are using the smart 274 // format, then pretend we were running it locally. 275 if len(values) == 1 && c.out.Name() == "smart" { 276 result, ok := values[0].(map[string]interface{}) 277 if !ok { 278 return errors.New("couldn't read action output") 279 } 280 if res, ok := result["Error"].(string); ok { 281 return errors.New(res) 282 } 283 ctx.Stdout.Write(formatOutput(result, "Stdout")) 284 ctx.Stderr.Write(formatOutput(result, "Stderr")) 285 if code, ok := result["ReturnCode"].(int); ok && code != 0 { 286 return cmd.NewRcPassthroughError(code) 287 } 288 // Message should always contain only errors. 289 if res, ok := result["Message"].(string); ok && res != "" { 290 ctx.Stderr.Write([]byte(res)) 291 } 292 293 return nil 294 } 295 296 return c.out.Write(ctx, values) 297 } 298 299 type actionReceiver struct { 300 receiverType string 301 tag names.Tag 302 } 303 304 type actionQuery struct { 305 receiver actionReceiver 306 actionTag names.ActionTag 307 } 308 309 // RunClient exposes the capabilities required by the CLI 310 type RunClient interface { 311 action.APIClient 312 RunOnAllMachines(commands string, timeout time.Duration) ([]params.ActionResult, error) 313 Run(params.RunParams) ([]params.ActionResult, error) 314 } 315 316 // In order to be able to easily mock out the API side for testing, 317 // the API client is retrieved using a function. 318 var getRunAPIClient = func(c *runCommand) (RunClient, error) { 319 root, err := c.NewAPIRoot() 320 if err != nil { 321 return nil, errors.Trace(err) 322 } 323 return actionapi.NewClient(root), errors.Trace(err) 324 } 325 326 // getActionResult abstracts over the action CLI function that we use here to fetch results 327 var getActionResult = func(c RunClient, actionId string, wait *time.Timer) (params.ActionResult, error) { 328 return action.GetActionResult(c, actionId, wait) 329 } 330 331 var afterFunc = func(d time.Duration) <-chan time.Time { 332 return time.After(d) 333 } 334 335 // entities is a convenience constructor for params.Entities. 336 func entities(actions []actionQuery) params.Entities { 337 entities := params.Entities{ 338 Entities: make([]params.Entity, len(actions)), 339 } 340 for i, action := range actions { 341 entities.Entities[i].Tag = action.actionTag.String() 342 } 343 return entities 344 } 345 346 func formatOutput(results map[string]interface{}, key string) []byte { 347 res, ok := results[key].(string) 348 if !ok { 349 return []byte("") 350 } 351 if enc, ok := results[key+".encoding"].(string); ok && enc != "" { 352 switch enc { 353 case "base64": 354 decoded, err := base64.StdEncoding.DecodeString(res) 355 if err != nil { 356 return []byte("expected b64 encoded string, got " + res) 357 } 358 return decoded 359 default: 360 return []byte(res) 361 } 362 } 363 return []byte(res) 364 }