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  }