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: &params.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: &params.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: &params.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: &params.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"