github.com/axw/juju@v0.0.0-20161005053422-4bd6544d08d4/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 }