Fix a few job archival issues
[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   if opts.output is None:
65     selected_fields = _LIST_DEF_FIELDS
66   elif opts.output.startswith("+"):
67     selected_fields = _LIST_DEF_FIELDS + opts.output[1:].split(",")
68   else:
69     selected_fields = opts.output.split(",")
70
71   output = GetClient().QueryJobs(args, selected_fields)
72   if not opts.no_headers:
73     # TODO: Implement more fields
74     headers = {
75       "id": "ID",
76       "status": "Status",
77       "ops": "OpCodes",
78       "opresult": "OpCode_result",
79       "opstatus": "OpCode_status",
80       "oplog": "OpCode_log",
81       "summary": "Summary",
82       "opstart": "OpCode_start",
83       "opexec": "OpCode_exec",
84       "opend": "OpCode_end",
85       "start_ts": "Start",
86       "end_ts": "End",
87       "received_ts": "Received",
88       "lock_status": "LockStatus",
89       }
90   else:
91     headers = None
92
93   # change raw values to nicer strings
94   for row_id, row in enumerate(output):
95     if row is None:
96       ToStderr("No such job: %s" % args[row_id])
97       continue
98
99     for idx, field in enumerate(selected_fields):
100       val = row[idx]
101       if field == "status":
102         if val in _USER_JOB_STATUS:
103           val = _USER_JOB_STATUS[val]
104         else:
105           raise errors.ProgrammerError("Unknown job status code '%s'" % val)
106       elif field == "summary":
107         val = ",".join(val)
108       elif field in ("start_ts", "end_ts", "received_ts"):
109         val = FormatTimestamp(val)
110       elif field in ("opstart", "opexec", "opend"):
111         val = [FormatTimestamp(entry) for entry in val]
112       elif field == "lock_status" and not val:
113         val = "-"
114
115       row[idx] = str(val)
116
117   data = GenerateTable(separator=opts.separator, headers=headers,
118                        fields=selected_fields, data=output)
119   for line in data:
120     ToStdout(line)
121
122   return 0
123
124
125 def ArchiveJobs(opts, args):
126   """Archive jobs.
127
128   @param opts: the command line options selected by the user
129   @type args: list
130   @param args: should contain the job IDs to be archived
131   @rtype: int
132   @return: the desired exit code
133
134   """
135   client = GetClient()
136
137   rcode = 0
138   for job_id in args:
139     if not client.ArchiveJob(job_id):
140       ToStderr("Failed to archive job with ID '%s'", job_id)
141       rcode = 1
142
143   return rcode
144
145
146 def AutoArchiveJobs(opts, args):
147   """Archive jobs based on age.
148
149   This will archive jobs based on their age, or all jobs if a 'all' is
150   passed.
151
152   @param opts: the command line options selected by the user
153   @type args: list
154   @param args: should contain only one element, the age as a time spec
155       that can be parsed by L{ganeti.cli.ParseTimespec} or the
156       keyword I{all}, which will cause all jobs to be archived
157   @rtype: int
158   @return: the desired exit code
159
160   """
161   client = GetClient()
162
163   age = args[0]
164
165   if age == 'all':
166     age = -1
167   else:
168     age = ParseTimespec(age)
169
170   (archived_count, jobs_left) = client.AutoArchiveJobs(age)
171   ToStdout("Archived %s jobs, %s unchecked left", archived_count, jobs_left)
172
173   return 0
174
175
176 def CancelJobs(opts, args):
177   """Cancel not-yet-started jobs.
178
179   @param opts: the command line options selected by the user
180   @type args: list
181   @param args: should contain the job IDs to be cancelled
182   @rtype: int
183   @return: the desired exit code
184
185   """
186   client = GetClient()
187
188   for job_id in args:
189     (_, msg) = client.CancelJob(job_id)
190     ToStdout(msg)
191
192   # TODO: Different exit value if not all jobs were canceled?
193   return 0
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   result = GetClient().QueryJobs(args, selected_fields)
223
224   first = True
225
226   for idx, entry in enumerate(result):
227     if not first:
228       format_msg(0, "")
229     else:
230       first = False
231
232     if entry is None:
233       if idx <= len(args):
234         format_msg(0, "Job ID %s not found" % args[idx])
235       else:
236         # this should not happen, when we don't pass args it will be a
237         # valid job returned
238         format_msg(0, "Job ID requested as argument %s not found" % (idx + 1))
239       continue
240
241     (job_id, status, ops, opresult, opstatus, oplog,
242      opstart, opexec, opend, recv_ts, start_ts, end_ts) = entry
243     format_msg(0, "Job ID: %s" % job_id)
244     if status in _USER_JOB_STATUS:
245       status = _USER_JOB_STATUS[status]
246     else:
247       raise errors.ProgrammerError("Unknown job status code '%s'" % status)
248
249     format_msg(1, "Status: %s" % status)
250
251     if recv_ts is not None:
252       format_msg(1, "Received:         %s" % FormatTimestamp(recv_ts))
253     else:
254       format_msg(1, "Missing received timestamp (%s)" % str(recv_ts))
255
256     if start_ts is not None:
257       if recv_ts is not None:
258         d1 = start_ts[0] - recv_ts[0] + (start_ts[1] - recv_ts[1]) / 1000000.0
259         delta = " (delta %.6fs)" % d1
260       else:
261         delta = ""
262       format_msg(1, "Processing start: %s%s" %
263                  (FormatTimestamp(start_ts), delta))
264     else:
265       format_msg(1, "Processing start: unknown (%s)" % str(start_ts))
266
267     if end_ts is not None:
268       if start_ts is not None:
269         d2 = end_ts[0] - start_ts[0] + (end_ts[1] - start_ts[1]) / 1000000.0
270         delta = " (delta %.6fs)" % d2
271       else:
272         delta = ""
273       format_msg(1, "Processing end:   %s%s" %
274                  (FormatTimestamp(end_ts), delta))
275     else:
276       format_msg(1, "Processing end:   unknown (%s)" % str(end_ts))
277
278     if end_ts is not None and recv_ts is not None:
279       d3 = end_ts[0] - recv_ts[0] + (end_ts[1] - recv_ts[1]) / 1000000.0
280       format_msg(1, "Total processing time: %.6f seconds" % d3)
281     else:
282       format_msg(1, "Total processing time: N/A")
283     format_msg(1, "Opcodes:")
284     for (opcode, result, status, log, s_ts, x_ts, e_ts) in \
285             zip(ops, opresult, opstatus, oplog, opstart, opexec, opend):
286       format_msg(2, "%s" % opcode["OP_ID"])
287       format_msg(3, "Status: %s" % status)
288       if isinstance(s_ts, (tuple, list)):
289         format_msg(3, "Processing start: %s" % FormatTimestamp(s_ts))
290       else:
291         format_msg(3, "No processing start time")
292       if isinstance(x_ts, (tuple, list)):
293         format_msg(3, "Execution start:  %s" % FormatTimestamp(x_ts))
294       else:
295         format_msg(3, "No execution start time")
296       if isinstance(e_ts, (tuple, list)):
297         format_msg(3, "Processing end:   %s" % FormatTimestamp(e_ts))
298       else:
299         format_msg(3, "No processing end time")
300       format_msg(3, "Input fields:")
301       for key, val in opcode.iteritems():
302         if key == "OP_ID":
303           continue
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           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 commands = {
359   'list': (
360     ListJobs, [ArgJobId()],
361     [NOHDR_OPT, SEP_OPT, FIELDS_OPT],
362     "[job_id ...]",
363     "List the jobs and their status. The available fields are"
364     " (see the man page for details): id, status, op_list,"
365     " op_status, op_result."
366     " The default field"
367     " list is (in order): %s." % utils.CommaJoin(_LIST_DEF_FIELDS)),
368   'archive': (
369     ArchiveJobs, [ArgJobId(min=1)], [],
370     "<job-id> [<job-id> ...]", "Archive specified jobs"),
371   'autoarchive': (
372     AutoArchiveJobs,
373     [ArgSuggest(min=1, max=1, choices=["1d", "1w", "4w", "all"])],
374     [],
375     "<age>", "Auto archive jobs older than the given age"),
376   'cancel': (
377     CancelJobs, [ArgJobId(min=1)], [],
378     "<job-id> [<job-id> ...]", "Cancel specified jobs"),
379   'info': (
380     ShowJobs, [ArgJobId(min=1)], [],
381     "<job-id> [<job-id> ...]",
382     "Show detailed information about the specified jobs"),
383   'watch': (
384     WatchJob, [ArgJobId(min=1, max=1)], [],
385     "<job-id>", "Follows a job and prints its output as it arrives"),
386   }
387
388
389 if __name__ == '__main__':
390   sys.exit(GenericMain(commands))