github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/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/clock" 13 "github.com/juju/cmd" 14 "github.com/juju/cmd/cmdtesting" 15 gitjujutesting "github.com/juju/testing" 16 jc "github.com/juju/testing/checkers" 17 "github.com/juju/utils" 18 "github.com/juju/utils/exec" 19 gc "gopkg.in/check.v1" 20 "gopkg.in/juju/names.v2" 21 22 "github.com/juju/juju/apiserver/common" 23 "github.com/juju/juju/apiserver/params" 24 "github.com/juju/juju/cmd/juju/action" 25 "github.com/juju/juju/cmd/modelcmd" 26 "github.com/juju/juju/jujuclient/jujuclienttesting" 27 "github.com/juju/juju/testing" 28 ) 29 30 type RunSuite struct { 31 testing.FakeJujuXDGDataHomeSuite 32 } 33 34 var _ = gc.Suite(&RunSuite{}) 35 36 func newTestRunCommand(clock clock.Clock) cmd.Command { 37 return newRunCommand(jujuclienttesting.MinimalStore(), clock.After) 38 } 39 40 func (*RunSuite) TestTargetArgParsing(c *gc.C) { 41 for i, test := range []struct { 42 message string 43 args []string 44 all bool 45 machines []string 46 units []string 47 applications []string 48 commands string 49 errMatch string 50 }{{ 51 message: "no args", 52 errMatch: "no commands specified", 53 }, { 54 message: "no target", 55 args: []string{"sudo reboot"}, 56 errMatch: "You must specify a target, either through --all, --machine, --application or --unit", 57 }, { 58 message: "command to all machines", 59 args: []string{"--all", "sudo reboot"}, 60 all: true, 61 commands: "sudo reboot", 62 }, { 63 message: "multiple args", 64 args: []string{"--all", "echo", "la lia"}, 65 all: true, 66 commands: `echo "la lia"`, 67 }, { 68 message: "all and defined machines", 69 args: []string{"--all", "--machine=1,2", "sudo reboot"}, 70 errMatch: `You cannot specify --all and individual machines`, 71 }, { 72 message: "command to machines 1, 2, and 1/kvm/0", 73 args: []string{"--machine=1,2,1/kvm/0", "sudo reboot"}, 74 commands: "sudo reboot", 75 machines: []string{"1", "2", "1/kvm/0"}, 76 }, { 77 message: "bad machine names", 78 args: []string{"--machine=foo,machine-2", "sudo reboot"}, 79 errMatch: "" + 80 "The following run targets are not valid:\n" + 81 " \"foo\" is not a valid machine id\n" + 82 " \"machine-2\" is not a valid machine id", 83 }, { 84 message: "all and defined applications", 85 args: []string{"--all", "--application=wordpress,mysql", "sudo reboot"}, 86 errMatch: `You cannot specify --all and individual applications`, 87 }, { 88 message: "command to applications wordpress and mysql", 89 args: []string{"--application=wordpress,mysql", "sudo reboot"}, 90 commands: "sudo reboot", 91 applications: []string{"wordpress", "mysql"}, 92 }, { 93 message: "command to application mysql", 94 args: []string{"--app", "mysql", "uname -a"}, 95 commands: "uname -a", 96 applications: []string{"mysql"}, 97 }, { 98 message: "bad application names", 99 args: []string{"--application", "foo,2,foo/0", "sudo reboot"}, 100 errMatch: "" + 101 "The following run targets are not valid:\n" + 102 " \"2\" is not a valid application name\n" + 103 " \"foo/0\" is not a valid application name", 104 }, { 105 message: "command to application mysql", 106 args: []string{"--app", "mysql", "sudo reboot"}, 107 commands: "sudo reboot", 108 applications: []string{"mysql"}, 109 }, { 110 message: "command to application wordpress", 111 args: []string{"-a", "wordpress", "sudo reboot"}, 112 commands: "sudo reboot", 113 applications: []string{"wordpress"}, 114 }, { 115 message: "all and defined units", 116 args: []string{"--all", "--unit=wordpress/0,mysql/1", "sudo reboot"}, 117 errMatch: `You cannot specify --all and individual units`, 118 }, { 119 message: "command to valid unit", 120 args: []string{"-u", "mysql/0", "sudo reboot"}, 121 commands: "sudo reboot", 122 units: []string{"mysql/0"}, 123 }, { 124 message: "command to valid units", 125 args: []string{"--unit=wordpress/0,wordpress/1,mysql/0", "sudo reboot"}, 126 commands: "sudo reboot", 127 units: []string{"wordpress/0", "wordpress/1", "mysql/0"}, 128 }, { 129 message: "bad unit names", 130 args: []string{"--unit", "foo,2,foo/0,foo/$leader", "sudo reboot"}, 131 errMatch: "" + 132 "The following run targets are not valid:\n" + 133 " \"foo\" is not a valid unit name\n" + 134 " \"2\" is not a valid unit name\n" + 135 " \"foo/\\$leader\" is not a valid unit name", 136 }, { 137 message: "command to mixed valid targets", 138 args: []string{"--machine=0", "--unit=wordpress/0,wordpress/1,consul/leader", "--application=mysql", "sudo reboot"}, 139 commands: "sudo reboot", 140 machines: []string{"0"}, 141 applications: []string{"mysql"}, 142 units: []string{"wordpress/0", "wordpress/1", "consul/leader"}, 143 }} { 144 c.Log(fmt.Sprintf("%v: %s", i, test.message)) 145 cmd := &runCommand{} 146 cmd.SetClientStore(jujuclienttesting.MinimalStore()) 147 runCmd := modelcmd.Wrap(cmd) 148 cmdtesting.TestInit(c, runCmd, test.args, test.errMatch) 149 if test.errMatch == "" { 150 c.Check(cmd.all, gc.Equals, test.all) 151 c.Check(cmd.machines, gc.DeepEquals, test.machines) 152 c.Check(cmd.applications, gc.DeepEquals, test.applications) 153 c.Check(cmd.units, gc.DeepEquals, test.units) 154 c.Check(cmd.commands, gc.Equals, test.commands) 155 } 156 } 157 } 158 159 func (*RunSuite) TestTimeoutArgParsing(c *gc.C) { 160 for i, test := range []struct { 161 message string 162 args []string 163 errMatch string 164 timeout time.Duration 165 }{{ 166 message: "default time", 167 args: []string{"--all", "sudo reboot"}, 168 timeout: 5 * time.Minute, 169 }, { 170 message: "invalid time", 171 args: []string{"--timeout=foo", "--all", "sudo reboot"}, 172 errMatch: `invalid value "foo" for option --timeout: time: invalid duration foo`, 173 }, { 174 message: "two hours", 175 args: []string{"--timeout=2h", "--all", "sudo reboot"}, 176 timeout: 2 * time.Hour, 177 }, { 178 message: "3 minutes 30 seconds", 179 args: []string{"--timeout=3m30s", "--all", "sudo reboot"}, 180 timeout: (3 * time.Minute) + (30 * time.Second), 181 }} { 182 c.Log(fmt.Sprintf("%v: %s", i, test.message)) 183 cmd := &runCommand{} 184 cmd.SetClientStore(jujuclienttesting.MinimalStore()) 185 runCmd := modelcmd.Wrap(cmd) 186 cmdtesting.TestInit(c, runCmd, test.args, test.errMatch) 187 if test.errMatch == "" { 188 c.Check(cmd.timeout, gc.Equals, test.timeout) 189 } 190 } 191 } 192 193 func (s *RunSuite) TestConvertRunResults(c *gc.C) { 194 for i, test := range []struct { 195 message string 196 results params.ActionResult 197 query actionQuery 198 expected map[string]interface{} 199 }{{ 200 message: "in case of error we print receiver and failed action id", 201 results: makeActionResult(mockResponse{ 202 error: ¶ms.Error{ 203 Message: "whoops", 204 }, 205 }, ""), 206 query: makeActionQuery(validUUID, "MachineId", names.NewMachineTag("1")), 207 expected: map[string]interface{}{ 208 "Error": "whoops", 209 "MachineId": "1", 210 "Action": validUUID, 211 }, 212 }, { 213 message: "different action tag from query tag", 214 results: makeActionResult(mockResponse{machineTag: "not-a-tag"}, "invalid"), 215 query: makeActionQuery(validUUID, "MachineId", names.NewMachineTag("1")), 216 expected: map[string]interface{}{ 217 "Error": `expected action tag "action-` + validUUID + `", got "invalid"`, 218 "MachineId": "1", 219 "Action": validUUID, 220 }, 221 }, { 222 message: "different response tag from query tag", 223 results: makeActionResult(mockResponse{machineTag: "not-a-tag"}, "action-"+validUUID), 224 query: makeActionQuery(validUUID, "MachineId", names.NewMachineTag("1")), 225 expected: map[string]interface{}{ 226 "Error": `expected action receiver "machine-1", got "not-a-tag"`, 227 "MachineId": "1", 228 "Action": validUUID, 229 }, 230 }, { 231 message: "minimum is machine id", 232 results: makeActionResult(mockResponse{machineTag: "machine-1"}, "action-"+validUUID), 233 query: makeActionQuery(validUUID, "MachineId", names.NewMachineTag("1")), 234 expected: map[string]interface{}{ 235 "MachineId": "1", 236 "Stdout": "", 237 }, 238 }, { 239 message: "other fields are copied if there", 240 results: makeActionResult(mockResponse{ 241 unitTag: "unit-unit-0", 242 stdout: "stdout", 243 stderr: "stderr", 244 message: "msg", 245 code: "42", 246 }, "action-"+validUUID), 247 query: makeActionQuery(validUUID, "UnitId", names.NewUnitTag("unit/0")), 248 expected: map[string]interface{}{ 249 "UnitId": "unit/0", 250 "Stdout": "stdout", 251 "Stderr": "stderr", 252 "Message": "msg", 253 "ReturnCode": 42, 254 }, 255 }} { 256 c.Log(fmt.Sprintf("%v: %s", i, test.message)) 257 result := ConvertActionResults(test.results, test.query) 258 c.Check(result, jc.DeepEquals, test.expected) 259 } 260 } 261 262 func (s *RunSuite) TestRunForMachineAndUnit(c *gc.C) { 263 mock := s.setupMockAPI() 264 machineResponse := mockResponse{ 265 stdout: "megatron\n", 266 machineTag: "machine-0", 267 } 268 unitResponse := mockResponse{ 269 stdout: "bumblebee", 270 unitTag: "unit-unit-0", 271 } 272 mock.setResponse("0", machineResponse) 273 mock.setResponse("unit/0", unitResponse) 274 275 machineResult := mock.runResponses["0"] 276 unitResult := mock.runResponses["unit/0"] 277 mock.actionResponses = map[string]params.ActionResult{ 278 mock.receiverIdMap["0"]: machineResult, 279 mock.receiverIdMap["unit/0"]: unitResult, 280 } 281 282 machineQuery := makeActionQuery(mock.receiverIdMap["0"], "MachineId", names.NewMachineTag("0")) 283 unitQuery := makeActionQuery(mock.receiverIdMap["unit/0"], "UnitId", names.NewUnitTag("unit/0")) 284 unformatted := []interface{}{ 285 ConvertActionResults(machineResult, machineQuery), 286 ConvertActionResults(unitResult, unitQuery), 287 } 288 289 buff := &bytes.Buffer{} 290 err := cmd.FormatJson(buff, unformatted) 291 c.Assert(err, jc.ErrorIsNil) 292 293 context, err := cmdtesting.RunCommand(c, newTestRunCommand(&mockClock{}), 294 "--format=json", "--machine=0", "--unit=unit/0", "hostname", 295 ) 296 c.Assert(err, jc.ErrorIsNil) 297 298 c.Check(cmdtesting.Stdout(context), gc.Equals, buff.String()) 299 } 300 301 func (s *RunSuite) TestBlockRunForMachineAndUnit(c *gc.C) { 302 mock := s.setupMockAPI() 303 // Block operation 304 mock.block = true 305 _, err := cmdtesting.RunCommand(c, newTestRunCommand(&mockClock{}), 306 "--format=json", "--machine=0", "--unit=unit/0", "hostname", 307 ) 308 testing.AssertOperationWasBlocked(c, err, ".*To enable changes.*") 309 } 310 311 func (s *RunSuite) TestAllMachines(c *gc.C) { 312 mock := s.setupMockAPI() 313 mock.setMachinesAlive("0", "1", "2") 314 response0 := mockResponse{ 315 stdout: "megatron\n", 316 machineTag: "machine-0", 317 } 318 response1 := mockResponse{ 319 message: "command timed out", 320 machineTag: "machine-1", 321 } 322 response2 := mockResponse{ 323 message: "command timed out", 324 machineTag: "machine-2", 325 } 326 mock.setResponse("0", response0) 327 mock.setResponse("1", response1) 328 mock.setResponse("2", response2) 329 330 machine0Result := mock.runResponses["0"] 331 machine1Result := mock.runResponses["1"] 332 mock.actionResponses = map[string]params.ActionResult{ 333 mock.receiverIdMap["0"]: machine0Result, 334 mock.receiverIdMap["1"]: machine1Result, 335 } 336 337 machine0Query := makeActionQuery(mock.receiverIdMap["0"], "MachineId", names.NewMachineTag("0")) 338 machine1Query := makeActionQuery(mock.receiverIdMap["1"], "MachineId", names.NewMachineTag("1")) 339 unformatted := []interface{}{ 340 ConvertActionResults(machine0Result, machine0Query), 341 ConvertActionResults(machine1Result, machine1Query), 342 map[string]interface{}{ 343 "Action": mock.receiverIdMap["2"], 344 "MachineId": "2", 345 "Error": "action not found", 346 }, 347 } 348 349 buff := &bytes.Buffer{} 350 err := cmd.FormatJson(buff, unformatted) 351 c.Assert(err, jc.ErrorIsNil) 352 353 context, err := cmdtesting.RunCommand(c, newTestRunCommand(&mockClock{}), "--format=json", "--all", "hostname") 354 c.Assert(err, jc.ErrorIsNil) 355 356 c.Check(cmdtesting.Stdout(context), gc.Equals, buff.String()) 357 c.Check(cmdtesting.Stderr(context), gc.Equals, "") 358 } 359 360 func (s *RunSuite) TestTimeout(c *gc.C) { 361 mock := s.setupMockAPI() 362 mock.setMachinesAlive("0", "1", "2") 363 response0 := mockResponse{ 364 stdout: "megatron\n", 365 machineTag: "machine-0", 366 } 367 response1 := mockResponse{ 368 machineTag: "machine-1", 369 status: params.ActionPending, 370 } 371 response2 := mockResponse{ 372 machineTag: "machine-2", 373 status: params.ActionRunning, 374 } 375 mock.setResponse("0", response0) 376 mock.setResponse("1", response1) 377 mock.setResponse("2", response2) 378 379 machine0Result := mock.runResponses["0"] 380 machine1Result := mock.runResponses["1"] 381 machine2Result := mock.runResponses["1"] 382 mock.actionResponses = map[string]params.ActionResult{ 383 mock.receiverIdMap["0"]: machine0Result, 384 mock.receiverIdMap["1"]: machine1Result, 385 mock.receiverIdMap["2"]: machine2Result, 386 } 387 388 machine0Query := makeActionQuery(mock.receiverIdMap["0"], "MachineId", names.NewMachineTag("0")) 389 390 var buf bytes.Buffer 391 err := cmd.FormatJson(&buf, []interface{}{ 392 ConvertActionResults(machine0Result, machine0Query), 393 }) 394 c.Assert(err, jc.ErrorIsNil) 395 396 var clock mockClock 397 context, err := cmdtesting.RunCommand( 398 c, newTestRunCommand(&clock), 399 "--format=json", "--all", "hostname", "--timeout", "99s", 400 ) 401 c.Assert(err, gc.ErrorMatches, "timed out waiting for results from: machine 1, machine 2") 402 403 c.Check(cmdtesting.Stdout(context), gc.Equals, buf.String()) 404 c.Check(cmdtesting.Stderr(context), gc.Equals, "") 405 clock.CheckCalls(c, []gitjujutesting.StubCall{ 406 {"After", []interface{}{99 * time.Second}}, 407 {"After", []interface{}{1 * time.Second}}, 408 {"After", []interface{}{1 * time.Second}}, 409 }) 410 } 411 412 func (s *RunSuite) TestUnitLeaderSyntaxWithUnsupportedAPIVersion(c *gc.C) { 413 var ( 414 clock mockClock 415 mock = s.setupMockAPI() 416 ) 417 418 mock.bestAPIVersion = 2 419 _, err := cmdtesting.RunCommand( 420 c, newTestRunCommand(&clock), 421 "--unit", "foo/leader", "hostname", 422 ) 423 424 expErr := fmt.Sprintf("unable to determine leader for application %q"+ 425 "\nleader determination is unsupported by this API"+ 426 "\neither upgrade your controller, or explicitly specify a unit", "foo") 427 c.Assert(err, gc.ErrorMatches, expErr) 428 } 429 430 type mockClock struct { 431 gitjujutesting.Stub 432 clock.Clock 433 timeoutCh chan time.Time 434 } 435 436 func (c *mockClock) After(d time.Duration) <-chan time.Time { 437 c.MethodCall(c, "After", d) 438 ch := make(chan time.Time) 439 if d == time.Second { 440 // This is a sleepy sleep call, while we're waiting 441 // for actions to be run. We simulate sleeping a 442 // couple of times, and then close the timeout 443 // channel. 444 if len(c.Calls()) >= 3 { 445 close(c.timeoutCh) 446 } else { 447 close(ch) 448 } 449 } else { 450 // This is the initial time.After call for the timeout. 451 // Once we've gone through the loop waiting for results 452 // a couple of times, we'll close this to indicate that 453 // a timeout occurred. 454 if c.timeoutCh != nil { 455 panic("time.After called for timeout multiple times") 456 } 457 c.timeoutCh = ch 458 } 459 return ch 460 } 461 462 func (s *RunSuite) TestBlockAllMachines(c *gc.C) { 463 mock := s.setupMockAPI() 464 // Block operation 465 mock.block = true 466 _, err := cmdtesting.RunCommand(c, newTestRunCommand(&mockClock{}), "--format=json", "--all", "hostname") 467 testing.AssertOperationWasBlocked(c, err, ".*To enable changes.*") 468 } 469 470 func (s *RunSuite) TestSingleResponse(c *gc.C) { 471 mock := s.setupMockAPI() 472 mock.setMachinesAlive("0") 473 mockResponse := mockResponse{ 474 stdout: "stdout\n", 475 stderr: "stderr\n", 476 code: "42", 477 machineTag: "machine-0", 478 } 479 mock.setResponse("0", mockResponse) 480 481 machineResult := mock.runResponses["0"] 482 mock.actionResponses = map[string]params.ActionResult{ 483 mock.receiverIdMap["0"]: machineResult, 484 } 485 486 query := makeActionQuery(mock.receiverIdMap["0"], "MachineId", names.NewMachineTag("0")) 487 unformatted := []interface{}{ 488 ConvertActionResults(machineResult, query), 489 } 490 491 jsonFormatted := &bytes.Buffer{} 492 err := cmd.FormatJson(jsonFormatted, unformatted) 493 c.Assert(err, jc.ErrorIsNil) 494 495 yamlFormatted := &bytes.Buffer{} 496 err = cmd.FormatYaml(yamlFormatted, unformatted) 497 c.Assert(err, jc.ErrorIsNil) 498 499 for i, test := range []struct { 500 message string 501 format string 502 stdout string 503 stderr string 504 errorMatch string 505 }{{ 506 message: "smart (default)", 507 stdout: "stdout\n", 508 stderr: "stderr\n", 509 errorMatch: "subprocess encountered error code 42", 510 }, { 511 message: "yaml output", 512 format: "yaml", 513 stdout: yamlFormatted.String(), 514 }, { 515 message: "json output", 516 format: "json", 517 stdout: jsonFormatted.String(), 518 }} { 519 c.Log(fmt.Sprintf("%v: %s", i, test.message)) 520 args := []string{} 521 if test.format != "" { 522 args = append(args, "--format", test.format) 523 } 524 args = append(args, "--all", "ignored") 525 context, err := cmdtesting.RunCommand(c, newTestRunCommand(&mockClock{}), args...) 526 if test.errorMatch != "" { 527 c.Check(err, gc.ErrorMatches, test.errorMatch) 528 } else { 529 c.Check(err, jc.ErrorIsNil) 530 } 531 c.Check(cmdtesting.Stdout(context), gc.Equals, test.stdout) 532 c.Check(cmdtesting.Stderr(context), gc.Equals, test.stderr) 533 } 534 } 535 536 func (s *RunSuite) setupMockAPI() *mockRunAPI { 537 mock := &mockRunAPI{} 538 s.PatchValue(&getRunAPIClient, func(_ *runCommand) (RunClient, error) { 539 return mock, nil 540 }) 541 return mock 542 } 543 544 type mockRunAPI struct { 545 action.APIClient 546 stdout string 547 stderr string 548 code int 549 // machines, applications, units 550 machines map[string]bool 551 runResponses map[string]params.ActionResult 552 actionResponses map[string]params.ActionResult 553 receiverIdMap map[string]string 554 block bool 555 // 556 bestAPIVersion int 557 } 558 559 type mockResponse struct { 560 stdout interface{} 561 stderr interface{} 562 code interface{} 563 error *params.Error 564 message string 565 machineTag string 566 unitTag string 567 status string 568 } 569 570 var _ RunClient = (*mockRunAPI)(nil) 571 572 func (m *mockRunAPI) setMachinesAlive(ids ...string) { 573 if m.machines == nil { 574 m.machines = make(map[string]bool) 575 } 576 for _, id := range ids { 577 m.machines[id] = true 578 } 579 } 580 581 func makeActionQuery(actionID string, receiverType string, receiverTag names.Tag) actionQuery { 582 return actionQuery{ 583 actionTag: names.NewActionTag(actionID), 584 receiver: actionReceiver{ 585 receiverType: receiverType, 586 tag: receiverTag, 587 }, 588 } 589 } 590 591 func makeActionResult(mock mockResponse, actionTag string) params.ActionResult { 592 var receiverTag string 593 if mock.unitTag != "" { 594 receiverTag = mock.unitTag 595 } else { 596 receiverTag = mock.machineTag 597 } 598 if actionTag == "" { 599 actionTag = names.NewActionTag(utils.MustNewUUID().String()).String() 600 } 601 return params.ActionResult{ 602 Action: ¶ms.Action{ 603 Tag: actionTag, 604 Receiver: receiverTag, 605 }, 606 Message: mock.message, 607 Status: mock.status, 608 Error: mock.error, 609 Output: map[string]interface{}{ 610 "Stdout": mock.stdout, 611 "Stderr": mock.stderr, 612 "Code": mock.code, 613 }, 614 } 615 } 616 617 func (m *mockRunAPI) setResponse(id string, mock mockResponse) { 618 if m.runResponses == nil { 619 m.runResponses = make(map[string]params.ActionResult) 620 } 621 if m.receiverIdMap == nil { 622 m.receiverIdMap = make(map[string]string) 623 } 624 actionTag := names.NewActionTag(utils.MustNewUUID().String()) 625 m.receiverIdMap[id] = actionTag.Id() 626 m.runResponses[id] = makeActionResult(mock, actionTag.String()) 627 } 628 629 func (*mockRunAPI) Close() error { 630 return nil 631 } 632 633 func (m *mockRunAPI) RunOnAllMachines(commands string, timeout time.Duration) ([]params.ActionResult, error) { 634 var result []params.ActionResult 635 636 if m.block { 637 return result, common.OperationBlockedError("the operation has been blocked") 638 } 639 sortedMachineIds := make([]string, 0, len(m.machines)) 640 for machineId := range m.machines { 641 sortedMachineIds = append(sortedMachineIds, machineId) 642 } 643 sort.Strings(sortedMachineIds) 644 645 for _, machineId := range sortedMachineIds { 646 response, found := m.runResponses[machineId] 647 if !found { 648 // Consider this a timeout 649 response = params.ActionResult{ 650 Action: ¶ms.Action{ 651 Receiver: names.NewMachineTag(machineId).String(), 652 }, 653 Message: exec.ErrCancelled.Error(), 654 } 655 } 656 result = append(result, response) 657 } 658 659 return result, nil 660 } 661 662 func (m *mockRunAPI) Run(runParams params.RunParams) ([]params.ActionResult, error) { 663 var result []params.ActionResult 664 665 if m.block { 666 return result, common.OperationBlockedError("the operation has been blocked") 667 } 668 // Just add in ids that match in order. 669 for _, id := range runParams.Machines { 670 response, found := m.runResponses[id] 671 if found { 672 result = append(result, response) 673 } 674 } 675 // mock ignores applications 676 for _, id := range runParams.Units { 677 response, found := m.runResponses[id] 678 if found { 679 result = append(result, response) 680 } 681 } 682 683 return result, nil 684 } 685 686 func (m *mockRunAPI) Actions(actionTags params.Entities) (params.ActionResults, error) { 687 results := params.ActionResults{Results: make([]params.ActionResult, len(actionTags.Entities))} 688 689 for i, entity := range actionTags.Entities { 690 response, found := m.actionResponses[entity.Tag[len("action-"):]] 691 if !found { 692 results.Results[i] = params.ActionResult{ 693 Error: ¶ms.Error{ 694 Message: "action not found", 695 }, 696 } 697 continue 698 } 699 results.Results[i] = response 700 } 701 702 return results, nil 703 } 704 705 func (m *mockRunAPI) BestAPIVersion() int { 706 return m.bestAPIVersion 707 } 708 709 // validUUID is a UUID used in tests 710 var validUUID = "01234567-89ab-cdef-0123-456789abcdef"