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