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