Commit e1c701e7 authored by Michael Hanselmann's avatar Michael Hanselmann
Browse files

gnt-job cancel: Confirmation and selection of jobs



New parameters, “--pending”, “--queued” and “--waiting”, are added to
select all jobs in the respective state. If one of those options is used
and “--force” is not given, the user is asked to confirm the operation.
Unit tests are included.
Signed-off-by: default avatarMichael Hanselmann <hansmi@google.com>
Reviewed-by: default avatarIustin Pop <iustin@google.com>
parent a57b2bb7
......@@ -881,6 +881,7 @@ python_tests = \
test/ganeti.cli_unittest.py \
test/ganeti.client.gnt_cluster_unittest.py \
test/ganeti.client.gnt_instance_unittest.py \
test/ganeti.client.gnt_job_unittest.py \
test/ganeti.cmdlib_unittest.py \
test/ganeti.compat_unittest.py \
test/ganeti.confd.client_unittest.py \
......
......@@ -60,6 +60,16 @@ def _FormatStatus(value):
raise errors.ProgrammerError("Unknown job status code '%s'" % value)
_JOB_LIST_FORMAT = {
"status": (_FormatStatus, False),
"summary": (lambda value: ",".join(str(item) for item in value), False),
}
_JOB_LIST_FORMAT.update(dict.fromkeys(["opstart", "opexec", "opend"],
(lambda value: map(FormatTimestamp,
value),
None)))
def _ParseJobIds(args):
"""Parses a list of string job IDs into integers.
......@@ -90,19 +100,11 @@ def ListJobs(opts, args):
if opts.archived and "archived" not in selected_fields:
selected_fields.append("archived")
fmtoverride = {
"status": (_FormatStatus, False),
"summary": (lambda value: ",".join(str(item) for item in value), False),
}
fmtoverride.update(dict.fromkeys(["opstart", "opexec", "opend"],
(lambda value: map(FormatTimestamp, value),
None)))
qfilter = qlang.MakeSimpleFilter("status", opts.status_filter)
return GenericList(constants.QR_JOB, selected_fields, args, None,
opts.separator, not opts.no_headers,
format_override=fmtoverride, verbose=opts.verbose,
format_override=_JOB_LIST_FORMAT, verbose=opts.verbose,
force_filter=opts.force_filter, namefield="id",
qfilter=qfilter, isnumeric=True)
......@@ -172,7 +174,7 @@ def AutoArchiveJobs(opts, args):
return 0
def CancelJobs(opts, args):
def CancelJobs(opts, args, cl=None, _stdout_fn=ToStdout, _ask_fn=AskUser):
"""Cancel not-yet-started jobs.
@param opts: the command line options selected by the user
......@@ -182,16 +184,42 @@ def CancelJobs(opts, args):
@return: the desired exit code
"""
client = GetClient()
if cl is None:
cl = GetClient()
result = constants.EXIT_SUCCESS
for job_id in args:
(success, msg) = client.CancelJob(job_id)
if bool(args) ^ (opts.status_filter is None):
raise errors.OpPrereqError("Either a status filter or job ID(s) must be"
" specified and never both", errors.ECODE_INVAL)
if opts.status_filter is not None:
response = cl.Query(constants.QR_JOB, ["id", "status", "summary"],
qlang.MakeSimpleFilter("status", opts.status_filter))
jobs = [i for ((_, i), _, _) in response.data]
if not jobs:
raise errors.OpPrereqError("No jobs with the requested status have been"
" found", errors.ECODE_STATE)
if not opts.force:
(_, table) = FormatQueryResult(response, header=True,
format_override=_JOB_LIST_FORMAT)
for line in table:
_stdout_fn(line)
if not _ask_fn("Cancel job(s) listed above?"):
return constants.EXIT_CONFIRMATION
else:
jobs = args
for job_id in jobs:
(success, msg) = cl.CancelJob(job_id)
if not success:
result = constants.EXIT_FAILURE
ToStdout(msg)
_stdout_fn(msg)
return result
......@@ -362,11 +390,8 @@ def WatchJob(opts, args):
_PENDING_OPT = \
cli_option("--pending", default=None,
action="store_const", dest="status_filter",
const=frozenset([
constants.JOB_STATUS_QUEUED,
constants.JOB_STATUS_WAITING,
]),
help="Show only jobs pending execution")
const=constants.JOBS_PENDING,
help="Select jobs pending execution or being cancelled")
_RUNNING_OPT = \
cli_option("--running", default=None,
......@@ -395,6 +420,22 @@ _ARCHIVED_OPT = \
action="store_true", dest="archived",
help="Include archived jobs in list (slow and expensive)")
_QUEUED_OPT = \
cli_option("--queued", default=None,
action="store_const", dest="status_filter",
const=frozenset([
constants.JOB_STATUS_QUEUED,
]),
help="Select queued jobs only")
_WAITING_OPT = \
cli_option("--waiting", default=None,
action="store_const", dest="status_filter",
const=frozenset([
constants.JOB_STATUS_WAITING,
]),
help="Select waiting jobs only")
commands = {
"list": (
......@@ -420,8 +461,11 @@ commands = {
[],
"<age>", "Auto archive jobs older than the given age"),
"cancel": (
CancelJobs, [ArgJobId(min=1)], [],
"<job-id> [<job-id> ...]", "Cancel specified jobs"),
CancelJobs, [ArgJobId()],
[FORCE_OPT, _PENDING_OPT, _QUEUED_OPT, _WAITING_OPT],
"{[--force] {--pending | --queued | --waiting} |"
" <job-id> [<job-id> ...]}",
"Cancel jobs"),
"info": (
ShowJobs, [ArgJobId(min=1)], [],
"<job-id> [<job-id> ...]",
......
......@@ -41,11 +41,14 @@ if the string all is passed.
CANCEL
~~~~~~
**cancel** {*id*}
| **cancel**
| {[\--force] {\--pending | \--queued | \--waiting} | *job-id* ...}
Cancel the job identified by the given *id*. Only jobs that have
Cancel the job(s) identified by the given *job id*. Only jobs that have
not yet started to run can be canceled; that is, jobs in either the
*queued* or *waiting* state.
*queued* or *waiting* state. To skip a confirmation, pass ``--force``.
``--queued`` and ``waiting`` can be used to cancel all jobs in the
respective state, ``--pending`` includes both.
INFO
~~~~
......
#!/usr/bin/python
#
# Copyright (C) 2012 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.
"""Script for testing ganeti.client.gnt_job"""
import unittest
import optparse
from ganeti.client import gnt_job
from ganeti import utils
from ganeti import errors
from ganeti import query
from ganeti import qlang
from ganeti import objects
from ganeti import compat
from ganeti import constants
import testutils
class _ClientForCancelJob:
def __init__(self, cancel_cb, query_cb):
self.cancelled = []
self._cancel_cb = cancel_cb
self._query_cb = query_cb
def CancelJob(self, job_id):
self.cancelled.append(job_id)
return self._cancel_cb(job_id)
def Query(self, kind, selected, qfilter):
assert kind == constants.QR_JOB
assert selected == ["id", "status", "summary"]
fields = query.GetAllFields(query._GetQueryFields(query.JOB_FIELDS,
selected))
return objects.QueryResponse(data=self._query_cb(qfilter),
fields=fields)
class TestCancelJob(unittest.TestCase):
def setUp(self):
unittest.TestCase.setUp(self)
self.stdout = []
def _ToStdout(self, line):
self.stdout.append(line)
def _Ask(self, answer, question):
self.assertTrue(question.endswith("?"))
return answer
def testStatusFilterAndArguments(self):
opts = optparse.Values(dict(status_filter=frozenset()))
try:
gnt_job.CancelJobs(opts, ["a"], cl=NotImplemented,
_stdout_fn=NotImplemented, _ask_fn=NotImplemented)
except errors.OpPrereqError, err:
self.assertEqual(err.args[1], errors.ECODE_INVAL)
else:
self.fail("Did not raise exception")
def _TestArguments(self, force):
opts = optparse.Values(dict(status_filter=None, force=force))
def _CancelCb(job_id):
self.assertTrue(job_id in ("24185", "3252"))
return (True, "%s will be cancelled" % job_id)
cl = _ClientForCancelJob(_CancelCb, NotImplemented)
self.assertEqual(gnt_job.CancelJobs(opts, ["24185", "3252"], cl=cl,
_stdout_fn=self._ToStdout,
_ask_fn=NotImplemented),
constants.EXIT_SUCCESS)
self.assertEqual(cl.cancelled, ["24185", "3252"])
self.assertEqual(self.stdout, [
"24185 will be cancelled",
"3252 will be cancelled",
])
def testArgumentsWithForce(self):
self._TestArguments(True)
def testArgumentsNoForce(self):
self._TestArguments(False)
def testArgumentsWithError(self):
opts = optparse.Values(dict(status_filter=None, force=True))
def _CancelCb(job_id):
if job_id == "10788":
return (False, "error %s" % job_id)
else:
return (True, "%s will be cancelled" % job_id)
cl = _ClientForCancelJob(_CancelCb, NotImplemented)
self.assertEqual(gnt_job.CancelJobs(opts, ["203", "10788", "30801"], cl=cl,
_stdout_fn=self._ToStdout,
_ask_fn=NotImplemented),
constants.EXIT_FAILURE)
self.assertEqual(cl.cancelled, ["203", "10788", "30801"])
self.assertEqual(self.stdout, [
"203 will be cancelled",
"error 10788",
"30801 will be cancelled",
])
def testFilterPending(self):
opts = optparse.Values(dict(status_filter=constants.JOBS_PENDING,
force=False))
def _Query(qfilter):
# Need to sort as constants.JOBS_PENDING has no stable order
assert isinstance(constants.JOBS_PENDING, frozenset)
self.assertEqual(sorted(qfilter),
sorted(qlang.MakeSimpleFilter("status",
constants.JOBS_PENDING)))
return [
[(constants.RS_UNAVAIL, None),
(constants.RS_UNAVAIL, None),
(constants.RS_UNAVAIL, None)],
[(constants.RS_NORMAL, 32532),
(constants.RS_NORMAL, constants.JOB_STATUS_QUEUED),
(constants.RS_NORMAL, ["op1", "op2", "op3"])],
]
cl = _ClientForCancelJob(NotImplemented, _Query)
result = gnt_job.CancelJobs(opts, [], cl=cl,
_stdout_fn=self._ToStdout,
_ask_fn=compat.partial(self._Ask, False))
self.assertEqual(result, constants.EXIT_CONFIRMATION)
if __name__ == "__main__":
testutils.GanetiTestProgram()
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment