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