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  }