github.com/cloudbase/juju-core@v0.0.0-20140504232958-a7271ac7912f/cmd/juju/run_test.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 "fmt" 8 "time" 9 10 gc "launchpad.net/gocheck" 11 12 "launchpad.net/juju-core/cmd" 13 "launchpad.net/juju-core/state/api/params" 14 "launchpad.net/juju-core/testing" 15 jc "launchpad.net/juju-core/testing/checkers" 16 "launchpad.net/juju-core/utils/exec" 17 ) 18 19 type RunSuite struct { 20 testing.FakeHomeSuite 21 } 22 23 var _ = gc.Suite(&RunSuite{}) 24 25 func (*RunSuite) TestTargetArgParsing(c *gc.C) { 26 for i, test := range []struct { 27 message string 28 args []string 29 all bool 30 machines []string 31 units []string 32 services []string 33 commands string 34 errMatch string 35 }{{ 36 message: "no args", 37 errMatch: "no commands specified", 38 }, { 39 message: "no target", 40 args: []string{"sudo reboot"}, 41 errMatch: "You must specify a target, either through --all, --machine, --service or --unit", 42 }, { 43 message: "too many args", 44 args: []string{"--all", "sudo reboot", "oops"}, 45 errMatch: `unrecognized args: \["oops"\]`, 46 }, { 47 message: "command to all machines", 48 args: []string{"--all", "sudo reboot"}, 49 all: true, 50 commands: "sudo reboot", 51 }, { 52 message: "all and defined machines", 53 args: []string{"--all", "--machine=1,2", "sudo reboot"}, 54 errMatch: `You cannot specify --all and individual machines`, 55 }, { 56 message: "command to machines 1, 2, and 1/kvm/0", 57 args: []string{"--machine=1,2,1/kvm/0", "sudo reboot"}, 58 commands: "sudo reboot", 59 machines: []string{"1", "2", "1/kvm/0"}, 60 }, { 61 message: "bad machine names", 62 args: []string{"--machine=foo,machine-2", "sudo reboot"}, 63 errMatch: "" + 64 "The following run targets are not valid:\n" + 65 " \"foo\" is not a valid machine id\n" + 66 " \"machine-2\" is not a valid machine id", 67 }, { 68 message: "all and defined services", 69 args: []string{"--all", "--service=wordpress,mysql", "sudo reboot"}, 70 errMatch: `You cannot specify --all and individual services`, 71 }, { 72 message: "command to services wordpress and mysql", 73 args: []string{"--service=wordpress,mysql", "sudo reboot"}, 74 commands: "sudo reboot", 75 services: []string{"wordpress", "mysql"}, 76 }, { 77 message: "bad service names", 78 args: []string{"--service", "foo,2,foo/0", "sudo reboot"}, 79 errMatch: "" + 80 "The following run targets are not valid:\n" + 81 " \"2\" is not a valid service name\n" + 82 " \"foo/0\" is not a valid service name", 83 }, { 84 message: "all and defined units", 85 args: []string{"--all", "--unit=wordpress/0,mysql/1", "sudo reboot"}, 86 errMatch: `You cannot specify --all and individual units`, 87 }, { 88 message: "command to valid units", 89 args: []string{"--unit=wordpress/0,wordpress/1,mysql/0", "sudo reboot"}, 90 commands: "sudo reboot", 91 units: []string{"wordpress/0", "wordpress/1", "mysql/0"}, 92 }, { 93 message: "bad unit names", 94 args: []string{"--unit", "foo,2,foo/0", "sudo reboot"}, 95 errMatch: "" + 96 "The following run targets are not valid:\n" + 97 " \"foo\" is not a valid unit name\n" + 98 " \"2\" is not a valid unit name", 99 }, { 100 message: "command to mixed valid targets", 101 args: []string{"--machine=0", "--unit=wordpress/0,wordpress/1", "--service=mysql", "sudo reboot"}, 102 commands: "sudo reboot", 103 machines: []string{"0"}, 104 services: []string{"mysql"}, 105 units: []string{"wordpress/0", "wordpress/1"}, 106 }} { 107 c.Log(fmt.Sprintf("%v: %s", i, test.message)) 108 runCmd := &RunCommand{} 109 testing.TestInit(c, runCmd, test.args, test.errMatch) 110 if test.errMatch == "" { 111 c.Check(runCmd.all, gc.Equals, test.all) 112 c.Check(runCmd.machines, gc.DeepEquals, test.machines) 113 c.Check(runCmd.services, gc.DeepEquals, test.services) 114 c.Check(runCmd.units, gc.DeepEquals, test.units) 115 c.Check(runCmd.commands, gc.Equals, test.commands) 116 } 117 } 118 } 119 120 func (*RunSuite) TestTimeoutArgParsing(c *gc.C) { 121 for i, test := range []struct { 122 message string 123 args []string 124 errMatch string 125 timeout time.Duration 126 }{{ 127 message: "default time", 128 args: []string{"--all", "sudo reboot"}, 129 timeout: 5 * time.Minute, 130 }, { 131 message: "invalid time", 132 args: []string{"--timeout=foo", "--all", "sudo reboot"}, 133 errMatch: `invalid value "foo" for flag --timeout: time: invalid duration foo`, 134 }, { 135 message: "two hours", 136 args: []string{"--timeout=2h", "--all", "sudo reboot"}, 137 timeout: 2 * time.Hour, 138 }, { 139 message: "3 minutes 30 seconds", 140 args: []string{"--timeout=3m30s", "--all", "sudo reboot"}, 141 timeout: (3 * time.Minute) + (30 * time.Second), 142 }} { 143 c.Log(fmt.Sprintf("%v: %s", i, test.message)) 144 runCmd := &RunCommand{} 145 testing.TestInit(c, runCmd, test.args, test.errMatch) 146 if test.errMatch == "" { 147 c.Check(runCmd.timeout, gc.Equals, test.timeout) 148 } 149 } 150 } 151 152 func (s *RunSuite) TestConvertRunResults(c *gc.C) { 153 for i, test := range []struct { 154 message string 155 results []params.RunResult 156 expected interface{} 157 }{{ 158 message: "empty", 159 expected: []interface{}{}, 160 }, { 161 message: "minimum is machine id and stdout", 162 results: []params.RunResult{ 163 makeRunResult(mockResponse{machineId: "1"}), 164 }, 165 expected: []interface{}{ 166 map[string]interface{}{ 167 "MachineId": "1", 168 "Stdout": "", 169 }}, 170 }, { 171 message: "other fields are copied if there", 172 results: []params.RunResult{ 173 makeRunResult(mockResponse{ 174 machineId: "1", 175 stdout: "stdout", 176 stderr: "stderr", 177 code: 42, 178 unitId: "unit/0", 179 error: "error", 180 }), 181 }, 182 expected: []interface{}{ 183 map[string]interface{}{ 184 "MachineId": "1", 185 "Stdout": "stdout", 186 "Stderr": "stderr", 187 "ReturnCode": 42, 188 "UnitId": "unit/0", 189 "Error": "error", 190 }}, 191 }, { 192 message: "stdout and stderr are base64 encoded if not valid utf8", 193 results: []params.RunResult{ 194 params.RunResult{ 195 ExecResponse: exec.ExecResponse{ 196 Stdout: []byte{0xff}, 197 Stderr: []byte{0xfe}, 198 }, 199 MachineId: "jake", 200 }, 201 }, 202 expected: []interface{}{ 203 map[string]interface{}{ 204 "MachineId": "jake", 205 "Stdout": "/w==", 206 "Stdout.encoding": "base64", 207 "Stderr": "/g==", 208 "Stderr.encoding": "base64", 209 }}, 210 }, { 211 message: "more than one", 212 results: []params.RunResult{ 213 makeRunResult(mockResponse{machineId: "1"}), 214 makeRunResult(mockResponse{machineId: "2"}), 215 makeRunResult(mockResponse{machineId: "3"}), 216 }, 217 expected: []interface{}{ 218 map[string]interface{}{ 219 "MachineId": "1", 220 "Stdout": "", 221 }, 222 map[string]interface{}{ 223 "MachineId": "2", 224 "Stdout": "", 225 }, 226 map[string]interface{}{ 227 "MachineId": "3", 228 "Stdout": "", 229 }, 230 }, 231 }} { 232 c.Log(fmt.Sprintf("%v: %s", i, test.message)) 233 result := ConvertRunResults(test.results) 234 c.Check(result, jc.DeepEquals, test.expected) 235 } 236 } 237 238 func (s *RunSuite) TestRunForMachineAndUnit(c *gc.C) { 239 mock := s.setupMockAPI() 240 machineResponse := mockResponse{ 241 stdout: "megatron\n", 242 machineId: "0", 243 } 244 unitResponse := mockResponse{ 245 stdout: "bumblebee", 246 machineId: "1", 247 unitId: "unit/0", 248 } 249 mock.setResponse("0", machineResponse) 250 mock.setResponse("unit/0", unitResponse) 251 252 unformatted := ConvertRunResults([]params.RunResult{ 253 makeRunResult(machineResponse), 254 makeRunResult(unitResponse), 255 }) 256 257 jsonFormatted, err := cmd.FormatJson(unformatted) 258 c.Assert(err, gc.IsNil) 259 260 context, err := testing.RunCommand(c, &RunCommand{}, []string{ 261 "--format=json", "--machine=0", "--unit=unit/0", "hostname", 262 }) 263 c.Assert(err, gc.IsNil) 264 265 c.Check(testing.Stdout(context), gc.Equals, string(jsonFormatted)+"\n") 266 } 267 268 func (s *RunSuite) TestAllMachines(c *gc.C) { 269 mock := s.setupMockAPI() 270 mock.setMachinesAlive("0", "1") 271 response0 := mockResponse{ 272 stdout: "megatron\n", 273 machineId: "0", 274 } 275 response1 := mockResponse{ 276 error: "command timed out", 277 machineId: "1", 278 } 279 mock.setResponse("0", response0) 280 281 unformatted := ConvertRunResults([]params.RunResult{ 282 makeRunResult(response0), 283 makeRunResult(response1), 284 }) 285 286 jsonFormatted, err := cmd.FormatJson(unformatted) 287 c.Assert(err, gc.IsNil) 288 289 context, err := testing.RunCommand(c, &RunCommand{}, []string{ 290 "--format=json", "--all", "hostname", 291 }) 292 c.Assert(err, gc.IsNil) 293 294 c.Check(testing.Stdout(context), gc.Equals, string(jsonFormatted)+"\n") 295 } 296 297 func (s *RunSuite) TestSingleResponse(c *gc.C) { 298 mock := s.setupMockAPI() 299 mock.setMachinesAlive("0") 300 mockResponse := mockResponse{ 301 stdout: "stdout\n", 302 stderr: "stderr\n", 303 code: 42, 304 machineId: "0", 305 } 306 mock.setResponse("0", mockResponse) 307 unformatted := ConvertRunResults([]params.RunResult{ 308 makeRunResult(mockResponse)}) 309 yamlFormatted, err := cmd.FormatYaml(unformatted) 310 c.Assert(err, gc.IsNil) 311 jsonFormatted, err := cmd.FormatJson(unformatted) 312 c.Assert(err, gc.IsNil) 313 314 for i, test := range []struct { 315 message string 316 format string 317 stdout string 318 stderr string 319 errorMatch string 320 }{{ 321 message: "smart (default)", 322 stdout: "stdout\n", 323 stderr: "stderr\n", 324 errorMatch: "rc: 42", 325 }, { 326 message: "yaml output", 327 format: "yaml", 328 stdout: string(yamlFormatted) + "\n", 329 }, { 330 message: "json output", 331 format: "json", 332 stdout: string(jsonFormatted) + "\n", 333 }} { 334 c.Log(fmt.Sprintf("%v: %s", i, test.message)) 335 args := []string{} 336 if test.format != "" { 337 args = append(args, "--format", test.format) 338 } 339 args = append(args, "--all", "ignored") 340 context, err := testing.RunCommand(c, &RunCommand{}, args) 341 if test.errorMatch != "" { 342 c.Check(err, gc.ErrorMatches, test.errorMatch) 343 } else { 344 c.Check(err, gc.IsNil) 345 } 346 c.Check(testing.Stdout(context), gc.Equals, test.stdout) 347 c.Check(testing.Stderr(context), gc.Equals, test.stderr) 348 } 349 } 350 351 func (s *RunSuite) setupMockAPI() *mockRunAPI { 352 mock := &mockRunAPI{} 353 s.PatchValue(&getAPIClient, func(name string) (RunClient, error) { 354 return mock, nil 355 }) 356 return mock 357 } 358 359 type mockRunAPI struct { 360 stdout string 361 stderr string 362 code int 363 // machines, services, units 364 machines map[string]bool 365 responses map[string]params.RunResult 366 } 367 368 type mockResponse struct { 369 stdout string 370 stderr string 371 code int 372 error string 373 machineId string 374 unitId string 375 } 376 377 var _ RunClient = (*mockRunAPI)(nil) 378 379 func (m *mockRunAPI) setMachinesAlive(ids ...string) { 380 if m.machines == nil { 381 m.machines = make(map[string]bool) 382 } 383 for _, id := range ids { 384 m.machines[id] = true 385 } 386 } 387 388 func makeRunResult(mock mockResponse) params.RunResult { 389 return params.RunResult{ 390 ExecResponse: exec.ExecResponse{ 391 Stdout: []byte(mock.stdout), 392 Stderr: []byte(mock.stderr), 393 Code: mock.code, 394 }, 395 MachineId: mock.machineId, 396 UnitId: mock.unitId, 397 Error: mock.error, 398 } 399 } 400 401 func (m *mockRunAPI) setResponse(id string, mock mockResponse) { 402 if m.responses == nil { 403 m.responses = make(map[string]params.RunResult) 404 } 405 m.responses[id] = makeRunResult(mock) 406 } 407 408 func (*mockRunAPI) Close() error { 409 return nil 410 } 411 412 func (m *mockRunAPI) RunOnAllMachines(commands string, timeout time.Duration) ([]params.RunResult, error) { 413 var result []params.RunResult 414 for machine := range m.machines { 415 response, found := m.responses[machine] 416 if !found { 417 // Consider this a timeout 418 response = params.RunResult{MachineId: machine, Error: "command timed out"} 419 } 420 result = append(result, response) 421 } 422 423 return result, nil 424 } 425 426 func (m *mockRunAPI) Run(runParams params.RunParams) ([]params.RunResult, error) { 427 var result []params.RunResult 428 // Just add in ids that match in order. 429 for _, id := range runParams.Machines { 430 response, found := m.responses[id] 431 if found { 432 result = append(result, response) 433 } 434 } 435 // mock ignores services 436 for _, id := range runParams.Units { 437 response, found := m.responses[id] 438 if found { 439 result = append(result, response) 440 } 441 } 442 443 return result, nil 444 }