github.com/axw/juju@v0.0.0-20161005053422-4bd6544d08d4/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 "bytes" 8 "fmt" 9 "sort" 10 "time" 11 12 "github.com/juju/cmd" 13 jc "github.com/juju/testing/checkers" 14 "github.com/juju/utils" 15 "github.com/juju/utils/exec" 16 gc "gopkg.in/check.v1" 17 "gopkg.in/juju/names.v2" 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, --application 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 applications", 76 args: []string{"--all", "--application=wordpress,mysql", "sudo reboot"}, 77 errMatch: `You cannot specify --all and individual applications`, 78 }, { 79 message: "command to applications wordpress and mysql", 80 args: []string{"--application=wordpress,mysql", "sudo reboot"}, 81 commands: "sudo reboot", 82 services: []string{"wordpress", "mysql"}, 83 }, { 84 message: "bad application names", 85 args: []string{"--application", "foo,2,foo/0", "sudo reboot"}, 86 errMatch: "" + 87 "The following run targets are not valid:\n" + 88 " \"2\" is not a valid application name\n" + 89 " \"foo/0\" is not a valid application 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", "--application=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 buff := &bytes.Buffer{} 258 err := cmd.FormatJson(buff, unformatted) 259 c.Assert(err, jc.ErrorIsNil) 260 261 context, err := testing.RunCommand(c, newRunCommand(), 262 "--format=json", "--machine=0", "--unit=unit/0", "hostname", 263 ) 264 c.Assert(err, jc.ErrorIsNil) 265 266 c.Check(testing.Stdout(context), gc.Equals, buff.String()) 267 } 268 269 func (s *RunSuite) TestBlockRunForMachineAndUnit(c *gc.C) { 270 mock := s.setupMockAPI() 271 // Block operation 272 mock.block = true 273 _, err := testing.RunCommand(c, newRunCommand(), 274 "--format=json", "--machine=0", "--unit=unit/0", "hostname", 275 ) 276 testing.AssertOperationWasBlocked(c, err, ".*To enable changes.*") 277 } 278 279 func (s *RunSuite) TestAllMachines(c *gc.C) { 280 mock := s.setupMockAPI() 281 mock.setMachinesAlive("0", "1", "2") 282 response0 := mockResponse{ 283 stdout: "megatron\n", 284 machineTag: "machine-0", 285 } 286 response1 := mockResponse{ 287 message: "command timed out", 288 machineTag: "machine-1", 289 } 290 response2 := mockResponse{ 291 message: "command timed out", 292 machineTag: "machine-2", 293 } 294 mock.setResponse("0", response0) 295 mock.setResponse("1", response1) 296 mock.setResponse("2", response2) 297 298 machine0Result := mock.runResponses["0"] 299 machine1Result := mock.runResponses["1"] 300 mock.actionResponses = map[string]params.ActionResult{ 301 mock.receiverIdMap["0"]: machine0Result, 302 mock.receiverIdMap["1"]: machine1Result, 303 } 304 305 machine0Query := makeActionQuery(mock.receiverIdMap["0"], "MachineId", names.NewMachineTag("0")) 306 machine1Query := makeActionQuery(mock.receiverIdMap["1"], "MachineId", names.NewMachineTag("1")) 307 unformatted := []interface{}{ 308 ConvertActionResults(machine0Result, machine0Query), 309 ConvertActionResults(machine1Result, machine1Query), 310 map[string]interface{}{ 311 "Action": mock.receiverIdMap["2"], 312 "MachineId": "2", 313 "Error": "action not found", 314 }, 315 } 316 317 buff := &bytes.Buffer{} 318 err := cmd.FormatJson(buff, unformatted) 319 c.Assert(err, jc.ErrorIsNil) 320 321 context, err := testing.RunCommand(c, newRunCommand(), "--format=json", "--all", "hostname") 322 c.Assert(err, jc.ErrorIsNil) 323 324 c.Check(testing.Stdout(context), gc.Equals, buff.String()) 325 c.Check(testing.Stderr(context), gc.Equals, "") 326 } 327 328 func (s *RunSuite) TestBlockAllMachines(c *gc.C) { 329 mock := s.setupMockAPI() 330 // Block operation 331 mock.block = true 332 _, err := testing.RunCommand(c, newRunCommand(), "--format=json", "--all", "hostname") 333 testing.AssertOperationWasBlocked(c, err, ".*To enable changes.*") 334 } 335 336 func (s *RunSuite) TestSingleResponse(c *gc.C) { 337 mock := s.setupMockAPI() 338 mock.setMachinesAlive("0") 339 mockResponse := mockResponse{ 340 stdout: "stdout\n", 341 stderr: "stderr\n", 342 code: "42", 343 machineTag: "machine-0", 344 } 345 mock.setResponse("0", mockResponse) 346 347 machineResult := mock.runResponses["0"] 348 mock.actionResponses = map[string]params.ActionResult{ 349 mock.receiverIdMap["0"]: machineResult, 350 } 351 352 query := makeActionQuery(mock.receiverIdMap["0"], "MachineId", names.NewMachineTag("0")) 353 unformatted := []interface{}{ 354 ConvertActionResults(machineResult, query), 355 } 356 357 jsonFormatted := &bytes.Buffer{} 358 err := cmd.FormatJson(jsonFormatted, unformatted) 359 c.Assert(err, jc.ErrorIsNil) 360 361 yamlFormatted := &bytes.Buffer{} 362 err = cmd.FormatYaml(yamlFormatted, unformatted) 363 c.Assert(err, jc.ErrorIsNil) 364 365 for i, test := range []struct { 366 message string 367 format string 368 stdout string 369 stderr string 370 errorMatch string 371 }{{ 372 message: "smart (default)", 373 stdout: "stdout\n", 374 stderr: "stderr\n", 375 errorMatch: "subprocess encountered error code 42", 376 }, { 377 message: "yaml output", 378 format: "yaml", 379 stdout: yamlFormatted.String(), 380 }, { 381 message: "json output", 382 format: "json", 383 stdout: jsonFormatted.String(), 384 }} { 385 c.Log(fmt.Sprintf("%v: %s", i, test.message)) 386 args := []string{} 387 if test.format != "" { 388 args = append(args, "--format", test.format) 389 } 390 args = append(args, "--all", "ignored") 391 context, err := testing.RunCommand(c, newRunCommand(), args...) 392 if test.errorMatch != "" { 393 c.Check(err, gc.ErrorMatches, test.errorMatch) 394 } else { 395 c.Check(err, jc.ErrorIsNil) 396 } 397 c.Check(testing.Stdout(context), gc.Equals, test.stdout) 398 c.Check(testing.Stderr(context), gc.Equals, test.stderr) 399 } 400 } 401 402 func (s *RunSuite) setupMockAPI() *mockRunAPI { 403 mock := &mockRunAPI{} 404 s.PatchValue(&getRunAPIClient, func(_ *runCommand) (RunClient, error) { 405 return mock, nil 406 }) 407 return mock 408 } 409 410 type mockRunAPI struct { 411 action.APIClient 412 stdout string 413 stderr string 414 code int 415 // machines, services, units 416 machines map[string]bool 417 runResponses map[string]params.ActionResult 418 actionResponses map[string]params.ActionResult 419 receiverIdMap map[string]string 420 block bool 421 } 422 423 type mockResponse struct { 424 stdout interface{} 425 stderr interface{} 426 code interface{} 427 error *params.Error 428 message string 429 machineTag string 430 unitTag string 431 } 432 433 var _ RunClient = (*mockRunAPI)(nil) 434 435 func (m *mockRunAPI) setMachinesAlive(ids ...string) { 436 if m.machines == nil { 437 m.machines = make(map[string]bool) 438 } 439 for _, id := range ids { 440 m.machines[id] = true 441 } 442 } 443 444 func makeActionQuery(actionID string, receiverType string, receiverTag names.Tag) actionQuery { 445 return actionQuery{ 446 actionTag: names.NewActionTag(actionID), 447 receiver: actionReceiver{ 448 receiverType: receiverType, 449 tag: receiverTag, 450 }, 451 } 452 } 453 454 func makeActionResult(mock mockResponse, actionTag string) params.ActionResult { 455 var receiverTag string 456 if mock.unitTag != "" { 457 receiverTag = mock.unitTag 458 } else { 459 receiverTag = mock.machineTag 460 } 461 if actionTag == "" { 462 actionTag = names.NewActionTag(utils.MustNewUUID().String()).String() 463 } 464 return params.ActionResult{ 465 Action: ¶ms.Action{ 466 Tag: actionTag, 467 Receiver: receiverTag, 468 }, 469 Message: mock.message, 470 Error: mock.error, 471 Output: map[string]interface{}{ 472 "Stdout": mock.stdout, 473 "Stderr": mock.stderr, 474 "Code": mock.code, 475 }, 476 } 477 } 478 479 func (m *mockRunAPI) setResponse(id string, mock mockResponse) { 480 if m.runResponses == nil { 481 m.runResponses = make(map[string]params.ActionResult) 482 } 483 if m.receiverIdMap == nil { 484 m.receiverIdMap = make(map[string]string) 485 } 486 actionTag := names.NewActionTag(utils.MustNewUUID().String()) 487 m.receiverIdMap[id] = actionTag.Id() 488 m.runResponses[id] = makeActionResult(mock, actionTag.String()) 489 } 490 491 func (*mockRunAPI) Close() error { 492 return nil 493 } 494 495 func (m *mockRunAPI) RunOnAllMachines(commands string, timeout time.Duration) ([]params.ActionResult, error) { 496 var result []params.ActionResult 497 498 if m.block { 499 return result, common.OperationBlockedError("the operation has been blocked") 500 } 501 sortedMachineIds := make([]string, 0, len(m.machines)) 502 for machineId := range m.machines { 503 sortedMachineIds = append(sortedMachineIds, machineId) 504 } 505 sort.Strings(sortedMachineIds) 506 507 for _, machineId := range sortedMachineIds { 508 response, found := m.runResponses[machineId] 509 if !found { 510 // Consider this a timeout 511 response = params.ActionResult{ 512 Action: ¶ms.Action{ 513 Receiver: names.NewMachineTag(machineId).String(), 514 }, 515 Message: exec.ErrCancelled.Error(), 516 } 517 } 518 result = append(result, response) 519 } 520 521 return result, nil 522 } 523 524 func (m *mockRunAPI) Run(runParams params.RunParams) ([]params.ActionResult, error) { 525 var result []params.ActionResult 526 527 if m.block { 528 return result, common.OperationBlockedError("the operation has been blocked") 529 } 530 // Just add in ids that match in order. 531 for _, id := range runParams.Machines { 532 response, found := m.runResponses[id] 533 if found { 534 result = append(result, response) 535 } 536 } 537 // mock ignores services 538 for _, id := range runParams.Units { 539 response, found := m.runResponses[id] 540 if found { 541 result = append(result, response) 542 } 543 } 544 545 return result, nil 546 } 547 548 func (m *mockRunAPI) Actions(actionTags params.Entities) (params.ActionResults, error) { 549 results := params.ActionResults{Results: make([]params.ActionResult, len(actionTags.Entities))} 550 551 for i, entity := range actionTags.Entities { 552 response, found := m.actionResponses[entity.Tag[len("action-"):]] 553 if !found { 554 results.Results[i] = params.ActionResult{ 555 Error: ¶ms.Error{ 556 Message: "action not found", 557 }, 558 } 559 continue 560 } 561 results.Results[i] = response 562 } 563 564 return results, nil 565 } 566 567 // validUUID is a UUID used in tests 568 var validUUID = "01234567-89ab-cdef-0123-456789abcdef"