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, ¶ms.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, ¶ms.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 }