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