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

Reply via email to