github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/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 "regexp" 10 "strconv" 11 "strings" 12 "time" 13 14 "github.com/juju/cmd" 15 "github.com/juju/errors" 16 "github.com/juju/gnuflag" 17 "github.com/juju/utils" 18 "gopkg.in/juju/names.v2" 19 20 actionapi "github.com/juju/juju/api/action" 21 "github.com/juju/juju/apiserver/params" 22 jujucmd "github.com/juju/juju/cmd" 23 "github.com/juju/juju/cmd/juju/action" 24 "github.com/juju/juju/cmd/juju/block" 25 "github.com/juju/juju/cmd/modelcmd" 26 "github.com/juju/juju/jujuclient" 27 ) 28 29 // leaderSnippet is a regular expression for unit ID-like syntax that is used 30 // to indicate the current leader for an application. 31 const leaderSnippet = "(" + names.ApplicationSnippet + ")/leader" 32 33 var validLeader = regexp.MustCompile("^" + leaderSnippet + "$") 34 35 func newDefaultRunCommand(store jujuclient.ClientStore) cmd.Command { 36 return newRunCommand(store, time.After) 37 } 38 39 func newRunCommand(store jujuclient.ClientStore, timeAfter func(time.Duration) <-chan time.Time) cmd.Command { 40 cmd := modelcmd.Wrap(&runCommand{ 41 timeAfter: timeAfter, 42 }) 43 cmd.SetClientStore(store) 44 return cmd 45 } 46 47 // runCommand is responsible for running arbitrary commands on remote machines. 48 type runCommand struct { 49 modelcmd.ModelCommandBase 50 modelcmd.IAASOnlyCommand 51 out cmd.Output 52 all bool 53 timeout time.Duration 54 machines []string 55 applications []string 56 units []string 57 commands string 58 timeAfter func(time.Duration) <-chan time.Time 59 } 60 61 const runDoc = ` 62 Run a shell command on the specified targets. Only admin users of a model 63 are able to use this command. 64 65 Targets are specified using either machine ids, application names or unit 66 names. At least one target specifier is needed. 67 68 Multiple values can be set for --machine, --application, and --unit by using 69 comma separated values. 70 71 If the target is a machine, the command is run as the "root" user on 72 the remote machine. 73 74 Some options are shortened for usabilty purpose in CLI 75 --application can also be specified as --app and -a 76 --unit can also be specified as -u 77 78 Valid unit identifiers are: 79 a standard unit ID, such as mysql/0 or; 80 leader syntax of the form <application>/leader, such as mysql/leader. 81 82 If the target is an application, the command is run on all units for that 83 application. For example, if there was an application "mysql" and that application 84 had two units, "mysql/0" and "mysql/1", then 85 --application mysql 86 is equivalent to 87 --unit mysql/0,mysql/1 88 89 Commands run for applications or units are executed in a 'hook context' for 90 the unit. 91 92 --all is provided as a simple way to run the command on all the machines 93 in the model. If you specify --all you cannot provide additional 94 targets. 95 96 Since juju run creates actions, you can query for the status of commands 97 started with juju run by calling "juju show-action-status --name juju-run". 98 99 If you need to pass options to the command being run, you must precede the 100 command and its arguments with "--", to tell "juju run" to stop processing 101 those arguments. For example: 102 103 juju run --all -- hostname -f 104 ` 105 106 func (c *runCommand) Info() *cmd.Info { 107 return jujucmd.Info(&cmd.Info{ 108 Name: "run", 109 Args: "<commands>", 110 Purpose: "Run the commands on the remote targets specified.", 111 Doc: runDoc, 112 }) 113 } 114 115 func (c *runCommand) SetFlags(f *gnuflag.FlagSet) { 116 c.ModelCommandBase.SetFlags(f) 117 c.out.AddFlags(f, "default", map[string]cmd.Formatter{ 118 "yaml": cmd.FormatYaml, 119 "json": cmd.FormatJson, 120 // default is used to format a single result specially. 121 "default": cmd.FormatYaml, 122 }) 123 f.BoolVar(&c.all, "all", false, "Run the commands on all the machines") 124 f.DurationVar(&c.timeout, "timeout", 5*time.Minute, "How long to wait before the remote command is considered to have failed") 125 f.Var(cmd.NewStringsValue(nil, &c.machines), "machine", "One or more machine ids") 126 f.Var(cmd.NewStringsValue(nil, &c.applications), "a", "One or more application names") 127 f.Var(cmd.NewStringsValue(nil, &c.applications), "app", "") 128 f.Var(cmd.NewStringsValue(nil, &c.applications), "application", "") 129 f.Var(cmd.NewStringsValue(nil, &c.units), "u", "One or more unit ids") 130 f.Var(cmd.NewStringsValue(nil, &c.units), "unit", "") 131 } 132 133 func (c *runCommand) Init(args []string) error { 134 if len(args) == 0 { 135 return errors.Errorf("no commands specified") 136 } 137 if len(args) == 1 { 138 // If just one argument is specified, we don't pass it through 139 // utils.CommandString in case it contains multiple arguments 140 // (e.g. juju run --all "sudo whatever"). Passing it through 141 // utils.CommandString would quote the string, which the backend 142 // does not expect. 143 c.commands = args[0] 144 } else { 145 c.commands = utils.CommandString(args...) 146 } 147 148 if c.all { 149 if len(c.machines) != 0 { 150 return errors.Errorf("You cannot specify --all and individual machines") 151 } 152 if len(c.applications) != 0 { 153 return errors.Errorf("You cannot specify --all and individual applications") 154 } 155 if len(c.units) != 0 { 156 return errors.Errorf("You cannot specify --all and individual units") 157 } 158 } else { 159 if len(c.machines) == 0 && len(c.applications) == 0 && len(c.units) == 0 { 160 return errors.Errorf("You must specify a target, either through --all, --machine, --application or --unit") 161 } 162 } 163 164 var nameErrors []string 165 for _, machineId := range c.machines { 166 if !names.IsValidMachine(machineId) { 167 nameErrors = append(nameErrors, fmt.Sprintf(" %q is not a valid machine id", machineId)) 168 } 169 } 170 for _, application := range c.applications { 171 if !names.IsValidApplication(application) { 172 nameErrors = append(nameErrors, fmt.Sprintf(" %q is not a valid application name", application)) 173 } 174 } 175 for _, unit := range c.units { 176 if validLeader.MatchString(unit) { 177 continue 178 } 179 180 if !names.IsValidUnit(unit) { 181 nameErrors = append(nameErrors, fmt.Sprintf(" %q is not a valid unit name", unit)) 182 } 183 } 184 if len(nameErrors) > 0 { 185 return errors.Errorf("The following run targets are not valid:\n%s", 186 strings.Join(nameErrors, "\n")) 187 } 188 189 return nil 190 } 191 192 // ConvertActionResults takes the results from the api and creates a map 193 // suitable for format conversion to YAML or JSON. 194 func ConvertActionResults(result params.ActionResult, query actionQuery) map[string]interface{} { 195 values := make(map[string]interface{}) 196 values[query.receiver.receiverType] = query.receiver.tag.Id() 197 if result.Error != nil { 198 values["Error"] = result.Error.Error() 199 values["Action"] = query.actionTag.Id() 200 return values 201 } 202 if result.Action.Tag != query.actionTag.String() { 203 values["Error"] = fmt.Sprintf("expected action tag %q, got %q", query.actionTag.String(), result.Action.Tag) 204 values["Action"] = query.actionTag.Id() 205 return values 206 } 207 if result.Action.Receiver != query.receiver.tag.String() { 208 values["Error"] = fmt.Sprintf("expected action receiver %q, got %q", query.receiver.tag.String(), result.Action.Receiver) 209 values["Action"] = query.actionTag.Id() 210 return values 211 } 212 if result.Message != "" { 213 values["Message"] = result.Message 214 } 215 // We always want to have a string for stdout, but only show stderr, 216 // code and error if they are there. 217 if res, ok := result.Output["Stdout"].(string); ok { 218 values["Stdout"] = strings.Replace(res, "\r\n", "\n", -1) 219 if res, ok := result.Output["StdoutEncoding"].(string); ok && res != "" { 220 values["Stdout.encoding"] = res 221 } 222 } else { 223 values["Stdout"] = "" 224 } 225 if res, ok := result.Output["Stderr"].(string); ok && res != "" { 226 values["Stderr"] = strings.Replace(res, "\r\n", "\n", -1) 227 if res, ok := result.Output["StderrEncoding"].(string); ok && res != "" { 228 values["Stderr.encoding"] = res 229 } 230 } 231 if res, ok := result.Output["Code"].(string); ok { 232 code, err := strconv.Atoi(res) 233 if err == nil && code != 0 { 234 values["ReturnCode"] = code 235 } 236 } 237 return values 238 } 239 240 func (c *runCommand) Run(ctx *cmd.Context) error { 241 client, err := getRunAPIClient(c) 242 if err != nil { 243 return err 244 } 245 defer client.Close() 246 247 var runResults []params.ActionResult 248 if c.all { 249 runResults, err = client.RunOnAllMachines(c.commands, c.timeout) 250 } else { 251 // Make sure the server supports <application>/leader syntax 252 for _, unit := range c.units { 253 if validLeader.MatchString(unit) && client.BestAPIVersion() < 3 { 254 app := strings.Split(unit, "/")[0] 255 return errors.Errorf("unable to determine leader for application %q"+ 256 "\nleader determination is unsupported by this API"+ 257 "\neither upgrade your controller, or explicitly specify a unit", app) 258 } 259 } 260 261 params := params.RunParams{ 262 Commands: c.commands, 263 Timeout: c.timeout, 264 Machines: c.machines, 265 Applications: c.applications, 266 Units: c.units, 267 } 268 runResults, err = client.Run(params) 269 } 270 271 if err != nil { 272 return block.ProcessBlockedError(err, block.BlockChange) 273 } 274 275 actionsToQuery := []actionQuery{} 276 for _, result := range runResults { 277 if result.Error != nil { 278 fmt.Fprintf(ctx.GetStderr(), "couldn't queue one action: %v\n", result.Error) 279 continue 280 } 281 actionTag, err := names.ParseActionTag(result.Action.Tag) 282 if err != nil { 283 fmt.Fprintf(ctx.GetStderr(), "got invalid action tag %v for receiver %v\n", result.Action.Tag, result.Action.Receiver) 284 continue 285 } 286 receiverTag, err := names.ActionReceiverFromTag(result.Action.Receiver) 287 if err != nil { 288 fmt.Fprintf(ctx.GetStderr(), "got invalid action receiver tag %v for action %v\n", result.Action.Receiver, result.Action.Tag) 289 continue 290 } 291 var receiverType string 292 switch receiverTag.(type) { 293 case names.UnitTag: 294 receiverType = "UnitId" 295 case names.MachineTag: 296 receiverType = "MachineId" 297 default: 298 receiverType = "ReceiverId" 299 } 300 actionsToQuery = append(actionsToQuery, actionQuery{ 301 actionTag: actionTag, 302 receiver: actionReceiver{ 303 receiverType: receiverType, 304 tag: receiverTag, 305 }}) 306 } 307 308 if len(actionsToQuery) == 0 { 309 return errors.New("no actions were successfully enqueued, aborting") 310 } 311 312 timeout := c.timeAfter(c.timeout) 313 values := []interface{}{} 314 for len(actionsToQuery) > 0 { 315 actionResults, err := client.Actions(entities(actionsToQuery)) 316 if err != nil { 317 return errors.Trace(err) 318 } 319 320 newActionsToQuery := []actionQuery{} 321 for i, result := range actionResults.Results { 322 if result.Error == nil { 323 switch result.Status { 324 case params.ActionRunning, params.ActionPending: 325 newActionsToQuery = append(newActionsToQuery, actionsToQuery[i]) 326 continue 327 } 328 } 329 330 values = append(values, ConvertActionResults(result, actionsToQuery[i])) 331 } 332 actionsToQuery = newActionsToQuery 333 334 if len(actionsToQuery) > 0 { 335 var timedOut bool 336 select { 337 case <-timeout: 338 timedOut = true 339 case <-c.timeAfter(1 * time.Second): 340 // TODO(axw) 2017-02-07 #1662451 341 // use a watcher instead of polling. 342 // this should be easier once we implement 343 // action grouping 344 } 345 if timedOut { 346 break 347 } 348 } 349 } 350 351 // If we are just dealing with one result, AND we are using the default 352 // format, then pretend we were running it locally. 353 if len(actionsToQuery) == 0 && len(values) == 1 && c.out.Name() == "default" { 354 result, ok := values[0].(map[string]interface{}) 355 if !ok { 356 return errors.New("couldn't read action output") 357 } 358 if res, ok := result["Error"].(string); ok { 359 return errors.New(res) 360 } 361 ctx.Stdout.Write(formatOutput(result, "Stdout")) 362 ctx.Stderr.Write(formatOutput(result, "Stderr")) 363 if code, ok := result["ReturnCode"].(int); ok && code != 0 { 364 return cmd.NewRcPassthroughError(code) 365 } 366 // Message should always contain only errors. 367 if res, ok := result["Message"].(string); ok && res != "" { 368 ctx.Stderr.Write([]byte(res)) 369 } 370 371 return nil 372 } 373 374 if len(values) > 0 { 375 if err := c.out.Write(ctx, values); err != nil { 376 return err 377 } 378 } 379 380 if n := len(actionsToQuery); n > 0 { 381 // There are action results remaining, so return an error. 382 suffix := "" 383 if n > 1 { 384 suffix = "s" 385 } 386 receivers := make([]string, n) 387 for i, actionToQuery := range actionsToQuery { 388 receivers[i] = names.ReadableString(actionToQuery.receiver.tag) 389 } 390 return errors.Errorf( 391 "timed out waiting for result%s from: %s", 392 suffix, strings.Join(receivers, ", "), 393 ) 394 } 395 return nil 396 } 397 398 type actionReceiver struct { 399 receiverType string 400 tag names.Tag 401 } 402 403 type actionQuery struct { 404 receiver actionReceiver 405 actionTag names.ActionTag 406 } 407 408 // RunClient exposes the capabilities required by the CLI 409 type RunClient interface { 410 action.APIClient 411 RunOnAllMachines(commands string, timeout time.Duration) ([]params.ActionResult, error) 412 Run(params.RunParams) ([]params.ActionResult, error) 413 } 414 415 // In order to be able to easily mock out the API side for testing, 416 // the API client is retrieved using a function. 417 var getRunAPIClient = func(c *runCommand) (RunClient, error) { 418 root, err := c.NewAPIRoot() 419 if err != nil { 420 return nil, errors.Trace(err) 421 } 422 return actionapi.NewClient(root), errors.Trace(err) 423 } 424 425 // getActionResult abstracts over the action CLI function that we use here to fetch results 426 var getActionResult = func(c RunClient, actionId string, wait *time.Timer) (params.ActionResult, error) { 427 return action.GetActionResult(c, actionId, wait) 428 } 429 430 // entities is a convenience constructor for params.Entities. 431 func entities(actions []actionQuery) params.Entities { 432 entities := params.Entities{ 433 Entities: make([]params.Entity, len(actions)), 434 } 435 for i, action := range actions { 436 entities.Entities[i].Tag = action.actionTag.String() 437 } 438 return entities 439 } 440 441 func formatOutput(results map[string]interface{}, key string) []byte { 442 res, ok := results[key].(string) 443 if !ok { 444 return []byte("") 445 } 446 if enc, ok := results[key+".encoding"].(string); ok && enc != "" { 447 switch enc { 448 case "base64": 449 decoded, err := base64.StdEncoding.DecodeString(res) 450 if err != nil { 451 return []byte("expected b64 encoded string, got " + res) 452 } 453 return decoded 454 default: 455 return []byte(res) 456 } 457 } 458 return []byte(res) 459 }