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 }