gnt-job 10.8 KB
Newer Older
Iustin Pop's avatar
Iustin Pop committed
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/usr/bin/python
#

# Copyright (C) 2006, 2007 Google Inc.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301, USA.

Iustin Pop's avatar
Iustin Pop committed
21
"""Job related commands"""
Iustin Pop's avatar
Iustin Pop committed
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)
Iustin Pop's avatar
Iustin Pop committed
27
# C0103: Invalid name gnt-job
28

Iustin Pop's avatar
Iustin Pop committed
29
30
31
32
33
import sys

from ganeti.cli import *
from ganeti import constants
from ganeti import errors
34
from ganeti import utils
35
from ganeti import cli
Iustin Pop's avatar
Iustin Pop committed
36
37


Iustin Pop's avatar
Iustin Pop committed
38
#: default list of fields for L{ListJobs}
39
_LIST_DEF_FIELDS = ["id", "status", "summary"]
40

Iustin Pop's avatar
Iustin Pop committed
41
42
#: map converting the job status contants to user-visible
#: names
43
44
_USER_JOB_STATUS = {
  constants.JOB_STATUS_QUEUED: "queued",
Iustin Pop's avatar
Iustin Pop committed
45
  constants.JOB_STATUS_WAITLOCK: "waiting",
46
  constants.JOB_STATUS_CANCELING: "canceling",
47
48
49
50
51
52
  constants.JOB_STATUS_RUNNING: "running",
  constants.JOB_STATUS_CANCELED: "canceled",
  constants.JOB_STATUS_SUCCESS: "success",
  constants.JOB_STATUS_ERROR: "error",
  }

53

Iustin Pop's avatar
Iustin Pop committed
54
55
56
def ListJobs(opts, args):
  """List the jobs

Iustin Pop's avatar
Iustin Pop committed
57
58
59
60
61
62
  @param opts: the command line options selected by the user
  @type args: list
  @param args: should be an empty list
  @rtype: int
  @return: the desired exit code

Iustin Pop's avatar
Iustin Pop committed
63
64
  """
  if opts.output is None:
65
66
67
    selected_fields = _LIST_DEF_FIELDS
  elif opts.output.startswith("+"):
    selected_fields = _LIST_DEF_FIELDS + opts.output[1:].split(",")
Iustin Pop's avatar
Iustin Pop committed
68
69
70
  else:
    selected_fields = opts.output.split(",")

Iustin Pop's avatar
Iustin Pop committed
71
  output = GetClient().QueryJobs(args, selected_fields)
Iustin Pop's avatar
Iustin Pop committed
72
  if not opts.no_headers:
73
    # TODO: Implement more fields
Iustin Pop's avatar
Iustin Pop committed
74
75
76
    headers = {
      "id": "ID",
      "status": "Status",
77
78
79
      "ops": "OpCodes",
      "opresult": "OpCode_result",
      "opstatus": "OpCode_status",
80
      "oplog": "OpCode_log",
81
      "summary": "Summary",
82
83
84
85
86
      "opstart": "OpCode_start",
      "opend": "OpCode_end",
      "start_ts": "Start",
      "end_ts": "End",
      "received_ts": "Received",
87
      "lock_status": "LockStatus",
Iustin Pop's avatar
Iustin Pop committed
88
89
90
91
92
      }
  else:
    headers = None

  # change raw values to nicer strings
93
94
95
96
97
  for row_id, row in enumerate(output):
    if row is None:
      ToStderr("No such job: %s" % args[row_id])
      continue

Iustin Pop's avatar
Iustin Pop committed
98
99
100
    for idx, field in enumerate(selected_fields):
      val = row[idx]
      if field == "status":
101
102
        if val in _USER_JOB_STATUS:
          val = _USER_JOB_STATUS[val]
Iustin Pop's avatar
Iustin Pop committed
103
104
        else:
          raise errors.ProgrammerError("Unknown job status code '%s'" % val)
105
106
      elif field == "summary":
        val = ",".join(val)
107
108
109
110
      elif field in ("start_ts", "end_ts", "received_ts"):
        val = FormatTimestamp(val)
      elif field in ("opstart", "opend"):
        val = [FormatTimestamp(entry) for entry in val]
111
112
      elif field == "lock_status" and not val:
        val = "-"
Iustin Pop's avatar
Iustin Pop committed
113
114
115
116

      row[idx] = str(val)

  data = GenerateTable(separator=opts.separator, headers=headers,
Iustin Pop's avatar
Iustin Pop committed
117
                       fields=selected_fields, data=output)
Iustin Pop's avatar
Iustin Pop committed
118
  for line in data:
119
    ToStdout(line)
Iustin Pop's avatar
Iustin Pop committed
120
121
122
123

  return 0


124
def ArchiveJobs(opts, args):
Iustin Pop's avatar
Iustin Pop committed
125
126
127
128
129
130
131
132
133
  """Archive jobs.

  @param opts: the command line options selected by the user
  @type args: list
  @param args: should contain the job IDs to be archived
  @rtype: int
  @return: the desired exit code

  """
134
135
136
137
138
139
140
141
  client = GetClient()

  for job_id in args:
    client.ArchiveJob(job_id)

  return 0


Iustin Pop's avatar
Iustin Pop committed
142
def AutoArchiveJobs(opts, args):
Iustin Pop's avatar
Iustin Pop committed
143
144
145
146
147
148
149
150
  """Archive jobs based on age.

  This will archive jobs based on their age, or all jobs if a 'all' is
  passed.

  @param opts: the command line options selected by the user
  @type args: list
  @param args: should contain only one element, the age as a time spec
Iustin Pop's avatar
Iustin Pop committed
151
152
      that can be parsed by L{ganeti.cli.ParseTimespec} or the
      keyword I{all}, which will cause all jobs to be archived
Iustin Pop's avatar
Iustin Pop committed
153
154
155
156
  @rtype: int
  @return: the desired exit code

  """
Iustin Pop's avatar
Iustin Pop committed
157
158
159
160
161
162
163
164
165
  client = GetClient()

  age = args[0]

  if age == 'all':
    age = -1
  else:
    age = ParseTimespec(age)

166
167
168
  (archived_count, jobs_left) = client.AutoArchiveJobs(age)
  ToStdout("Archived %s jobs, %s unchecked left", archived_count, jobs_left)

Iustin Pop's avatar
Iustin Pop committed
169
170
171
  return 0


172
def CancelJobs(opts, args):
Iustin Pop's avatar
Iustin Pop committed
173
174
175
176
177
178
179
180
181
  """Cancel not-yet-started jobs.

  @param opts: the command line options selected by the user
  @type args: list
  @param args: should contain the job IDs to be cancelled
  @rtype: int
  @return: the desired exit code

  """
182
183
184
  client = GetClient()

  for job_id in args:
Iustin Pop's avatar
Iustin Pop committed
185
    (_, msg) = client.CancelJob(job_id)
186
    ToStdout(msg)
187

188
  # TODO: Different exit value if not all jobs were canceled?
189
190
191
  return 0


Iustin Pop's avatar
Iustin Pop committed
192
def ShowJobs(opts, args):
Iustin Pop's avatar
Iustin Pop committed
193
194
195
196
197
198
199
  """Show detailed information about jobs.

  @param opts: the command line options selected by the user
  @type args: list
  @param args: should contain the job IDs to be queried
  @rtype: int
  @return: the desired exit code
Iustin Pop's avatar
Iustin Pop committed
200
201
202
203

  """
  def format(level, text):
    """Display the text indented."""
204
    ToStdout("%s%s", "  " * level, text)
Iustin Pop's avatar
Iustin Pop committed
205
206
207
208

  def result_helper(value):
    """Format a result field in a nice way."""
    if isinstance(value, (tuple, list)):
209
      return "[%s]" % utils.CommaJoin(value)
Iustin Pop's avatar
Iustin Pop committed
210
211
212
    else:
      return str(value)

213
214
215
216
  selected_fields = [
    "id", "status", "ops", "opresult", "opstatus", "oplog",
    "opstart", "opend", "received_ts", "start_ts", "end_ts",
    ]
Iustin Pop's avatar
Iustin Pop committed
217
218
219
220
221

  result = GetClient().QueryJobs(args, selected_fields)

  first = True

222
  for idx, entry in enumerate(result):
Iustin Pop's avatar
Iustin Pop committed
223
224
225
226
    if not first:
      format(0, "")
    else:
      first = False
227
228

    if entry is None:
229
230
231
232
233
234
      if idx <= len(args):
        format(0, "Job ID %s not found" % args[idx])
      else:
        # this should not happen, when we don't pass args it will be a
        # valid job returned
        format(0, "Job ID requested as argument %s not found" % (idx + 1))
235
236
237
238
      continue

    (job_id, status, ops, opresult, opstatus, oplog,
     opstart, opend, recv_ts, start_ts, end_ts) = entry
Iustin Pop's avatar
Iustin Pop committed
239
240
241
242
    format(0, "Job ID: %s" % job_id)
    if status in _USER_JOB_STATUS:
      status = _USER_JOB_STATUS[status]
    else:
243
      raise errors.ProgrammerError("Unknown job status code '%s'" % status)
Iustin Pop's avatar
Iustin Pop committed
244
245

    format(1, "Status: %s" % status)
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276

    if recv_ts is not None:
      format(1, "Received:         %s" % FormatTimestamp(recv_ts))
    else:
      format(1, "Missing received timestamp (%s)" % str(recv_ts))

    if start_ts is not None:
      if recv_ts is not None:
        d1 = start_ts[0] - recv_ts[0] + (start_ts[1] - recv_ts[1]) / 1000000.0
        delta = " (delta %.6fs)" % d1
      else:
        delta = ""
      format(1, "Processing start: %s%s" % (FormatTimestamp(start_ts), delta))
    else:
      format(1, "Processing start: unknown (%s)" % str(start_ts))

    if end_ts is not None:
      if start_ts is not None:
        d2 = end_ts[0] - start_ts[0] + (end_ts[1] - start_ts[1]) / 1000000.0
        delta = " (delta %.6fs)" % d2
      else:
        delta = ""
      format(1, "Processing end:   %s%s" % (FormatTimestamp(end_ts), delta))
    else:
      format(1, "Processing end:   unknown (%s)" % str(end_ts))

    if end_ts is not None and recv_ts is not None:
      d3 = end_ts[0] - recv_ts[0] + (end_ts[1] - recv_ts[1]) / 1000000.0
      format(1, "Total processing time: %.6f seconds" % d3)
    else:
      format(1, "Total processing time: N/A")
Iustin Pop's avatar
Iustin Pop committed
277
    format(1, "Opcodes:")
278
279
    for (opcode, result, status, log, s_ts, e_ts) in \
            zip(ops, opresult, opstatus, oplog, opstart, opend):
Iustin Pop's avatar
Iustin Pop committed
280
281
      format(2, "%s" % opcode["OP_ID"])
      format(3, "Status: %s" % status)
282
283
284
285
286
287
288
289
      if isinstance(s_ts, (tuple, list)):
        format(3, "Processing start: %s" % FormatTimestamp(s_ts))
      else:
        format(3, "No processing start time")
      if isinstance(e_ts, (tuple, list)):
        format(3, "Processing end:   %s" % FormatTimestamp(e_ts))
      else:
        format(3, "No processing end time")
Iustin Pop's avatar
Iustin Pop committed
290
291
292
293
294
      format(3, "Input fields:")
      for key, val in opcode.iteritems():
        if key == "OP_ID":
          continue
        if isinstance(val, (tuple, list)):
295
          val = ",".join([str(item) for item in val])
Iustin Pop's avatar
Iustin Pop committed
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
        format(4, "%s: %s" % (key, val))
      if result is None:
        format(3, "No output data")
      elif isinstance(result, (tuple, list)):
        if not result:
          format(3, "Result: empty sequence")
        else:
          format(3, "Result:")
          for elem in result:
            format(4, result_helper(elem))
      elif isinstance(result, dict):
        if not result:
          format(3, "Result: empty dictionary")
        else:
          for key, val in result.iteritems():
            format(4, "%s: %s" % (key, result_helper(val)))
      else:
        format(3, "Result: %s" % result)
314
      format(3, "Execution log:")
315
316
      for serial, log_ts, log_type, log_msg in log:
        time_txt = FormatTimestamp(log_ts)
317
        encoded = utils.SafeEncode(log_msg)
318
        format(4, "%s:%s:%s %s" % (serial, time_txt, log_type, encoded))
Iustin Pop's avatar
Iustin Pop committed
319
320
321
  return 0


322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
def WatchJob(opts, args):
  """Follow a job and print its output as it arrives.

  @param opts: the command line options selected by the user
  @type args: list
  @param args: Contains the job ID
  @rtype: int
  @return: the desired exit code

  """
  job_id = args[0]

  msg = ("Output from job %s follows" % job_id)
  ToStdout(msg)
  ToStdout("-" * len(msg))

  retcode = 0
  try:
    cli.PollJob(job_id)
  except errors.GenericError, err:
    (retcode, job_result) = cli.FormatError(err)
    ToStderr("Job %s failed: %s", job_id, job_result)

  return retcode


Iustin Pop's avatar
Iustin Pop committed
348
commands = {
349
350
  'list': (
    ListJobs, [ArgJobId()],
351
    [NOHDR_OPT, SEP_OPT, FIELDS_OPT],
352
353
354
355
356
    "[job_id ...]",
    "List the jobs and their status. The available fields are"
    " (see the man page for details): id, status, op_list,"
    " op_status, op_result."
    " The default field"
357
    " list is (in order): %s." % utils.CommaJoin(_LIST_DEF_FIELDS)),
358
  'archive': (
359
    ArchiveJobs, [ArgJobId(min=1)], [],
360
361
362
363
    "<job-id> [<job-id> ...]", "Archive specified jobs"),
  'autoarchive': (
    AutoArchiveJobs,
    [ArgSuggest(min=1, max=1, choices=["1d", "1w", "4w"])],
364
    [],
365
366
    "<age>", "Auto archive jobs older than the given age"),
  'cancel': (
367
    CancelJobs, [ArgJobId(min=1)], [],
368
369
    "<job-id> [<job-id> ...]", "Cancel specified jobs"),
  'info': (
370
    ShowJobs, [ArgJobId(min=1)], [],
371
372
373
    "<job-id> [<job-id> ...]",
    "Show detailed information about the specified jobs"),
  'watch': (
374
    WatchJob, [ArgJobId(min=1, max=1)], [],
375
    "<job-id>", "Follows a job and prints its output as it arrives"),
Iustin Pop's avatar
Iustin Pop committed
376
377
378
379
380
  }


if __name__ == '__main__':
  sys.exit(GenericMain(commands))