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