Revision e1c701e7

b/Makefile.am
881 881
	test/ganeti.cli_unittest.py \
882 882
	test/ganeti.client.gnt_cluster_unittest.py \
883 883
	test/ganeti.client.gnt_instance_unittest.py \
884
	test/ganeti.client.gnt_job_unittest.py \
884 885
	test/ganeti.cmdlib_unittest.py \
885 886
	test/ganeti.compat_unittest.py \
886 887
	test/ganeti.confd.client_unittest.py \
b/lib/client/gnt_job.py
60 60
    raise errors.ProgrammerError("Unknown job status code '%s'" % value)
61 61

  
62 62

  
63
_JOB_LIST_FORMAT = {
64
  "status": (_FormatStatus, False),
65
  "summary": (lambda value: ",".join(str(item) for item in value), False),
66
  }
67
_JOB_LIST_FORMAT.update(dict.fromkeys(["opstart", "opexec", "opend"],
68
                                      (lambda value: map(FormatTimestamp,
69
                                                         value),
70
                                       None)))
71

  
72

  
63 73
def _ParseJobIds(args):
64 74
  """Parses a list of string job IDs into integers.
65 75

  
......
90 100
  if opts.archived and "archived" not in selected_fields:
91 101
    selected_fields.append("archived")
92 102

  
93
  fmtoverride = {
94
    "status": (_FormatStatus, False),
95
    "summary": (lambda value: ",".join(str(item) for item in value), False),
96
    }
97
  fmtoverride.update(dict.fromkeys(["opstart", "opexec", "opend"],
98
                                   (lambda value: map(FormatTimestamp, value),
99
                                    None)))
100

  
101 103
  qfilter = qlang.MakeSimpleFilter("status", opts.status_filter)
102 104

  
103 105
  return GenericList(constants.QR_JOB, selected_fields, args, None,
104 106
                     opts.separator, not opts.no_headers,
105
                     format_override=fmtoverride, verbose=opts.verbose,
107
                     format_override=_JOB_LIST_FORMAT, verbose=opts.verbose,
106 108
                     force_filter=opts.force_filter, namefield="id",
107 109
                     qfilter=qfilter, isnumeric=True)
108 110

  
......
172 174
  return 0
173 175

  
174 176

  
175
def CancelJobs(opts, args):
177
def CancelJobs(opts, args, cl=None, _stdout_fn=ToStdout, _ask_fn=AskUser):
176 178
  """Cancel not-yet-started jobs.
177 179

  
178 180
  @param opts: the command line options selected by the user
......
182 184
  @return: the desired exit code
183 185

  
184 186
  """
185
  client = GetClient()
187
  if cl is None:
188
    cl = GetClient()
189

  
186 190
  result = constants.EXIT_SUCCESS
187 191

  
188
  for job_id in args:
189
    (success, msg) = client.CancelJob(job_id)
192
  if bool(args) ^ (opts.status_filter is None):
193
    raise errors.OpPrereqError("Either a status filter or job ID(s) must be"
194
                               " specified and never both", errors.ECODE_INVAL)
195

  
196
  if opts.status_filter is not None:
197
    response = cl.Query(constants.QR_JOB, ["id", "status", "summary"],
198
                        qlang.MakeSimpleFilter("status", opts.status_filter))
199

  
200
    jobs = [i for ((_, i), _, _) in response.data]
201
    if not jobs:
202
      raise errors.OpPrereqError("No jobs with the requested status have been"
203
                                 " found", errors.ECODE_STATE)
204

  
205
    if not opts.force:
206
      (_, table) = FormatQueryResult(response, header=True,
207
                                     format_override=_JOB_LIST_FORMAT)
208
      for line in table:
209
        _stdout_fn(line)
210

  
211
      if not _ask_fn("Cancel job(s) listed above?"):
212
        return constants.EXIT_CONFIRMATION
213
  else:
214
    jobs = args
215

  
216
  for job_id in jobs:
217
    (success, msg) = cl.CancelJob(job_id)
190 218

  
191 219
    if not success:
192 220
      result = constants.EXIT_FAILURE
193 221

  
194
    ToStdout(msg)
222
    _stdout_fn(msg)
195 223

  
196 224
  return result
197 225

  
......
362 390
_PENDING_OPT = \
363 391
  cli_option("--pending", default=None,
364 392
             action="store_const", dest="status_filter",
365
             const=frozenset([
366
               constants.JOB_STATUS_QUEUED,
367
               constants.JOB_STATUS_WAITING,
368
               ]),
369
             help="Show only jobs pending execution")
393
             const=constants.JOBS_PENDING,
394
             help="Select jobs pending execution or being cancelled")
370 395

  
371 396
_RUNNING_OPT = \
372 397
  cli_option("--running", default=None,
......
395 420
             action="store_true", dest="archived",
396 421
             help="Include archived jobs in list (slow and expensive)")
397 422

  
423
_QUEUED_OPT = \
424
  cli_option("--queued", default=None,
425
             action="store_const", dest="status_filter",
426
             const=frozenset([
427
               constants.JOB_STATUS_QUEUED,
428
               ]),
429
             help="Select queued jobs only")
430

  
431
_WAITING_OPT = \
432
  cli_option("--waiting", default=None,
433
             action="store_const", dest="status_filter",
434
             const=frozenset([
435
               constants.JOB_STATUS_WAITING,
436
               ]),
437
             help="Select waiting jobs only")
438

  
398 439

  
399 440
commands = {
400 441
  "list": (
......
420 461
    [],
421 462
    "<age>", "Auto archive jobs older than the given age"),
422 463
  "cancel": (
423
    CancelJobs, [ArgJobId(min=1)], [],
424
    "<job-id> [<job-id> ...]", "Cancel specified jobs"),
464
    CancelJobs, [ArgJobId()],
465
    [FORCE_OPT, _PENDING_OPT, _QUEUED_OPT, _WAITING_OPT],
466
    "{[--force] {--pending | --queued | --waiting} |"
467
    " <job-id> [<job-id> ...]}",
468
    "Cancel jobs"),
425 469
  "info": (
426 470
    ShowJobs, [ArgJobId(min=1)], [],
427 471
    "<job-id> [<job-id> ...]",
b/man/gnt-job.rst
41 41
CANCEL
42 42
~~~~~~
43 43

  
44
**cancel** {*id*}
44
| **cancel**
45
| {[\--force] {\--pending | \--queued | \--waiting} | *job-id* ...}
45 46

  
46
Cancel the job identified by the given *id*. Only jobs that have
47
Cancel the job(s) identified by the given *job id*. Only jobs that have
47 48
not yet started to run can be canceled; that is, jobs in either the
48
*queued* or *waiting* state.
49
*queued* or *waiting* state. To skip a confirmation, pass ``--force``.
50
``--queued`` and ``waiting`` can be used to cancel all jobs in the
51
respective state, ``--pending`` includes both.
49 52

  
50 53
INFO
51 54
~~~~
b/test/ganeti.client.gnt_job_unittest.py
1
#!/usr/bin/python
2
#
3

  
4
# Copyright (C) 2012 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
"""Script for testing ganeti.client.gnt_job"""
23

  
24
import unittest
25
import optparse
26

  
27
from ganeti.client import gnt_job
28
from ganeti import utils
29
from ganeti import errors
30
from ganeti import query
31
from ganeti import qlang
32
from ganeti import objects
33
from ganeti import compat
34
from ganeti import constants
35

  
36
import testutils
37

  
38

  
39
class _ClientForCancelJob:
40
  def __init__(self, cancel_cb, query_cb):
41
    self.cancelled = []
42
    self._cancel_cb = cancel_cb
43
    self._query_cb = query_cb
44

  
45
  def CancelJob(self, job_id):
46
    self.cancelled.append(job_id)
47
    return self._cancel_cb(job_id)
48

  
49
  def Query(self, kind, selected, qfilter):
50
    assert kind == constants.QR_JOB
51
    assert selected == ["id", "status", "summary"]
52

  
53
    fields = query.GetAllFields(query._GetQueryFields(query.JOB_FIELDS,
54
                                                      selected))
55

  
56
    return objects.QueryResponse(data=self._query_cb(qfilter),
57
                                 fields=fields)
58

  
59

  
60
class TestCancelJob(unittest.TestCase):
61
  def setUp(self):
62
    unittest.TestCase.setUp(self)
63
    self.stdout = []
64

  
65
  def _ToStdout(self, line):
66
    self.stdout.append(line)
67

  
68
  def _Ask(self, answer, question):
69
    self.assertTrue(question.endswith("?"))
70
    return answer
71

  
72
  def testStatusFilterAndArguments(self):
73
    opts = optparse.Values(dict(status_filter=frozenset()))
74
    try:
75
      gnt_job.CancelJobs(opts, ["a"], cl=NotImplemented,
76
                         _stdout_fn=NotImplemented, _ask_fn=NotImplemented)
77
    except errors.OpPrereqError, err:
78
      self.assertEqual(err.args[1], errors.ECODE_INVAL)
79
    else:
80
      self.fail("Did not raise exception")
81

  
82
  def _TestArguments(self, force):
83
    opts = optparse.Values(dict(status_filter=None, force=force))
84

  
85
    def _CancelCb(job_id):
86
      self.assertTrue(job_id in ("24185", "3252"))
87
      return (True, "%s will be cancelled" % job_id)
88

  
89
    cl = _ClientForCancelJob(_CancelCb, NotImplemented)
90
    self.assertEqual(gnt_job.CancelJobs(opts, ["24185", "3252"], cl=cl,
91
                                        _stdout_fn=self._ToStdout,
92
                                        _ask_fn=NotImplemented),
93
                     constants.EXIT_SUCCESS)
94
    self.assertEqual(cl.cancelled, ["24185", "3252"])
95
    self.assertEqual(self.stdout, [
96
      "24185 will be cancelled",
97
      "3252 will be cancelled",
98
      ])
99

  
100
  def testArgumentsWithForce(self):
101
    self._TestArguments(True)
102

  
103
  def testArgumentsNoForce(self):
104
    self._TestArguments(False)
105

  
106
  def testArgumentsWithError(self):
107
    opts = optparse.Values(dict(status_filter=None, force=True))
108

  
109
    def _CancelCb(job_id):
110
      if job_id == "10788":
111
        return (False, "error %s" % job_id)
112
      else:
113
        return (True, "%s will be cancelled" % job_id)
114

  
115
    cl = _ClientForCancelJob(_CancelCb, NotImplemented)
116
    self.assertEqual(gnt_job.CancelJobs(opts, ["203", "10788", "30801"], cl=cl,
117
                                        _stdout_fn=self._ToStdout,
118
                                        _ask_fn=NotImplemented),
119
                     constants.EXIT_FAILURE)
120
    self.assertEqual(cl.cancelled, ["203", "10788", "30801"])
121
    self.assertEqual(self.stdout, [
122
      "203 will be cancelled",
123
      "error 10788",
124
      "30801 will be cancelled",
125
      ])
126

  
127
  def testFilterPending(self):
128
    opts = optparse.Values(dict(status_filter=constants.JOBS_PENDING,
129
                                force=False))
130

  
131
    def _Query(qfilter):
132
      # Need to sort as constants.JOBS_PENDING has no stable order
133
      assert isinstance(constants.JOBS_PENDING, frozenset)
134
      self.assertEqual(sorted(qfilter),
135
                       sorted(qlang.MakeSimpleFilter("status",
136
                                                     constants.JOBS_PENDING)))
137

  
138
      return [
139
        [(constants.RS_UNAVAIL, None),
140
         (constants.RS_UNAVAIL, None),
141
         (constants.RS_UNAVAIL, None)],
142
        [(constants.RS_NORMAL, 32532),
143
         (constants.RS_NORMAL, constants.JOB_STATUS_QUEUED),
144
         (constants.RS_NORMAL, ["op1", "op2", "op3"])],
145
        ]
146

  
147
    cl = _ClientForCancelJob(NotImplemented, _Query)
148

  
149
    result = gnt_job.CancelJobs(opts, [], cl=cl,
150
                                _stdout_fn=self._ToStdout,
151
                                _ask_fn=compat.partial(self._Ask, False))
152
    self.assertEqual(result, constants.EXIT_CONFIRMATION)
153

  
154

  
155
if __name__ == "__main__":
156
  testutils.GanetiTestProgram()

Also available in: Unified diff