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  }