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