github.com/wallyworld/juju@v0.0.0-20161013125918-6cf1bc9d917a/cmd/juju/action/run.go (about)

     1  // Copyright 2014, 2015 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package action
     5  
     6  import (
     7  	"regexp"
     8  	"strings"
     9  
    10  	"github.com/juju/cmd"
    11  	"github.com/juju/errors"
    12  	"github.com/juju/gnuflag"
    13  	"gopkg.in/juju/names.v2"
    14  	yaml "gopkg.in/yaml.v2"
    15  
    16  	"github.com/juju/juju/apiserver/params"
    17  	"github.com/juju/juju/cmd/juju/common"
    18  	"github.com/juju/juju/cmd/modelcmd"
    19  	"github.com/juju/juju/cmd/output"
    20  )
    21  
    22  var keyRule = regexp.MustCompile("^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$")
    23  
    24  func NewRunCommand() cmd.Command {
    25  	return modelcmd.Wrap(&runCommand{})
    26  }
    27  
    28  // runCommand enqueues an Action for running on the given unit with given
    29  // params
    30  type runCommand struct {
    31  	ActionCommandBase
    32  	unitTag      names.UnitTag
    33  	actionName   string
    34  	paramsYAML   cmd.FileVar
    35  	parseStrings bool
    36  	out          cmd.Output
    37  	args         [][]string
    38  }
    39  
    40  const runDoc = `
    41  Queue an Action for execution on a given unit, with a given set of params.
    42  The Action ID is returned for use with 'juju show-action-output <ID>' or
    43  'juju show-action-status <ID>'.
    44   
    45  Params are validated according to the charm for the unit's application.  The 
    46  valid params can be seen using "juju actions <application> --schema".
    47  Params may be in a yaml file which is passed with the --params flag, or they
    48  may be specified by a key.key.key...=value format (see examples below.)
    49  
    50  Params given in the CLI invocation will be parsed as YAML unless the
    51  --string-args flag is set.  This can be helpful for values such as 'y', which
    52  is a boolean true in YAML.
    53  
    54  If --params is passed, along with key.key...=value explicit arguments, the
    55  explicit arguments will override the parameter file.
    56  
    57  Examples:
    58  
    59  $ juju run-action mysql/3 backup 
    60  action: <ID>
    61  
    62  $ juju show-action-output <ID>
    63  result:
    64    status: success
    65    file:
    66      size: 873.2
    67      units: GB
    68      name: foo.sql
    69  
    70  $ juju run-action mysql/3 backup --params parameters.yml
    71  ...
    72  Params sent will be the contents of parameters.yml.
    73  ...
    74  
    75  $ juju run-action mysql/3 backup out=out.tar.bz2 file.kind=xz file.quality=high
    76  ...
    77  Params sent will be:
    78  
    79  out: out.tar.bz2
    80  file:
    81    kind: xz
    82    quality: high
    83  ...
    84  
    85  $ juju run-action mysql/3 backup --params p.yml file.kind=xz file.quality=high
    86  ...
    87  If p.yml contains:
    88  
    89  file:
    90    location: /var/backups/mysql/
    91    kind: gzip
    92  
    93  then the merged args passed will be:
    94  
    95  file:
    96    location: /var/backups/mysql/
    97    kind: xz
    98    quality: high
    99  ...
   100  
   101  $ juju run-action sleeper/0 pause time=1000
   102  ...
   103  
   104  $ juju run-action sleeper/0 pause --string-args time=1000
   105  ...
   106  The value for the "time" param will be the string literal "1000".
   107  `
   108  
   109  // ActionNameRule describes the format an action name must match to be valid.
   110  var ActionNameRule = regexp.MustCompile("^[a-z](?:[a-z-]*[a-z])?$")
   111  
   112  // SetFlags offers an option for YAML output.
   113  func (c *runCommand) SetFlags(f *gnuflag.FlagSet) {
   114  	c.ActionCommandBase.SetFlags(f)
   115  	c.out.AddFlags(f, "yaml", output.DefaultFormatters)
   116  	f.Var(&c.paramsYAML, "params", "Path to yaml-formatted params file")
   117  	f.BoolVar(&c.parseStrings, "string-args", false, "Use raw string values of CLI args")
   118  }
   119  
   120  func (c *runCommand) Info() *cmd.Info {
   121  	return &cmd.Info{
   122  		Name:    "run-action",
   123  		Args:    "<unit> <action name> [key.key.key...=value]",
   124  		Purpose: "Queue an action for execution.",
   125  		Doc:     runDoc,
   126  	}
   127  }
   128  
   129  // Init gets the unit tag, and checks for other correct args.
   130  func (c *runCommand) Init(args []string) error {
   131  	switch len(args) {
   132  	case 0:
   133  		return errors.New("no unit specified")
   134  	case 1:
   135  		return errors.New("no action specified")
   136  	default:
   137  		// Grab and verify the unit and action names.
   138  		unitName := args[0]
   139  		if !names.IsValidUnit(unitName) {
   140  			return errors.Errorf("invalid unit name %q", unitName)
   141  		}
   142  		ActionName := args[1]
   143  		if valid := ActionNameRule.MatchString(ActionName); !valid {
   144  			return errors.Errorf("invalid action name %q", ActionName)
   145  		}
   146  		c.unitTag = names.NewUnitTag(unitName)
   147  		c.actionName = ActionName
   148  		if len(args) == 2 {
   149  			return nil
   150  		}
   151  		// Parse CLI key-value args if they exist.
   152  		c.args = make([][]string, 0)
   153  		for _, arg := range args[2:] {
   154  			thisArg := strings.SplitN(arg, "=", 2)
   155  			if len(thisArg) != 2 {
   156  				return errors.Errorf("argument %q must be of the form key...=value", arg)
   157  			}
   158  			keySlice := strings.Split(thisArg[0], ".")
   159  			// check each key for validity
   160  			for _, key := range keySlice {
   161  				if valid := keyRule.MatchString(key); !valid {
   162  					return errors.Errorf("key %q must start and end with lowercase alphanumeric, and contain only lowercase alphanumeric and hyphens", key)
   163  				}
   164  			}
   165  			// c.args={..., [key, key, key, key, value]}
   166  			c.args = append(c.args, append(keySlice, thisArg[1]))
   167  		}
   168  		return nil
   169  	}
   170  }
   171  
   172  func (c *runCommand) Run(ctx *cmd.Context) error {
   173  	api, err := c.NewActionAPIClient()
   174  	if err != nil {
   175  		return err
   176  	}
   177  	defer api.Close()
   178  
   179  	actionParams := map[string]interface{}{}
   180  
   181  	if c.paramsYAML.Path != "" {
   182  		b, err := c.paramsYAML.Read(ctx)
   183  		if err != nil {
   184  			return err
   185  		}
   186  
   187  		err = yaml.Unmarshal(b, &actionParams)
   188  		if err != nil {
   189  			return err
   190  		}
   191  
   192  		conformantParams, err := common.ConformYAML(actionParams)
   193  		if err != nil {
   194  			return err
   195  		}
   196  
   197  		betterParams, ok := conformantParams.(map[string]interface{})
   198  		if !ok {
   199  			return errors.New("params must contain a YAML map with string keys")
   200  		}
   201  
   202  		actionParams = betterParams
   203  	}
   204  
   205  	// If we had explicit args {..., [key, key, key, key, value], ...}
   206  	// then iterate and set params ..., key.key.key.key=value, ...
   207  	for _, argSlice := range c.args {
   208  		valueIndex := len(argSlice) - 1
   209  		keys := argSlice[:valueIndex]
   210  		value := argSlice[valueIndex]
   211  		cleansedValue := interface{}(value)
   212  		if !c.parseStrings {
   213  			err := yaml.Unmarshal([]byte(value), &cleansedValue)
   214  			if err != nil {
   215  				return err
   216  			}
   217  		}
   218  		// Insert the value in the map.
   219  		addValueToMap(keys, cleansedValue, actionParams)
   220  	}
   221  
   222  	conformantParams, err := common.ConformYAML(actionParams)
   223  	if err != nil {
   224  		return err
   225  	}
   226  
   227  	typedConformantParams, ok := conformantParams.(map[string]interface{})
   228  	if !ok {
   229  		return errors.Errorf("params must be a map, got %T", typedConformantParams)
   230  	}
   231  
   232  	actionParam := params.Actions{
   233  		Actions: []params.Action{{
   234  			Receiver:   c.unitTag.String(),
   235  			Name:       c.actionName,
   236  			Parameters: actionParams,
   237  		}},
   238  	}
   239  
   240  	results, err := api.Enqueue(actionParam)
   241  	if err != nil {
   242  		return err
   243  	}
   244  	if len(results.Results) != 1 {
   245  		return errors.New("illegal number of results returned")
   246  	}
   247  
   248  	result := results.Results[0]
   249  
   250  	if result.Error != nil {
   251  		return result.Error
   252  	}
   253  
   254  	if result.Action == nil {
   255  		return errors.New("action failed to enqueue")
   256  	}
   257  
   258  	tag, err := names.ParseActionTag(result.Action.Tag)
   259  	if err != nil {
   260  		return err
   261  	}
   262  
   263  	output := map[string]string{"Action queued with id": tag.Id()}
   264  	return c.out.Write(ctx, output)
   265  }