Commit 6395cebb authored by Michael Hanselmann's avatar Michael Hanselmann

RAPI: Add new request data format for instance creation

As mentioned in commit d975f482, the current way of creating an
instance via RAPI is not very flexible. With this patch, a new
instance creation request data format is introduced and documented.
Support can be detected by checking the list of features returned
by the /2/features resource for the value "instance-create-reqv1".
Signed-off-by: default avatarMichael Hanselmann <hansmi@google.com>
Reviewed-by: default avatarGuido Trotter <ultrotter@google.com>
parent 5ef5cfea
......@@ -336,6 +336,7 @@ python_tests = \
test/ganeti.opcodes_unittest.py \
test/ganeti.rapi.client_unittest.py \
test/ganeti.rapi.resources_unittest.py \
test/ganeti.rapi.rlib2_unittest.py \
test/ganeti.serializer_unittest.py \
test/ganeti.ssh_unittest.py \
test/ganeti.uidpool_unittest.py \
......
......@@ -369,6 +369,57 @@ nodes selected for the instance.
Returns: a job ID that can be used later for polling.
Body parameters:
``__version__`` (int, required)
Must be ``1`` (older Ganeti versions used a different format for
instance creation requests, version ``0``, but that format is not
documented).
``name`` (string, required)
Instance name
``disk_template`` (string, required)
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
optional ``mode`` value denoting the disk access mode (``ro`` or
``rw``).
``nics`` (list, required)
List of NIC (network interface) definitions. Example: ``[{}, {},
{"ip": "1.2.3.4"}]``. Each NIC definition can contain the optional
values ``ip``, ``mode``, ``link`` and ``bridge``.
``os`` (string)
Instance operating system.
``force_variant`` (bool)
Whether to force an unknown variant.
``pnode`` (string)
Primary node.
``snode`` (string)
Secondary node.
``src_node`` (string)
Source node for import.
``src_path`` (string)
Source directory for import.
``start`` (bool)
Whether to start instance after creation.
``ip_check`` (bool)
Whether to ensure instance's IP address is inactive.
``name_check`` (bool)
Whether to ensure instance's name is resolvable.
``file_storage_dir`` (string)
File storage directory.
``file_driver`` (string)
File storage driver.
``iallocator`` (string)
Instance allocator name.
``hypervisor`` (string)
Hypervisor name.
``hvparams`` (dict)
Hypervisor parameters, hypervisor-dependent.
``beparams``
Backend parameters.
``/2/instances/[instance_name]``
++++++++++++++++++++++++++++++++
......
......@@ -45,6 +45,7 @@ from ganeti import opcodes
from ganeti import http
from ganeti import constants
from ganeti import cli
from ganeti import utils
from ganeti import rapi
from ganeti.rapi import baserlib
......@@ -86,6 +87,9 @@ _NR_MAP = {
# Request data version field
_REQ_DATA_VERSION = "__version__"
# Feature string for instance creation request data version 1
_INST_CREATE_REQV1 = "instance-create-reqv1"
# Timeout for /2/jobs/[job_id]/wait. Gives job up to 10 seconds to change.
_WFJC_TIMEOUT = 10
......@@ -127,7 +131,7 @@ class R_2_features(baserlib.R_Generic):
"""Returns list of optional RAPI features implemented.
"""
return []
return [_INST_CREATE_REQV1]
class R_2_os(baserlib.R_Generic):
......@@ -486,6 +490,100 @@ class R_2_nodes_name_storage_repair(baserlib.R_Generic):
return baserlib.SubmitJob([op])
def _ParseInstanceCreateRequestVersion1(data, dry_run):
"""Parses an instance creation request version 1.
@rtype: L{opcodes.OpCreateInstance}
@return: Instance creation opcode
"""
# Disks
disks_input = baserlib.CheckParameter(data, "disks", exptype=list)
disks = []
for idx, i in enumerate(disks_input):
baserlib.CheckType(i, dict, "Disk %d specification" % idx)
# Size is mandatory
try:
size = i["size"]
except KeyError:
raise http.HttpBadRequest("Disk %d specification wrong: missing disk"
" size" % idx)
disk = {
"size": size,
}
# Optional disk access mode
try:
disk_access = i["mode"]
except KeyError:
pass
else:
disk["mode"] = disk_access
disks.append(disk)
assert len(disks_input) == len(disks)
# Network interfaces
nics_input = baserlib.CheckParameter(data, "nics", exptype=list)
nics = []
for idx, i in enumerate(nics_input):
baserlib.CheckType(i, dict, "NIC %d specification" % idx)
nic = {}
for field in ["mode", "ip", "link", "bridge"]:
try:
value = i[field]
except KeyError:
continue
nic[field] = value
nics.append(nic)
assert len(nics_input) == len(nics)
# HV/BE parameters
hvparams = baserlib.CheckParameter(data, "hvparams", default={})
utils.ForceDictType(hvparams, constants.HVS_PARAMETER_TYPES)
beparams = baserlib.CheckParameter(data, "beparams", default={})
utils.ForceDictType(beparams, constants.BES_PARAMETER_TYPES)
return opcodes.OpCreateInstance(
mode=baserlib.CheckParameter(data, "mode"),
instance_name=baserlib.CheckParameter(data, "name"),
os_type=baserlib.CheckParameter(data, "os", default=None),
force_variant=baserlib.CheckParameter(data, "force_variant",
default=False),
pnode=baserlib.CheckParameter(data, "pnode", default=None),
snode=baserlib.CheckParameter(data, "snode", default=None),
disk_template=baserlib.CheckParameter(data, "disk_template"),
disks=disks,
nics=nics,
src_node=baserlib.CheckParameter(data, "src_node", default=None),
src_path=baserlib.CheckParameter(data, "src_path", default=None),
start=baserlib.CheckParameter(data, "start", default=True),
wait_for_sync=True,
ip_check=baserlib.CheckParameter(data, "ip_check", default=True),
name_check=baserlib.CheckParameter(data, "name_check", default=True),
file_storage_dir=baserlib.CheckParameter(data, "file_storage_dir",
default=None),
file_driver=baserlib.CheckParameter(data, "file_driver",
default=constants.FD_LOOP),
iallocator=baserlib.CheckParameter(data, "iallocator", default=None),
hypervisor=baserlib.CheckParameter(data, "hypervisor", default=None),
hvparams=hvparams,
beparams=beparams,
dry_run=dry_run,
)
class R_2_instances(baserlib.R_Generic):
"""/2/instances resource.
......@@ -509,10 +607,13 @@ class R_2_instances(baserlib.R_Generic):
def _ParseVersion0CreateRequest(self):
"""Parses an instance creation request version 0.
Request data version 0 is deprecated and should not be used anymore.
@rtype: L{opcodes.OpCreateInstance}
@return: Instance creation opcode
"""
# Do not modify anymore, request data version 0 is deprecated
beparams = baserlib.MakeParamsDict(self.req.request_body,
constants.BES_PARAMETERS)
hvparams = baserlib.MakeParamsDict(self.req.request_body,
......@@ -541,6 +642,7 @@ class R_2_instances(baserlib.R_Generic):
if fn("bridge", None) is not None:
nics[0]["bridge"] = fn("bridge")
# Do not modify anymore, request data version 0 is deprecated
return opcodes.OpCreateInstance(
mode=constants.INSTANCE_CREATE,
instance_name=fn('name'),
......@@ -559,7 +661,7 @@ class R_2_instances(baserlib.R_Generic):
hvparams=hvparams,
beparams=beparams,
file_storage_dir=fn('file_storage_dir', None),
file_driver=fn('file_driver', 'loop'),
file_driver=fn('file_driver', constants.FD_LOOP),
dry_run=bool(self.dryRun()),
)
......@@ -577,6 +679,9 @@ class R_2_instances(baserlib.R_Generic):
if data_version == 0:
op = self._ParseVersion0CreateRequest()
elif data_version == 1:
op = _ParseInstanceCreateRequestVersion1(self.req.request_body,
self.dryRun())
else:
raise http.HttpBadRequest("Unsupported request data version %s" %
data_version)
......
#!/usr/bin/python
#
# Copyright (C) 2010 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 unittesting the RAPI rlib2 module
"""
import unittest
import tempfile
from ganeti import constants
from ganeti import opcodes
from ganeti import compat
from ganeti import http
from ganeti.rapi import rlib2
import testutils
class TestParseInstanceCreateRequestVersion1(testutils.GanetiTestCase):
def setUp(self):
testutils.GanetiTestCase.setUp(self)
self.Parse = rlib2._ParseInstanceCreateRequestVersion1
def test(self):
disk_variants = [
# No disks
[],
# Two disks
[{"size": 5, }, {"size": 100, }],
# Disk with mode
[{"size": 123, "mode": constants.DISK_RDWR, }],
# With unknown setting
[{"size": 123, "unknown": 999 }],
]
nic_variants = [
# No NIC
[],
# Three NICs
[{}, {}, {}],
# Two NICs
[
{ "ip": "1.2.3.4", "mode": constants.NIC_MODE_ROUTED, },
{ "mode": constants.NIC_MODE_BRIDGED, "link": "n0", "bridge": "br1", },
],
# Unknown settings
[{ "unknown": 999, }, { "foobar": "Hello World", }],
]
beparam_variants = [
None,
{},
{ constants.BE_VCPUS: 2, },
{ constants.BE_MEMORY: 123, },
{ constants.BE_VCPUS: 2,
constants.BE_MEMORY: 1024,
constants.BE_AUTO_BALANCE: True, }
]
hvparam_variants = [
None,
{ constants.HV_BOOT_ORDER: "anc", },
{ constants.HV_KERNEL_PATH: "/boot/fookernel",
constants.HV_ROOT_PATH: "/dev/hda1", },
]
for mode in [constants.INSTANCE_CREATE, constants.INSTANCE_IMPORT]:
for nics in nic_variants:
for disk_template in constants.DISK_TEMPLATES:
for disks in disk_variants:
for beparams in beparam_variants:
for hvparams in hvparam_variants:
data = {
"name": "inst1.example.com",
"hypervisor": constants.HT_FAKE,
"disks": disks,
"nics": nics,
"mode": mode,
"disk_template": disk_template,
}
if beparams is not None:
data["beparams"] = beparams
if hvparams is not None:
data["hvparams"] = hvparams
for dry_run in [False, True]:
op = self.Parse(data, dry_run)
self.assert_(isinstance(op, opcodes.OpCreateInstance))
self.assertEqual(op.mode, mode)
self.assertEqual(op.disk_template, disk_template)
self.assertEqual(op.dry_run, dry_run)
self.assertEqual(len(op.disks), len(disks))
self.assertEqual(len(op.nics), len(nics))
self.assert_(compat.all(opdisk.get("size") ==
disk.get("size") and
opdisk.get("mode") ==
disk.get("mode") and
"unknown" not in opdisk
for opdisk, disk in zip(op.disks,
disks)))
self.assert_(compat.all(opnic.get("size") ==
nic.get("size") and
opnic.get("mode") ==
nic.get("mode") and
"unknown" not in opnic and
"foobar" not in opnic
for opnic, nic in zip(op.nics, nics)))
if beparams is None:
self.assertEqualValues(op.beparams, {})
else:
self.assertEqualValues(op.beparams, beparams)
if hvparams is None:
self.assertEqualValues(op.hvparams, {})
else:
self.assertEqualValues(op.hvparams, hvparams)
def testErrors(self):
# Test all required fields
reqfields = {
"name": "inst1.example.com",
"disks": [],
"nics": [],
"mode": constants.INSTANCE_CREATE,
"disk_template": constants.DT_PLAIN
}
for name in reqfields.keys():
self.assertRaises(http.HttpBadRequest, self.Parse,
dict(i for i in reqfields.iteritems() if i[0] != name),
False)
# Invalid disks and nics
for field in ["disks", "nics"]:
invalid_values = [None, 1, "", {}, [1, 2, 3], ["hda1", "hda2"]]
if field == "disks":
invalid_values.append([
# Disks without size
{},
{ "mode": constants.DISK_RDWR, },
])
for invvalue in invalid_values:
data = reqfields.copy()
data[field] = invvalue
self.assertRaises(http.HttpBadRequest, self.Parse, data, False)
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