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