As mentioned in commit d975f482d, 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: Michael Hanselmann <[email protected]> --- Makefile.am | 1 + doc/rapi.rst | 51 ++++++++++ lib/rapi/rlib2.py | 109 +++++++++++++++++++++- test/ganeti.rapi.rlib2_unittest.py | 184 ++++++++++++++++++++++++++++++++++++ 4 files changed, 343 insertions(+), 2 deletions(-) create mode 100755 test/ganeti.rapi.rlib2_unittest.py diff --git a/Makefile.am b/Makefile.am index ddc0bdf..2387e90 100644 --- a/Makefile.am +++ b/Makefile.am @@ -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 \ diff --git a/doc/rapi.rst b/doc/rapi.rst index 4c1763d..570b6a1 100644 --- a/doc/rapi.rst +++ b/doc/rapi.rst @@ -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]`` ++++++++++++++++++++++++++++++++ diff --git a/lib/rapi/rlib2.py b/lib/rapi/rlib2.py index bef6bb0..3b30f0f 100644 --- a/lib/rapi/rlib2.py +++ b/lib/rapi/rlib2.py @@ -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.EnforceType(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.EnforceType(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) diff --git a/test/ganeti.rapi.rlib2_unittest.py b/test/ganeti.rapi.rlib2_unittest.py new file mode 100755 index 0000000..70f163e --- /dev/null +++ b/test/ganeti.rapi.rlib2_unittest.py @@ -0,0 +1,184 @@ +#!/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() -- 1.7.0.4
