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