diff --git a/lib/jstore.py b/lib/jstore.py index e735c7145ec2fcaea754b47c75fd09031c8243c2..1249e96a95a9dd94c2e541ac81b02639a999b6c0 100644 --- a/lib/jstore.py +++ b/lib/jstore.py @@ -203,4 +203,14 @@ def GetArchiveDirectory(job_id): @return: Directory name """ - return str(int(job_id) / JOBS_PER_ARCHIVE_DIRECTORY) + return str(ParseJobId(job_id) / JOBS_PER_ARCHIVE_DIRECTORY) + + +def ParseJobId(job_id): + """Parses a job ID and converts it to integer. + + """ + try: + return int(job_id) + except (ValueError, TypeError): + raise errors.ParameterError("Invalid job ID '%s'" % job_id) diff --git a/lib/query.py b/lib/query.py index a8f19f0a5629cb429c0b5b4705c8fad8037af50c..cd8fdbe4b48781f3e9b964332b332eadd3ea5b99 100644 --- a/lib/query.py +++ b/lib/query.py @@ -64,6 +64,7 @@ from ganeti import objects from ganeti import ht from ganeti import runtime from ganeti import qlang +from ganeti import jstore from ganeti.constants import (QFT_UNKNOWN, QFT_TEXT, QFT_BOOL, QFT_NUMBER, QFT_UNIT, QFT_TIMESTAMP, QFT_OTHER, @@ -103,8 +104,10 @@ from ganeti.constants import (QFT_UNKNOWN, QFT_TEXT, QFT_BOOL, QFT_NUMBER, # Query field flags QFF_HOSTNAME = 0x01 QFF_IP_ADDRESS = 0x02 -# Next values: 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x100, 0x200 -QFF_ALL = (QFF_HOSTNAME | QFF_IP_ADDRESS) +QFF_JOB_ID = 0x04 +QFF_SPLIT_TIMESTAMP = 0x08 +# Next values: 0x10, 0x20, 0x40, 0x80, 0x100, 0x200 +QFF_ALL = (QFF_HOSTNAME | QFF_IP_ADDRESS | QFF_JOB_ID | QFF_SPLIT_TIMESTAMP) FIELD_NAME_RE = re.compile(r"^[a-z0-9/._]+$") TITLE_RE = re.compile(r"^[^\s]+$") @@ -345,6 +348,36 @@ def _PrepareRegex(pattern): raise errors.ParameterError("Invalid regex pattern (%s)" % err) +def _PrepareSplitTimestamp(value): + """Prepares a value for comparison by L{_MakeSplitTimestampComparison}. + + """ + if ht.TNumber(value): + return value + else: + return utils.MergeTime(value) + + +def _MakeSplitTimestampComparison(fn): + """Compares split timestamp values after converting to float. + + """ + return lambda lhs, rhs: fn(utils.MergeTime(lhs), rhs) + + +def _MakeComparisonChecks(fn): + """Prepares flag-specific comparisons using a comparison function. + + """ + return [ + (QFF_SPLIT_TIMESTAMP, _MakeSplitTimestampComparison(fn), + _PrepareSplitTimestamp), + (QFF_JOB_ID, lambda lhs, rhs: fn(jstore.ParseJobId(lhs), rhs), + jstore.ParseJobId), + (None, fn, None), + ] + + class _FilterCompilerHelper: """Converts a query filter to a callable usable for filtering. @@ -363,7 +396,7 @@ class _FilterCompilerHelper: List of tuples containing flags and a callable receiving the left- and right-hand side of the operator. The flags are an OR-ed value of C{QFF_*} - (e.g. L{QFF_HOSTNAME}). + (e.g. L{QFF_HOSTNAME} or L{QFF_SPLIT_TIMESTAMP}). Order matters. The first item with flags will be used. Flags are checked using binary AND. @@ -374,6 +407,8 @@ class _FilterCompilerHelper: lambda lhs, rhs: utils.MatchNameComponent(rhs, [lhs], case_sensitive=False), None), + (QFF_SPLIT_TIMESTAMP, _MakeSplitTimestampComparison(operator.eq), + _PrepareSplitTimestamp), (None, operator.eq, None), ] @@ -403,18 +438,10 @@ class _FilterCompilerHelper: qlang.OP_NOT_EQUAL: (_OPTYPE_BINARY, [(flags, compat.partial(_WrapNot, fn), valprepfn) for (flags, fn, valprepfn) in _EQUALITY_CHECKS]), - qlang.OP_LT: (_OPTYPE_BINARY, [ - (None, operator.lt, None), - ]), - qlang.OP_GT: (_OPTYPE_BINARY, [ - (None, operator.gt, None), - ]), - qlang.OP_LE: (_OPTYPE_BINARY, [ - (None, operator.le, None), - ]), - qlang.OP_GE: (_OPTYPE_BINARY, [ - (None, operator.ge, None), - ]), + qlang.OP_LT: (_OPTYPE_BINARY, _MakeComparisonChecks(operator.lt)), + qlang.OP_LE: (_OPTYPE_BINARY, _MakeComparisonChecks(operator.le)), + qlang.OP_GT: (_OPTYPE_BINARY, _MakeComparisonChecks(operator.gt)), + qlang.OP_GE: (_OPTYPE_BINARY, _MakeComparisonChecks(operator.ge)), qlang.OP_REGEXP: (_OPTYPE_BINARY, [ (None, lambda lhs, rhs: rhs.search(lhs), _PrepareRegex), ]), @@ -2239,7 +2266,7 @@ def _BuildJobFields(): """ fields = [ (_MakeField("id", "ID", QFT_TEXT, "Job ID"), - None, 0, lambda _, (job_id, job): job_id), + None, QFF_JOB_ID, lambda _, (job_id, job): job_id), (_MakeField("status", "Status", QFT_TEXT, "Job status"), None, 0, _JobUnavail(lambda job: job.CalcStatus())), (_MakeField("priority", "Priority", QFT_NUMBER, @@ -2270,20 +2297,25 @@ def _BuildJobFields(): (_MakeField("oppriority", "OpCode_prio", QFT_OTHER, "List of opcode priorities"), None, 0, _PerJobOp(operator.attrgetter("priority"))), - (_MakeField("received_ts", "Received", QFT_OTHER, - "Timestamp of when job was received"), - None, 0, _JobTimestamp(operator.attrgetter("received_timestamp"))), - (_MakeField("start_ts", "Start", QFT_OTHER, - "Timestamp of job start"), - None, 0, _JobTimestamp(operator.attrgetter("start_timestamp"))), - (_MakeField("end_ts", "End", QFT_OTHER, - "Timestamp of job end"), - None, 0, _JobTimestamp(operator.attrgetter("end_timestamp"))), (_MakeField("summary", "Summary", QFT_OTHER, "List of per-opcode summaries"), None, 0, _PerJobOp(lambda op: op.input.Summary())), ] + # Timestamp fields + for (name, attr, title, desc) in [ + ("received_ts", "received_timestamp", "Received", + "Timestamp of when job was received"), + ("start_ts", "start_timestamp", "Start", "Timestamp of job start"), + ("end_ts", "end_timestamp", "End", "Timestamp of job end"), + ]: + getter = operator.attrgetter(attr) + fields.extend([ + (_MakeField(name, title, QFT_OTHER, + "%s (tuple containing seconds and microseconds)" % desc), + None, QFF_SPLIT_TIMESTAMP, _JobTimestamp(getter)), + ]) + return _PrepareFieldList(fields, []) diff --git a/test/ganeti.query_unittest.py b/test/ganeti.query_unittest.py index ca83d1cc9c99ddecb45553730af2af5e98b8a64f..7efcfefbb0972c85fa8b24b7d7de620558a0781a 100755 --- a/test/ganeti.query_unittest.py +++ b/test/ganeti.query_unittest.py @@ -1777,6 +1777,90 @@ class TestQueryFilter(unittest.TestCase): self.assertEqual(q.Query(data), [[(constants.RS_NORMAL, i)] for i in range(50, 100)]) + def testFilterLessGreaterJobId(self): + fielddefs = query._PrepareFieldList([ + (query._MakeField("id", "ID", constants.QFT_TEXT, "Job ID"), + None, query.QFF_JOB_ID, lambda ctx, item: item), + ], []) + + data = ["1", "2", "3", "10", "102", "120", "125", "15", "100", "7"] + + assert data != utils.NiceSort(data), "Test data should not be sorted" + + q = query.Query(fielddefs, ["id"], qfilter=["<", "id", "20"]) + self.assertTrue(q.RequestedNames() is None) + self.assertEqual(q.Query(data), [ + [(constants.RS_NORMAL, "1")], + [(constants.RS_NORMAL, "2")], + [(constants.RS_NORMAL, "3")], + [(constants.RS_NORMAL, "10")], + [(constants.RS_NORMAL, "15")], + [(constants.RS_NORMAL, "7")], + ]) + + q = query.Query(fielddefs, ["id"], qfilter=[">=", "id", "100"]) + self.assertTrue(q.RequestedNames() is None) + self.assertEqual(q.Query(data), [ + [(constants.RS_NORMAL, "102")], + [(constants.RS_NORMAL, "120")], + [(constants.RS_NORMAL, "125")], + [(constants.RS_NORMAL, "100")], + ]) + + # Integers are no valid job IDs + self.assertRaises(errors.ParameterError, query.Query, + fielddefs, ["id"], qfilter=[">=", "id", 10]) + + def testFilterLessGreaterSplitTimestamp(self): + fielddefs = query._PrepareFieldList([ + (query._MakeField("ts", "Timestamp", constants.QFT_OTHER, "Timestamp"), + None, query.QFF_SPLIT_TIMESTAMP, lambda ctx, item: item), + ], []) + + data = [ + utils.SplitTime(0), + utils.SplitTime(0.1), + utils.SplitTime(18224.7872), + utils.SplitTime(919896.12623), + utils.SplitTime(999), + utils.SplitTime(989.9999), + ] + + for i in [0, [0, 0]]: + q = query.Query(fielddefs, ["ts"], qfilter=["<", "ts", i]) + self.assertTrue(q.RequestedNames() is None) + self.assertEqual(q.Query(data), []) + + q = query.Query(fielddefs, ["ts"], qfilter=["<", "ts", 1000]) + self.assertTrue(q.RequestedNames() is None) + self.assertEqual(q.Query(data), [ + [(constants.RS_NORMAL, (0, 0))], + [(constants.RS_NORMAL, (0, 100000))], + [(constants.RS_NORMAL, (999, 0))], + [(constants.RS_NORMAL, (989, 999900))], + ]) + + q = query.Query(fielddefs, ["ts"], qfilter=[">=", "ts", 5000.3]) + self.assertTrue(q.RequestedNames() is None) + self.assertEqual(q.Query(data), [ + [(constants.RS_NORMAL, (18224, 787200))], + [(constants.RS_NORMAL, (919896, 126230))], + ]) + + for i in [18224.7772, utils.SplitTime(18224.7772)]: + q = query.Query(fielddefs, ["ts"], qfilter=[">=", "ts", i]) + self.assertTrue(q.RequestedNames() is None) + self.assertEqual(q.Query(data), [ + [(constants.RS_NORMAL, (18224, 787200))], + [(constants.RS_NORMAL, (919896, 126230))], + ]) + + q = query.Query(fielddefs, ["ts"], qfilter=[">", "ts", 18224.7880]) + self.assertTrue(q.RequestedNames() is None) + self.assertEqual(q.Query(data), [ + [(constants.RS_NORMAL, (919896, 126230))], + ]) + if __name__ == "__main__": testutils.GanetiTestProgram()