github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/cmd/juju/action/run.go (about)

     1  // Copyright 2014-2017 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package action
     5  
     6  import (
     7  	"regexp"
     8  	"strings"
     9  	"time"
    10  
    11  	"github.com/juju/cmd"
    12  	"github.com/juju/errors"
    13  	"github.com/juju/gnuflag"
    14  	"gopkg.in/juju/charm.v6"
    15  	"gopkg.in/juju/names.v2"
    16  	"gopkg.in/yaml.v2"
    17  
    18  	"github.com/juju/juju/apiserver/params"
    19  	jujucmd "github.com/juju/juju/cmd"
    20  	"github.com/juju/juju/cmd/juju/common"
    21  	"github.com/juju/juju/cmd/modelcmd"
    22  	"github.com/juju/juju/cmd/output"
    23  )
    24  
    25  // leaderSnippet is a regular expression for unit ID-like syntax that is used
    26  // to indicate the current leader for an application.
    27  const leaderSnippet = "(" + names.ApplicationSnippet + ")/leader"
    28  
    29  var validLeader = regexp.MustCompile("^" + leaderSnippet + "$")
    30  
    31  // nameRule describes the name format of an action or keyName must match to be valid.
    32  var nameRule = charm.GetActionNameRule()
    33  
    34  func NewRunCommand() cmd.Command {
    35  	return modelcmd.Wrap(&runCommand{})
    36  }
    37  
    38  // runCommand enqueues an Action for running on the given unit with given
    39  // params
    40  type runCommand struct {
    41  	ActionCommandBase
    42  	api           APIClient
    43  	unitReceivers []string
    44  	leaders       map[string]string
    45  	actionName    string
    46  	paramsYAML    cmd.FileVar
    47  	parseStrings  bool
    48  	wait          waitFlag
    49  	out           cmd.Output
    50  	args          [][]string
    51  }
    52  
    53  const runDoc = `
    54  Queue an Action for execution on a given unit, with a given set of params.
    55  The Action ID is returned for use with 'juju show-action-output <ID>' or
    56  'juju show-action-status <ID>'.
    57  
    58  Valid unit identifiers are: 
    59    a standard unit ID, such as mysql/0 or;
    60    leader syntax of the form <application>/leader, such as mysql/leader.
    61  
    62  If the leader syntax is used, the leader unit for the application will be
    63  resolved before the action is enqueued.
    64  
    65  Params are validated according to the charm for the unit's application.  The
    66  valid params can be seen using "juju actions <application> --schema".
    67  Params may be in a yaml file which is passed with the --params option, or they
    68  may be specified by a key.key.key...=value format (see examples below.)
    69  
    70  Params given in the CLI invocation will be parsed as YAML unless the
    71  --string-args option is set.  This can be helpful for values such as 'y', which
    72  is a boolean true in YAML.
    73  
    74  If --params is passed, along with key.key...=value explicit arguments, the
    75  explicit arguments will override the parameter file.
    76  
    77  Examples:
    78  
    79  $ juju run-action mysql/3 backup --wait
    80  action-id: <ID>
    81  result:
    82    status: success
    83    file:
    84      size: 873.2
    85      units: GB
    86      name: foo.sql
    87  
    88  
    89  $ juju run-action mysql/3 backup
    90  action: <ID>
    91  
    92  $ juju run-action mysql/leader backup
    93  resolved leader: mysql/0
    94  action: <ID>
    95  
    96  $ juju show-action-output <ID>
    97  result:
    98    status: success
    99    file:
   100      size: 873.2
   101      units: GB
   102      name: foo.sql
   103  
   104  $ juju run-action mysql/3 backup --params parameters.yml
   105  ...
   106  Params sent will be the contents of parameters.yml.
   107  ...
   108  
   109  $ juju run-action mysql/3 backup out=out.tar.bz2 file.kind=xz file.quality=high
   110  ...
   111  Params sent will be:
   112  
   113  out: out.tar.bz2
   114  file:
   115    kind: xz
   116    quality: high
   117  ...
   118  
   119  $ juju run-action mysql/3 backup --params p.yml file.kind=xz file.quality=high
   120  ...
   121  If p.yml contains:
   122  
   123  file:
   124    location: /var/backups/mysql/
   125    kind: gzip
   126  
   127  then the merged args passed will be:
   128  
   129  file:
   130    location: /var/backups/mysql/
   131    kind: xz
   132    quality: high
   133  ...
   134  
   135  $ juju run-action sleeper/0 pause time=1000
   136  ...
   137  
   138  $ juju run-action sleeper/0 pause --string-args time=1000
   139  ...
   140  The value for the "time" param will be the string literal "1000".
   141  `
   142  
   143  // SetFlags offers an option for YAML output.
   144  func (c *runCommand) SetFlags(f *gnuflag.FlagSet) {
   145  	c.ActionCommandBase.SetFlags(f)
   146  	c.out.AddFlags(f, "yaml", output.DefaultFormatters)
   147  	f.Var(&c.paramsYAML, "params", "Path to yaml-formatted params file")
   148  	f.BoolVar(&c.parseStrings, "string-args", false, "Use raw string values of CLI args")
   149  	f.Var(&c.wait, "wait", "Wait for results, with optional timeout")
   150  }
   151  
   152  func (c *runCommand) Info() *cmd.Info {
   153  	return jujucmd.Info(&cmd.Info{
   154  		Name:    "run-action",
   155  		Args:    "<unit> [<unit> ...] <action name> [key.key.key...=value]",
   156  		Purpose: "Queue an action for execution.",
   157  		Doc:     runDoc,
   158  	})
   159  }
   160  
   161  // Init gets the unit tag(s), action name and action arguments.
   162  func (c *runCommand) Init(args []string) (err error) {
   163  	for _, arg := range args {
   164  		if names.IsValidUnit(arg) || validLeader.MatchString(arg) {
   165  			c.unitReceivers = append(c.unitReceivers, arg)
   166  		} else if nameRule.MatchString(arg) {
   167  			c.actionName = arg
   168  			break
   169  		} else {
   170  			return errors.Errorf("invalid unit or action name %q", arg)
   171  		}
   172  	}
   173  	if len(c.unitReceivers) == 0 {
   174  		return errors.New("no unit specified")
   175  	}
   176  	if c.actionName == "" {
   177  		return errors.New("no action specified")
   178  	}
   179  
   180  	// Parse CLI key-value args if they exist.
   181  	c.args = make([][]string, 0)
   182  	for _, arg := range args[len(c.unitReceivers)+1:] {
   183  		thisArg := strings.SplitN(arg, "=", 2)
   184  		if len(thisArg) != 2 {
   185  			return errors.Errorf("argument %q must be of the form key...=value", arg)
   186  		}
   187  		keySlice := strings.Split(thisArg[0], ".")
   188  		// check each key for validity
   189  		for _, key := range keySlice {
   190  			if valid := nameRule.MatchString(key); !valid {
   191  				return errors.Errorf("key %q must start and end with lowercase alphanumeric, "+
   192  					"and contain only lowercase alphanumeric and hyphens", key)
   193  			}
   194  		}
   195  		// c.args={..., [key, key, key, key, value]}
   196  		c.args = append(c.args, append(keySlice, thisArg[1]))
   197  	}
   198  	return nil
   199  }
   200  
   201  func (c *runCommand) Run(ctx *cmd.Context) error {
   202  	if err := c.ensureAPI(); err != nil {
   203  		return errors.Trace(err)
   204  	}
   205  	defer c.api.Close()
   206  
   207  	actionParams := map[string]interface{}{}
   208  	if c.paramsYAML.Path != "" {
   209  		b, err := c.paramsYAML.Read(ctx)
   210  		if err != nil {
   211  			return err
   212  		}
   213  
   214  		err = yaml.Unmarshal(b, &actionParams)
   215  		if err != nil {
   216  			return err
   217  		}
   218  
   219  		conformantParams, err := common.ConformYAML(actionParams)
   220  		if err != nil {
   221  			return err
   222  		}
   223  
   224  		betterParams, ok := conformantParams.(map[string]interface{})
   225  		if !ok {
   226  			return errors.New("params must contain a YAML map with string keys")
   227  		}
   228  
   229  		actionParams = betterParams
   230  	}
   231  
   232  	// If we had explicit args {..., [key, key, key, key, value], ...}
   233  	// then iterate and set params ..., key.key.key.key=value, ...
   234  	for _, argSlice := range c.args {
   235  		valueIndex := len(argSlice) - 1
   236  		keys := argSlice[:valueIndex]
   237  		value := argSlice[valueIndex]
   238  		cleansedValue := interface{}(value)
   239  		if !c.parseStrings {
   240  			err := yaml.Unmarshal([]byte(value), &cleansedValue)
   241  			if err != nil {
   242  				return err
   243  			}
   244  		}
   245  		// Insert the value in the map.
   246  		addValueToMap(keys, cleansedValue, actionParams)
   247  	}
   248  
   249  	conformantParams, err := common.ConformYAML(actionParams)
   250  	if err != nil {
   251  		return err
   252  	}
   253  
   254  	typedConformantParams, ok := conformantParams.(map[string]interface{})
   255  	if !ok {
   256  		return errors.Errorf("params must be a map, got %T", typedConformantParams)
   257  	}
   258  
   259  	actions := make([]params.Action, len(c.unitReceivers))
   260  	for i, unitReceiver := range c.unitReceivers {
   261  		if strings.HasSuffix(unitReceiver, "leader") {
   262  			if c.api.BestAPIVersion() < 3 {
   263  				app := strings.Split(unitReceiver, "/")[0]
   264  				return errors.Errorf("unable to determine leader for application %q"+
   265  					"\nleader determination is unsupported by this API"+
   266  					"\neither upgrade your controller, or explicitly specify a unit", app)
   267  			}
   268  			actions[i].Receiver = unitReceiver
   269  		} else {
   270  			actions[i].Receiver = names.NewUnitTag(unitReceiver).String()
   271  		}
   272  		actions[i].Name = c.actionName
   273  		actions[i].Parameters = actionParams
   274  	}
   275  	results, err := c.api.Enqueue(params.Actions{Actions: actions})
   276  	if err != nil {
   277  		return err
   278  	}
   279  
   280  	if len(results.Results) != len(c.unitReceivers) {
   281  		return errors.New("illegal number of results returned")
   282  	}
   283  
   284  	for _, result := range results.Results {
   285  		if result.Error != nil {
   286  			return result.Error
   287  		}
   288  		if result.Action == nil {
   289  			return errors.Errorf("action failed to enqueue on %q", result.Action.Receiver)
   290  		}
   291  		tag, err := names.ParseActionTag(result.Action.Tag)
   292  		if err != nil {
   293  			return err
   294  		}
   295  
   296  		// Legacy Juju 1.25 output format for a single unit, no wait.
   297  		if !c.wait.forever && c.wait.d.Nanoseconds() <= 0 && len(results.Results) == 1 {
   298  			out := map[string]string{"Action queued with id": tag.Id()}
   299  			return c.out.Write(ctx, out)
   300  		}
   301  	}
   302  
   303  	out := make(map[string]interface{}, len(results.Results))
   304  
   305  	// Immediate return. This is the default, although rarely
   306  	// what cli users want. We should consider changing this
   307  	// default with Juju 3.0.
   308  	if !c.wait.forever && c.wait.d.Nanoseconds() <= 0 {
   309  		for _, result := range results.Results {
   310  			out[result.Action.Receiver] = result.Action.Tag
   311  			actionTag, err := names.ParseActionTag(result.Action.Tag)
   312  			if err != nil {
   313  				return err
   314  			}
   315  			unitTag, err := names.ParseUnitTag(result.Action.Receiver)
   316  			if err != nil {
   317  				return err
   318  			}
   319  			out[result.Action.Receiver] = map[string]string{
   320  				"id":   actionTag.Id(),
   321  				"unit": unitTag.Id(),
   322  			}
   323  		}
   324  		return c.out.Write(ctx, out)
   325  	}
   326  
   327  	var wait *time.Timer
   328  	if c.wait.d.Nanoseconds() <= 0 {
   329  		// Indefinite wait. Discard the tick.
   330  		wait = time.NewTimer(0 * time.Second)
   331  		_ = <-wait.C
   332  	} else {
   333  		wait = time.NewTimer(c.wait.d)
   334  	}
   335  
   336  	for _, result := range results.Results {
   337  		tag, err := names.ParseActionTag(result.Action.Tag)
   338  		if err != nil {
   339  			return err
   340  		}
   341  		result, err = GetActionResult(c.api, tag.Id(), wait)
   342  		if err != nil {
   343  			return errors.Trace(err)
   344  		}
   345  		unitTag, err := names.ParseUnitTag(result.Action.Receiver)
   346  		if err != nil {
   347  			return err
   348  		}
   349  		d := FormatActionResult(result)
   350  		d["id"] = tag.Id()       // Action ID is required in case we timed out.
   351  		d["unit"] = unitTag.Id() // Formatted unit is nice to have.
   352  		out[result.Action.Receiver] = d
   353  	}
   354  	return c.out.Write(ctx, out)
   355  }
   356  
   357  func (c *runCommand) ensureAPI() (err error) {
   358  	if c.api != nil {
   359  		return nil
   360  	}
   361  	c.api, err = c.NewActionAPIClient()
   362  	return errors.Trace(err)
   363  }