Commit 0f979a34 authored by Guido Trotter's avatar Guido Trotter

Merge branch 'devel-2.2'

* devel-2.2:
  RAPI client: Support modifying instances
  RAPI: Allow modifying instance
  Small fixes for instance creation via RAPI documentation
  gnt-debug: Extend job queue tests
  jqueue: Mark opcodes following failed ones as failed, too
  jqueue: Work around race condition between job processing and archival
  jqueue: More checks for cancelling queued job
  errors: Function to check whether value is encoded error
  jqueue: Add more debug output
  gnt-backup: Pass error code to OpPrereqError
  Fix --master-netdev arg name in gnt-cluster(8)
  Restore 'tablet mouse on vnc' behavior
  Document the usb_mouse hv parameter
  Revert "Add -usbdevice tablet to KVM when using vnc"
Signed-off-by: default avatarGuido Trotter <ultrotter@google.com>
Reviewed-by: default avatarIustin Pop <iustin@google.com>
parents 7845b8c8 623fea30
......@@ -375,12 +375,12 @@ Body parameters:
Must be ``1`` (older Ganeti versions used a different format for
instance creation requests, version ``0``, but that format is not
documented).
``mode``
Instance creation mode (string, required).
``mode`` (string, required)
Instance creation mode.
``name`` (string, required)
Instance name
Instance name.
``disk_template`` (string, required)
Disk template for instance
Disk template for instance.
``disks`` (list, required)
List of disk definitions. Example: ``[{"size": 100}, {"size": 5}]``.
Each disk definition must contain a ``size`` value and can contain an
......@@ -417,7 +417,7 @@ Body parameters:
File storage driver.
``iallocator`` (string)
Instance allocator name.
``source_handshake``
``source_handshake`` (list)
Signed handshake from source (remote import only).
``source_x509_ca`` (string)
Source X509 CA in PEM format (remote import only).
......@@ -427,7 +427,7 @@ Body parameters:
Hypervisor name.
``hvparams`` (dict)
Hypervisor parameters, hypervisor-dependent.
``beparams``
``beparams`` (dict)
Backend parameters.
......@@ -671,6 +671,45 @@ Body parameters:
Whether to ensure instance's name is resolvable.
``/2/instances/[instance_name]/modify``
++++++++++++++++++++++++++++++++++++++++
Modifies an instance.
Supports the following commands: ``PUT``.
``PUT``
~~~~~~~
Returns a job ID.
Body parameters:
``osparams`` (dict)
Dictionary with OS parameters.
``hvparams`` (dict)
Hypervisor parameters, hypervisor-dependent.
``beparams`` (dict)
Backend parameters.
``force`` (bool)
Whether to force the operation.
``nics`` (list)
List of NIC changes. Each item is of the form ``(op, settings)``.
``op`` can be ``add`` to add a new NIC with the specified settings,
``remove`` to remove the last NIC or a number to modify the settings
of the NIC with that index.
``disks`` (list)
List of disk changes. See ``nics``.
``disk_template`` (string)
Disk template for instance.
``remote_node`` (string)
Secondary node (used when changing disk template).
``os_name`` (string)
Change instance's OS name. Does not reinstall the instance.
``force_variant`` (bool)
Whether to force an unknown variant.
``/2/instances/[instance_name]/tags``
+++++++++++++++++++++++++++++++++++++
......
......@@ -10037,6 +10037,7 @@ class LUTestJobqueue(NoHooksLU):
self.LogInfo("Executing")
if self.op.log_messages:
self._Notify(False, constants.JQT_STARTMSG, len(self.op.log_messages))
for idx, msg in enumerate(self.op.log_messages):
self.LogInfo("Sending log message %s", idx + 1)
feedback_fn(constants.JQT_MSGPREFIX + msg)
......
......@@ -843,10 +843,12 @@ JQT_MSGPREFIX = "TESTMSG="
JQT_EXPANDNAMES = "expandnames"
JQT_EXEC = "exec"
JQT_LOGMSG = "logmsg"
JQT_STARTMSG = "startmsg"
JQT_ALL = frozenset([
JQT_EXPANDNAMES,
JQT_EXEC,
JQT_LOGMSG,
JQT_STARTMSG,
])
# max dynamic devices
......
......@@ -400,17 +400,33 @@ def EncodeException(err):
return (err.__class__.__name__, err.args)
def MaybeRaise(result):
"""If this looks like an encoded Ganeti exception, raise it.
def GetEncodedError(result):
"""If this looks like an encoded Ganeti exception, return it.
This function tries to parse the passed argument and if it looks
like an encoding done by EncodeException, it will re-raise it.
like an encoding done by EncodeException, it will return the class
object and arguments.
"""
tlt = (tuple, list)
if (isinstance(result, tlt) and len(result) == 2 and
isinstance(result[1], tlt)):
# custom ganeti errors
err_class = GetErrorClass(result[0])
if err_class is not None:
raise err_class, tuple(result[1])
errcls = GetErrorClass(result[0])
if errcls:
return (errcls, tuple(result[1]))
return None
def MaybeRaise(result):
"""If this looks like an encoded Ganeti exception, raise it.
This function tries to parse the passed argument and if it looks
like an encoding done by EncodeException, it will re-raise it.
"""
error = GetEncodedError(result)
if error:
(errcls, args) = error
raise errcls, args
......@@ -567,11 +567,14 @@ class KVMHypervisor(hv_base.BaseHypervisor):
kvm_cmd.extend(['-append', ' '.join(root_append)])
mouse_type = hvp[constants.HV_USB_MOUSE]
vnc_bind_address = hvp[constants.HV_VNC_BIND_ADDRESS]
if mouse_type:
kvm_cmd.extend(['-usb'])
kvm_cmd.extend(['-usbdevice', mouse_type])
elif vnc_bind_address:
kvm_cmd.extend(['-usbdevice', constants.HT_MOUSE_TABLET])
vnc_bind_address = hvp[constants.HV_VNC_BIND_ADDRESS]
if vnc_bind_address:
if netutils.IP4Address.IsValid(vnc_bind_address):
if instance.network_port > constants.VNC_BASE_PORT:
......@@ -606,10 +609,6 @@ class KVMHypervisor(hv_base.BaseHypervisor):
vnc_arg = 'unix:%s/%s.vnc' % (vnc_bind_address, instance.name)
kvm_cmd.extend(['-vnc', vnc_arg])
# Also add a tablet USB device to act as a mouse
# This solves various mouse alignment issues
kvm_cmd.extend(['-usbdevice', 'tablet'])
else:
kvm_cmd.extend(['-nographic'])
......
......@@ -420,6 +420,15 @@ class _OpExecCallbacks(mcpu.OpExecCbBase):
self._job = job
self._op = op
def _CheckCancel(self):
"""Raises an exception to cancel the job if asked to.
"""
# Cancel here if we were asked to
if self._op.status == constants.OP_STATUS_CANCELING:
logging.debug("Canceling opcode")
raise CancelJob()
@locking.ssynchronized(_QUEUE, shared=1)
def NotifyStart(self):
"""Mark the opcode as running, not lock-waiting.
......@@ -437,9 +446,9 @@ class _OpExecCallbacks(mcpu.OpExecCbBase):
self._job.lock_status = None
# Cancel here if we were asked to
if self._op.status == constants.OP_STATUS_CANCELING:
raise CancelJob()
self._CheckCancel()
logging.debug("Opcode is now running")
self._op.status = constants.OP_STATUS_RUNNING
self._op.exec_timestamp = TimeStampNow()
......@@ -478,9 +487,15 @@ class _OpExecCallbacks(mcpu.OpExecCbBase):
Called whenever the LU processor is waiting for a lock or has acquired one.
"""
assert self._op.status in (constants.OP_STATUS_WAITLOCK,
constants.OP_STATUS_CANCELING)
# Not getting the queue lock because this is a single assignment
self._job.lock_status = msg
# Cancel here if we were asked to
self._CheckCancel()
class _JobChangesChecker(object):
def __init__(self, fields, prev_job_info, prev_log_serial):
......@@ -713,8 +728,11 @@ class _JobQueueWorker(workerpool.BaseWorker):
queue.acquire(shared=1)
try:
if op.status == constants.OP_STATUS_CANCELED:
logging.debug("Canceling opcode")
raise CancelJob()
assert op.status == constants.OP_STATUS_QUEUED
logging.debug("Opcode %s/%s waiting for locks",
idx + 1, count)
op.status = constants.OP_STATUS_WAITLOCK
op.result = None
op.start_timestamp = TimeStampNow()
......@@ -732,9 +750,18 @@ class _JobQueueWorker(workerpool.BaseWorker):
queue.acquire(shared=1)
try:
logging.debug("Opcode %s/%s succeeded", idx + 1, count)
op.status = constants.OP_STATUS_SUCCESS
op.result = result
op.end_timestamp = TimeStampNow()
if idx == count - 1:
job.lock_status = None
job.end_timestamp = TimeStampNow()
# Consistency check
assert compat.all(i.status == constants.OP_STATUS_SUCCESS
for i in job.ops)
queue.UpdateJobUnlocked(job)
finally:
queue.release()
......@@ -748,6 +775,7 @@ class _JobQueueWorker(workerpool.BaseWorker):
queue.acquire(shared=1)
try:
try:
logging.debug("Opcode %s/%s failed", idx + 1, count)
op.status = constants.OP_STATUS_ERROR
if isinstance(err, errors.GenericError):
to_encode = err
......@@ -757,7 +785,20 @@ class _JobQueueWorker(workerpool.BaseWorker):
op.end_timestamp = TimeStampNow()
logging.info("Op %s/%s: Error in opcode %s: %s",
idx + 1, count, op_summary, err)
to_encode = errors.OpExecError("Preceding opcode failed")
job.MarkUnfinishedOps(constants.OP_STATUS_ERROR,
errors.EncodeException(to_encode))
# Consistency check
assert compat.all(i.status == constants.OP_STATUS_SUCCESS
for i in job.ops[:idx])
assert compat.all(i.status == constants.OP_STATUS_ERROR and
errors.GetEncodedError(i.result)
for i in job.ops[idx:])
finally:
job.lock_status = None
job.end_timestamp = TimeStampNow()
queue.UpdateJobUnlocked(job)
finally:
queue.release()
......@@ -768,6 +809,9 @@ class _JobQueueWorker(workerpool.BaseWorker):
try:
job.MarkUnfinishedOps(constants.OP_STATUS_CANCELED,
"Job canceled by request")
job.lock_status = None
job.end_timestamp = TimeStampNow()
queue.UpdateJobUnlocked(job)
finally:
queue.release()
except errors.GenericError, err:
......@@ -775,19 +819,8 @@ class _JobQueueWorker(workerpool.BaseWorker):
except:
logging.exception("Unhandled exception")
finally:
queue.acquire(shared=1)
try:
try:
job.lock_status = None
job.end_timestamp = TimeStampNow()
queue.UpdateJobUnlocked(job)
finally:
job_id = job.id
status = job.CalcStatus()
finally:
queue.release()
logging.info("Finished job %s, status = %s", job_id, status)
status = job.CalcStatus()
logging.info("Finished job %s, status = %s", job.id, status)
class _JobQueueWorkerPool(workerpool.WorkerPool):
......@@ -1025,6 +1058,7 @@ class JobQueue(object):
names and the second one with the node addresses
"""
# TODO: Change to "tuple(map(list, zip(*self._nodes.items())))"?
name_list = self._nodes.keys()
addr_list = [self._nodes[name] for name in name_list]
return name_list, addr_list
......
......@@ -725,6 +725,23 @@ class GanetiRapiClient(object):
("/%s/instances/%s" %
(GANETI_RAPI_VERSION, instance)), query, None)
def ModifyInstance(self, instance, **kwargs):
"""Modifies an instance.
More details for parameters can be found in the RAPI documentation.
@type instance: string
@param instance: Instance name
@rtype: int
@return: job id
"""
body = kwargs
return self._SendRequest(HTTP_PUT,
("/%s/instances/%s/modify" %
(GANETI_RAPI_VERSION, instance)), None, body)
def GetInstanceTags(self, instance):
"""Gets tags for an instance.
......
......@@ -217,6 +217,8 @@ def GetHandlers(node_name_pattern, instance_name_pattern, job_id_pattern):
rlib2.R_2_instances_name_migrate,
re.compile(r'^/2/instances/(%s)/rename$' % instance_name_pattern):
rlib2.R_2_instances_name_rename,
re.compile(r'^/2/instances/(%s)/modify$' % instance_name_pattern):
rlib2.R_2_instances_name_modify,
"/2/jobs": rlib2.R_2_jobs,
re.compile(r"^/2/jobs/(%s)$" % job_id_pattern):
......
......@@ -1055,6 +1055,56 @@ class R_2_instances_name_rename(baserlib.R_Generic):
return baserlib.SubmitJob([op])
def _ParseModifyInstanceRequest(name, data):
"""Parses a request for modifying an instance.
@rtype: L{opcodes.OpSetInstanceParams}
@return: Instance modify opcode
"""
osparams = baserlib.CheckParameter(data, "osparams", default={})
force = baserlib.CheckParameter(data, "force", default=False)
nics = baserlib.CheckParameter(data, "nics", default=[])
disks = baserlib.CheckParameter(data, "disks", default=[])
disk_template = baserlib.CheckParameter(data, "disk_template", default=None)
remote_node = baserlib.CheckParameter(data, "remote_node", default=None)
os_name = baserlib.CheckParameter(data, "os_name", default=None)
force_variant = baserlib.CheckParameter(data, "force_variant", default=False)
# HV/BE parameters
hvparams = baserlib.CheckParameter(data, "hvparams", default={})
utils.ForceDictType(hvparams, constants.HVS_PARAMETER_TYPES,
allowed_values=[constants.VALUE_DEFAULT])
beparams = baserlib.CheckParameter(data, "beparams", default={})
utils.ForceDictType(beparams, constants.BES_PARAMETER_TYPES,
allowed_values=[constants.VALUE_DEFAULT])
return opcodes.OpSetInstanceParams(instance_name=name, hvparams=hvparams,
beparams=beparams, osparams=osparams,
force=force, nics=nics, disks=disks,
disk_template=disk_template,
remote_node=remote_node, os_name=os_name,
force_variant=force_variant)
class R_2_instances_name_modify(baserlib.R_Generic):
"""/2/instances/[instance_name]/modify resource.
"""
def PUT(self):
"""Changes some parameters of an instance.
@return: a job id
"""
baserlib.CheckType(self.request_body, dict, "Body contents")
op = _ParseModifyInstanceRequest(self.items[0], self.request_body)
return baserlib.SubmitJob([op])
class _R_Tags(baserlib.R_Generic):
""" Quasiclass for tagging resources
......
......@@ -228,7 +228,7 @@
<sbr>
<arg>-g <replaceable>vg-name</replaceable></arg>
<sbr>
<arg>--master-netdev <replaceable>vg-name</replaceable></arg>
<arg>--master-netdev <replaceable>interface-name</replaceable></arg>
<sbr>
<arg>-m <replaceable>mac-prefix</replaceable></arg>
<sbr>
......
......@@ -729,6 +729,18 @@
</listitem>
</varlistentry>
<varlistentry>
<term>usb_mouse</term>
<listitem>
<simpara>Valid for the KVM hypervisor.</simpara>
<simpara>This option specifies the usb mouse type to be used.
It can be <quote>mouse</quote> or <quote>tablet</quote>. When
using VNC it's recommended to set it to <quote>tablet</quote>.
</simpara>
</listitem>
</varlistentry>
</variablelist>
</para>
......
......@@ -162,6 +162,8 @@ def RunCommonInstanceTests(instance):
if qa_config.TestEnabled('instance-modify'):
RunTest(qa_instance.TestInstanceModify, instance)
if qa_rapi.Enabled():
RunTest(qa_rapi.TestRapiInstanceModify, instance)
if qa_config.TestEnabled('instance-console'):
RunTest(qa_instance.TestInstanceConsole, instance)
......
......@@ -358,6 +358,28 @@ def TestRapiInstanceRename(instance, rename_target):
_WaitForRapiJob(_rapi_client.RenameInstance(name1, name2))
def TestRapiInstanceModify(instance):
"""Test modifying instance via RAPI"""
def _ModifyInstance(**kwargs):
_WaitForRapiJob(_rapi_client.ModifyInstance(instance["name"], **kwargs))
_ModifyInstance(hvparams={
constants.HV_KERNEL_ARGS: "single",
})
_ModifyInstance(beparams={
constants.BE_VCPUS: 3,
})
_ModifyInstance(beparams={
constants.BE_VCPUS: constants.VALUE_DEFAULT,
})
_ModifyInstance(hvparams={
constants.HV_KERNEL_ARGS: constants.VALUE_DEFAULT,
})
def TestInterClusterInstanceMove(src_instance, dest_instance, pnode, snode):
"""Test tools/move-instance"""
master = qa_config.GetMasterNode()
......
......@@ -75,7 +75,8 @@ def ExportInstance(opts, args):
ignore_remove_failures = opts.ignore_remove_failures
if not opts.node:
raise errors.OpPrereqError("Target node must be specified")
raise errors.OpPrereqError("Target node must be specified",
errors.ECODE_INVAL)
op = opcodes.OpExportInstance(instance_name=args[0],
target_node=opts.node,
......
......@@ -164,14 +164,16 @@ class _JobQueueTestReporter(cli.StdioJobPollReportCb):
"""
cli.StdioJobPollReportCb.__init__(self)
self._testmsgs = []
self._expected_msgcount = 0
self._all_testmsgs = []
self._testmsgs = None
self._job_id = None
def GetTestMessages(self):
"""Returns all test log messages received so far.
"""
return self._testmsgs
return self._all_testmsgs
def GetJobId(self):
"""Returns the job ID.
......@@ -195,7 +197,12 @@ class _JobQueueTestReporter(cli.StdioJobPollReportCb):
elif (log_type == constants.ELOG_MESSAGE and
log_msg.startswith(constants.JQT_MSGPREFIX)):
self._testmsgs.append(log_msg[len(constants.JQT_MSGPREFIX):])
if self._testmsgs is None:
raise errors.OpExecError("Received test message without a preceding"
" start message")
testmsg = log_msg[len(constants.JQT_MSGPREFIX):]
self._testmsgs.append(testmsg)
self._all_testmsgs.append(testmsg)
return
return cli.StdioJobPollReportCb.ReportLogMessage(self, job_id, serial,
......@@ -236,7 +243,10 @@ class _JobQueueTestReporter(cli.StdioJobPollReportCb):
" not '%s' as expected" %
(status, constants.JOB_STATUS_RUNNING))
if test == constants.JQT_LOGMSG:
if test == constants.JQT_STARTMSG:
logging.debug("Expecting %s test messages", arg)
self._testmsgs = []
elif test == constants.JQT_LOGMSG:
if len(self._testmsgs) != arg:
raise errors.OpExecError("Received %s test messages when %s are"
" expected" % (len(self._testmsgs), arg))
......@@ -249,47 +259,120 @@ def TestJobqueue(opts, _):
"""Runs a few tests on the job queue.
"""
test_messages = [
"Hello World",
"A",
"",
"B"
"Foo|bar|baz",
utils.TimestampForFilename(),
]
for fail in [False, True]:
if fail:
ToStdout("Testing job failure")
(TM_SUCCESS,
TM_MULTISUCCESS,
TM_FAIL,
TM_PARTFAIL) = range(4)
TM_ALL = frozenset([TM_SUCCESS, TM_MULTISUCCESS, TM_FAIL, TM_PARTFAIL])
for mode in TM_ALL:
test_messages = [
"Testing mode %s" % mode,
"Hello World",
"A",
"",
"B"
"Foo|bar|baz",
utils.TimestampForFilename(),
]
fail = mode in (TM_FAIL, TM_PARTFAIL)
if mode == TM_PARTFAIL:
ToStdout("Testing partial job failure")
ops = [
opcodes.OpTestJobqueue(notify_waitlock=True, notify_exec=True,
log_messages=test_messages, fail=False),
opcodes.OpTestJobqueue(notify_waitlock=True, notify_exec=True,
log_messages=test_messages, fail=False),
opcodes.OpTestJobqueue(notify_waitlock=True, notify_exec=True,
log_messages=test_messages, fail=True),
opcodes.OpTestJobqueue(notify_waitlock=True, notify_exec=True,
log_messages=test_messages, fail=False),
]
expect_messages = 3 * [test_messages]
expect_opstatus = [
constants.OP_STATUS_SUCCESS,
constants.OP_STATUS_SUCCESS,
constants.OP_STATUS_ERROR,
constants.OP_STATUS_ERROR,
]
expect_resultlen = 2
elif mode == TM_MULTISUCCESS:
ToStdout("Testing multiple successful opcodes")
ops = [
opcodes.OpTestJobqueue(notify_waitlock=True, notify_exec=True,
log_messages=test_messages, fail=False),
opcodes.OpTestJobqueue(notify_waitlock=True, notify_exec=True,
log_messages=test_messages, fail=False),
]
expect_messages = 2 * [test_messages]
expect_opstatus = [
constants.OP_STATUS_SUCCESS,
constants.OP_STATUS_SUCCESS,
]
expect_resultlen = 2
else:
ToStdout("Testing job success")
op = opcodes.OpTestJobqueue(notify_waitlock=True,
notify_exec=True,
log_messages=test_messages,
fail=fail)
if mode == TM_SUCCESS:
ToStdout("Testing job success")
expect_opstatus = [constants.OP_STATUS_SUCCESS]
elif mode == TM_FAIL:
ToStdout("Testing job failure")
expect_opstatus = [constants.OP_STATUS_ERROR]
else:
raise errors.ProgrammerError("Unknown test mode %s" % mode)
ops = [
opcodes.OpTestJobqueue(notify_waitlock=True,
notify_exec=True,
log_messages=test_messages,
fail=fail)
]
expect_messages = [test_messages]
expect_resultlen = 1
cl = cli.GetClient()
cli.SetGenericOpcodeOpts(ops, opts)
# Send job to master daemon
job_id = cli.SendJob(ops, cl=cl)
reporter = _JobQueueTestReporter()
results = None
try:
SubmitOpCode(op, reporter=reporter, opts=opts)
except errors.OpExecError:
results = cli.PollJob(job_id, cl=cl, reporter=reporter)
except errors.OpExecError, err:
if not fail:
raise
# Ignore error
ToStdout("Ignoring error: %s", err)
else:
if fail:
raise errors.OpExecError("Job didn't fail when it should")
# Check length of result
if fail:
if results is not None:
raise errors.OpExecError("Received result from failed job")
elif len(results) != expect_resultlen:
raise errors.OpExecError("Received %s results (%s), expected %s" %
(len(results), results, expect_resultlen))
# Check received log messages
if reporter.GetTestMessages() != test_messages:
all_messages = [i for j in expect_messages for i in j]