Merge branch 'devel-2.1' into stable-2.1
[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       "opend": "OpCode_end",
84       "start_ts": "Start",
85       "end_ts": "End",
86       "received_ts": "Received",
87       "lock_status": "LockStatus",
88       }
89   else:
90     headers = None
91
92   # change raw values to nicer strings
93   for row_id, row in enumerate(output):
94     if row is None:
95       ToStderr("No such job: %s" % args[row_id])
96       continue
97
98     for idx, field in enumerate(selected_fields):
99       val = row[idx]
100       if field == "status":
101         if val in _USER_JOB_STATUS:
102           val = _USER_JOB_STATUS[val]
103         else:
104           raise errors.ProgrammerError("Unknown job status code '%s'" % val)
105       elif field == "summary":
106         val = ",".join(val)
107       elif field in ("start_ts", "end_ts", "received_ts"):
108         val = FormatTimestamp(val)
109       elif field in ("opstart", "opend"):
110         val = [FormatTimestamp(entry) for entry in val]
111       elif field == "lock_status" and not val:
112         val = "-"
113
114       row[idx] = str(val)
115
116   data = GenerateTable(separator=opts.separator, headers=headers,
117                        fields=selected_fields, data=output)
118   for line in data:
119     ToStdout(line)
120
121   return 0
122
123
124 def ArchiveJobs(opts, args):
125   """Archive jobs.
126
127   @param opts: the command line options selected by the user
128   @type args: list
129   @param args: should contain the job IDs to be archived
130   @rtype: int
131   @return: the desired exit code
132
133   """
134   client = GetClient()
135
136   for job_id in args:
137     client.ArchiveJob(job_id)
138
139   return 0
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
184   for job_id in args:
185     (_, msg) = client.CancelJob(job_id)
186     ToStdout(msg)
187
188   # TODO: Different exit value if not all jobs were canceled?
189   return 0
190
191
192 def ShowJobs(opts, args):
193   """Show detailed information about jobs.
194
195   @param opts: the command line options selected by the user
196   @type args: list
197   @param args: should contain the job IDs to be queried
198   @rtype: int
199   @return: the desired exit code
200
201   """
202   def format(level, text):
203     """Display the text indented."""
204     ToStdout("%s%s", "  " * level, text)
205
206   def result_helper(value):
207     """Format a result field in a nice way."""
208     if isinstance(value, (tuple, list)):
209       return "[%s]" % utils.CommaJoin(value)
210     else:
211       return str(value)
212
213   selected_fields = [
214     "id", "status", "ops", "opresult", "opstatus", "oplog",
215     "opstart", "opend", "received_ts", "start_ts", "end_ts",
216     ]
217
218   result = GetClient().QueryJobs(args, selected_fields)
219
220   first = True
221
222   for idx, entry in enumerate(result):
223     if not first:
224       format(0, "")
225     else:
226       first = False
227
228     if entry is None:
229       if idx <= len(args):
230         format(0, "Job ID %s not found" % args[idx])
231       else:
232         # this should not happen, when we don't pass args it will be a
233         # valid job returned
234         format(0, "Job ID requested as argument %s not found" % (idx + 1))
235       continue
236
237     (job_id, status, ops, opresult, opstatus, oplog,
238      opstart, opend, recv_ts, start_ts, end_ts) = entry
239     format(0, "Job ID: %s" % job_id)
240     if status in _USER_JOB_STATUS:
241       status = _USER_JOB_STATUS[status]
242     else:
243       raise errors.ProgrammerError("Unknown job status code '%s'" % status)
244
245     format(1, "Status: %s" % status)
246
247     if recv_ts is not None:
248       format(1, "Received:         %s" % FormatTimestamp(recv_ts))
249     else:
250       format(1, "Missing received timestamp (%s)" % str(recv_ts))
251
252     if start_ts is not None:
253       if recv_ts is not None:
254         d1 = start_ts[0] - recv_ts[0] + (start_ts[1] - recv_ts[1]) / 1000000.0
255         delta = " (delta %.6fs)" % d1
256       else:
257         delta = ""
258       format(1, "Processing start: %s%s" % (FormatTimestamp(start_ts), delta))
259     else:
260       format(1, "Processing start: unknown (%s)" % str(start_ts))
261
262     if end_ts is not None:
263       if start_ts is not None:
264         d2 = end_ts[0] - start_ts[0] + (end_ts[1] - start_ts[1]) / 1000000.0
265         delta = " (delta %.6fs)" % d2
266       else:
267         delta = ""
268       format(1, "Processing end:   %s%s" % (FormatTimestamp(end_ts), delta))
269     else:
270       format(1, "Processing end:   unknown (%s)" % str(end_ts))
271
272     if end_ts is not None and recv_ts is not None:
273       d3 = end_ts[0] - recv_ts[0] + (end_ts[1] - recv_ts[1]) / 1000000.0
274       format(1, "Total processing time: %.6f seconds" % d3)
275     else:
276       format(1, "Total processing time: N/A")
277     format(1, "Opcodes:")
278     for (opcode, result, status, log, s_ts, e_ts) in \
279             zip(ops, opresult, opstatus, oplog, opstart, opend):
280       format(2, "%s" % opcode["OP_ID"])
281       format(3, "Status: %s" % status)
282       if isinstance(s_ts, (tuple, list)):
283         format(3, "Processing start: %s" % FormatTimestamp(s_ts))
284       else:
285         format(3, "No processing start time")
286       if isinstance(e_ts, (tuple, list)):
287         format(3, "Processing end:   %s" % FormatTimestamp(e_ts))
288       else:
289         format(3, "No processing end time")
290       format(3, "Input fields:")
291       for key, val in opcode.iteritems():
292         if key == "OP_ID":
293           continue
294         if isinstance(val, (tuple, list)):
295           val = ",".join([str(item) for item in val])
296         format(4, "%s: %s" % (key, val))
297       if result is None:
298         format(3, "No output data")
299       elif isinstance(result, (tuple, list)):
300         if not result:
301           format(3, "Result: empty sequence")
302         else:
303           format(3, "Result:")
304           for elem in result:
305             format(4, result_helper(elem))
306       elif isinstance(result, dict):
307         if not result:
308           format(3, "Result: empty dictionary")
309         else:
310           for key, val in result.iteritems():
311             format(4, "%s: %s" % (key, result_helper(val)))
312       else:
313         format(3, "Result: %s" % result)
314       format(3, "Execution log:")
315       for serial, log_ts, log_type, log_msg in log:
316         time_txt = FormatTimestamp(log_ts)
317         encoded = utils.SafeEncode(log_msg)
318         format(4, "%s:%s:%s %s" % (serial, time_txt, log_type, encoded))
319   return 0
320
321
322 def WatchJob(opts, args):
323   """Follow a job and print its output as it arrives.
324
325   @param opts: the command line options selected by the user
326   @type args: list
327   @param args: Contains the job ID
328   @rtype: int
329   @return: the desired exit code
330
331   """
332   job_id = args[0]
333
334   msg = ("Output from job %s follows" % job_id)
335   ToStdout(msg)
336   ToStdout("-" * len(msg))
337
338   retcode = 0
339   try:
340     cli.PollJob(job_id)
341   except errors.GenericError, err:
342     (retcode, job_result) = cli.FormatError(err)
343     ToStderr("Job %s failed: %s", job_id, job_result)
344
345   return retcode
346
347
348 commands = {
349   'list': (
350     ListJobs, [ArgJobId()],
351     [NOHDR_OPT, SEP_OPT, FIELDS_OPT],
352     "[job_id ...]",
353     "List the jobs and their status. The available fields are"
354     " (see the man page for details): id, status, op_list,"
355     " op_status, op_result."
356     " The default field"
357     " list is (in order): %s." % utils.CommaJoin(_LIST_DEF_FIELDS)),
358   'archive': (
359     ArchiveJobs, [ArgJobId(min=1)], [],
360     "<job-id> [<job-id> ...]", "Archive specified jobs"),
361   'autoarchive': (
362     AutoArchiveJobs,
363     [ArgSuggest(min=1, max=1, choices=["1d", "1w", "4w"])],
364     [],
365     "<age>", "Auto archive jobs older than the given age"),
366   'cancel': (
367     CancelJobs, [ArgJobId(min=1)], [],
368     "<job-id> [<job-id> ...]", "Cancel specified jobs"),
369   'info': (
370     ShowJobs, [ArgJobId(min=1)], [],
371     "<job-id> [<job-id> ...]",
372     "Show detailed information about the specified jobs"),
373   'watch': (
374     WatchJob, [ArgJobId(min=1, max=1)], [],
375     "<job-id>", "Follows a job and prints its output as it arrives"),
376   }
377
378
379 if __name__ == '__main__':
380   sys.exit(GenericMain(commands))