github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/cmd/juju/action/run.go (about) 1 // Copyright 2014-2017 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package action 5 6 import ( 7 "regexp" 8 "strings" 9 "time" 10 11 "github.com/juju/cmd" 12 "github.com/juju/errors" 13 "github.com/juju/gnuflag" 14 "gopkg.in/juju/charm.v6" 15 "gopkg.in/juju/names.v2" 16 "gopkg.in/yaml.v2" 17 18 "github.com/juju/juju/apiserver/params" 19 jujucmd "github.com/juju/juju/cmd" 20 "github.com/juju/juju/cmd/juju/common" 21 "github.com/juju/juju/cmd/modelcmd" 22 "github.com/juju/juju/cmd/output" 23 ) 24 25 // leaderSnippet is a regular expression for unit ID-like syntax that is used 26 // to indicate the current leader for an application. 27 const leaderSnippet = "(" + names.ApplicationSnippet + ")/leader" 28 29 var validLeader = regexp.MustCompile("^" + leaderSnippet + "$") 30 31 // nameRule describes the name format of an action or keyName must match to be valid. 32 var nameRule = charm.GetActionNameRule() 33 34 func NewRunCommand() cmd.Command { 35 return modelcmd.Wrap(&runCommand{}) 36 } 37 38 // runCommand enqueues an Action for running on the given unit with given 39 // params 40 type runCommand struct { 41 ActionCommandBase 42 api APIClient 43 unitReceivers []string 44 leaders map[string]string 45 actionName string 46 paramsYAML cmd.FileVar 47 parseStrings bool 48 wait waitFlag 49 out cmd.Output 50 args [][]string 51 } 52 53 const runDoc = ` 54 Queue an Action for execution on a given unit, with a given set of params. 55 The Action ID is returned for use with 'juju show-action-output <ID>' or 56 'juju show-action-status <ID>'. 57 58 Valid unit identifiers are: 59 a standard unit ID, such as mysql/0 or; 60 leader syntax of the form <application>/leader, such as mysql/leader. 61 62 If the leader syntax is used, the leader unit for the application will be 63 resolved before the action is enqueued. 64 65 Params are validated according to the charm for the unit's application. The 66 valid params can be seen using "juju actions <application> --schema". 67 Params may be in a yaml file which is passed with the --params option, or they 68 may be specified by a key.key.key...=value format (see examples below.) 69 70 Params given in the CLI invocation will be parsed as YAML unless the 71 --string-args option is set. This can be helpful for values such as 'y', which 72 is a boolean true in YAML. 73 74 If --params is passed, along with key.key...=value explicit arguments, the 75 explicit arguments will override the parameter file. 76 77 Examples: 78 79 $ juju run-action mysql/3 backup --wait 80 action-id: <ID> 81 result: 82 status: success 83 file: 84 size: 873.2 85 units: GB 86 name: foo.sql 87 88 89 $ juju run-action mysql/3 backup 90 action: <ID> 91 92 $ juju run-action mysql/leader backup 93 resolved leader: mysql/0 94 action: <ID> 95 96 $ juju show-action-output <ID> 97 result: 98 status: success 99 file: 100 size: 873.2 101 units: GB 102 name: foo.sql 103 104 $ juju run-action mysql/3 backup --params parameters.yml 105 ... 106 Params sent will be the contents of parameters.yml. 107 ... 108 109 $ juju run-action mysql/3 backup out=out.tar.bz2 file.kind=xz file.quality=high 110 ... 111 Params sent will be: 112 113 out: out.tar.bz2 114 file: 115 kind: xz 116 quality: high 117 ... 118 119 $ juju run-action mysql/3 backup --params p.yml file.kind=xz file.quality=high 120 ... 121 If p.yml contains: 122 123 file: 124 location: /var/backups/mysql/ 125 kind: gzip 126 127 then the merged args passed will be: 128 129 file: 130 location: /var/backups/mysql/ 131 kind: xz 132 quality: high 133 ... 134 135 $ juju run-action sleeper/0 pause time=1000 136 ... 137 138 $ juju run-action sleeper/0 pause --string-args time=1000 139 ... 140 The value for the "time" param will be the string literal "1000". 141 ` 142 143 // SetFlags offers an option for YAML output. 144 func (c *runCommand) SetFlags(f *gnuflag.FlagSet) { 145 c.ActionCommandBase.SetFlags(f) 146 c.out.AddFlags(f, "yaml", output.DefaultFormatters) 147 f.Var(&c.paramsYAML, "params", "Path to yaml-formatted params file") 148 f.BoolVar(&c.parseStrings, "string-args", false, "Use raw string values of CLI args") 149 f.Var(&c.wait, "wait", "Wait for results, with optional timeout") 150 } 151 152 func (c *runCommand) Info() *cmd.Info { 153 return jujucmd.Info(&cmd.Info{ 154 Name: "run-action", 155 Args: "<unit> [<unit> ...] <action name> [key.key.key...=value]", 156 Purpose: "Queue an action for execution.", 157 Doc: runDoc, 158 }) 159 } 160 161 // Init gets the unit tag(s), action name and action arguments. 162 func (c *runCommand) Init(args []string) (err error) { 163 for _, arg := range args { 164 if names.IsValidUnit(arg) || validLeader.MatchString(arg) { 165 c.unitReceivers = append(c.unitReceivers, arg) 166 } else if nameRule.MatchString(arg) { 167 c.actionName = arg 168 break 169 } else { 170 return errors.Errorf("invalid unit or action name %q", arg) 171 } 172 } 173 if len(c.unitReceivers) == 0 { 174 return errors.New("no unit specified") 175 } 176 if c.actionName == "" { 177 return errors.New("no action specified") 178 } 179 180 // Parse CLI key-value args if they exist. 181 c.args = make([][]string, 0) 182 for _, arg := range args[len(c.unitReceivers)+1:] { 183 thisArg := strings.SplitN(arg, "=", 2) 184 if len(thisArg) != 2 { 185 return errors.Errorf("argument %q must be of the form key...=value", arg) 186 } 187 keySlice := strings.Split(thisArg[0], ".") 188 // check each key for validity 189 for _, key := range keySlice { 190 if valid := nameRule.MatchString(key); !valid { 191 return errors.Errorf("key %q must start and end with lowercase alphanumeric, "+ 192 "and contain only lowercase alphanumeric and hyphens", key) 193 } 194 } 195 // c.args={..., [key, key, key, key, value]} 196 c.args = append(c.args, append(keySlice, thisArg[1])) 197 } 198 return nil 199 } 200 201 func (c *runCommand) Run(ctx *cmd.Context) error { 202 if err := c.ensureAPI(); err != nil { 203 return errors.Trace(err) 204 } 205 defer c.api.Close() 206 207 actionParams := map[string]interface{}{} 208 if c.paramsYAML.Path != "" { 209 b, err := c.paramsYAML.Read(ctx) 210 if err != nil { 211 return err 212 } 213 214 err = yaml.Unmarshal(b, &actionParams) 215 if err != nil { 216 return err 217 } 218 219 conformantParams, err := common.ConformYAML(actionParams) 220 if err != nil { 221 return err 222 } 223 224 betterParams, ok := conformantParams.(map[string]interface{}) 225 if !ok { 226 return errors.New("params must contain a YAML map with string keys") 227 } 228 229 actionParams = betterParams 230 } 231 232 // If we had explicit args {..., [key, key, key, key, value], ...} 233 // then iterate and set params ..., key.key.key.key=value, ... 234 for _, argSlice := range c.args { 235 valueIndex := len(argSlice) - 1 236 keys := argSlice[:valueIndex] 237 value := argSlice[valueIndex] 238 cleansedValue := interface{}(value) 239 if !c.parseStrings { 240 err := yaml.Unmarshal([]byte(value), &cleansedValue) 241 if err != nil { 242 return err 243 } 244 } 245 // Insert the value in the map. 246 addValueToMap(keys, cleansedValue, actionParams) 247 } 248 249 conformantParams, err := common.ConformYAML(actionParams) 250 if err != nil { 251 return err 252 } 253 254 typedConformantParams, ok := conformantParams.(map[string]interface{}) 255 if !ok { 256 return errors.Errorf("params must be a map, got %T", typedConformantParams) 257 } 258 259 actions := make([]params.Action, len(c.unitReceivers)) 260 for i, unitReceiver := range c.unitReceivers { 261 if strings.HasSuffix(unitReceiver, "leader") { 262 if c.api.BestAPIVersion() < 3 { 263 app := strings.Split(unitReceiver, "/")[0] 264 return errors.Errorf("unable to determine leader for application %q"+ 265 "\nleader determination is unsupported by this API"+ 266 "\neither upgrade your controller, or explicitly specify a unit", app) 267 } 268 actions[i].Receiver = unitReceiver 269 } else { 270 actions[i].Receiver = names.NewUnitTag(unitReceiver).String() 271 } 272 actions[i].Name = c.actionName 273 actions[i].Parameters = actionParams 274 } 275 results, err := c.api.Enqueue(params.Actions{Actions: actions}) 276 if err != nil { 277 return err 278 } 279 280 if len(results.Results) != len(c.unitReceivers) { 281 return errors.New("illegal number of results returned") 282 } 283 284 for _, result := range results.Results { 285 if result.Error != nil { 286 return result.Error 287 } 288 if result.Action == nil { 289 return errors.Errorf("action failed to enqueue on %q", result.Action.Receiver) 290 } 291 tag, err := names.ParseActionTag(result.Action.Tag) 292 if err != nil { 293 return err 294 } 295 296 // Legacy Juju 1.25 output format for a single unit, no wait. 297 if !c.wait.forever && c.wait.d.Nanoseconds() <= 0 && len(results.Results) == 1 { 298 out := map[string]string{"Action queued with id": tag.Id()} 299 return c.out.Write(ctx, out) 300 } 301 } 302 303 out := make(map[string]interface{}, len(results.Results)) 304 305 // Immediate return. This is the default, although rarely 306 // what cli users want. We should consider changing this 307 // default with Juju 3.0. 308 if !c.wait.forever && c.wait.d.Nanoseconds() <= 0 { 309 for _, result := range results.Results { 310 out[result.Action.Receiver] = result.Action.Tag 311 actionTag, err := names.ParseActionTag(result.Action.Tag) 312 if err != nil { 313 return err 314 } 315 unitTag, err := names.ParseUnitTag(result.Action.Receiver) 316 if err != nil { 317 return err 318 } 319 out[result.Action.Receiver] = map[string]string{ 320 "id": actionTag.Id(), 321 "unit": unitTag.Id(), 322 } 323 } 324 return c.out.Write(ctx, out) 325 } 326 327 var wait *time.Timer 328 if c.wait.d.Nanoseconds() <= 0 { 329 // Indefinite wait. Discard the tick. 330 wait = time.NewTimer(0 * time.Second) 331 _ = <-wait.C 332 } else { 333 wait = time.NewTimer(c.wait.d) 334 } 335 336 for _, result := range results.Results { 337 tag, err := names.ParseActionTag(result.Action.Tag) 338 if err != nil { 339 return err 340 } 341 result, err = GetActionResult(c.api, tag.Id(), wait) 342 if err != nil { 343 return errors.Trace(err) 344 } 345 unitTag, err := names.ParseUnitTag(result.Action.Receiver) 346 if err != nil { 347 return err 348 } 349 d := FormatActionResult(result) 350 d["id"] = tag.Id() // Action ID is required in case we timed out. 351 d["unit"] = unitTag.Id() // Formatted unit is nice to have. 352 out[result.Action.Receiver] = d 353 } 354 return c.out.Write(ctx, out) 355 } 356 357 func (c *runCommand) ensureAPI() (err error) { 358 if c.api != nil { 359 return nil 360 } 361 c.api, err = c.NewActionAPIClient() 362 return errors.Trace(err) 363 }