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