gnt-job info: Sort input fields
[ganeti-local] / scripts / gnt-job
1 #!/usr/bin/python
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-msg=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 import sys
30
31 from ganeti.cli import *
32 from ganeti import constants
33 from ganeti import errors
34 from ganeti import utils
35 from ganeti import cli
36
37
38 #: default list of fields for L{ListJobs}
39 _LIST_DEF_FIELDS = ["id", "status", "summary"]
40
41 #: map converting the job status contants to user-visible
42 #: names
43 _USER_JOB_STATUS = {
44   constants.JOB_STATUS_QUEUED: "queued",
45   constants.JOB_STATUS_WAITLOCK: "waiting",
46   constants.JOB_STATUS_CANCELING: "canceling",
47   constants.JOB_STATUS_RUNNING: "running",
48   constants.JOB_STATUS_CANCELED: "canceled",
49   constants.JOB_STATUS_SUCCESS: "success",
50   constants.JOB_STATUS_ERROR: "error",
51   }
52
53
54 def ListJobs(opts, args):
55   """List the jobs
56
57   @param opts: the command line options selected by the user
58   @type args: list
59   @param args: should be an empty list
60   @rtype: int
61   @return: the desired exit code
62
63   """
64   selected_fields = ParseFields(opts.output, _LIST_DEF_FIELDS)
65
66   output = GetClient().QueryJobs(args, selected_fields)
67   if not opts.no_headers:
68     # TODO: Implement more fields
69     headers = {
70       "id": "ID",
71       "status": "Status",
72       "ops": "OpCodes",
73       "opresult": "OpCode_result",
74       "opstatus": "OpCode_status",
75       "oplog": "OpCode_log",
76       "summary": "Summary",
77       "opstart": "OpCode_start",
78       "opexec": "OpCode_exec",
79       "opend": "OpCode_end",
80       "start_ts": "Start",
81       "end_ts": "End",
82       "received_ts": "Received",
83       }
84   else:
85     headers = None
86
87   # change raw values to nicer strings
88   for row_id, row in enumerate(output):
89     if row is None:
90       ToStderr("No such job: %s" % args[row_id])
91       continue
92
93     for idx, field in enumerate(selected_fields):
94       val = row[idx]
95       if field == "status":
96         if val in _USER_JOB_STATUS:
97           val = _USER_JOB_STATUS[val]
98         else:
99           raise errors.ProgrammerError("Unknown job status code '%s'" % val)
100       elif field == "summary":
101         val = ",".join(val)
102       elif field in ("start_ts", "end_ts", "received_ts"):
103         val = FormatTimestamp(val)
104       elif field in ("opstart", "opexec", "opend"):
105         val = [FormatTimestamp(entry) for entry in val]
106
107       row[idx] = str(val)
108
109   data = GenerateTable(separator=opts.separator, headers=headers,
110                        fields=selected_fields, data=output)
111   for line in data:
112     ToStdout(line)
113
114   return 0
115
116
117 def ArchiveJobs(opts, args):
118   """Archive jobs.
119
120   @param opts: the command line options selected by the user
121   @type args: list
122   @param args: should contain the job IDs to be archived
123   @rtype: int
124   @return: the desired exit code
125
126   """
127   client = GetClient()
128
129   rcode = 0
130   for job_id in args:
131     if not client.ArchiveJob(job_id):
132       ToStderr("Failed to archive job with ID '%s'", job_id)
133       rcode = 1
134
135   return rcode
136
137
138 def AutoArchiveJobs(opts, args):
139   """Archive jobs based on age.
140
141   This will archive jobs based on their age, or all jobs if a 'all' is
142   passed.
143
144   @param opts: the command line options selected by the user
145   @type args: list
146   @param args: should contain only one element, the age as a time spec
147       that can be parsed by L{ganeti.cli.ParseTimespec} or the
148       keyword I{all}, which will cause all jobs to be archived
149   @rtype: int
150   @return: the desired exit code
151
152   """
153   client = GetClient()
154
155   age = args[0]
156
157   if age == 'all':
158     age = -1
159   else:
160     age = ParseTimespec(age)
161
162   (archived_count, jobs_left) = client.AutoArchiveJobs(age)
163   ToStdout("Archived %s jobs, %s unchecked left", archived_count, jobs_left)
164
165   return 0
166
167
168 def CancelJobs(opts, args):
169   """Cancel not-yet-started jobs.
170
171   @param opts: the command line options selected by the user
172   @type args: list
173   @param args: should contain the job IDs to be cancelled
174   @rtype: int
175   @return: the desired exit code
176
177   """
178   client = GetClient()
179
180   for job_id in args:
181     (_, msg) = client.CancelJob(job_id)
182     ToStdout(msg)
183
184   # TODO: Different exit value if not all jobs were canceled?
185   return 0
186
187
188 def ShowJobs(opts, args):
189   """Show detailed information about jobs.
190
191   @param opts: the command line options selected by the user
192   @type args: list
193   @param args: should contain the job IDs to be queried
194   @rtype: int
195   @return: the desired exit code
196
197   """
198   def format_msg(level, text):
199     """Display the text indented."""
200     ToStdout("%s%s", "  " * level, text)
201
202   def result_helper(value):
203     """Format a result field in a nice way."""
204     if isinstance(value, (tuple, list)):
205       return "[%s]" % utils.CommaJoin(value)
206     else:
207       return str(value)
208
209   selected_fields = [
210     "id", "status", "ops", "opresult", "opstatus", "oplog",
211     "opstart", "opexec", "opend", "received_ts", "start_ts", "end_ts",
212     ]
213
214   result = GetClient().QueryJobs(args, selected_fields)
215
216   first = True
217
218   for idx, entry in enumerate(result):
219     if not first:
220       format_msg(0, "")
221     else:
222       first = False
223
224     if entry is None:
225       if idx <= len(args):
226         format_msg(0, "Job ID %s not found" % args[idx])
227       else:
228         # this should not happen, when we don't pass args it will be a
229         # valid job returned
230         format_msg(0, "Job ID requested as argument %s not found" % (idx + 1))
231       continue
232
233     (job_id, status, ops, opresult, opstatus, oplog,
234      opstart, opexec, opend, recv_ts, start_ts, end_ts) = entry
235     format_msg(0, "Job ID: %s" % job_id)
236     if status in _USER_JOB_STATUS:
237       status = _USER_JOB_STATUS[status]
238     else:
239       raise errors.ProgrammerError("Unknown job status code '%s'" % status)
240
241     format_msg(1, "Status: %s" % status)
242
243     if recv_ts is not None:
244       format_msg(1, "Received:         %s" % FormatTimestamp(recv_ts))
245     else:
246       format_msg(1, "Missing received timestamp (%s)" % str(recv_ts))
247
248     if start_ts is not None:
249       if recv_ts is not None:
250         d1 = start_ts[0] - recv_ts[0] + (start_ts[1] - recv_ts[1]) / 1000000.0
251         delta = " (delta %.6fs)" % d1
252       else:
253         delta = ""
254       format_msg(1, "Processing start: %s%s" %
255                  (FormatTimestamp(start_ts), delta))
256     else:
257       format_msg(1, "Processing start: unknown (%s)" % str(start_ts))
258
259     if end_ts is not None:
260       if start_ts is not None:
261         d2 = end_ts[0] - start_ts[0] + (end_ts[1] - start_ts[1]) / 1000000.0
262         delta = " (delta %.6fs)" % d2
263       else:
264         delta = ""
265       format_msg(1, "Processing end:   %s%s" %
266                  (FormatTimestamp(end_ts), delta))
267     else:
268       format_msg(1, "Processing end:   unknown (%s)" % str(end_ts))
269
270     if end_ts is not None and recv_ts is not None:
271       d3 = end_ts[0] - recv_ts[0] + (end_ts[1] - recv_ts[1]) / 1000000.0
272       format_msg(1, "Total processing time: %.6f seconds" % d3)
273     else:
274       format_msg(1, "Total processing time: N/A")
275     format_msg(1, "Opcodes:")
276     for (opcode, result, status, log, s_ts, x_ts, e_ts) in \
277             zip(ops, opresult, opstatus, oplog, opstart, opexec, opend):
278       format_msg(2, "%s" % opcode["OP_ID"])
279       format_msg(3, "Status: %s" % status)
280       if isinstance(s_ts, (tuple, list)):
281         format_msg(3, "Processing start: %s" % FormatTimestamp(s_ts))
282       else:
283         format_msg(3, "No processing start time")
284       if isinstance(x_ts, (tuple, list)):
285         format_msg(3, "Execution start:  %s" % FormatTimestamp(x_ts))
286       else:
287         format_msg(3, "No execution start time")
288       if isinstance(e_ts, (tuple, list)):
289         format_msg(3, "Processing end:   %s" % FormatTimestamp(e_ts))
290       else:
291         format_msg(3, "No processing end time")
292       format_msg(3, "Input fields:")
293       for key in utils.NiceSort(opcode.keys()):
294         if key == "OP_ID":
295           continue
296         val = opcode[key]
297         if isinstance(val, (tuple, list)):
298           val = ",".join([str(item) for item in val])
299         format_msg(4, "%s: %s" % (key, val))
300       if result is None:
301         format_msg(3, "No output data")
302       elif isinstance(result, (tuple, list)):
303         if not result:
304           format_msg(3, "Result: empty sequence")
305         else:
306           format_msg(3, "Result:")
307           for elem in result:
308             format_msg(4, result_helper(elem))
309       elif isinstance(result, dict):
310         if not result:
311           format_msg(3, "Result: empty dictionary")
312         else:
313           for key, val in result.iteritems():
314             format_msg(4, "%s: %s" % (key, result_helper(val)))
315       else:
316         format_msg(3, "Result: %s" % result)
317       format_msg(3, "Execution log:")
318       for serial, log_ts, log_type, log_msg in log:
319         time_txt = FormatTimestamp(log_ts)
320         encoded = FormatLogMessage(log_type, log_msg)
321         format_msg(4, "%s:%s:%s %s" % (serial, time_txt, log_type, encoded))
322   return 0
323
324
325 def WatchJob(opts, args):
326   """Follow a job and print its output as it arrives.
327
328   @param opts: the command line options selected by the user
329   @type args: list
330   @param args: Contains the job ID
331   @rtype: int
332   @return: the desired exit code
333
334   """
335   job_id = args[0]
336
337   msg = ("Output from job %s follows" % job_id)
338   ToStdout(msg)
339   ToStdout("-" * len(msg))
340
341   retcode = 0
342   try:
343     cli.PollJob(job_id)
344   except errors.GenericError, err:
345     (retcode, job_result) = cli.FormatError(err)
346     ToStderr("Job %s failed: %s", job_id, job_result)
347
348   return retcode
349
350
351 commands = {
352   'list': (
353     ListJobs, [ArgJobId()],
354     [NOHDR_OPT, SEP_OPT, FIELDS_OPT],
355     "[job_id ...]",
356     "List the jobs and their status. The available fields are"
357     " (see the man page for details): id, status, op_list,"
358     " op_status, op_result."
359     " The default field"
360     " list is (in order): %s." % utils.CommaJoin(_LIST_DEF_FIELDS)),
361   'archive': (
362     ArchiveJobs, [ArgJobId(min=1)], [],
363     "<job-id> [<job-id> ...]", "Archive specified jobs"),
364   'autoarchive': (
365     AutoArchiveJobs,
366     [ArgSuggest(min=1, max=1, choices=["1d", "1w", "4w", "all"])],
367     [],
368     "<age>", "Auto archive jobs older than the given age"),
369   'cancel': (
370     CancelJobs, [ArgJobId(min=1)], [],
371     "<job-id> [<job-id> ...]", "Cancel specified jobs"),
372   'info': (
373     ShowJobs, [ArgJobId(min=1)], [],
374     "<job-id> [<job-id> ...]",
375     "Show detailed information about the specified jobs"),
376   'watch': (
377     WatchJob, [ArgJobId(min=1, max=1)], [],
378     "<job-id>", "Follows a job and prints its output as it arrives"),
379   }
380
381
382 if __name__ == '__main__':
383   sys.exit(GenericMain(commands))