github.com/mattyw/juju@v0.0.0-20140610034352-732aecd63861/cmd/juju/run_test.go (about)

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