github.com/cloudbase/juju-core@v0.0.0-20140504232958-a7271ac7912f/cmd/juju/run.go (about) 1 // Copyright 2013 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package main 5 6 import ( 7 "encoding/base64" 8 "errors" 9 "fmt" 10 "strings" 11 "time" 12 "unicode/utf8" 13 14 "launchpad.net/gnuflag" 15 16 "launchpad.net/juju-core/cmd" 17 "launchpad.net/juju-core/juju" 18 "launchpad.net/juju-core/names" 19 "launchpad.net/juju-core/state/api/params" 20 ) 21 22 // RunCommand is responsible for running arbitrary commands on remote machines. 23 type RunCommand struct { 24 cmd.EnvCommandBase 25 out cmd.Output 26 all bool 27 timeout time.Duration 28 machines []string 29 services []string 30 units []string 31 commands string 32 } 33 34 const runDoc = ` 35 Run the commands on the specified targets. 36 37 Targets are specified using either machine ids, service names or unit 38 names. At least one target specifier is needed. 39 40 Multiple values can be set for --machine, --service, and --unit by using 41 comma separated values. 42 43 If the target is a machine, the command is run as the "ubuntu" user on 44 the remote machine. 45 46 If the target is a service, the command is run on all units for that 47 service. For example, if there was a service "mysql" and that service 48 had two units, "mysql/0" and "mysql/1", then 49 --service mysql 50 is equivalent to 51 --unit mysql/0,mysql/1 52 53 Commands run for services or units are executed in a 'hook context' for 54 the unit. 55 56 --all is provided as a simple way to run the command on all the machines 57 in the environment. If you specify --all you cannot provide additional 58 targets. 59 60 ` 61 62 func (c *RunCommand) Info() *cmd.Info { 63 return &cmd.Info{ 64 Name: "run", 65 Args: "<commands>", 66 Purpose: "run the commands on the remote targets specified", 67 Doc: runDoc, 68 } 69 } 70 71 func (c *RunCommand) SetFlags(f *gnuflag.FlagSet) { 72 c.EnvCommandBase.SetFlags(f) 73 c.out.AddFlags(f, "smart", cmd.DefaultFormatters) 74 f.BoolVar(&c.all, "all", false, "run the commands on all the machines") 75 f.DurationVar(&c.timeout, "timeout", 5*time.Minute, "how long to wait before the remote command is considered to have failed") 76 f.Var(cmd.NewStringsValue(nil, &c.machines), "machine", "one or more machine ids") 77 f.Var(cmd.NewStringsValue(nil, &c.services), "service", "one or more service names") 78 f.Var(cmd.NewStringsValue(nil, &c.units), "unit", "one or more unit ids") 79 } 80 81 func (c *RunCommand) Init(args []string) error { 82 if len(args) == 0 { 83 return errors.New("no commands specified") 84 } 85 c.commands, args = args[0], args[1:] 86 87 if c.all { 88 if len(c.machines) != 0 { 89 return fmt.Errorf("You cannot specify --all and individual machines") 90 } 91 if len(c.services) != 0 { 92 return fmt.Errorf("You cannot specify --all and individual services") 93 } 94 if len(c.units) != 0 { 95 return fmt.Errorf("You cannot specify --all and individual units") 96 } 97 } else { 98 if len(c.machines) == 0 && len(c.services) == 0 && len(c.units) == 0 { 99 return fmt.Errorf("You must specify a target, either through --all, --machine, --service or --unit") 100 } 101 } 102 103 var nameErrors []string 104 for _, machineId := range c.machines { 105 if !names.IsMachine(machineId) { 106 nameErrors = append(nameErrors, fmt.Sprintf(" %q is not a valid machine id", machineId)) 107 } 108 } 109 for _, service := range c.services { 110 if !names.IsService(service) { 111 nameErrors = append(nameErrors, fmt.Sprintf(" %q is not a valid service name", service)) 112 } 113 } 114 for _, unit := range c.units { 115 if !names.IsUnit(unit) { 116 nameErrors = append(nameErrors, fmt.Sprintf(" %q is not a valid unit name", unit)) 117 } 118 } 119 if len(nameErrors) > 0 { 120 return fmt.Errorf("The following run targets are not valid:\n%s", 121 strings.Join(nameErrors, "\n")) 122 } 123 124 return cmd.CheckEmpty(args) 125 } 126 127 func encodeBytes(input []byte) (value string, encoding string) { 128 if utf8.Valid(input) { 129 value = string(input) 130 encoding = "utf8" 131 } else { 132 value = base64.StdEncoding.EncodeToString(input) 133 encoding = "base64" 134 } 135 return value, encoding 136 } 137 138 func storeOutput(values map[string]interface{}, key string, input []byte) { 139 value, encoding := encodeBytes(input) 140 values[key] = value 141 if encoding != "utf8" { 142 values[key+".encoding"] = encoding 143 } 144 } 145 146 // ConvertRunResults takes the results from the api and creates a map 147 // suitable for format converstion to YAML or JSON. 148 func ConvertRunResults(runResults []params.RunResult) interface{} { 149 var results = make([]interface{}, len(runResults)) 150 151 for i, result := range runResults { 152 // We always want to have a string for stdout, but only show stderr, 153 // code and error if they are there. 154 values := make(map[string]interface{}) 155 values["MachineId"] = result.MachineId 156 if result.UnitId != "" { 157 values["UnitId"] = result.UnitId 158 159 } 160 storeOutput(values, "Stdout", result.Stdout) 161 if len(result.Stderr) > 0 { 162 storeOutput(values, "Stderr", result.Stderr) 163 } 164 if result.Code != 0 { 165 values["ReturnCode"] = result.Code 166 } 167 if result.Error != "" { 168 values["Error"] = result.Error 169 } 170 results[i] = values 171 } 172 173 return results 174 } 175 176 func (c *RunCommand) Run(ctx *cmd.Context) error { 177 client, err := getAPIClient(c.EnvName) 178 if err != nil { 179 return err 180 } 181 defer client.Close() 182 183 var runResults []params.RunResult 184 if c.all { 185 runResults, err = client.RunOnAllMachines(c.commands, c.timeout) 186 } else { 187 params := params.RunParams{ 188 Commands: c.commands, 189 Timeout: c.timeout, 190 Machines: c.machines, 191 Services: c.services, 192 Units: c.units, 193 } 194 runResults, err = client.Run(params) 195 } 196 197 if err != nil { 198 return err 199 } 200 201 // If we are just dealing with one result, AND we are using the smart 202 // format, then pretend we were running it locally. 203 if len(runResults) == 1 && c.out.Name() == "smart" { 204 result := runResults[0] 205 ctx.Stdout.Write(result.Stdout) 206 ctx.Stderr.Write(result.Stderr) 207 if result.Error != "" { 208 // Convert the error string back into an error object. 209 return fmt.Errorf("%s", result.Error) 210 } 211 if result.Code != 0 { 212 return cmd.NewRcPassthroughError(result.Code) 213 } 214 return nil 215 } 216 217 c.out.Write(ctx, ConvertRunResults(runResults)) 218 return nil 219 } 220 221 // In order to be able to easily mock out the API side for testing, 222 // the API client is got using a function. 223 224 type RunClient interface { 225 Close() error 226 RunOnAllMachines(commands string, timeout time.Duration) ([]params.RunResult, error) 227 Run(run params.RunParams) ([]params.RunResult, error) 228 } 229 230 // Here we need the signature to be correct for the interface. 231 var getAPIClient = func(name string) (RunClient, error) { 232 return juju.NewAPIClientFromName(name) 233 }