github.com/mhilton/juju-juju@v0.0.0-20150901100907-a94dd2c73455/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  	jc "github.com/juju/testing/checkers"
    14  	"github.com/juju/utils/exec"
    15  	gc "gopkg.in/check.v1"
    16  
    17  	"github.com/juju/juju/apiserver/common"
    18  	"github.com/juju/juju/apiserver/params"
    19  	"github.com/juju/juju/cmd/envcmd"
    20  	"github.com/juju/juju/testing"
    21  )
    22  
    23  type RunSuite struct {
    24  	testing.FakeJujuHomeSuite
    25  }
    26  
    27  var _ = gc.Suite(&RunSuite{})
    28  
    29  func (*RunSuite) TestTargetArgParsing(c *gc.C) {
    30  	for i, test := range []struct {
    31  		message  string
    32  		args     []string
    33  		all      bool
    34  		machines []string
    35  		units    []string
    36  		services []string
    37  		commands string
    38  		errMatch string
    39  	}{{
    40  		message:  "no args",
    41  		errMatch: "no commands specified",
    42  	}, {
    43  		message:  "no target",
    44  		args:     []string{"sudo reboot"},
    45  		errMatch: "You must specify a target, either through --all, --machine, --service or --unit",
    46  	}, {
    47  		message:  "too many args",
    48  		args:     []string{"--all", "sudo reboot", "oops"},
    49  		errMatch: `unrecognized args: \["oops"\]`,
    50  	}, {
    51  		message:  "command to all machines",
    52  		args:     []string{"--all", "sudo reboot"},
    53  		all:      true,
    54  		commands: "sudo reboot",
    55  	}, {
    56  		message:  "all and defined machines",
    57  		args:     []string{"--all", "--machine=1,2", "sudo reboot"},
    58  		errMatch: `You cannot specify --all and individual machines`,
    59  	}, {
    60  		message:  "command to machines 1, 2, and 1/kvm/0",
    61  		args:     []string{"--machine=1,2,1/kvm/0", "sudo reboot"},
    62  		commands: "sudo reboot",
    63  		machines: []string{"1", "2", "1/kvm/0"},
    64  	}, {
    65  		message: "bad machine names",
    66  		args:    []string{"--machine=foo,machine-2", "sudo reboot"},
    67  		errMatch: "" +
    68  			"The following run targets are not valid:\n" +
    69  			"  \"foo\" is not a valid machine id\n" +
    70  			"  \"machine-2\" is not a valid machine id",
    71  	}, {
    72  		message:  "all and defined services",
    73  		args:     []string{"--all", "--service=wordpress,mysql", "sudo reboot"},
    74  		errMatch: `You cannot specify --all and individual services`,
    75  	}, {
    76  		message:  "command to services wordpress and mysql",
    77  		args:     []string{"--service=wordpress,mysql", "sudo reboot"},
    78  		commands: "sudo reboot",
    79  		services: []string{"wordpress", "mysql"},
    80  	}, {
    81  		message: "bad service names",
    82  		args:    []string{"--service", "foo,2,foo/0", "sudo reboot"},
    83  		errMatch: "" +
    84  			"The following run targets are not valid:\n" +
    85  			"  \"2\" is not a valid service name\n" +
    86  			"  \"foo/0\" is not a valid service name",
    87  	}, {
    88  		message:  "all and defined units",
    89  		args:     []string{"--all", "--unit=wordpress/0,mysql/1", "sudo reboot"},
    90  		errMatch: `You cannot specify --all and individual units`,
    91  	}, {
    92  		message:  "command to valid units",
    93  		args:     []string{"--unit=wordpress/0,wordpress/1,mysql/0", "sudo reboot"},
    94  		commands: "sudo reboot",
    95  		units:    []string{"wordpress/0", "wordpress/1", "mysql/0"},
    96  	}, {
    97  		message: "bad unit names",
    98  		args:    []string{"--unit", "foo,2,foo/0", "sudo reboot"},
    99  		errMatch: "" +
   100  			"The following run targets are not valid:\n" +
   101  			"  \"foo\" is not a valid unit name\n" +
   102  			"  \"2\" is not a valid unit name",
   103  	}, {
   104  		message:  "command to mixed valid targets",
   105  		args:     []string{"--machine=0", "--unit=wordpress/0,wordpress/1", "--service=mysql", "sudo reboot"},
   106  		commands: "sudo reboot",
   107  		machines: []string{"0"},
   108  		services: []string{"mysql"},
   109  		units:    []string{"wordpress/0", "wordpress/1"},
   110  	}} {
   111  		c.Log(fmt.Sprintf("%v: %s", i, test.message))
   112  		runCmd := &RunCommand{}
   113  		testing.TestInit(c, envcmd.Wrap(runCmd), test.args, test.errMatch)
   114  		if test.errMatch == "" {
   115  			c.Check(runCmd.all, gc.Equals, test.all)
   116  			c.Check(runCmd.machines, gc.DeepEquals, test.machines)
   117  			c.Check(runCmd.services, gc.DeepEquals, test.services)
   118  			c.Check(runCmd.units, gc.DeepEquals, test.units)
   119  			c.Check(runCmd.commands, gc.Equals, test.commands)
   120  		}
   121  	}
   122  }
   123  
   124  func (*RunSuite) TestTimeoutArgParsing(c *gc.C) {
   125  	for i, test := range []struct {
   126  		message  string
   127  		args     []string
   128  		errMatch string
   129  		timeout  time.Duration
   130  	}{{
   131  		message: "default time",
   132  		args:    []string{"--all", "sudo reboot"},
   133  		timeout: 5 * time.Minute,
   134  	}, {
   135  		message:  "invalid time",
   136  		args:     []string{"--timeout=foo", "--all", "sudo reboot"},
   137  		errMatch: `invalid value "foo" for flag --timeout: time: invalid duration foo`,
   138  	}, {
   139  		message: "two hours",
   140  		args:    []string{"--timeout=2h", "--all", "sudo reboot"},
   141  		timeout: 2 * time.Hour,
   142  	}, {
   143  		message: "3 minutes 30 seconds",
   144  		args:    []string{"--timeout=3m30s", "--all", "sudo reboot"},
   145  		timeout: (3 * time.Minute) + (30 * time.Second),
   146  	}} {
   147  		c.Log(fmt.Sprintf("%v: %s", i, test.message))
   148  		runCmd := &RunCommand{}
   149  		testing.TestInit(c, envcmd.Wrap(runCmd), test.args, test.errMatch)
   150  		if test.errMatch == "" {
   151  			c.Check(runCmd.timeout, gc.Equals, test.timeout)
   152  		}
   153  	}
   154  }
   155  
   156  func (s *RunSuite) TestConvertRunResults(c *gc.C) {
   157  	for i, test := range []struct {
   158  		message  string
   159  		results  []params.RunResult
   160  		expected interface{}
   161  	}{{
   162  		message:  "empty",
   163  		expected: []interface{}{},
   164  	}, {
   165  		message: "minimum is machine id and stdout",
   166  		results: []params.RunResult{
   167  			makeRunResult(mockResponse{machineId: "1"}),
   168  		},
   169  		expected: []interface{}{
   170  			map[string]interface{}{
   171  				"MachineId": "1",
   172  				"Stdout":    "",
   173  			}},
   174  	}, {
   175  		message: "other fields are copied if there",
   176  		results: []params.RunResult{
   177  			makeRunResult(mockResponse{
   178  				machineId: "1",
   179  				stdout:    "stdout",
   180  				stderr:    "stderr",
   181  				code:      42,
   182  				unitId:    "unit/0",
   183  				error:     "error",
   184  			}),
   185  		},
   186  		expected: []interface{}{
   187  			map[string]interface{}{
   188  				"MachineId":  "1",
   189  				"Stdout":     "stdout",
   190  				"Stderr":     "stderr",
   191  				"ReturnCode": 42,
   192  				"UnitId":     "unit/0",
   193  				"Error":      "error",
   194  			}},
   195  	}, {
   196  		message: "stdout and stderr are base64 encoded if not valid utf8",
   197  		results: []params.RunResult{
   198  			{
   199  				ExecResponse: exec.ExecResponse{
   200  					Stdout: []byte{0xff},
   201  					Stderr: []byte{0xfe},
   202  				},
   203  				MachineId: "jake",
   204  			},
   205  		},
   206  		expected: []interface{}{
   207  			map[string]interface{}{
   208  				"MachineId":       "jake",
   209  				"Stdout":          "/w==",
   210  				"Stdout.encoding": "base64",
   211  				"Stderr":          "/g==",
   212  				"Stderr.encoding": "base64",
   213  			}},
   214  	}, {
   215  		message: "more than one",
   216  		results: []params.RunResult{
   217  			makeRunResult(mockResponse{machineId: "1"}),
   218  			makeRunResult(mockResponse{machineId: "2"}),
   219  			makeRunResult(mockResponse{machineId: "3"}),
   220  		},
   221  		expected: []interface{}{
   222  			map[string]interface{}{
   223  				"MachineId": "1",
   224  				"Stdout":    "",
   225  			},
   226  			map[string]interface{}{
   227  				"MachineId": "2",
   228  				"Stdout":    "",
   229  			},
   230  			map[string]interface{}{
   231  				"MachineId": "3",
   232  				"Stdout":    "",
   233  			},
   234  		},
   235  	}} {
   236  		c.Log(fmt.Sprintf("%v: %s", i, test.message))
   237  		result := ConvertRunResults(test.results)
   238  		c.Check(result, jc.DeepEquals, test.expected)
   239  	}
   240  }
   241  
   242  func (s *RunSuite) TestRunForMachineAndUnit(c *gc.C) {
   243  	mock := s.setupMockAPI()
   244  	machineResponse := mockResponse{
   245  		stdout:    "megatron\n",
   246  		machineId: "0",
   247  	}
   248  	unitResponse := mockResponse{
   249  		stdout:    "bumblebee",
   250  		machineId: "1",
   251  		unitId:    "unit/0",
   252  	}
   253  	mock.setResponse("0", machineResponse)
   254  	mock.setResponse("unit/0", unitResponse)
   255  
   256  	unformatted := ConvertRunResults([]params.RunResult{
   257  		makeRunResult(machineResponse),
   258  		makeRunResult(unitResponse),
   259  	})
   260  
   261  	jsonFormatted, err := cmd.FormatJson(unformatted)
   262  	c.Assert(err, jc.ErrorIsNil)
   263  
   264  	context, err := testing.RunCommand(c, envcmd.Wrap(&RunCommand{}),
   265  		"--format=json", "--machine=0", "--unit=unit/0", "hostname",
   266  	)
   267  	c.Assert(err, jc.ErrorIsNil)
   268  
   269  	c.Check(testing.Stdout(context), gc.Equals, string(jsonFormatted)+"\n")
   270  }
   271  
   272  func (s *RunSuite) TestBlockRunForMachineAndUnit(c *gc.C) {
   273  	mock := s.setupMockAPI()
   274  	// Block operation
   275  	mock.block = true
   276  	_, err := testing.RunCommand(c, envcmd.Wrap(&RunCommand{}),
   277  		"--format=json", "--machine=0", "--unit=unit/0", "hostname",
   278  		"-e blah",
   279  	)
   280  	c.Assert(err, gc.ErrorMatches, cmd.ErrSilent.Error())
   281  	// msg is logged
   282  	stripped := strings.Replace(c.GetTestLog(), "\n", "", -1)
   283  	c.Check(stripped, gc.Matches, ".*To unblock changes.*")
   284  }
   285  
   286  func (s *RunSuite) TestAllMachines(c *gc.C) {
   287  	mock := s.setupMockAPI()
   288  	mock.setMachinesAlive("0", "1")
   289  	response0 := mockResponse{
   290  		stdout:    "megatron\n",
   291  		machineId: "0",
   292  	}
   293  	response1 := mockResponse{
   294  		error:     "command timed out",
   295  		machineId: "1",
   296  	}
   297  	mock.setResponse("0", response0)
   298  
   299  	unformatted := ConvertRunResults([]params.RunResult{
   300  		makeRunResult(response0),
   301  		makeRunResult(response1),
   302  	})
   303  
   304  	jsonFormatted, err := cmd.FormatJson(unformatted)
   305  	c.Assert(err, jc.ErrorIsNil)
   306  
   307  	context, err := testing.RunCommand(c, &RunCommand{}, "--format=json", "--all", "hostname")
   308  	c.Assert(err, jc.ErrorIsNil)
   309  
   310  	c.Check(testing.Stdout(context), gc.Equals, string(jsonFormatted)+"\n")
   311  }
   312  
   313  func (s *RunSuite) TestBlockAllMachines(c *gc.C) {
   314  	mock := s.setupMockAPI()
   315  	// Block operation
   316  	mock.block = true
   317  	_, err := testing.RunCommand(c, &RunCommand{}, "--format=json", "--all", "hostname")
   318  	c.Assert(err, gc.ErrorMatches, cmd.ErrSilent.Error())
   319  	// msg is logged
   320  	stripped := strings.Replace(c.GetTestLog(), "\n", "", -1)
   321  	c.Check(stripped, gc.Matches, ".*To unblock changes.*")
   322  }
   323  
   324  func (s *RunSuite) TestSingleResponse(c *gc.C) {
   325  	mock := s.setupMockAPI()
   326  	mock.setMachinesAlive("0")
   327  	mockResponse := mockResponse{
   328  		stdout:    "stdout\n",
   329  		stderr:    "stderr\n",
   330  		code:      42,
   331  		machineId: "0",
   332  	}
   333  	mock.setResponse("0", mockResponse)
   334  	unformatted := ConvertRunResults([]params.RunResult{
   335  		makeRunResult(mockResponse)})
   336  	yamlFormatted, err := cmd.FormatYaml(unformatted)
   337  	c.Assert(err, jc.ErrorIsNil)
   338  	jsonFormatted, err := cmd.FormatJson(unformatted)
   339  	c.Assert(err, jc.ErrorIsNil)
   340  
   341  	for i, test := range []struct {
   342  		message    string
   343  		format     string
   344  		stdout     string
   345  		stderr     string
   346  		errorMatch string
   347  	}{{
   348  		message:    "smart (default)",
   349  		stdout:     "stdout\n",
   350  		stderr:     "stderr\n",
   351  		errorMatch: "subprocess encountered error code 42",
   352  	}, {
   353  		message: "yaml output",
   354  		format:  "yaml",
   355  		stdout:  string(yamlFormatted) + "\n",
   356  	}, {
   357  		message: "json output",
   358  		format:  "json",
   359  		stdout:  string(jsonFormatted) + "\n",
   360  	}} {
   361  		c.Log(fmt.Sprintf("%v: %s", i, test.message))
   362  		args := []string{}
   363  		if test.format != "" {
   364  			args = append(args, "--format", test.format)
   365  		}
   366  		args = append(args, "--all", "ignored")
   367  		context, err := testing.RunCommand(c, envcmd.Wrap(&RunCommand{}), args...)
   368  		if test.errorMatch != "" {
   369  			c.Check(err, gc.ErrorMatches, test.errorMatch)
   370  		} else {
   371  			c.Check(err, jc.ErrorIsNil)
   372  		}
   373  		c.Check(testing.Stdout(context), gc.Equals, test.stdout)
   374  		c.Check(testing.Stderr(context), gc.Equals, test.stderr)
   375  	}
   376  }
   377  
   378  func (s *RunSuite) setupMockAPI() *mockRunAPI {
   379  	mock := &mockRunAPI{}
   380  	s.PatchValue(&getRunAPIClient, func(_ *RunCommand) (RunClient, error) {
   381  		return mock, nil
   382  	})
   383  	return mock
   384  }
   385  
   386  type mockRunAPI struct {
   387  	stdout string
   388  	stderr string
   389  	code   int
   390  	// machines, services, units
   391  	machines  map[string]bool
   392  	responses map[string]params.RunResult
   393  	block     bool
   394  }
   395  
   396  type mockResponse struct {
   397  	stdout    string
   398  	stderr    string
   399  	code      int
   400  	error     string
   401  	machineId string
   402  	unitId    string
   403  }
   404  
   405  var _ RunClient = (*mockRunAPI)(nil)
   406  
   407  func (m *mockRunAPI) setMachinesAlive(ids ...string) {
   408  	if m.machines == nil {
   409  		m.machines = make(map[string]bool)
   410  	}
   411  	for _, id := range ids {
   412  		m.machines[id] = true
   413  	}
   414  }
   415  
   416  func makeRunResult(mock mockResponse) params.RunResult {
   417  	return params.RunResult{
   418  		ExecResponse: exec.ExecResponse{
   419  			Stdout: []byte(mock.stdout),
   420  			Stderr: []byte(mock.stderr),
   421  			Code:   mock.code,
   422  		},
   423  		MachineId: mock.machineId,
   424  		UnitId:    mock.unitId,
   425  		Error:     mock.error,
   426  	}
   427  }
   428  
   429  func (m *mockRunAPI) setResponse(id string, mock mockResponse) {
   430  	if m.responses == nil {
   431  		m.responses = make(map[string]params.RunResult)
   432  	}
   433  	m.responses[id] = makeRunResult(mock)
   434  }
   435  
   436  func (*mockRunAPI) Close() error {
   437  	return nil
   438  }
   439  
   440  func (m *mockRunAPI) RunOnAllMachines(commands string, timeout time.Duration) ([]params.RunResult, error) {
   441  	var result []params.RunResult
   442  
   443  	if m.block {
   444  		return result, common.ErrOperationBlocked("The operation has been blocked.")
   445  	}
   446  	sortedMachineIds := make([]string, 0, len(m.machines))
   447  	for machineId := range m.machines {
   448  		sortedMachineIds = append(sortedMachineIds, machineId)
   449  	}
   450  	sort.Strings(sortedMachineIds)
   451  
   452  	for _, machineId := range sortedMachineIds {
   453  		response, found := m.responses[machineId]
   454  		if !found {
   455  			// Consider this a timeout
   456  			response = params.RunResult{MachineId: machineId, Error: "command timed out"}
   457  		}
   458  		result = append(result, response)
   459  	}
   460  
   461  	return result, nil
   462  }
   463  
   464  func (m *mockRunAPI) Run(runParams params.RunParams) ([]params.RunResult, error) {
   465  	var result []params.RunResult
   466  
   467  	if m.block {
   468  		return result, common.ErrOperationBlocked("The operation has been blocked.")
   469  	}
   470  	// Just add in ids that match in order.
   471  	for _, id := range runParams.Machines {
   472  		response, found := m.responses[id]
   473  		if found {
   474  			result = append(result, response)
   475  		}
   476  	}
   477  	// mock ignores services
   478  	for _, id := range runParams.Units {
   479  		response, found := m.responses[id]
   480  		if found {
   481  			result = append(result, response)
   482  		}
   483  	}
   484  
   485  	return result, nil
   486  }