Add support for classic queries
[ganeti-local] / test / ganeti.cli_unittest.py
1 #!/usr/bin/python
2 #
3
4 # Copyright (C) 2008, 2011 Google Inc.
5 #
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful, but
12 # WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14 # General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19 # 02110-1301, USA.
20
21
22 """Script for unittesting the cli module"""
23
24 import unittest
25 import time
26 from cStringIO import StringIO
27
28 import ganeti
29 import testutils
30
31 from ganeti import constants
32 from ganeti import cli
33 from ganeti import errors
34 from ganeti import utils
35 from ganeti import objects
36 from ganeti import qlang
37 from ganeti.errors import OpPrereqError, ParameterError
38
39
40 class TestParseTimespec(unittest.TestCase):
41   """Testing case for ParseTimespec"""
42
43   def testValidTimes(self):
44     """Test valid timespecs"""
45     test_data = [
46       ('1s', 1),
47       ('1', 1),
48       ('1m', 60),
49       ('1h', 60 * 60),
50       ('1d', 60 * 60 * 24),
51       ('1w', 60 * 60 * 24 * 7),
52       ('4h', 4 * 60 * 60),
53       ('61m', 61 * 60),
54       ]
55     for value, expected_result in test_data:
56       self.failUnlessEqual(cli.ParseTimespec(value), expected_result)
57
58   def testInvalidTime(self):
59     """Test invalid timespecs"""
60     test_data = [
61       '1y',
62       '',
63       'aaa',
64       's',
65       ]
66     for value in test_data:
67       self.failUnlessRaises(OpPrereqError, cli.ParseTimespec, value)
68
69
70 class TestSplitKeyVal(unittest.TestCase):
71   """Testing case for cli._SplitKeyVal"""
72   DATA = "a=b,c,no_d,-e"
73   RESULT = {"a": "b", "c": True, "d": False, "e": None}
74
75   def testSplitKeyVal(self):
76     """Test splitting"""
77     self.failUnlessEqual(cli._SplitKeyVal("option", self.DATA), self.RESULT)
78
79   def testDuplicateParam(self):
80     """Test duplicate parameters"""
81     for data in ("a=1,a=2", "a,no_a"):
82       self.failUnlessRaises(ParameterError, cli._SplitKeyVal,
83                             "option", data)
84
85   def testEmptyData(self):
86     """Test how we handle splitting an empty string"""
87     self.failUnlessEqual(cli._SplitKeyVal("option", ""), {})
88
89
90 class TestIdentKeyVal(unittest.TestCase):
91   """Testing case for cli.check_ident_key_val"""
92
93   def testIdentKeyVal(self):
94     """Test identkeyval"""
95     def cikv(value):
96       return cli.check_ident_key_val("option", "opt", value)
97
98     self.assertEqual(cikv("foo:bar"), ("foo", {"bar": True}))
99     self.assertEqual(cikv("foo:bar=baz"), ("foo", {"bar": "baz"}))
100     self.assertEqual(cikv("bar:b=c,c=a"), ("bar", {"b": "c", "c": "a"}))
101     self.assertEqual(cikv("no_bar"), ("bar", False))
102     self.assertRaises(ParameterError, cikv, "no_bar:foo")
103     self.assertRaises(ParameterError, cikv, "no_bar:foo=baz")
104     self.assertEqual(cikv("-foo"), ("foo", None))
105     self.assertRaises(ParameterError, cikv, "-foo:a=c")
106
107     # Check negative numbers
108     self.assertEqual(cikv("-1:remove"), ("-1", {
109       "remove": True,
110       }))
111     self.assertEqual(cikv("-29447:add,size=4G"), ("-29447", {
112       "add": True,
113       "size": "4G",
114       }))
115     for i in ["-:", "-"]:
116       self.assertEqual(cikv(i), ("", None))
117
118
119 class TestToStream(unittest.TestCase):
120   """Test the ToStream functions"""
121
122   def testBasic(self):
123     for data in ["foo",
124                  "foo %s",
125                  "foo %(test)s",
126                  "foo %s %s",
127                  "",
128                  ]:
129       buf = StringIO()
130       cli._ToStream(buf, data)
131       self.failUnlessEqual(buf.getvalue(), data+'\n')
132
133   def testParams(self):
134       buf = StringIO()
135       cli._ToStream(buf, "foo %s", 1)
136       self.failUnlessEqual(buf.getvalue(), "foo 1\n")
137       buf = StringIO()
138       cli._ToStream(buf, "foo %s", (15,16))
139       self.failUnlessEqual(buf.getvalue(), "foo (15, 16)\n")
140       buf = StringIO()
141       cli._ToStream(buf, "foo %s %s", "a", "b")
142       self.failUnlessEqual(buf.getvalue(), "foo a b\n")
143
144
145 class TestGenerateTable(unittest.TestCase):
146   HEADERS = dict([("f%s" % i, "Field%s" % i) for i in range(5)])
147
148   FIELDS1 = ["f1", "f2"]
149   DATA1 = [
150     ["abc", 1234],
151     ["foobar", 56],
152     ["b", -14],
153     ]
154
155   def _test(self, headers, fields, separator, data,
156             numfields, unitfields, units, expected):
157     table = cli.GenerateTable(headers, fields, separator, data,
158                               numfields=numfields, unitfields=unitfields,
159                               units=units)
160     self.assertEqual(table, expected)
161
162   def testPlain(self):
163     exp = [
164       "Field1 Field2",
165       "abc    1234",
166       "foobar 56",
167       "b      -14",
168       ]
169     self._test(self.HEADERS, self.FIELDS1, None, self.DATA1,
170                None, None, "m", exp)
171
172   def testNoFields(self):
173     self._test(self.HEADERS, [], None, [[], []],
174                None, None, "m", ["", "", ""])
175     self._test(None, [], None, [[], []],
176                None, None, "m", ["", ""])
177
178   def testSeparator(self):
179     for sep in ["#", ":", ",", "^", "!", "%", "|", "###", "%%", "!!!", "||"]:
180       exp = [
181         "Field1%sField2" % sep,
182         "abc%s1234" % sep,
183         "foobar%s56" % sep,
184         "b%s-14" % sep,
185         ]
186       self._test(self.HEADERS, self.FIELDS1, sep, self.DATA1,
187                  None, None, "m", exp)
188
189   def testNoHeader(self):
190     exp = [
191       "abc    1234",
192       "foobar 56",
193       "b      -14",
194       ]
195     self._test(None, self.FIELDS1, None, self.DATA1,
196                None, None, "m", exp)
197
198   def testUnknownField(self):
199     headers = {
200       "f1": "Field1",
201       }
202     exp = [
203       "Field1 UNKNOWN",
204       "abc    1234",
205       "foobar 56",
206       "b      -14",
207       ]
208     self._test(headers, ["f1", "UNKNOWN"], None, self.DATA1,
209                None, None, "m", exp)
210
211   def testNumfields(self):
212     fields = ["f1", "f2", "f3"]
213     data = [
214       ["abc", 1234, 0],
215       ["foobar", 56, 3],
216       ["b", -14, "-"],
217       ]
218     exp = [
219       "Field1 Field2 Field3",
220       "abc      1234      0",
221       "foobar     56      3",
222       "b         -14      -",
223       ]
224     self._test(self.HEADERS, fields, None, data,
225                ["f2", "f3"], None, "m", exp)
226
227   def testUnitfields(self):
228     expnosep = [
229       "Field1 Field2 Field3",
230       "abc      1234     0M",
231       "foobar     56     3M",
232       "b         -14      -",
233       ]
234
235     expsep = [
236       "Field1:Field2:Field3",
237       "abc:1234:0M",
238       "foobar:56:3M",
239       "b:-14:-",
240       ]
241
242     for sep, expected in [(None, expnosep), (":", expsep)]:
243       fields = ["f1", "f2", "f3"]
244       data = [
245         ["abc", 1234, 0],
246         ["foobar", 56, 3],
247         ["b", -14, "-"],
248         ]
249       self._test(self.HEADERS, fields, sep, data,
250                  ["f2", "f3"], ["f3"], "h", expected)
251
252   def testUnusual(self):
253     data = [
254       ["%", "xyz"],
255       ["%%", "abc"],
256       ]
257     exp = [
258       "Field1 Field2",
259       "%      xyz",
260       "%%     abc",
261       ]
262     self._test(self.HEADERS, ["f1", "f2"], None, data,
263                None, None, "m", exp)
264
265
266 class TestFormatQueryResult(unittest.TestCase):
267   def test(self):
268     fields = [
269       objects.QueryFieldDefinition(name="name", title="Name",
270                                    kind=constants.QFT_TEXT),
271       objects.QueryFieldDefinition(name="size", title="Size",
272                                    kind=constants.QFT_NUMBER),
273       objects.QueryFieldDefinition(name="act", title="Active",
274                                    kind=constants.QFT_BOOL),
275       objects.QueryFieldDefinition(name="mem", title="Memory",
276                                    kind=constants.QFT_UNIT),
277       objects.QueryFieldDefinition(name="other", title="SomeList",
278                                    kind=constants.QFT_OTHER),
279       ]
280
281     response = objects.QueryResponse(fields=fields, data=[
282       [(constants.RS_NORMAL, "nodeA"), (constants.RS_NORMAL, 128),
283        (constants.RS_NORMAL, False), (constants.RS_NORMAL, 1468006),
284        (constants.RS_NORMAL, [])],
285       [(constants.RS_NORMAL, "other"), (constants.RS_NORMAL, 512),
286        (constants.RS_NORMAL, True), (constants.RS_NORMAL, 16),
287        (constants.RS_NORMAL, [1, 2, 3])],
288       [(constants.RS_NORMAL, "xyz"), (constants.RS_NORMAL, 1024),
289        (constants.RS_NORMAL, True), (constants.RS_NORMAL, 4096),
290        (constants.RS_NORMAL, [{}, {}])],
291       ])
292
293     self.assertEqual(cli.FormatQueryResult(response, unit="h", header=True),
294       (cli.QR_NORMAL, [
295       "Name  Size Active Memory SomeList",
296       "nodeA  128 N        1.4T []",
297       "other  512 Y         16M [1, 2, 3]",
298       "xyz   1024 Y        4.0G [{}, {}]",
299       ]))
300
301   def testTimestampAndUnit(self):
302     fields = [
303       objects.QueryFieldDefinition(name="name", title="Name",
304                                    kind=constants.QFT_TEXT),
305       objects.QueryFieldDefinition(name="size", title="Size",
306                                    kind=constants.QFT_UNIT),
307       objects.QueryFieldDefinition(name="mtime", title="ModTime",
308                                    kind=constants.QFT_TIMESTAMP),
309       ]
310
311     response = objects.QueryResponse(fields=fields, data=[
312       [(constants.RS_NORMAL, "a"), (constants.RS_NORMAL, 1024),
313        (constants.RS_NORMAL, 0)],
314       [(constants.RS_NORMAL, "b"), (constants.RS_NORMAL, 144996),
315        (constants.RS_NORMAL, 1291746295)],
316       ])
317
318     self.assertEqual(cli.FormatQueryResult(response, unit="m", header=True),
319       (cli.QR_NORMAL, [
320       "Name   Size ModTime",
321       "a      1024 %s" % utils.FormatTime(0),
322       "b    144996 %s" % utils.FormatTime(1291746295),
323       ]))
324
325   def testOverride(self):
326     fields = [
327       objects.QueryFieldDefinition(name="name", title="Name",
328                                    kind=constants.QFT_TEXT),
329       objects.QueryFieldDefinition(name="cust", title="Custom",
330                                    kind=constants.QFT_OTHER),
331       objects.QueryFieldDefinition(name="xt", title="XTime",
332                                    kind=constants.QFT_TIMESTAMP),
333       ]
334
335     response = objects.QueryResponse(fields=fields, data=[
336       [(constants.RS_NORMAL, "x"), (constants.RS_NORMAL, ["a", "b", "c"]),
337        (constants.RS_NORMAL, 1234)],
338       [(constants.RS_NORMAL, "y"), (constants.RS_NORMAL, range(10)),
339        (constants.RS_NORMAL, 1291746295)],
340       ])
341
342     override = {
343       "cust": (utils.CommaJoin, False),
344       "xt": (hex, True),
345       }
346
347     self.assertEqual(cli.FormatQueryResult(response, unit="h", header=True,
348                                            format_override=override),
349       (cli.QR_NORMAL, [
350       "Name Custom                            XTime",
351       "x    a, b, c                           0x4d2",
352       "y    0, 1, 2, 3, 4, 5, 6, 7, 8, 9 0x4cfe7bf7",
353       ]))
354
355   def testSeparator(self):
356     fields = [
357       objects.QueryFieldDefinition(name="name", title="Name",
358                                    kind=constants.QFT_TEXT),
359       objects.QueryFieldDefinition(name="count", title="Count",
360                                    kind=constants.QFT_NUMBER),
361       objects.QueryFieldDefinition(name="desc", title="Description",
362                                    kind=constants.QFT_TEXT),
363       ]
364
365     response = objects.QueryResponse(fields=fields, data=[
366       [(constants.RS_NORMAL, "instance1.example.com"),
367        (constants.RS_NORMAL, 21125), (constants.RS_NORMAL, "Hello World!")],
368       [(constants.RS_NORMAL, "mail.other.net"),
369        (constants.RS_NORMAL, -9000), (constants.RS_NORMAL, "a,b,c")],
370       ])
371
372     for sep in [":", "|", "#", "|||", "###", "@@@", "@#@"]:
373       for header in [None, "Name%sCount%sDescription" % (sep, sep)]:
374         exp = []
375         if header:
376           exp.append(header)
377         exp.extend([
378           "instance1.example.com%s21125%sHello World!" % (sep, sep),
379           "mail.other.net%s-9000%sa,b,c" % (sep, sep),
380           ])
381
382         self.assertEqual(cli.FormatQueryResult(response, separator=sep,
383                                                header=bool(header)),
384                          (cli.QR_NORMAL, exp))
385
386   def testStatusWithUnknown(self):
387     fields = [
388       objects.QueryFieldDefinition(name="id", title="ID",
389                                    kind=constants.QFT_NUMBER),
390       objects.QueryFieldDefinition(name="unk", title="unk",
391                                    kind=constants.QFT_UNKNOWN),
392       objects.QueryFieldDefinition(name="unavail", title="Unavail",
393                                    kind=constants.QFT_BOOL),
394       objects.QueryFieldDefinition(name="nodata", title="NoData",
395                                    kind=constants.QFT_TEXT),
396       objects.QueryFieldDefinition(name="offline", title="OffLine",
397                                    kind=constants.QFT_TEXT),
398       ]
399
400     response = objects.QueryResponse(fields=fields, data=[
401       [(constants.RS_NORMAL, 1), (constants.RS_UNKNOWN, None),
402        (constants.RS_NORMAL, False), (constants.RS_NORMAL, ""),
403        (constants.RS_OFFLINE, None)],
404       [(constants.RS_NORMAL, 2), (constants.RS_UNKNOWN, None),
405        (constants.RS_NODATA, None), (constants.RS_NORMAL, "x"),
406        (constants.RS_OFFLINE, None)],
407       [(constants.RS_NORMAL, 3), (constants.RS_UNKNOWN, None),
408        (constants.RS_NORMAL, False), (constants.RS_UNAVAIL, None),
409        (constants.RS_OFFLINE, None)],
410       ])
411
412     self.assertEqual(cli.FormatQueryResult(response, header=True,
413                                            separator="|", verbose=True),
414       (cli.QR_UNKNOWN, [
415       "ID|unk|Unavail|NoData|OffLine",
416       "1|(unknown)|N||(offline)",
417       "2|(unknown)|(nodata)|x|(offline)",
418       "3|(unknown)|N|(unavail)|(offline)",
419       ]))
420     self.assertEqual(cli.FormatQueryResult(response, header=True,
421                                            separator="|", verbose=False),
422       (cli.QR_UNKNOWN, [
423       "ID|unk|Unavail|NoData|OffLine",
424       "1|??|N||*",
425       "2|??|?|x|*",
426       "3|??|N|-|*",
427       ]))
428
429   def testNoData(self):
430     fields = [
431       objects.QueryFieldDefinition(name="id", title="ID",
432                                    kind=constants.QFT_NUMBER),
433       objects.QueryFieldDefinition(name="name", title="Name",
434                                    kind=constants.QFT_TEXT),
435       ]
436
437     response = objects.QueryResponse(fields=fields, data=[])
438
439     self.assertEqual(cli.FormatQueryResult(response, header=True),
440                      (cli.QR_NORMAL, ["ID Name"]))
441
442   def testNoDataWithUnknown(self):
443     fields = [
444       objects.QueryFieldDefinition(name="id", title="ID",
445                                    kind=constants.QFT_NUMBER),
446       objects.QueryFieldDefinition(name="unk", title="unk",
447                                    kind=constants.QFT_UNKNOWN),
448       ]
449
450     response = objects.QueryResponse(fields=fields, data=[])
451
452     self.assertEqual(cli.FormatQueryResult(response, header=False),
453                      (cli.QR_UNKNOWN, []))
454
455   def testStatus(self):
456     fields = [
457       objects.QueryFieldDefinition(name="id", title="ID",
458                                    kind=constants.QFT_NUMBER),
459       objects.QueryFieldDefinition(name="unavail", title="Unavail",
460                                    kind=constants.QFT_BOOL),
461       objects.QueryFieldDefinition(name="nodata", title="NoData",
462                                    kind=constants.QFT_TEXT),
463       objects.QueryFieldDefinition(name="offline", title="OffLine",
464                                    kind=constants.QFT_TEXT),
465       ]
466
467     response = objects.QueryResponse(fields=fields, data=[
468       [(constants.RS_NORMAL, 1), (constants.RS_NORMAL, False),
469        (constants.RS_NORMAL, ""), (constants.RS_OFFLINE, None)],
470       [(constants.RS_NORMAL, 2), (constants.RS_NODATA, None),
471        (constants.RS_NORMAL, "x"), (constants.RS_NORMAL, "abc")],
472       [(constants.RS_NORMAL, 3), (constants.RS_NORMAL, False),
473        (constants.RS_UNAVAIL, None), (constants.RS_OFFLINE, None)],
474       ])
475
476     self.assertEqual(cli.FormatQueryResult(response, header=False,
477                                            separator="|", verbose=True),
478       (cli.QR_INCOMPLETE, [
479       "1|N||(offline)",
480       "2|(nodata)|x|abc",
481       "3|N|(unavail)|(offline)",
482       ]))
483     self.assertEqual(cli.FormatQueryResult(response, header=False,
484                                            separator="|", verbose=False),
485       (cli.QR_INCOMPLETE, [
486       "1|N||*",
487       "2|?|x|abc",
488       "3|N|-|*",
489       ]))
490
491   def testInvalidFieldType(self):
492     fields = [
493       objects.QueryFieldDefinition(name="x", title="x",
494                                    kind="#some#other#type"),
495       ]
496
497     response = objects.QueryResponse(fields=fields, data=[])
498
499     self.assertRaises(NotImplementedError, cli.FormatQueryResult, response)
500
501   def testInvalidFieldStatus(self):
502     fields = [
503       objects.QueryFieldDefinition(name="x", title="x",
504                                    kind=constants.QFT_TEXT),
505       ]
506
507     response = objects.QueryResponse(fields=fields, data=[[(-1, None)]])
508     self.assertRaises(NotImplementedError, cli.FormatQueryResult, response)
509
510     response = objects.QueryResponse(fields=fields, data=[[(-1, "x")]])
511     self.assertRaises(AssertionError, cli.FormatQueryResult, response)
512
513   def testEmptyFieldTitle(self):
514     fields = [
515       objects.QueryFieldDefinition(name="x", title="",
516                                    kind=constants.QFT_TEXT),
517       ]
518
519     response = objects.QueryResponse(fields=fields, data=[])
520     self.assertRaises(AssertionError, cli.FormatQueryResult, response)
521
522
523 class _MockJobPollCb(cli.JobPollCbBase, cli.JobPollReportCbBase):
524   def __init__(self, tc, job_id):
525     self.tc = tc
526     self.job_id = job_id
527     self._wfjcr = []
528     self._jobstatus = []
529     self._expect_notchanged = False
530     self._expect_log = []
531
532   def CheckEmpty(self):
533     self.tc.assertFalse(self._wfjcr)
534     self.tc.assertFalse(self._jobstatus)
535     self.tc.assertFalse(self._expect_notchanged)
536     self.tc.assertFalse(self._expect_log)
537
538   def AddWfjcResult(self, *args):
539     self._wfjcr.append(args)
540
541   def AddQueryJobsResult(self, *args):
542     self._jobstatus.append(args)
543
544   def WaitForJobChangeOnce(self, job_id, fields,
545                            prev_job_info, prev_log_serial):
546     self.tc.assertEqual(job_id, self.job_id)
547     self.tc.assertEqualValues(fields, ["status"])
548     self.tc.assertFalse(self._expect_notchanged)
549     self.tc.assertFalse(self._expect_log)
550
551     (exp_prev_job_info, exp_prev_log_serial, result) = self._wfjcr.pop(0)
552     self.tc.assertEqualValues(prev_job_info, exp_prev_job_info)
553     self.tc.assertEqual(prev_log_serial, exp_prev_log_serial)
554
555     if result == constants.JOB_NOTCHANGED:
556       self._expect_notchanged = True
557     elif result:
558       (_, logmsgs) = result
559       if logmsgs:
560         self._expect_log.extend(logmsgs)
561
562     return result
563
564   def QueryJobs(self, job_ids, fields):
565     self.tc.assertEqual(job_ids, [self.job_id])
566     self.tc.assertEqualValues(fields, ["status", "opstatus", "opresult"])
567     self.tc.assertFalse(self._expect_notchanged)
568     self.tc.assertFalse(self._expect_log)
569
570     result = self._jobstatus.pop(0)
571     self.tc.assertEqual(len(fields), len(result))
572     return [result]
573
574   def ReportLogMessage(self, job_id, serial, timestamp, log_type, log_msg):
575     self.tc.assertEqual(job_id, self.job_id)
576     self.tc.assertEqualValues((serial, timestamp, log_type, log_msg),
577                               self._expect_log.pop(0))
578
579   def ReportNotChanged(self, job_id, status):
580     self.tc.assertEqual(job_id, self.job_id)
581     self.tc.assert_(self._expect_notchanged)
582     self._expect_notchanged = False
583
584
585 class TestGenericPollJob(testutils.GanetiTestCase):
586   def testSuccessWithLog(self):
587     job_id = 29609
588     cbs = _MockJobPollCb(self, job_id)
589
590     cbs.AddWfjcResult(None, None, constants.JOB_NOTCHANGED)
591
592     cbs.AddWfjcResult(None, None,
593                       ((constants.JOB_STATUS_QUEUED, ), None))
594
595     cbs.AddWfjcResult((constants.JOB_STATUS_QUEUED, ), None,
596                       constants.JOB_NOTCHANGED)
597
598     cbs.AddWfjcResult((constants.JOB_STATUS_QUEUED, ), None,
599                       ((constants.JOB_STATUS_RUNNING, ),
600                        [(1, utils.SplitTime(1273491611.0),
601                          constants.ELOG_MESSAGE, "Step 1"),
602                         (2, utils.SplitTime(1273491615.9),
603                          constants.ELOG_MESSAGE, "Step 2"),
604                         (3, utils.SplitTime(1273491625.02),
605                          constants.ELOG_MESSAGE, "Step 3"),
606                         (4, utils.SplitTime(1273491635.05),
607                          constants.ELOG_MESSAGE, "Step 4"),
608                         (37, utils.SplitTime(1273491645.0),
609                          constants.ELOG_MESSAGE, "Step 5"),
610                         (203, utils.SplitTime(127349155.0),
611                          constants.ELOG_MESSAGE, "Step 6")]))
612
613     cbs.AddWfjcResult((constants.JOB_STATUS_RUNNING, ), 203,
614                       ((constants.JOB_STATUS_RUNNING, ),
615                        [(300, utils.SplitTime(1273491711.01),
616                          constants.ELOG_MESSAGE, "Step X"),
617                         (302, utils.SplitTime(1273491815.8),
618                          constants.ELOG_MESSAGE, "Step Y"),
619                         (303, utils.SplitTime(1273491925.32),
620                          constants.ELOG_MESSAGE, "Step Z")]))
621
622     cbs.AddWfjcResult((constants.JOB_STATUS_RUNNING, ), 303,
623                       ((constants.JOB_STATUS_SUCCESS, ), None))
624
625     cbs.AddQueryJobsResult(constants.JOB_STATUS_SUCCESS,
626                            [constants.OP_STATUS_SUCCESS,
627                             constants.OP_STATUS_SUCCESS],
628                            ["Hello World", "Foo man bar"])
629
630     self.assertEqual(["Hello World", "Foo man bar"],
631                      cli.GenericPollJob(job_id, cbs, cbs))
632     cbs.CheckEmpty()
633
634   def testJobLost(self):
635     job_id = 13746
636
637     cbs = _MockJobPollCb(self, job_id)
638     cbs.AddWfjcResult(None, None, constants.JOB_NOTCHANGED)
639     cbs.AddWfjcResult(None, None, None)
640     self.assertRaises(errors.JobLost, cli.GenericPollJob, job_id, cbs, cbs)
641     cbs.CheckEmpty()
642
643   def testError(self):
644     job_id = 31088
645
646     cbs = _MockJobPollCb(self, job_id)
647     cbs.AddWfjcResult(None, None, constants.JOB_NOTCHANGED)
648     cbs.AddWfjcResult(None, None, ((constants.JOB_STATUS_ERROR, ), None))
649     cbs.AddQueryJobsResult(constants.JOB_STATUS_ERROR,
650                            [constants.OP_STATUS_SUCCESS,
651                             constants.OP_STATUS_ERROR],
652                            ["Hello World", "Error code 123"])
653     self.assertRaises(errors.OpExecError, cli.GenericPollJob, job_id, cbs, cbs)
654     cbs.CheckEmpty()
655
656   def testError2(self):
657     job_id = 22235
658
659     cbs = _MockJobPollCb(self, job_id)
660     cbs.AddWfjcResult(None, None, ((constants.JOB_STATUS_ERROR, ), None))
661     encexc = errors.EncodeException(errors.LockError("problem"))
662     cbs.AddQueryJobsResult(constants.JOB_STATUS_ERROR,
663                            [constants.OP_STATUS_ERROR], [encexc])
664     self.assertRaises(errors.LockError, cli.GenericPollJob, job_id, cbs, cbs)
665     cbs.CheckEmpty()
666
667   def testWeirdError(self):
668     job_id = 28847
669
670     cbs = _MockJobPollCb(self, job_id)
671     cbs.AddWfjcResult(None, None, ((constants.JOB_STATUS_ERROR, ), None))
672     cbs.AddQueryJobsResult(constants.JOB_STATUS_ERROR,
673                            [constants.OP_STATUS_RUNNING,
674                             constants.OP_STATUS_RUNNING],
675                            [None, None])
676     self.assertRaises(errors.OpExecError, cli.GenericPollJob, job_id, cbs, cbs)
677     cbs.CheckEmpty()
678
679   def testCancel(self):
680     job_id = 4275
681
682     cbs = _MockJobPollCb(self, job_id)
683     cbs.AddWfjcResult(None, None, constants.JOB_NOTCHANGED)
684     cbs.AddWfjcResult(None, None, ((constants.JOB_STATUS_CANCELING, ), None))
685     cbs.AddQueryJobsResult(constants.JOB_STATUS_CANCELING,
686                            [constants.OP_STATUS_CANCELING,
687                             constants.OP_STATUS_CANCELING],
688                            [None, None])
689     self.assertRaises(errors.OpExecError, cli.GenericPollJob, job_id, cbs, cbs)
690     cbs.CheckEmpty()
691
692
693 class TestFormatLogMessage(unittest.TestCase):
694   def test(self):
695     self.assertEqual(cli.FormatLogMessage(constants.ELOG_MESSAGE,
696                                           "Hello World"),
697                      "Hello World")
698     self.assertRaises(TypeError, cli.FormatLogMessage,
699                       constants.ELOG_MESSAGE, [1, 2, 3])
700
701     self.assert_(cli.FormatLogMessage("some other type", (1, 2, 3)))
702
703
704 class TestParseFields(unittest.TestCase):
705   def test(self):
706     self.assertEqual(cli.ParseFields(None, []), [])
707     self.assertEqual(cli.ParseFields("name,foo,hello", []),
708                      ["name", "foo", "hello"])
709     self.assertEqual(cli.ParseFields(None, ["def", "ault", "fields", "here"]),
710                      ["def", "ault", "fields", "here"])
711     self.assertEqual(cli.ParseFields("name,foo", ["def", "ault"]),
712                      ["name", "foo"])
713     self.assertEqual(cli.ParseFields("+name,foo", ["def", "ault"]),
714                      ["def", "ault", "name", "foo"])
715
716
717 class TestConstants(unittest.TestCase):
718   def testPriority(self):
719     self.assertEqual(set(cli._PRIONAME_TO_VALUE.values()),
720                      set(constants.OP_PRIO_SUBMIT_VALID))
721     self.assertEqual(list(value for _, value in cli._PRIORITY_NAMES),
722                      sorted(constants.OP_PRIO_SUBMIT_VALID, reverse=True))
723
724
725 class TestParseNicOption(unittest.TestCase):
726   def test(self):
727     self.assertEqual(cli.ParseNicOption([("0", { "link": "eth0", })]),
728                      [{ "link": "eth0", }])
729     self.assertEqual(cli.ParseNicOption([("5", { "ip": "192.0.2.7", })]),
730                      [{}, {}, {}, {}, {}, { "ip": "192.0.2.7", }])
731
732   def testErrors(self):
733     for i in [None, "", "abc", "zero", "Hello World", "\0", []]:
734       self.assertRaises(errors.OpPrereqError, cli.ParseNicOption,
735                         [(i, { "link": "eth0", })])
736       self.assertRaises(errors.OpPrereqError, cli.ParseNicOption,
737                         [("0", i)])
738
739     self.assertRaises(errors.TypeEnforcementError, cli.ParseNicOption,
740                       [(0, { True: False, })])
741
742     self.assertRaises(errors.TypeEnforcementError, cli.ParseNicOption,
743                       [(3, { "mode": [], })])
744
745
746 class TestFormatResultError(unittest.TestCase):
747   def testNormal(self):
748     for verbose in [False, True]:
749       self.assertRaises(AssertionError, cli.FormatResultError,
750                         constants.RS_NORMAL, verbose)
751
752   def testUnknown(self):
753     for verbose in [False, True]:
754       self.assertRaises(NotImplementedError, cli.FormatResultError,
755                         "#some!other!status#", verbose)
756
757   def test(self):
758     for status in constants.RS_ALL:
759       if status == constants.RS_NORMAL:
760         continue
761
762       self.assertNotEqual(cli.FormatResultError(status, False),
763                           cli.FormatResultError(status, True))
764
765       result = cli.FormatResultError(status, True)
766       self.assertTrue(result.startswith("("))
767       self.assertTrue(result.endswith(")"))
768
769
770 class TestGetOnlineNodes(unittest.TestCase):
771   class _FakeClient:
772     def __init__(self):
773       self._query = []
774
775     def AddQueryResult(self, *args):
776       self._query.append(args)
777
778     def CountPending(self):
779       return len(self._query)
780
781     def Query(self, res, fields, qfilter):
782       if res != constants.QR_NODE:
783         raise Exception("Querying wrong resource")
784
785       (exp_fields, check_filter, result) = self._query.pop(0)
786
787       if exp_fields != fields:
788         raise Exception("Expected fields %s, got %s" % (exp_fields, fields))
789
790       if not (qfilter is None or check_filter(qfilter)):
791         raise Exception("Filter doesn't match expectations")
792
793       return objects.QueryResponse(fields=None, data=result)
794
795   def testEmpty(self):
796     cl = self._FakeClient()
797
798     cl.AddQueryResult(["name", "offline", "sip"], None, [])
799     self.assertEqual(cli.GetOnlineNodes(None, cl=cl), [])
800     self.assertEqual(cl.CountPending(), 0)
801
802   def testNoSpecialFilter(self):
803     cl = self._FakeClient()
804
805     cl.AddQueryResult(["name", "offline", "sip"], None, [
806       [(constants.RS_NORMAL, "master.example.com"),
807        (constants.RS_NORMAL, False),
808        (constants.RS_NORMAL, "192.0.2.1")],
809       [(constants.RS_NORMAL, "node2.example.com"),
810        (constants.RS_NORMAL, False),
811        (constants.RS_NORMAL, "192.0.2.2")],
812       ])
813     self.assertEqual(cli.GetOnlineNodes(None, cl=cl),
814                      ["master.example.com", "node2.example.com"])
815     self.assertEqual(cl.CountPending(), 0)
816
817   def testNoMaster(self):
818     cl = self._FakeClient()
819
820     def _CheckFilter(qfilter):
821       self.assertEqual(qfilter, [qlang.OP_NOT, [qlang.OP_TRUE, "master"]])
822       return True
823
824     cl.AddQueryResult(["name", "offline", "sip"], _CheckFilter, [
825       [(constants.RS_NORMAL, "node2.example.com"),
826        (constants.RS_NORMAL, False),
827        (constants.RS_NORMAL, "192.0.2.2")],
828       ])
829     self.assertEqual(cli.GetOnlineNodes(None, cl=cl, filter_master=True),
830                      ["node2.example.com"])
831     self.assertEqual(cl.CountPending(), 0)
832
833   def testSecondaryIpAddress(self):
834     cl = self._FakeClient()
835
836     cl.AddQueryResult(["name", "offline", "sip"], None, [
837       [(constants.RS_NORMAL, "master.example.com"),
838        (constants.RS_NORMAL, False),
839        (constants.RS_NORMAL, "192.0.2.1")],
840       [(constants.RS_NORMAL, "node2.example.com"),
841        (constants.RS_NORMAL, False),
842        (constants.RS_NORMAL, "192.0.2.2")],
843       ])
844     self.assertEqual(cli.GetOnlineNodes(None, cl=cl, secondary_ips=True),
845                      ["192.0.2.1", "192.0.2.2"])
846     self.assertEqual(cl.CountPending(), 0)
847
848   def testNoMasterFilterNodeName(self):
849     cl = self._FakeClient()
850
851     def _CheckFilter(qfilter):
852       self.assertEqual(qfilter,
853         [qlang.OP_AND,
854          [qlang.OP_OR] + [[qlang.OP_EQUAL, "name", name]
855                           for name in ["node2", "node3"]],
856          [qlang.OP_NOT, [qlang.OP_TRUE, "master"]]])
857       return True
858
859     cl.AddQueryResult(["name", "offline", "sip"], _CheckFilter, [
860       [(constants.RS_NORMAL, "node2.example.com"),
861        (constants.RS_NORMAL, False),
862        (constants.RS_NORMAL, "192.0.2.12")],
863       [(constants.RS_NORMAL, "node3.example.com"),
864        (constants.RS_NORMAL, False),
865        (constants.RS_NORMAL, "192.0.2.13")],
866       ])
867     self.assertEqual(cli.GetOnlineNodes(["node2", "node3"], cl=cl,
868                                         secondary_ips=True, filter_master=True),
869                      ["192.0.2.12", "192.0.2.13"])
870     self.assertEqual(cl.CountPending(), 0)
871
872   def testOfflineNodes(self):
873     cl = self._FakeClient()
874
875     cl.AddQueryResult(["name", "offline", "sip"], None, [
876       [(constants.RS_NORMAL, "master.example.com"),
877        (constants.RS_NORMAL, False),
878        (constants.RS_NORMAL, "192.0.2.1")],
879       [(constants.RS_NORMAL, "node2.example.com"),
880        (constants.RS_NORMAL, True),
881        (constants.RS_NORMAL, "192.0.2.2")],
882       [(constants.RS_NORMAL, "node3.example.com"),
883        (constants.RS_NORMAL, True),
884        (constants.RS_NORMAL, "192.0.2.3")],
885       ])
886     self.assertEqual(cli.GetOnlineNodes(None, cl=cl, nowarn=True),
887                      ["master.example.com"])
888     self.assertEqual(cl.CountPending(), 0)
889
890   def testNodeGroup(self):
891     cl = self._FakeClient()
892
893     def _CheckFilter(qfilter):
894       self.assertEqual(qfilter,
895         [qlang.OP_OR, [qlang.OP_EQUAL, "group", "foobar"],
896                       [qlang.OP_EQUAL, "group.uuid", "foobar"]])
897       return True
898
899     cl.AddQueryResult(["name", "offline", "sip"], _CheckFilter, [
900       [(constants.RS_NORMAL, "master.example.com"),
901        (constants.RS_NORMAL, False),
902        (constants.RS_NORMAL, "192.0.2.1")],
903       [(constants.RS_NORMAL, "node3.example.com"),
904        (constants.RS_NORMAL, False),
905        (constants.RS_NORMAL, "192.0.2.3")],
906       ])
907     self.assertEqual(cli.GetOnlineNodes(None, cl=cl, nodegroup="foobar"),
908                      ["master.example.com", "node3.example.com"])
909     self.assertEqual(cl.CountPending(), 0)
910
911
912 class TestFormatTimestamp(unittest.TestCase):
913   def testGood(self):
914     self.assertEqual(cli.FormatTimestamp((0, 1)),
915                      time.strftime("%F %T", time.localtime(0)) + ".000001")
916     self.assertEqual(cli.FormatTimestamp((1332944009, 17376)),
917                      (time.strftime("%F %T", time.localtime(1332944009)) +
918                       ".017376"))
919
920   def testWrong(self):
921     for i in [0, [], {}, "", [1]]:
922       self.assertEqual(cli.FormatTimestamp(i), "?")
923
924
925 class TestFormatUsage(unittest.TestCase):
926   def test(self):
927     binary = "gnt-unittest"
928     commands = {
929       "cmdA":
930         (NotImplemented, NotImplemented, NotImplemented, NotImplemented,
931          "description of A"),
932       "bbb":
933         (NotImplemented, NotImplemented, NotImplemented, NotImplemented,
934          "Hello World," * 10),
935       "longname":
936         (NotImplemented, NotImplemented, NotImplemented, NotImplemented,
937          "Another description"),
938       }
939
940     self.assertEqual(list(cli._FormatUsage(binary, commands)), [
941       "Usage: gnt-unittest {command} [options...] [argument...]",
942       "gnt-unittest <command> --help to see details, or man gnt-unittest",
943       "",
944       "Commands:",
945       (" bbb      - Hello World,Hello World,Hello World,Hello World,Hello"
946        " World,Hello"),
947       "            World,Hello World,Hello World,Hello World,Hello World,",
948       " cmdA     - description of A",
949       " longname - Another description",
950       "",
951       ])
952
953
954 class TestParseArgs(unittest.TestCase):
955   def testNoArguments(self):
956     for argv in [[], ["gnt-unittest"]]:
957       try:
958         cli._ParseArgs("gnt-unittest", argv, {}, {}, set())
959       except cli._ShowUsage, err:
960         self.assertTrue(err.exit_error)
961       else:
962         self.fail("Did not raise exception")
963
964   def testVersion(self):
965     for argv in [["test", "--version"], ["test", "--version", "somethingelse"]]:
966       try:
967         cli._ParseArgs("test", argv, {}, {}, set())
968       except cli._ShowVersion:
969         pass
970       else:
971         self.fail("Did not raise exception")
972
973   def testHelp(self):
974     for argv in [["test", "--help"], ["test", "--help", "somethingelse"]]:
975       try:
976         cli._ParseArgs("test", argv, {}, {}, set())
977       except cli._ShowUsage, err:
978         self.assertFalse(err.exit_error)
979       else:
980         self.fail("Did not raise exception")
981
982   def testUnknownCommandOrAlias(self):
983     for argv in [["test", "list"], ["test", "somethingelse", "--help"]]:
984       try:
985         cli._ParseArgs("test", argv, {}, {}, set())
986       except cli._ShowUsage, err:
987         self.assertTrue(err.exit_error)
988       else:
989         self.fail("Did not raise exception")
990
991   def testInvalidAliasList(self):
992     cmd = {
993       "list": NotImplemented,
994       "foo": NotImplemented,
995       }
996     aliases = {
997       "list": NotImplemented,
998       "foo": NotImplemented,
999       }
1000     assert sorted(cmd.keys()) == sorted(aliases.keys())
1001     self.assertRaises(AssertionError, cli._ParseArgs, "test",
1002                       ["test", "list"], cmd, aliases, set())
1003
1004   def testAliasForNonExistantCommand(self):
1005     cmd = {}
1006     aliases = {
1007       "list": NotImplemented,
1008       }
1009     self.assertRaises(errors.ProgrammerError, cli._ParseArgs, "test",
1010                       ["test", "list"], cmd, aliases, set())
1011
1012
1013 if __name__ == "__main__":
1014   testutils.GanetiTestProgram()