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