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