PageRenderTime 46ms CodeModel.GetById 17ms RepoModel.GetById 0ms app.codeStats 0ms

/cmd/juju/run_test.go

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