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