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