diff --git a/lib/rapi/client.py b/lib/rapi/client.py index e2d7be51a8f810ce6242fbc301b039078647d6fa..5088172aade436153bd65874c256920286daa3e9 100644 --- a/lib/rapi/client.py +++ b/lib/rapi/client.py @@ -60,6 +60,13 @@ NODE_ROLE_REGULAR = "regular" # Internal constants _REQ_DATA_VERSION_FIELD = "__version__" _INST_CREATE_REQV1 = "instance-create-reqv1" +_INST_NIC_PARAMS = frozenset(["mac", "ip", "mode", "link", "bridge"]) +_INST_CREATE_V0_DISK_PARAMS = frozenset(["size"]) +_INST_CREATE_V0_PARAMS = frozenset([ + "os", "pnode", "snode", "iallocator", "start", "ip_check", "name_check", + "hypervisor", "file_storage_dir", "file_driver", "dry_run", + ]) +_INST_CREATE_V0_DPARAMS = frozenset(["beparams", "hvparams"]) class Error(Exception): @@ -676,13 +683,82 @@ class GanetiRapiClient(object): body.update((key, value) for key, value in kwargs.iteritems() if key != "dry_run") else: - # TODO: Implement instance creation request data version 0 - # When implementing version 0, care should be taken to refuse unknown - # parameters and invalid values. The interface of this function must stay + # Old request format (version 0) + + # The following code must make sure that an exception is raised when an + # unsupported setting is requested by the caller. Otherwise this can lead + # to bugs difficult to find. The interface of this function must stay # exactly the same for version 0 and 1 (e.g. they aren't allowed to # require different data types). - raise NotImplementedError("Support for instance creation request data" - " version 0 is not yet implemented") + + # Validate disks + for idx, disk in enumerate(disks): + unsupported = set(disk.keys()) - _INST_CREATE_V0_DISK_PARAMS + if unsupported: + raise GanetiApiError("Server supports request version 0 only, but" + " disk %s specifies the unsupported parameters" + " %s, allowed are %s" % + (idx, unsupported, + list(_INST_CREATE_V0_DISK_PARAMS))) + + assert (len(_INST_CREATE_V0_DISK_PARAMS) == 1 and + "size" in _INST_CREATE_V0_DISK_PARAMS) + disk_sizes = [disk["size"] for disk in disks] + + # Validate NICs + if not nics: + raise GanetiApiError("Server supports request version 0 only, but" + " no NIC specified") + elif len(nics) > 1: + raise GanetiApiError("Server supports request version 0 only, but" + " more than one NIC specified") + + assert len(nics) == 1 + + unsupported = set(nics[0].keys()) - _INST_NIC_PARAMS + if unsupported: + raise GanetiApiError("Server supports request version 0 only, but" + " NIC 0 specifies the unsupported parameters %s," + " allowed are %s" % + (unsupported, list(_INST_NIC_PARAMS))) + + # Validate other parameters + unsupported = (set(kwargs.keys()) - _INST_CREATE_V0_PARAMS - + _INST_CREATE_V0_DPARAMS) + if unsupported: + allowed = _INST_CREATE_V0_PARAMS.union(_INST_CREATE_V0_DPARAMS) + raise GanetiApiError("Server supports request version 0 only, but" + " the following unsupported parameters are" + " specified: %s, allowed are %s" % + (unsupported, list(allowed))) + + # All required fields for request data version 0 + body = { + _REQ_DATA_VERSION_FIELD: 0, + "name": name, + "disk_template": disk_template, + "disks": disk_sizes, + } + + # NIC fields + assert len(nics) == 1 + assert not (set(body.keys()) & set(nics[0].keys())) + body.update(nics[0]) + + # Copy supported fields + assert not (set(body.keys()) & set(kwargs.keys())) + body.update(dict((key, value) for key, value in kwargs.items() + if key in _INST_CREATE_V0_PARAMS)) + + # Merge dictionaries + for i in (value for key, value in kwargs.items() + if key in _INST_CREATE_V0_DPARAMS): + assert not (set(body.keys()) & set(i.keys())) + body.update(i) + + assert not (set(kwargs.keys()) - + (_INST_CREATE_V0_PARAMS | _INST_CREATE_V0_DPARAMS)) + assert not (set(body.keys()) & _INST_CREATE_V0_DPARAMS) return self._SendRequest(HTTP_POST, "/%s/instances" % GANETI_RAPI_VERSION, query, body) diff --git a/test/ganeti.rapi.client_unittest.py b/test/ganeti.rapi.client_unittest.py index 54fd5ff23eb5839027d35c5cbc7366ecedc4151c..851ec138f8f03fb76b416c950acfd4a03ea4bd84 100755 --- a/test/ganeti.rapi.client_unittest.py +++ b/test/ganeti.rapi.client_unittest.py @@ -26,6 +26,7 @@ import re import unittest import warnings +from ganeti import constants from ganeti import http from ganeti import serializer @@ -89,6 +90,9 @@ class RapiMock(object): def AddResponse(self, response, code=200): self._responses.insert(0, (code, response)) + def CountPending(self): + return len(self._responses) + def GetLastHandler(self): return self._last_handler @@ -111,6 +115,15 @@ class RapiMock(object): return code, response +class TestConstants(unittest.TestCase): + def test(self): + self.assertEqual(client.GANETI_RAPI_PORT, constants.DEFAULT_RAPI_PORT) + self.assertEqual(client.GANETI_RAPI_VERSION, constants.RAPI_VERSION) + self.assertEqual(client._REQ_DATA_VERSION_FIELD, rlib2._REQ_DATA_VERSION) + self.assertEqual(client._INST_CREATE_REQV1, rlib2._INST_CREATE_REQV1) + self.assertEqual(client._INST_NIC_PARAMS, constants.INIC_PARAMS) + + class RapiMockTest(unittest.TestCase): def test(self): rapi = RapiMock() @@ -196,6 +209,10 @@ class GanetiRapiClientTests(testutils.GanetiTestCase): self.assertEqual(features, self.client.GetFeatures()) self.assertHandler(rlib2.R_2_features) + def testGetFeaturesNotFound(self): + self.rapi.AddResponse(None, code=404) + self.assertEqual([], self.client.GetFeatures()) + def testGetOperatingSystems(self): self.rapi.AddResponse("[\"beos\"]") self.assertEqual(["beos"], self.client.GetOperatingSystems()) @@ -259,10 +276,91 @@ class GanetiRapiClientTests(testutils.GanetiTestCase): self.assertQuery("static", ["1"]) def testCreateInstanceOldVersion(self): - self.rapi.AddResponse(serializer.DumpJson([])) - self.assertRaises(NotImplementedError, self.client.CreateInstance, - "create", "inst1.example.com", "plain", [], [], - dry_run=True) + # No NICs + self.rapi.AddResponse(None, code=404) + self.assertRaises(client.GanetiApiError, self.client.CreateInstance, + "create", "inst1.example.com", "plain", [], []) + self.assertEqual(self.rapi.CountPending(), 0) + + # More than one NIC + self.rapi.AddResponse(None, code=404) + self.assertRaises(client.GanetiApiError, self.client.CreateInstance, + "create", "inst1.example.com", "plain", [], + [{}, {}, {}]) + self.assertEqual(self.rapi.CountPending(), 0) + + # Unsupported NIC fields + self.rapi.AddResponse(None, code=404) + self.assertRaises(client.GanetiApiError, self.client.CreateInstance, + "create", "inst1.example.com", "plain", [], + [{"x": True, "y": False}]) + self.assertEqual(self.rapi.CountPending(), 0) + + # Unsupported disk fields + self.rapi.AddResponse(None, code=404) + self.assertRaises(client.GanetiApiError, self.client.CreateInstance, + "create", "inst1.example.com", "plain", + [{}, {"moo": "foo",}], [{}]) + self.assertEqual(self.rapi.CountPending(), 0) + + # Unsupported fields + self.rapi.AddResponse(None, code=404) + self.assertRaises(client.GanetiApiError, self.client.CreateInstance, + "create", "inst1.example.com", "plain", [], [{}], + hello_world=123) + self.assertEqual(self.rapi.CountPending(), 0) + + self.rapi.AddResponse(None, code=404) + self.assertRaises(client.GanetiApiError, self.client.CreateInstance, + "create", "inst1.example.com", "plain", [], [{}], + memory=128) + self.assertEqual(self.rapi.CountPending(), 0) + + # Normal creation + testnics = [ + [{}], + [{ "mac": constants.VALUE_AUTO, }], + [{ "ip": "192.0.2.99", "mode": constants.NIC_MODE_ROUTED, }], + ] + + testdisks = [ + [], + [{ "size": 128, }], + [{ "size": 321, }, { "size": 4096, }], + ] + + for idx, nics in enumerate(testnics): + for disks in testdisks: + beparams = { + constants.BE_MEMORY: 512, + constants.BE_AUTO_BALANCE: False, + } + hvparams = { + constants.HV_MIGRATION_PORT: 9876, + constants.HV_VNC_TLS: True, + } + + self.rapi.AddResponse(None, code=404) + self.rapi.AddResponse(serializer.DumpJson(3122617 + idx)) + job_id = self.client.CreateInstance("create", "inst1.example.com", + "plain", disks, nics, + pnode="node99", dry_run=True, + hvparams=hvparams, + beparams=beparams) + self.assertEqual(job_id, 3122617 + idx) + self.assertHandler(rlib2.R_2_instances) + self.assertDryRun() + self.assertEqual(self.rapi.CountPending(), 0) + + data = serializer.LoadJson(self.http.last_request.data) + self.assertEqual(data["name"], "inst1.example.com") + self.assertEqual(data["disk_template"], "plain") + self.assertEqual(data["pnode"], "node99") + self.assertEqual(data[constants.BE_MEMORY], 512) + self.assertEqual(data[constants.BE_AUTO_BALANCE], False) + self.assertEqual(data[constants.HV_MIGRATION_PORT], 9876) + self.assertEqual(data[constants.HV_VNC_TLS], True) + self.assertEqual(data["disks"], [disk["size"] for disk in disks]) def testCreateInstance(self): self.rapi.AddResponse(serializer.DumpJson([rlib2._INST_CREATE_REQV1]))