gnt-job list: Add options for commonly used filters
[ganeti-local] / lib / client / gnt_job.py
1 #
2 #
3
4 # Copyright (C) 2006, 2007 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 """Job related commands"""
22
23 # pylint: disable=W0401,W0613,W0614,C0103
24 # W0401: Wildcard import ganeti.cli
25 # W0613: Unused argument, since all functions follow the same API
26 # W0614: Unused import %s from wildcard import (since we need cli)
27 # C0103: Invalid name gnt-job
28
29 from ganeti.cli import *
30 from ganeti import constants
31 from ganeti import errors
32 from ganeti import utils
33 from ganeti import cli
34 from ganeti import qlang
35
36
37 #: default list of fields for L{ListJobs}
38 _LIST_DEF_FIELDS = ["id", "status", "summary"]
39
40 #: map converting the job status contants to user-visible
41 #: names
42 _USER_JOB_STATUS = {
43   constants.JOB_STATUS_QUEUED: "queued",
44   constants.JOB_STATUS_WAITING: "waiting",
45   constants.JOB_STATUS_CANCELING: "canceling",
46   constants.JOB_STATUS_RUNNING: "running",
47   constants.JOB_STATUS_CANCELED: "canceled",
48   constants.JOB_STATUS_SUCCESS: "success",
49   constants.JOB_STATUS_ERROR: "error",
50   }
51
52
53 def _FormatStatus(value):
54   """Formats a job status.
55
56   """
57   try:
58     return _USER_JOB_STATUS[value]
59   except KeyError:
60     raise errors.ProgrammerError("Unknown job status code '%s'" % value)
61
62
63 def ListJobs(opts, args):
64   """List the jobs
65
66   @param opts: the command line options selected by the user
67   @type args: list
68   @param args: should be an empty list
69   @rtype: int
70   @return: the desired exit code
71
72   """
73   selected_fields = ParseFields(opts.output, _LIST_DEF_FIELDS)
74
75   fmtoverride = {
76     "status": (_FormatStatus, False),
77     "summary": (lambda value: ",".join(str(item) for item in value), False),
78     }
79   fmtoverride.update(dict.fromkeys(["opstart", "opexec", "opend"],
80     (lambda value: map(FormatTimestamp, value), None)))
81
82   qfilter = qlang.MakeSimpleFilter("status", opts.status_filter)
83
84   return GenericList(constants.QR_JOB, selected_fields, args, None,
85                      opts.separator, not opts.no_headers,
86                      format_override=fmtoverride, verbose=opts.verbose,
87                      force_filter=opts.force_filter, namefield="id",
88                      qfilter=qfilter)
89
90
91 def ListJobFields(opts, args):
92   """List job fields.
93
94   @param opts: the command line options selected by the user
95   @type args: list
96   @param args: fields to list, or empty for all
97   @rtype: int
98   @return: the desired exit code
99
100   """
101   return GenericListFields(constants.QR_JOB, args, opts.separator,
102                            not opts.no_headers)
103
104
105 def ArchiveJobs(opts, args):
106   """Archive jobs.
107
108   @param opts: the command line options selected by the user
109   @type args: list
110   @param args: should contain the job IDs to be archived
111   @rtype: int
112   @return: the desired exit code
113
114   """
115   client = GetClient()
116
117   rcode = 0
118   for job_id in args:
119     if not client.ArchiveJob(job_id):
120       ToStderr("Failed to archive job with ID '%s'", job_id)
121       rcode = 1
122
123   return rcode
124
125
126 def AutoArchiveJobs(opts, args):
127   """Archive jobs based on age.
128
129   This will archive jobs based on their age, or all jobs if a 'all' is
130   passed.
131
132   @param opts: the command line options selected by the user
133   @type args: list
134   @param args: should contain only one element, the age as a time spec
135       that can be parsed by L{ganeti.cli.ParseTimespec} or the
136       keyword I{all}, which will cause all jobs to be archived
137   @rtype: int
138   @return: the desired exit code
139
140   """
141   client = GetClient()
142
143   age = args[0]
144
145   if age == "all":
146     age = -1
147   else:
148     age = ParseTimespec(age)
149
150   (archived_count, jobs_left) = client.AutoArchiveJobs(age)
151   ToStdout("Archived %s jobs, %s unchecked left", archived_count, jobs_left)
152
153   return 0
154
155
156 def CancelJobs(opts, args):
157   """Cancel not-yet-started jobs.
158
159   @param opts: the command line options selected by the user
160   @type args: list
161   @param args: should contain the job IDs to be cancelled
162   @rtype: int
163   @return: the desired exit code
164
165   """
166   client = GetClient()
167   result = constants.EXIT_SUCCESS
168
169   for job_id in args:
170     (success, msg) = client.CancelJob(job_id)
171
172     if not success:
173       result = constants.EXIT_FAILURE
174
175     ToStdout(msg)
176
177   return result
178
179
180 def ShowJobs(opts, args):
181   """Show detailed information about jobs.
182
183   @param opts: the command line options selected by the user
184   @type args: list
185   @param args: should contain the job IDs to be queried
186   @rtype: int
187   @return: the desired exit code
188
189   """
190   def format_msg(level, text):
191     """Display the text indented."""
192     ToStdout("%s%s", "  " * level, text)
193
194   def result_helper(value):
195     """Format a result field in a nice way."""
196     if isinstance(value, (tuple, list)):
197       return "[%s]" % utils.CommaJoin(value)
198     else:
199       return str(value)
200
201   selected_fields = [
202     "id", "status", "ops", "opresult", "opstatus", "oplog",
203     "opstart", "opexec", "opend", "received_ts", "start_ts", "end_ts",
204     ]
205
206   result = GetClient().Query(constants.QR_JOB, selected_fields,
207                              qlang.MakeSimpleFilter("id", args)).data
208
209   first = True
210
211   for entry in result:
212     if not first:
213       format_msg(0, "")
214     else:
215       first = False
216
217     ((_, job_id), (rs_status, status), (_, ops), (_, opresult), (_, opstatus),
218      (_, oplog), (_, opstart), (_, opexec), (_, opend), (_, recv_ts),
219      (_, start_ts), (_, end_ts)) = entry
220
221     # Detect non-normal results
222     if rs_status != constants.RS_NORMAL:
223       format_msg(0, "Job ID %s not found" % job_id)
224       continue
225
226     format_msg(0, "Job ID: %s" % job_id)
227     if status in _USER_JOB_STATUS:
228       status = _USER_JOB_STATUS[status]
229     else:
230       raise errors.ProgrammerError("Unknown job status code '%s'" % status)
231
232     format_msg(1, "Status: %s" % status)
233
234     if recv_ts is not None:
235       format_msg(1, "Received:         %s" % FormatTimestamp(recv_ts))
236     else:
237       format_msg(1, "Missing received timestamp (%s)" % str(recv_ts))
238
239     if start_ts is not None:
240       if recv_ts is not None:
241         d1 = start_ts[0] - recv_ts[0] + (start_ts[1] - recv_ts[1]) / 1000000.0
242         delta = " (delta %.6fs)" % d1
243       else:
244         delta = ""
245       format_msg(1, "Processing start: %s%s" %
246                  (FormatTimestamp(start_ts), delta))
247     else:
248       format_msg(1, "Processing start: unknown (%s)" % str(start_ts))
249
250     if end_ts is not None:
251       if start_ts is not None:
252         d2 = end_ts[0] - start_ts[0] + (end_ts[1] - start_ts[1]) / 1000000.0
253         delta = " (delta %.6fs)" % d2
254       else:
255         delta = ""
256       format_msg(1, "Processing end:   %s%s" %
257                  (FormatTimestamp(end_ts), delta))
258     else:
259       format_msg(1, "Processing end:   unknown (%s)" % str(end_ts))
260
261     if end_ts is not None and recv_ts is not None:
262       d3 = end_ts[0] - recv_ts[0] + (end_ts[1] - recv_ts[1]) / 1000000.0
263       format_msg(1, "Total processing time: %.6f seconds" % d3)
264     else:
265       format_msg(1, "Total processing time: N/A")
266     format_msg(1, "Opcodes:")
267     for (opcode, result, status, log, s_ts, x_ts, e_ts) in \
268             zip(ops, opresult, opstatus, oplog, opstart, opexec, opend):
269       format_msg(2, "%s" % opcode["OP_ID"])
270       format_msg(3, "Status: %s" % status)
271       if isinstance(s_ts, (tuple, list)):
272         format_msg(3, "Processing start: %s" % FormatTimestamp(s_ts))
273       else:
274         format_msg(3, "No processing start time")
275       if isinstance(x_ts, (tuple, list)):
276         format_msg(3, "Execution start:  %s" % FormatTimestamp(x_ts))
277       else:
278         format_msg(3, "No execution start time")
279       if isinstance(e_ts, (tuple, list)):
280         format_msg(3, "Processing end:   %s" % FormatTimestamp(e_ts))
281       else:
282         format_msg(3, "No processing end time")
283       format_msg(3, "Input fields:")
284       for key in utils.NiceSort(opcode.keys()):
285         if key == "OP_ID":
286           continue
287         val = opcode[key]
288         if isinstance(val, (tuple, list)):
289           val = ",".join([str(item) for item in val])
290         format_msg(4, "%s: %s" % (key, val))
291       if result is None:
292         format_msg(3, "No output data")
293       elif isinstance(result, (tuple, list)):
294         if not result:
295           format_msg(3, "Result: empty sequence")
296         else:
297           format_msg(3, "Result:")
298           for elem in result:
299             format_msg(4, result_helper(elem))
300       elif isinstance(result, dict):
301         if not result:
302           format_msg(3, "Result: empty dictionary")
303         else:
304           format_msg(3, "Result:")
305           for key, val in result.iteritems():
306             format_msg(4, "%s: %s" % (key, result_helper(val)))
307       else:
308         format_msg(3, "Result: %s" % result)
309       format_msg(3, "Execution log:")
310       for serial, log_ts, log_type, log_msg in log:
311         time_txt = FormatTimestamp(log_ts)
312         encoded = FormatLogMessage(log_type, log_msg)
313         format_msg(4, "%s:%s:%s %s" % (serial, time_txt, log_type, encoded))
314   return 0
315
316
317 def WatchJob(opts, args):
318   """Follow a job and print its output as it arrives.
319
320   @param opts: the command line options selected by the user
321   @type args: list
322   @param args: Contains the job ID
323   @rtype: int
324   @return: the desired exit code
325
326   """
327   job_id = args[0]
328
329   msg = ("Output from job %s follows" % job_id)
330   ToStdout(msg)
331   ToStdout("-" * len(msg))
332
333   retcode = 0
334   try:
335     cli.PollJob(job_id)
336   except errors.GenericError, err:
337     (retcode, job_result) = cli.FormatError(err)
338     ToStderr("Job %s failed: %s", job_id, job_result)
339
340   return retcode
341
342
343 _PENDING_OPT = \
344   cli_option("--pending", default=None,
345              action="store_const", dest="status_filter",
346              const=frozenset([
347                constants.JOB_STATUS_QUEUED,
348                constants.JOB_STATUS_WAITING,
349                ]),
350              help="Show only jobs pending execution")
351
352 _RUNNING_OPT = \
353   cli_option("--running", default=None,
354              action="store_const", dest="status_filter",
355              const=frozenset([
356                constants.JOB_STATUS_RUNNING,
357                ]),
358              help="Show jobs currently running only")
359
360 _ERROR_OPT = \
361   cli_option("--error", default=None,
362              action="store_const", dest="status_filter",
363              const=frozenset([
364                constants.JOB_STATUS_ERROR,
365                ]),
366              help="Show failed jobs only")
367
368 _FINISHED_OPT = \
369   cli_option("--finished", default=None,
370              action="store_const", dest="status_filter",
371              const=constants.JOBS_FINALIZED,
372              help="Show finished jobs only")
373
374
375 commands = {
376   "list": (
377     ListJobs, [ArgJobId()],
378     [NOHDR_OPT, SEP_OPT, FIELDS_OPT, VERBOSE_OPT, FORCE_FILTER_OPT,
379      _PENDING_OPT, _RUNNING_OPT, _ERROR_OPT, _FINISHED_OPT],
380     "[job_id ...]",
381     "Lists the jobs and their status. The available fields can be shown"
382     " using the \"list-fields\" command (see the man page for details)."
383     " The default field list is (in order): %s." %
384     utils.CommaJoin(_LIST_DEF_FIELDS)),
385   "list-fields": (
386     ListJobFields, [ArgUnknown()],
387     [NOHDR_OPT, SEP_OPT],
388     "[fields...]",
389     "Lists all available fields for jobs"),
390   "archive": (
391     ArchiveJobs, [ArgJobId(min=1)], [],
392     "<job-id> [<job-id> ...]", "Archive specified jobs"),
393   "autoarchive": (
394     AutoArchiveJobs,
395     [ArgSuggest(min=1, max=1, choices=["1d", "1w", "4w", "all"])],
396     [],
397     "<age>", "Auto archive jobs older than the given age"),
398   "cancel": (
399     CancelJobs, [ArgJobId(min=1)], [],
400     "<job-id> [<job-id> ...]", "Cancel specified jobs"),
401   "info": (
402     ShowJobs, [ArgJobId(min=1)], [],
403     "<job-id> [<job-id> ...]",
404     "Show detailed information about the specified jobs"),
405   "watch": (
406     WatchJob, [ArgJobId(min=1, max=1)], [],
407     "<job-id>", "Follows a job and prints its output as it arrives"),
408   }
409
410
411 #: dictionary with aliases for commands
412 aliases = {
413   "show": "info",
414   }
415
416
417 def Main():
418   return GenericMain(commands, aliases=aliases)