github.com/makyo/juju@v0.0.0-20160425123129-2608902037e9/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 "github.com/juju/names" 14 jc "github.com/juju/testing/checkers" 15 "github.com/juju/utils" 16 "github.com/juju/utils/exec" 17 gc "gopkg.in/check.v1" 18 19 "github.com/juju/juju/apiserver/common" 20 "github.com/juju/juju/apiserver/params" 21 "github.com/juju/juju/cmd/juju/action" 22 "github.com/juju/juju/cmd/modelcmd" 23 "github.com/juju/juju/testing" 24 ) 25 26 type RunSuite struct { 27 testing.FakeJujuXDGDataHomeSuite 28 } 29 30 var _ = gc.Suite(&RunSuite{}) 31 32 func (*RunSuite) TestTargetArgParsing(c *gc.C) { 33 for i, test := range []struct { 34 message string 35 args []string 36 all bool 37 machines []string 38 units []string 39 services []string 40 commands string 41 errMatch string 42 }{{ 43 message: "no args", 44 errMatch: "no commands specified", 45 }, { 46 message: "no target", 47 args: []string{"sudo reboot"}, 48 errMatch: "You must specify a target, either through --all, --machine, --service or --unit", 49 }, { 50 message: "too many args", 51 args: []string{"--all", "sudo reboot", "oops"}, 52 errMatch: `unrecognized args: \["oops"\]`, 53 }, { 54 message: "command to all machines", 55 args: []string{"--all", "sudo reboot"}, 56 all: true, 57 commands: "sudo reboot", 58 }, { 59 message: "all and defined machines", 60 args: []string{"--all", "--machine=1,2", "sudo reboot"}, 61 errMatch: `You cannot specify --all and individual machines`, 62 }, { 63 message: "command to machines 1, 2, and 1/kvm/0", 64 args: []string{"--machine=1,2,1/kvm/0", "sudo reboot"}, 65 commands: "sudo reboot", 66 machines: []string{"1", "2", "1/kvm/0"}, 67 }, { 68 message: "bad machine names", 69 args: []string{"--machine=foo,machine-2", "sudo reboot"}, 70 errMatch: "" + 71 "The following run targets are not valid:\n" + 72 " \"foo\" is not a valid machine id\n" + 73 " \"machine-2\" is not a valid machine id", 74 }, { 75 message: "all and defined services", 76 args: []string{"--all", "--service=wordpress,mysql", "sudo reboot"}, 77 errMatch: `You cannot specify --all and individual services`, 78 }, { 79 message: "command to services wordpress and mysql", 80 args: []string{"--service=wordpress,mysql", "sudo reboot"}, 81 commands: "sudo reboot", 82 services: []string{"wordpress", "mysql"}, 83 }, { 84 message: "bad service names", 85 args: []string{"--service", "foo,2,foo/0", "sudo reboot"}, 86 errMatch: "" + 87 "The following run targets are not valid:\n" + 88 " \"2\" is not a valid service name\n" + 89 " \"foo/0\" is not a valid service name", 90 }, { 91 message: "all and defined units", 92 args: []string{"--all", "--unit=wordpress/0,mysql/1", "sudo reboot"}, 93 errMatch: `You cannot specify --all and individual units`, 94 }, { 95 message: "command to valid units", 96 args: []string{"--unit=wordpress/0,wordpress/1,mysql/0", "sudo reboot"}, 97 commands: "sudo reboot", 98 units: []string{"wordpress/0", "wordpress/1", "mysql/0"}, 99 }, { 100 message: "bad unit names", 101 args: []string{"--unit", "foo,2,foo/0", "sudo reboot"}, 102 errMatch: "" + 103 "The following run targets are not valid:\n" + 104 " \"foo\" is not a valid unit name\n" + 105 " \"2\" is not a valid unit name", 106 }, { 107 message: "command to mixed valid targets", 108 args: []string{"--machine=0", "--unit=wordpress/0,wordpress/1", "--service=mysql", "sudo reboot"}, 109 commands: "sudo reboot", 110 machines: []string{"0"}, 111 services: []string{"mysql"}, 112 units: []string{"wordpress/0", "wordpress/1"}, 113 }} { 114 c.Log(fmt.Sprintf("%v: %s", i, test.message)) 115 cmd := &runCommand{} 116 runCmd := modelcmd.Wrap(cmd) 117 testing.TestInit(c, runCmd, test.args, test.errMatch) 118 if test.errMatch == "" { 119 c.Check(cmd.all, gc.Equals, test.all) 120 c.Check(cmd.machines, gc.DeepEquals, test.machines) 121 c.Check(cmd.services, gc.DeepEquals, test.services) 122 c.Check(cmd.units, gc.DeepEquals, test.units) 123 c.Check(cmd.commands, gc.Equals, test.commands) 124 } 125 } 126 } 127 128 func (*RunSuite) TestTimeoutArgParsing(c *gc.C) { 129 for i, test := range []struct { 130 message string 131 args []string 132 errMatch string 133 timeout time.Duration 134 }{{ 135 message: "default time", 136 args: []string{"--all", "sudo reboot"}, 137 timeout: 5 * time.Minute, 138 }, { 139 message: "invalid time", 140 args: []string{"--timeout=foo", "--all", "sudo reboot"}, 141 errMatch: `invalid value "foo" for flag --timeout: time: invalid duration foo`, 142 }, { 143 message: "two hours", 144 args: []string{"--timeout=2h", "--all", "sudo reboot"}, 145 timeout: 2 * time.Hour, 146 }, { 147 message: "3 minutes 30 seconds", 148 args: []string{"--timeout=3m30s", "--all", "sudo reboot"}, 149 timeout: (3 * time.Minute) + (30 * time.Second), 150 }} { 151 c.Log(fmt.Sprintf("%v: %s", i, test.message)) 152 cmd := &runCommand{} 153 runCmd := modelcmd.Wrap(cmd) 154 testing.TestInit(c, runCmd, test.args, test.errMatch) 155 if test.errMatch == "" { 156 c.Check(cmd.timeout, gc.Equals, test.timeout) 157 } 158 } 159 } 160 161 func (s *RunSuite) TestConvertRunResults(c *gc.C) { 162 for i, test := range []struct { 163 message string 164 results params.ActionResult 165 query actionQuery 166 expected map[string]interface{} 167 }{{ 168 message: "in case of error we print receiver and failed action id", 169 results: makeActionResult(mockResponse{ 170 error: ¶ms.Error{ 171 Message: "whoops", 172 }, 173 }, ""), 174 query: makeActionQuery(validUUID, "MachineId", names.NewMachineTag("1")), 175 expected: map[string]interface{}{ 176 "Error": "whoops", 177 "MachineId": "1", 178 "Action": validUUID, 179 }, 180 }, { 181 message: "different action tag from query tag", 182 results: makeActionResult(mockResponse{machineTag: "not-a-tag"}, "invalid"), 183 query: makeActionQuery(validUUID, "MachineId", names.NewMachineTag("1")), 184 expected: map[string]interface{}{ 185 "Error": `expected action tag "action-` + validUUID + `", got "invalid"`, 186 "MachineId": "1", 187 "Action": validUUID, 188 }, 189 }, { 190 message: "different response tag from query tag", 191 results: makeActionResult(mockResponse{machineTag: "not-a-tag"}, "action-"+validUUID), 192 query: makeActionQuery(validUUID, "MachineId", names.NewMachineTag("1")), 193 expected: map[string]interface{}{ 194 "Error": `expected action receiver "machine-1", got "not-a-tag"`, 195 "MachineId": "1", 196 "Action": validUUID, 197 }, 198 }, { 199 message: "minimum is machine id", 200 results: makeActionResult(mockResponse{machineTag: "machine-1"}, "action-"+validUUID), 201 query: makeActionQuery(validUUID, "MachineId", names.NewMachineTag("1")), 202 expected: map[string]interface{}{ 203 "MachineId": "1", 204 "Stdout": "", 205 }, 206 }, { 207 message: "other fields are copied if there", 208 results: makeActionResult(mockResponse{ 209 unitTag: "unit-unit-0", 210 stdout: "stdout", 211 stderr: "stderr", 212 message: "msg", 213 code: "42", 214 }, "action-"+validUUID), 215 query: makeActionQuery(validUUID, "UnitId", names.NewUnitTag("unit/0")), 216 expected: map[string]interface{}{ 217 "UnitId": "unit/0", 218 "Stdout": "stdout", 219 "Stderr": "stderr", 220 "Message": "msg", 221 "ReturnCode": 42, 222 }, 223 }} { 224 c.Log(fmt.Sprintf("%v: %s", i, test.message)) 225 result := ConvertActionResults(test.results, test.query) 226 c.Check(result, jc.DeepEquals, test.expected) 227 } 228 } 229 230 func (s *RunSuite) TestRunForMachineAndUnit(c *gc.C) { 231 mock := s.setupMockAPI() 232 machineResponse := mockResponse{ 233 stdout: "megatron\n", 234 machineTag: "machine-0", 235 } 236 unitResponse := mockResponse{ 237 stdout: "bumblebee", 238 unitTag: "unit-unit-0", 239 } 240 mock.setResponse("0", machineResponse) 241 mock.setResponse("unit/0", unitResponse) 242 243 machineResult := mock.runResponses["0"] 244 unitResult := mock.runResponses["unit/0"] 245 mock.actionResponses = map[string]params.ActionResult{ 246 mock.receiverIdMap["0"]: machineResult, 247 mock.receiverIdMap["unit/0"]: unitResult, 248 } 249 250 machineQuery := makeActionQuery(mock.receiverIdMap["0"], "MachineId", names.NewMachineTag("0")) 251 unitQuery := makeActionQuery(mock.receiverIdMap["unit/0"], "UnitId", names.NewUnitTag("unit/0")) 252 unformatted := []interface{}{ 253 ConvertActionResults(machineResult, machineQuery), 254 ConvertActionResults(unitResult, unitQuery), 255 } 256 257 jsonFormatted, err := cmd.FormatJson(unformatted) 258 c.Assert(err, jc.ErrorIsNil) 259 260 context, err := testing.RunCommand(c, newRunCommand(), 261 "--format=json", "--machine=0", "--unit=unit/0", "hostname", 262 ) 263 c.Assert(err, jc.ErrorIsNil) 264 265 c.Check(testing.Stdout(context), gc.Equals, string(jsonFormatted)+"\n") 266 } 267 268 func (s *RunSuite) TestBlockRunForMachineAndUnit(c *gc.C) { 269 mock := s.setupMockAPI() 270 // Block operation 271 mock.block = true 272 _, err := testing.RunCommand(c, newRunCommand(), 273 "--format=json", "--machine=0", "--unit=unit/0", "hostname", 274 ) 275 c.Assert(err, gc.ErrorMatches, cmd.ErrSilent.Error()) 276 // msg is logged 277 stripped := strings.Replace(c.GetTestLog(), "\n", "", -1) 278 c.Check(stripped, gc.Matches, ".*To unblock changes.*") 279 } 280 281 func (s *RunSuite) TestAllMachines(c *gc.C) { 282 mock := s.setupMockAPI() 283 mock.setMachinesAlive("0", "1", "2") 284 response0 := mockResponse{ 285 stdout: "megatron\n", 286 machineTag: "machine-0", 287 } 288 response1 := mockResponse{ 289 message: "command timed out", 290 machineTag: "machine-1", 291 } 292 response2 := mockResponse{ 293 message: "command timed out", 294 machineTag: "machine-2", 295 } 296 mock.setResponse("0", response0) 297 mock.setResponse("1", response1) 298 mock.setResponse("2", response2) 299 300 machine0Result := mock.runResponses["0"] 301 machine1Result := mock.runResponses["1"] 302 mock.actionResponses = map[string]params.ActionResult{ 303 mock.receiverIdMap["0"]: machine0Result, 304 mock.receiverIdMap["1"]: machine1Result, 305 } 306 307 machine0Query := makeActionQuery(mock.receiverIdMap["0"], "MachineId", names.NewMachineTag("0")) 308 machine1Query := makeActionQuery(mock.receiverIdMap["1"], "MachineId", names.NewMachineTag("1")) 309 unformatted := []interface{}{ 310 ConvertActionResults(machine0Result, machine0Query), 311 ConvertActionResults(machine1Result, machine1Query), 312 map[string]interface{}{ 313 "Action": mock.receiverIdMap["2"], 314 "MachineId": "2", 315 "Error": "action not found", 316 }, 317 } 318 319 jsonFormatted, err := cmd.FormatJson(unformatted) 320 c.Assert(err, jc.ErrorIsNil) 321 322 context, err := testing.RunCommand(c, newRunCommand(), "--format=json", "--all", "hostname") 323 c.Assert(err, jc.ErrorIsNil) 324 325 c.Check(testing.Stdout(context), gc.Equals, string(jsonFormatted)+"\n") 326 c.Check(testing.Stderr(context), gc.Equals, "") 327 } 328 329 func (s *RunSuite) TestBlockAllMachines(c *gc.C) { 330 mock := s.setupMockAPI() 331 // Block operation 332 mock.block = true 333 _, err := testing.RunCommand(c, newRunCommand(), "--format=json", "--all", "hostname") 334 c.Assert(err, gc.ErrorMatches, cmd.ErrSilent.Error()) 335 // msg is logged 336 stripped := strings.Replace(c.GetTestLog(), "\n", "", -1) 337 c.Check(stripped, gc.Matches, ".*To unblock changes.*") 338 } 339 340 func (s *RunSuite) TestSingleResponse(c *gc.C) { 341 mock := s.setupMockAPI() 342 mock.setMachinesAlive("0") 343 mockResponse := mockResponse{ 344 stdout: "stdout\n", 345 stderr: "stderr\n", 346 code: "42", 347 machineTag: "machine-0", 348 } 349 mock.setResponse("0", mockResponse) 350 351 machineResult := mock.runResponses["0"] 352 mock.actionResponses = map[string]params.ActionResult{ 353 mock.receiverIdMap["0"]: machineResult, 354 } 355 356 query := makeActionQuery(mock.receiverIdMap["0"], "MachineId", names.NewMachineTag("0")) 357 unformatted := []interface{}{ 358 ConvertActionResults(machineResult, query), 359 } 360 361 jsonFormatted, err := cmd.FormatJson(unformatted) 362 c.Assert(err, jc.ErrorIsNil) 363 364 yamlFormatted, err := cmd.FormatYaml(unformatted) 365 c.Assert(err, jc.ErrorIsNil) 366 367 for i, test := range []struct { 368 message string 369 format string 370 stdout string 371 stderr string 372 errorMatch string 373 }{{ 374 message: "smart (default)", 375 stdout: "stdout\n", 376 stderr: "stderr\n", 377 errorMatch: "subprocess encountered error code 42", 378 }, { 379 message: "yaml output", 380 format: "yaml", 381 stdout: string(yamlFormatted) + "\n", 382 }, { 383 message: "json output", 384 format: "json", 385 stdout: string(jsonFormatted) + "\n", 386 }} { 387 c.Log(fmt.Sprintf("%v: %s", i, test.message)) 388 args := []string{} 389 if test.format != "" { 390 args = append(args, "--format", test.format) 391 } 392 args = append(args, "--all", "ignored") 393 context, err := testing.RunCommand(c, newRunCommand(), args...) 394 if test.errorMatch != "" { 395 c.Check(err, gc.ErrorMatches, test.errorMatch) 396 } else { 397 c.Check(err, jc.ErrorIsNil) 398 } 399 c.Check(testing.Stdout(context), gc.Equals, test.stdout) 400 c.Check(testing.Stderr(context), gc.Equals, test.stderr) 401 } 402 } 403 404 func (s *RunSuite) setupMockAPI() *mockRunAPI { 405 mock := &mockRunAPI{} 406 s.PatchValue(&getRunAPIClient, func(_ *runCommand) (RunClient, error) { 407 return mock, nil 408 }) 409 return mock 410 } 411 412 type mockRunAPI struct { 413 action.APIClient 414 stdout string 415 stderr string 416 code int 417 // machines, services, units 418 machines map[string]bool 419 runResponses map[string]params.ActionResult 420 actionResponses map[string]params.ActionResult 421 receiverIdMap map[string]string 422 block bool 423 } 424 425 type mockResponse struct { 426 stdout interface{} 427 stderr interface{} 428 code interface{} 429 error *params.Error 430 message string 431 machineTag string 432 unitTag string 433 } 434 435 var _ RunClient = (*mockRunAPI)(nil) 436 437 func (m *mockRunAPI) setMachinesAlive(ids ...string) { 438 if m.machines == nil { 439 m.machines = make(map[string]bool) 440 } 441 for _, id := range ids { 442 m.machines[id] = true 443 } 444 } 445 446 func makeActionQuery(actionID string, receiverType string, receiverTag names.Tag) actionQuery { 447 return actionQuery{ 448 actionTag: names.NewActionTag(actionID), 449 receiver: actionReceiver{ 450 receiverType: receiverType, 451 tag: receiverTag, 452 }, 453 } 454 } 455 456 func makeActionResult(mock mockResponse, actionTag string) params.ActionResult { 457 var receiverTag string 458 if mock.unitTag != "" { 459 receiverTag = mock.unitTag 460 } else { 461 receiverTag = mock.machineTag 462 } 463 if actionTag == "" { 464 actionTag = names.NewActionTag(utils.MustNewUUID().String()).String() 465 } 466 return params.ActionResult{ 467 Action: ¶ms.Action{ 468 Tag: actionTag, 469 Receiver: receiverTag, 470 }, 471 Message: mock.message, 472 Error: mock.error, 473 Output: map[string]interface{}{ 474 "Stdout": mock.stdout, 475 "Stderr": mock.stderr, 476 "Code": mock.code, 477 }, 478 } 479 } 480 481 func (m *mockRunAPI) setResponse(id string, mock mockResponse) { 482 if m.runResponses == nil { 483 m.runResponses = make(map[string]params.ActionResult) 484 } 485 if m.receiverIdMap == nil { 486 m.receiverIdMap = make(map[string]string) 487 } 488 actionTag := names.NewActionTag(utils.MustNewUUID().String()) 489 m.receiverIdMap[id] = actionTag.Id() 490 m.runResponses[id] = makeActionResult(mock, actionTag.String()) 491 } 492 493 func (*mockRunAPI) Close() error { 494 return nil 495 } 496 497 func (m *mockRunAPI) RunOnAllMachines(commands string, timeout time.Duration) ([]params.ActionResult, error) { 498 var result []params.ActionResult 499 500 if m.block { 501 return result, common.OperationBlockedError("the operation has been blocked") 502 } 503 sortedMachineIds := make([]string, 0, len(m.machines)) 504 for machineId := range m.machines { 505 sortedMachineIds = append(sortedMachineIds, machineId) 506 } 507 sort.Strings(sortedMachineIds) 508 509 for _, machineId := range sortedMachineIds { 510 response, found := m.runResponses[machineId] 511 if !found { 512 // Consider this a timeout 513 response = params.ActionResult{ 514 Action: ¶ms.Action{ 515 Receiver: names.NewMachineTag(machineId).String(), 516 }, 517 Message: exec.ErrCancelled.Error(), 518 } 519 } 520 result = append(result, response) 521 } 522 523 return result, nil 524 } 525 526 func (m *mockRunAPI) Run(runParams params.RunParams) ([]params.ActionResult, error) { 527 var result []params.ActionResult 528 529 if m.block { 530 return result, common.OperationBlockedError("the operation has been blocked") 531 } 532 // Just add in ids that match in order. 533 for _, id := range runParams.Machines { 534 response, found := m.runResponses[id] 535 if found { 536 result = append(result, response) 537 } 538 } 539 // mock ignores services 540 for _, id := range runParams.Units { 541 response, found := m.runResponses[id] 542 if found { 543 result = append(result, response) 544 } 545 } 546 547 return result, nil 548 } 549 550 func (m *mockRunAPI) Actions(actionTags params.Entities) (params.ActionResults, error) { 551 results := params.ActionResults{Results: make([]params.ActionResult, len(actionTags.Entities))} 552 553 for i, entity := range actionTags.Entities { 554 response, found := m.actionResponses[entity.Tag[len("action-"):]] 555 if !found { 556 results.Results[i] = params.ActionResult{ 557 Error: ¶ms.Error{ 558 Message: "action not found", 559 }, 560 } 561 continue 562 } 563 results.Results[i] = response 564 } 565 566 return results, nil 567 } 568 569 // validUUID is a UUID used in tests 570 var validUUID = "01234567-89ab-cdef-0123-456789abcdef"