github.com/altoros/juju-vmware@v0.0.0-20150312064031-f19ae857ccca/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  	"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/params"
    18  	"github.com/juju/juju/cmd/envcmd"
    19  	"github.com/juju/juju/testing"
    20  )
    21  
    22  type RunSuite struct {
    23  	testing.FakeJujuHomeSuite
    24  }
    25  
    26  var _ = gc.Suite(&RunSuite{})
    27  
    28  func (*RunSuite) TestTargetArgParsing(c *gc.C) {
    29  	for i, test := range []struct {
    30  		message  string
    31  		args     []string
    32  		all      bool
    33  		machines []string
    34  		units    []string
    35  		services []string
    36  		commands string
    37  		errMatch string
    38  	}{{
    39  		message:  "no args",
    40  		errMatch: "no commands specified",
    41  	}, {
    42  		message:  "no target",
    43  		args:     []string{"sudo reboot"},
    44  		errMatch: "You must specify a target, either through --all, --machine, --service or --unit",
    45  	}, {
    46  		message:  "too many args",
    47  		args:     []string{"--all", "sudo reboot", "oops"},
    48  		errMatch: `unrecognized args: \["oops"\]`,
    49  	}, {
    50  		message:  "command to all machines",
    51  		args:     []string{"--all", "sudo reboot"},
    52  		all:      true,
    53  		commands: "sudo reboot",
    54  	}, {
    55  		message:  "all and defined machines",
    56  		args:     []string{"--all", "--machine=1,2", "sudo reboot"},
    57  		errMatch: `You cannot specify --all and individual machines`,
    58  	}, {
    59  		message:  "command to machines 1, 2, and 1/kvm/0",
    60  		args:     []string{"--machine=1,2,1/kvm/0", "sudo reboot"},
    61  		commands: "sudo reboot",
    62  		machines: []string{"1", "2", "1/kvm/0"},
    63  	}, {
    64  		message: "bad machine names",
    65  		args:    []string{"--machine=foo,machine-2", "sudo reboot"},
    66  		errMatch: "" +
    67  			"The following run targets are not valid:\n" +
    68  			"  \"foo\" is not a valid machine id\n" +
    69  			"  \"machine-2\" is not a valid machine id",
    70  	}, {
    71  		message:  "all and defined services",
    72  		args:     []string{"--all", "--service=wordpress,mysql", "sudo reboot"},
    73  		errMatch: `You cannot specify --all and individual services`,
    74  	}, {
    75  		message:  "command to services wordpress and mysql",
    76  		args:     []string{"--service=wordpress,mysql", "sudo reboot"},
    77  		commands: "sudo reboot",
    78  		services: []string{"wordpress", "mysql"},
    79  	}, {
    80  		message: "bad service names",
    81  		args:    []string{"--service", "foo,2,foo/0", "sudo reboot"},
    82  		errMatch: "" +
    83  			"The following run targets are not valid:\n" +
    84  			"  \"2\" is not a valid service name\n" +
    85  			"  \"foo/0\" is not a valid service name",
    86  	}, {
    87  		message:  "all and defined units",
    88  		args:     []string{"--all", "--unit=wordpress/0,mysql/1", "sudo reboot"},
    89  		errMatch: `You cannot specify --all and individual units`,
    90  	}, {
    91  		message:  "command to valid units",
    92  		args:     []string{"--unit=wordpress/0,wordpress/1,mysql/0", "sudo reboot"},
    93  		commands: "sudo reboot",
    94  		units:    []string{"wordpress/0", "wordpress/1", "mysql/0"},
    95  	}, {
    96  		message: "bad unit names",
    97  		args:    []string{"--unit", "foo,2,foo/0", "sudo reboot"},
    98  		errMatch: "" +
    99  			"The following run targets are not valid:\n" +
   100  			"  \"foo\" is not a valid unit name\n" +
   101  			"  \"2\" is not a valid unit name",
   102  	}, {
   103  		message:  "command to mixed valid targets",
   104  		args:     []string{"--machine=0", "--unit=wordpress/0,wordpress/1", "--service=mysql", "sudo reboot"},
   105  		commands: "sudo reboot",
   106  		machines: []string{"0"},
   107  		services: []string{"mysql"},
   108  		units:    []string{"wordpress/0", "wordpress/1"},
   109  	}} {
   110  		c.Log(fmt.Sprintf("%v: %s", i, test.message))
   111  		runCmd := &RunCommand{}
   112  		testing.TestInit(c, envcmd.Wrap(runCmd), test.args, test.errMatch)
   113  		if test.errMatch == "" {
   114  			c.Check(runCmd.all, gc.Equals, test.all)
   115  			c.Check(runCmd.machines, gc.DeepEquals, test.machines)
   116  			c.Check(runCmd.services, gc.DeepEquals, test.services)
   117  			c.Check(runCmd.units, gc.DeepEquals, test.units)
   118  			c.Check(runCmd.commands, gc.Equals, test.commands)
   119  		}
   120  	}
   121  }
   122  
   123  func (*RunSuite) TestTimeoutArgParsing(c *gc.C) {
   124  	for i, test := range []struct {
   125  		message  string
   126  		args     []string
   127  		errMatch string
   128  		timeout  time.Duration
   129  	}{{
   130  		message: "default time",
   131  		args:    []string{"--all", "sudo reboot"},
   132  		timeout: 5 * time.Minute,
   133  	}, {
   134  		message:  "invalid time",
   135  		args:     []string{"--timeout=foo", "--all", "sudo reboot"},
   136  		errMatch: `invalid value "foo" for flag --timeout: time: invalid duration foo`,
   137  	}, {
   138  		message: "two hours",
   139  		args:    []string{"--timeout=2h", "--all", "sudo reboot"},
   140  		timeout: 2 * time.Hour,
   141  	}, {
   142  		message: "3 minutes 30 seconds",
   143  		args:    []string{"--timeout=3m30s", "--all", "sudo reboot"},
   144  		timeout: (3 * time.Minute) + (30 * time.Second),
   145  	}} {
   146  		c.Log(fmt.Sprintf("%v: %s", i, test.message))
   147  		runCmd := &RunCommand{}
   148  		testing.TestInit(c, envcmd.Wrap(runCmd), test.args, test.errMatch)
   149  		if test.errMatch == "" {
   150  			c.Check(runCmd.timeout, gc.Equals, test.timeout)
   151  		}
   152  	}
   153  }
   154  
   155  func (s *RunSuite) TestConvertRunResults(c *gc.C) {
   156  	for i, test := range []struct {
   157  		message  string
   158  		results  []params.RunResult
   159  		expected interface{}
   160  	}{{
   161  		message:  "empty",
   162  		expected: []interface{}{},
   163  	}, {
   164  		message: "minimum is machine id and stdout",
   165  		results: []params.RunResult{
   166  			makeRunResult(mockResponse{machineId: "1"}),
   167  		},
   168  		expected: []interface{}{
   169  			map[string]interface{}{
   170  				"MachineId": "1",
   171  				"Stdout":    "",
   172  			}},
   173  	}, {
   174  		message: "other fields are copied if there",
   175  		results: []params.RunResult{
   176  			makeRunResult(mockResponse{
   177  				machineId: "1",
   178  				stdout:    "stdout",
   179  				stderr:    "stderr",
   180  				code:      42,
   181  				unitId:    "unit/0",
   182  				error:     "error",
   183  			}),
   184  		},
   185  		expected: []interface{}{
   186  			map[string]interface{}{
   187  				"MachineId":  "1",
   188  				"Stdout":     "stdout",
   189  				"Stderr":     "stderr",
   190  				"ReturnCode": 42,
   191  				"UnitId":     "unit/0",
   192  				"Error":      "error",
   193  			}},
   194  	}, {
   195  		message: "stdout and stderr are base64 encoded if not valid utf8",
   196  		results: []params.RunResult{
   197  			{
   198  				ExecResponse: exec.ExecResponse{
   199  					Stdout: []byte{0xff},
   200  					Stderr: []byte{0xfe},
   201  				},
   202  				MachineId: "jake",
   203  			},
   204  		},
   205  		expected: []interface{}{
   206  			map[string]interface{}{
   207  				"MachineId":       "jake",
   208  				"Stdout":          "/w==",
   209  				"Stdout.encoding": "base64",
   210  				"Stderr":          "/g==",
   211  				"Stderr.encoding": "base64",
   212  			}},
   213  	}, {
   214  		message: "more than one",
   215  		results: []params.RunResult{
   216  			makeRunResult(mockResponse{machineId: "1"}),
   217  			makeRunResult(mockResponse{machineId: "2"}),
   218  			makeRunResult(mockResponse{machineId: "3"}),
   219  		},
   220  		expected: []interface{}{
   221  			map[string]interface{}{
   222  				"MachineId": "1",
   223  				"Stdout":    "",
   224  			},
   225  			map[string]interface{}{
   226  				"MachineId": "2",
   227  				"Stdout":    "",
   228  			},
   229  			map[string]interface{}{
   230  				"MachineId": "3",
   231  				"Stdout":    "",
   232  			},
   233  		},
   234  	}} {
   235  		c.Log(fmt.Sprintf("%v: %s", i, test.message))
   236  		result := ConvertRunResults(test.results)
   237  		c.Check(result, jc.DeepEquals, test.expected)
   238  	}
   239  }
   240  
   241  func (s *RunSuite) TestRunForMachineAndUnit(c *gc.C) {
   242  	mock := s.setupMockAPI()
   243  	machineResponse := mockResponse{
   244  		stdout:    "megatron\n",
   245  		machineId: "0",
   246  	}
   247  	unitResponse := mockResponse{
   248  		stdout:    "bumblebee",
   249  		machineId: "1",
   250  		unitId:    "unit/0",
   251  	}
   252  	mock.setResponse("0", machineResponse)
   253  	mock.setResponse("unit/0", unitResponse)
   254  
   255  	unformatted := ConvertRunResults([]params.RunResult{
   256  		makeRunResult(machineResponse),
   257  		makeRunResult(unitResponse),
   258  	})
   259  
   260  	jsonFormatted, err := cmd.FormatJson(unformatted)
   261  	c.Assert(err, jc.ErrorIsNil)
   262  
   263  	context, err := testing.RunCommand(c, envcmd.Wrap(&RunCommand{}),
   264  		"--format=json", "--machine=0", "--unit=unit/0", "hostname",
   265  	)
   266  	c.Assert(err, jc.ErrorIsNil)
   267  
   268  	c.Check(testing.Stdout(context), gc.Equals, string(jsonFormatted)+"\n")
   269  }
   270  
   271  func (s *RunSuite) TestBlockRunForMachineAndUnit(c *gc.C) {
   272  	mock := s.setupMockAPI()
   273  	// Block operation
   274  	mock.block = true
   275  	s.AssertConfigParameterUpdated(c, "block-all-changes", "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, &params.Error{
   445  			Code:    params.CodeOperationBlocked,
   446  			Message: "The operation has been blocked.",
   447  		}
   448  	}
   449  	sortedMachineIds := make([]string, 0, len(m.machines))
   450  	for machineId := range m.machines {
   451  		sortedMachineIds = append(sortedMachineIds, machineId)
   452  	}
   453  	sort.Strings(sortedMachineIds)
   454  
   455  	for _, machineId := range sortedMachineIds {
   456  		response, found := m.responses[machineId]
   457  		if !found {
   458  			// Consider this a timeout
   459  			response = params.RunResult{MachineId: machineId, Error: "command timed out"}
   460  		}
   461  		result = append(result, response)
   462  	}
   463  
   464  	return result, nil
   465  }
   466  
   467  func (m *mockRunAPI) Run(runParams params.RunParams) ([]params.RunResult, error) {
   468  	var result []params.RunResult
   469  
   470  	if m.block {
   471  		return result, &params.Error{
   472  			Code:    params.CodeOperationBlocked,
   473  			Message: "The operation has been blocked.",
   474  		}
   475  	}
   476  	// Just add in ids that match in order.
   477  	for _, id := range runParams.Machines {
   478  		response, found := m.responses[id]
   479  		if found {
   480  			result = append(result, response)
   481  		}
   482  	}
   483  	// mock ignores services
   484  	for _, id := range runParams.Units {
   485  		response, found := m.responses[id]
   486  		if found {
   487  			result = append(result, response)
   488  		}
   489  	}
   490  
   491  	return result, nil
   492  }