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