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