Add gnt-cluster commands to toggle the master IP
[ganeti-local] / lib / client / gnt_job.py
1 #
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=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
35
36 #: default list of fields for L{ListJobs}
37 _LIST_DEF_FIELDS = ["id", "status", "summary"]
38
39 #: map converting the job status contants to user-visible
40 #: names
41 _USER_JOB_STATUS = {
42   constants.JOB_STATUS_QUEUED: "queued",
43   constants.JOB_STATUS_WAITING: "waiting",
44   constants.JOB_STATUS_CANCELING: "canceling",
45   constants.JOB_STATUS_RUNNING: "running",
46   constants.JOB_STATUS_CANCELED: "canceled",
47   constants.JOB_STATUS_SUCCESS: "success",
48   constants.JOB_STATUS_ERROR: "error",
49   }
50
51
52 def ListJobs(opts, args):
53   """List the jobs
54
55   @param opts: the command line options selected by the user
56   @type args: list
57   @param args: should be an empty list
58   @rtype: int
59   @return: the desired exit code
60
61   """
62   selected_fields = ParseFields(opts.output, _LIST_DEF_FIELDS)
63
64   output = GetClient().QueryJobs(args, selected_fields)
65   if not opts.no_headers:
66     # TODO: Implement more fields
67     headers = {
68       "id": "ID",
69       "status": "Status",
70       "priority": "Prio",
71       "ops": "OpCodes",
72       "opresult": "OpCode_result",
73       "opstatus": "OpCode_status",
74       "oplog": "OpCode_log",
75       "summary": "Summary",
76       "opstart": "OpCode_start",
77       "opexec": "OpCode_exec",
78       "opend": "OpCode_end",
79       "oppriority": "OpCode_prio",
80       "start_ts": "Start",
81       "end_ts": "End",
82       "received_ts": "Received",
83       }
84   else:
85     headers = None
86
87   numfields = ["priority"]
88
89   # change raw values to nicer strings
90   for row_id, row in enumerate(output):
91     if row is None:
92       ToStderr("No such job: %s" % args[row_id])
93       continue
94
95     for idx, field in enumerate(selected_fields):
96       val = row[idx]
97       if field == "status":
98         if val in _USER_JOB_STATUS:
99           val = _USER_JOB_STATUS[val]
100         else:
101           raise errors.ProgrammerError("Unknown job status code '%s'" % val)
102       elif field == "summary":
103         val = ",".join(val)
104       elif field in ("start_ts", "end_ts", "received_ts"):
105         val = FormatTimestamp(val)
106       elif field in ("opstart", "opexec", "opend"):
107         val = [FormatTimestamp(entry) for entry in val]
108
109       row[idx] = str(val)
110
111   data = GenerateTable(separator=opts.separator, headers=headers,
112                        fields=selected_fields, data=output,
113                        numfields=numfields)
114   for line in data:
115     ToStdout(line)
116
117   return 0
118
119
120 def ArchiveJobs(opts, args):
121   """Archive jobs.
122
123   @param opts: the command line options selected by the user
124   @type args: list
125   @param args: should contain the job IDs to be archived
126   @rtype: int
127   @return: the desired exit code
128
129   """
130   client = GetClient()
131
132   rcode = 0
133   for job_id in args:
134     if not client.ArchiveJob(job_id):
135       ToStderr("Failed to archive job with ID '%s'", job_id)
136       rcode = 1
137
138   return rcode
139
140
141 def AutoArchiveJobs(opts, args):
142   """Archive jobs based on age.
143
144   This will archive jobs based on their age, or all jobs if a 'all' is
145   passed.
146
147   @param opts: the command line options selected by the user
148   @type args: list
149   @param args: should contain only one element, the age as a time spec
150       that can be parsed by L{ganeti.cli.ParseTimespec} or the
151       keyword I{all}, which will cause all jobs to be archived
152   @rtype: int
153   @return: the desired exit code
154
155   """
156   client = GetClient()
157
158   age = args[0]
159
160   if age == "all":
161     age = -1
162   else:
163     age = ParseTimespec(age)
164
165   (archived_count, jobs_left) = client.AutoArchiveJobs(age)
166   ToStdout("Archived %s jobs, %s unchecked left", archived_count, jobs_left)
167
168   return 0
169
170
171 def CancelJobs(opts, args):
172   """Cancel not-yet-started jobs.
173
174   @param opts: the command line options selected by the user
175   @type args: list
176   @param args: should contain the job IDs to be cancelled
177   @rtype: int
178   @return: the desired exit code
179
180   """
181   client = GetClient()
182   result = constants.EXIT_SUCCESS
183
184   for job_id in args:
185     (success, msg) = client.CancelJob(job_id)
186
187     if not success:
188       result = constants.EXIT_FAILURE
189
190     ToStdout(msg)
191
192   return result
193
194
195 def ShowJobs(opts, args):
196   """Show detailed information about jobs.
197
198   @param opts: the command line options selected by the user
199   @type args: list
200   @param args: should contain the job IDs to be queried
201   @rtype: int
202   @return: the desired exit code
203
204   """
205   def format_msg(level, text):
206     """Display the text indented."""
207     ToStdout("%s%s", "  " * level, text)
208
209   def result_helper(value):
210     """Format a result field in a nice way."""
211     if isinstance(value, (tuple, list)):
212       return "[%s]" % utils.CommaJoin(value)
213     else:
214       return str(value)
215
216   selected_fields = [
217     "id", "status", "ops", "opresult", "opstatus", "oplog",
218     "opstart", "opexec", "opend", "received_ts", "start_ts", "end_ts",
219     ]
220
221   result = GetClient().QueryJobs(args, selected_fields)
222
223   first = True
224
225   for idx, entry in enumerate(result):
226     if not first:
227       format_msg(0, "")
228     else:
229       first = False
230
231     if entry is None:
232       if idx <= len(args):
233         format_msg(0, "Job ID %s not found" % args[idx])
234       else:
235         # this should not happen, when we don't pass args it will be a
236         # valid job returned
237         format_msg(0, "Job ID requested as argument %s not found" % (idx + 1))
238       continue
239
240     (job_id, status, ops, opresult, opstatus, oplog,
241      opstart, opexec, opend, recv_ts, start_ts, end_ts) = entry
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 commands = {
360   "list": (
361     ListJobs, [ArgJobId()],
362     [NOHDR_OPT, SEP_OPT, FIELDS_OPT],
363     "[job_id ...]",
364     "List the jobs and their status. The available fields are"
365     " (see the man page for details): id, status, op_list,"
366     " op_status, op_result."
367     " The default field"
368     " list is (in order): %s." % utils.CommaJoin(_LIST_DEF_FIELDS)),
369   "archive": (
370     ArchiveJobs, [ArgJobId(min=1)], [],
371     "<job-id> [<job-id> ...]", "Archive specified jobs"),
372   "autoarchive": (
373     AutoArchiveJobs,
374     [ArgSuggest(min=1, max=1, choices=["1d", "1w", "4w", "all"])],
375     [],
376     "<age>", "Auto archive jobs older than the given age"),
377   "cancel": (
378     CancelJobs, [ArgJobId(min=1)], [],
379     "<job-id> [<job-id> ...]", "Cancel specified jobs"),
380   "info": (
381     ShowJobs, [ArgJobId(min=1)], [],
382     "<job-id> [<job-id> ...]",
383     "Show detailed information about the specified jobs"),
384   "watch": (
385     WatchJob, [ArgJobId(min=1, max=1)], [],
386     "<job-id>", "Follows a job and prints its output as it arrives"),
387   }
388
389
390 #: dictionary with aliases for commands
391 aliases = {
392   "show": "info",
393   }
394
395
396 def Main():
397   return GenericMain(commands, aliases=aliases)