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