diff --git a/NEWS b/NEWS index aec004633819a323f0627dfd737cc5c6fe48f16f..6ffbcb520508abbd418237ddc1d5211aef8d4426 100644 --- a/NEWS +++ b/NEWS @@ -1,10 +1,10 @@ News ==== -Version 2.5.0 rc1 +Version 2.5.0 rc2 ----------------- -*(Released Tue, 4 Oct 2011)* +*(Released Tue, 18 Oct 2011)* Incompatible/important changes and bugfixes ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -129,6 +129,14 @@ Misc - DRBD metadata volumes are overwritten with zeros during disk creation. +Version 2.5.0 rc1 +----------------- + +*(Released Tue, 4 Oct 2011)* + +This was the first release candidate of the 2.5 series. + + Version 2.5.0 beta3 ------------------- @@ -153,6 +161,18 @@ Version 2.5.0 beta1 This was the first beta release of the 2.5 series. +Version 2.4.5 +------------- + +*(unreleased)* + +- Fixed bug when parsing command line parameter values ending in + backslash +- Fixed assertion error after unclean master shutdown +- Disable HTTP client pool for RPC, significantly reducing memory usage + of master daemon + + Version 2.4.4 ------------- diff --git a/configure.ac b/configure.ac index 82e4a35a681ce6e1adaba9c10a39aae367a6c938..4234b467b282e257a72adccc7775681019499321 100644 --- a/configure.ac +++ b/configure.ac @@ -2,7 +2,7 @@ m4_define([gnt_version_major], [2]) m4_define([gnt_version_minor], [5]) m4_define([gnt_version_revision], [0]) -m4_define([gnt_version_suffix], [~rc1]) +m4_define([gnt_version_suffix], [~rc2]) m4_define([gnt_version_full], m4_format([%d.%d.%d%s], gnt_version_major, gnt_version_minor, diff --git a/doc/rapi.rst b/doc/rapi.rst index 977f24a68828bc7b2f8419491c8f0c8fedce9cf8..6dea27c617d191dd111ba466e4e2604053bf6a54 100644 --- a/doc/rapi.rst +++ b/doc/rapi.rst @@ -789,16 +789,15 @@ It supports the following commands: ``POST``. ``POST`` ~~~~~~~~ -Takes the parameters ``mode`` (one of ``replace_on_primary``, -``replace_on_secondary``, ``replace_new_secondary`` or -``replace_auto``), ``disks`` (comma separated list of disk indexes), -``remote_node`` and ``iallocator``. +Returns a job ID. + +Body parameters: -Either ``remote_node`` or ``iallocator`` needs to be defined when using -``mode=replace_new_secondary``. +.. opcode_params:: OP_INSTANCE_REPLACE_DISKS + :exclude: instance_name -``mode`` is a mandatory parameter. ``replace_auto`` tries to determine -the broken disk(s) on its own and replacing it. +Ganeti 2.4 and below used query parameters. Those are deprecated and +should no longer be used. ``/2/instances/[instance_name]/activate-disks`` diff --git a/lib/backend.py b/lib/backend.py index 57b31a9caf5c06bc367cdf197e387436eb8f1e2c..2434c4495394cfcb7b43475bfb2133e54df39848 100644 --- a/lib/backend.py +++ b/lib/backend.py @@ -2660,7 +2660,10 @@ def JobQueueRename(old, new): _EnsureJobQueueFile(old) _EnsureJobQueueFile(new) - utils.RenameFile(old, new, mkdir=True) + getents = runtime.GetEnts() + + utils.RenameFile(old, new, mkdir=True, mkdir_mode=0700, + dir_uid=getents.masterd_uid, dir_gid=getents.masterd_gid) def BlockdevClose(instance_name, disks): diff --git a/lib/cmdlib.py b/lib/cmdlib.py index daec49dd5374f1225d7e0e7ea5272b73b97f7d83..62cebc29aabb81f57404409149a1447da3c841aa 100644 --- a/lib/cmdlib.py +++ b/lib/cmdlib.py @@ -3162,7 +3162,7 @@ class LUGroupVerifyDisks(NoHooksLU): # any leftover items in nv_dict are missing LVs, let's arrange the data # better for key, inst in nv_dict.iteritems(): - res_missing.setdefault(inst, []).append(key) + res_missing.setdefault(inst, []).append(list(key)) return (res_nodes, list(res_instances), res_missing) diff --git a/lib/opcodes.py b/lib/opcodes.py index 0f5de5a8862ed3a332497ce4a0c41596f62e8e68..f00044a3b5bfc4fea279818460152a172d01f2ba 100644 --- a/lib/opcodes.py +++ b/lib/opcodes.py @@ -673,7 +673,8 @@ class OpGroupVerifyDisks(OpCode): ht.TAnd(ht.TIsLength(3), ht.TItems([ht.TDictOf(ht.TString, ht.TString), ht.TListOf(ht.TString), - ht.TDictOf(ht.TString, ht.TListOf(ht.TString))])) + ht.TDictOf(ht.TString, + ht.TListOf(ht.TListOf(ht.TString)))])) class OpClusterRepairDiskSizes(OpCode): diff --git a/lib/rapi/client.py b/lib/rapi/client.py index 213712dddc7640591e00c3ac1c5abba66211c19a..f41b6fca97432d5036e78255c27f0021c0c19c9e 100644 --- a/lib/rapi/client.py +++ b/lib/rapi/client.py @@ -63,6 +63,10 @@ REPLACE_DISK_SECONDARY = "replace_on_secondary" REPLACE_DISK_CHG = "replace_new_secondary" REPLACE_DISK_AUTO = "replace_auto" +NODE_EVAC_PRI = "primary-only" +NODE_EVAC_SEC = "secondary-only" +NODE_EVAC_ALL = "all" + NODE_ROLE_DRAINED = "drained" NODE_ROLE_MASTER_CANDIATE = "master-candidate" NODE_ROLE_MASTER = "master" @@ -956,7 +960,7 @@ class GanetiRapiClient(object): # pylint: disable=R0904 (GANETI_RAPI_VERSION, instance)), query, None) def ReplaceInstanceDisks(self, instance, disks=None, mode=REPLACE_DISK_AUTO, - remote_node=None, iallocator=None, dry_run=False): + remote_node=None, iallocator=None): """Replaces disks on an instance. @type instance: str @@ -971,8 +975,6 @@ class GanetiRapiClient(object): # pylint: disable=R0904 @type iallocator: str or None @param iallocator: instance allocator plugin to use (for use with replace_auto mode) - @type dry_run: bool - @param dry_run: whether to perform a dry run @rtype: string @return: job id @@ -982,18 +984,17 @@ class GanetiRapiClient(object): # pylint: disable=R0904 ("mode", mode), ] - if disks: + # TODO: Convert to body parameters + + if disks is not None: query.append(("disks", ",".join(str(idx) for idx in disks))) - if remote_node: + if remote_node is not None: query.append(("remote_node", remote_node)) - if iallocator: + if iallocator is not None: query.append(("iallocator", iallocator)) - if dry_run: - query.append(("dry-run", 1)) - return self._SendRequest(HTTP_POST, ("/%s/instances/%s/replace-disks" % (GANETI_RAPI_VERSION, instance)), query, None) @@ -1287,7 +1288,7 @@ class GanetiRapiClient(object): # pylint: disable=R0904 def EvacuateNode(self, node, iallocator=None, remote_node=None, dry_run=False, early_release=None, - primary=None, secondary=None, accept_old=False): + mode=None, accept_old=False): """Evacuates instances from a Ganeti node. @type node: str @@ -1300,10 +1301,8 @@ class GanetiRapiClient(object): # pylint: disable=R0904 @param dry_run: whether to perform a dry run @type early_release: bool @param early_release: whether to enable parallelization - @type primary: bool - @param primary: Whether to evacuate primary instances - @type secondary: bool - @param secondary: Whether to evacuate secondary instances + @type mode: string + @param mode: Node evacuation mode @type accept_old: bool @param accept_old: Whether caller is ready to accept old-style (pre-2.5) results @@ -1326,6 +1325,7 @@ class GanetiRapiClient(object): # pylint: disable=R0904 query.append(("dry-run", 1)) if _NODE_EVAC_RES1 in self.GetFeatures(): + # Server supports body parameters body = {} if iallocator is not None: @@ -1334,10 +1334,8 @@ class GanetiRapiClient(object): # pylint: disable=R0904 body["remote_node"] = remote_node if early_release is not None: body["early_release"] = early_release - if primary is not None: - body["primary"] = primary - if secondary is not None: - body["secondary"] = secondary + if mode is not None: + body["mode"] = mode else: # Pre-2.5 request format body = None @@ -1347,7 +1345,8 @@ class GanetiRapiClient(object): # pylint: disable=R0904 " not accept old-style results (parameter" " accept_old)") - if primary or primary is None or not (secondary is None or secondary): + # Pre-2.5 servers can only evacuate secondaries + if mode is not None and mode != NODE_EVAC_SEC: raise GanetiApiError("Server can only evacuate secondary instances") if iallocator: diff --git a/lib/rapi/rlib2.py b/lib/rapi/rlib2.py index 74fdf228c5ffbfb7e014505b5b96219b13e679ef..ab8927045c1f4abd8141191b338efc8782a2d493 100644 --- a/lib/rapi/rlib2.py +++ b/lib/rapi/rlib2.py @@ -1021,16 +1021,19 @@ def _ParseInstanceReplaceDisksRequest(name, data): # Parse disks try: - raw_disks = data["disks"] + raw_disks = data.pop("disks") except KeyError: pass else: - if not ht.TListOf(ht.TInt)(raw_disks): # pylint: disable=E1102 - # Backwards compatibility for strings of the format "1, 2, 3" - try: - data["disks"] = [int(part) for part in raw_disks.split(",")] - except (TypeError, ValueError), err: - raise http.HttpBadRequest("Invalid disk index passed: %s" % str(err)) + if raw_disks: + if ht.TListOf(ht.TInt)(raw_disks): # pylint: disable=E1102 + data["disks"] = raw_disks + else: + # Backwards compatibility for strings of the format "1, 2, 3" + try: + data["disks"] = [int(part) for part in raw_disks.split(",")] + except (TypeError, ValueError), err: + raise http.HttpBadRequest("Invalid disk index passed: %s" % str(err)) return baserlib.FillOpcode(opcodes.OpInstanceReplaceDisks, data, override) @@ -1043,7 +1046,20 @@ class R_2_instances_name_replace_disks(baserlib.R_Generic): """Replaces disks on an instance. """ - op = _ParseInstanceReplaceDisksRequest(self.items[0], self.request_body) + if self.request_body: + body = self.request_body + elif self.queryargs: + # Legacy interface, do not modify/extend + body = { + "remote_node": self._checkStringVariable("remote_node", default=None), + "mode": self._checkStringVariable("mode", default=None), + "disks": self._checkStringVariable("disks", default=None), + "iallocator": self._checkStringVariable("iallocator", default=None), + } + else: + body = {} + + op = _ParseInstanceReplaceDisksRequest(self.items[0], body) return baserlib.SubmitJob([op]) diff --git a/lib/tools/ensure_dirs.py b/lib/tools/ensure_dirs.py index 7abcce2a2bcf556326fd9b8df9663bac52a015b1..8cf01ae7a13632743d78ce9fe39821c33a72c106 100644 --- a/lib/tools/ensure_dirs.py +++ b/lib/tools/ensure_dirs.py @@ -227,6 +227,8 @@ def GetPaths(): getent.masterd_uid, getent.masterd_gid, False), (constants.JOB_QUEUE_SERIAL_FILE, FILE, 0600, getent.masterd_uid, getent.masterd_gid, False), + (constants.JOB_QUEUE_VERSION_FILE, FILE, 0600, + getent.masterd_uid, getent.masterd_gid, False), (constants.JOB_QUEUE_ARCHIVE_DIR, DIR, 0700, getent.masterd_uid, getent.masterd_gid), (rapi_dir, DIR, 0750, getent.rapi_uid, getent.masterd_gid), diff --git a/lib/utils/io.py b/lib/utils/io.py index 91899a210f4d3a70607f51d1549066af8acbb319..5d3a5e643db840a9ade4ca0a2f32e5263a9355bb 100644 --- a/lib/utils/io.py +++ b/lib/utils/io.py @@ -295,7 +295,8 @@ def RemoveDir(dirname): raise -def RenameFile(old, new, mkdir=False, mkdir_mode=0750): +def RenameFile(old, new, mkdir=False, mkdir_mode=0750, dir_uid=None, + dir_gid=None): """Renames a file. @type old: string @@ -306,6 +307,10 @@ def RenameFile(old, new, mkdir=False, mkdir_mode=0750): @param mkdir: Whether to create target directory if it doesn't exist @type mkdir_mode: int @param mkdir_mode: Mode for newly created directories + @type dir_uid: int + @param dir_uid: The uid for the (if fresh created) dir + @type dir_gid: int + @param dir_gid: The gid for the (if fresh created) dir """ try: @@ -316,7 +321,10 @@ def RenameFile(old, new, mkdir=False, mkdir_mode=0750): # as efficient. if mkdir and err.errno == errno.ENOENT: # Create directory and try again - Makedirs(os.path.dirname(new), mode=mkdir_mode) + dir_path = os.path.dirname(new) + Makedirs(dir_path, mode=mkdir_mode) + if not (dir_uid is None or dir_gid is None): + os.chown(dir_path, dir_uid, dir_gid) return os.rename(old, new) diff --git a/qa/ganeti-qa.py b/qa/ganeti-qa.py index 8b0c33889f36922cbf7db315afad918bf523811e..a2a4eb05819883d13cf622b3c6a06294bd676fdc 100755 --- a/qa/ganeti-qa.py +++ b/qa/ganeti-qa.py @@ -382,6 +382,7 @@ def RunHardwareFailureTests(instance, pnode, snode): if qa_config.TestEnabled("instance-replace-disks"): othernode = qa_config.AcquireNode(exclude=[pnode, snode]) try: + RunTestIf("rapi", qa_rapi.TestRapiInstanceReplaceDisks, instance) RunTest(qa_instance.TestReplaceDisks, instance, pnode, snode, othernode) finally: diff --git a/qa/qa_rapi.py b/qa/qa_rapi.py index 02218463c349dacf27c39e0052ef1d4c9a18e83e..a4c5921ce1386960ccaaa578e9f87378ba690b6b 100644 --- a/qa/qa_rapi.py +++ b/qa/qa_rapi.py @@ -618,6 +618,14 @@ def TestRapiInstanceReinstall(instance): _WaitForRapiJob(_rapi_client.ReinstallInstance(instance["name"])) +def TestRapiInstanceReplaceDisks(instance): + """Test replacing instance disks via RAPI""" + _WaitForRapiJob(_rapi_client.ReplaceInstanceDisks(instance["name"], + mode=constants.REPLACE_DISK_AUTO, disks=[])) + _WaitForRapiJob(_rapi_client.ReplaceInstanceDisks(instance["name"], + mode=constants.REPLACE_DISK_SEC, disks="0")) + + def TestRapiInstanceModify(instance): """Test modifying instance via RAPI""" def _ModifyInstance(**kwargs): diff --git a/test/ganeti.rapi.client_unittest.py b/test/ganeti.rapi.client_unittest.py index dc972d2875cb5c7440de133a5ca633574ad3fd05..1067656d3c1fd3ea2a65df3ea976ba2fac3d49c2 100755 --- a/test/ganeti.rapi.client_unittest.py +++ b/test/ganeti.rapi.client_unittest.py @@ -165,6 +165,11 @@ class TestConstants(unittest.TestCase): self.assertEqual(client.JOB_STATUS_FINALIZED, constants.JOBS_FINALIZED) self.assertEqual(client.JOB_STATUS_ALL, constants.JOB_STATUS_ALL) + # Node evacuation + self.assertEqual(client.NODE_EVAC_PRI, constants.IALLOCATOR_NEVAC_PRI) + self.assertEqual(client.NODE_EVAC_SEC, constants.IALLOCATOR_NEVAC_SEC) + self.assertEqual(client.NODE_EVAC_ALL, constants.IALLOCATOR_NEVAC_ALL) + # Legacy name self.assertEqual(client.JOB_STATUS_WAITLOCK, constants.JOB_STATUS_WAITING) @@ -669,24 +674,21 @@ class GanetiRapiClientTests(testutils.GanetiTestCase): def testReplaceInstanceDisks(self): self.rapi.AddResponse("999") job_id = self.client.ReplaceInstanceDisks("instance-name", - disks=[0, 1], dry_run=True, iallocator="hail") + disks=[0, 1], iallocator="hail") self.assertEqual(999, job_id) self.assertHandler(rlib2.R_2_instances_name_replace_disks) self.assertItems(["instance-name"]) self.assertQuery("disks", ["0,1"]) self.assertQuery("mode", ["replace_auto"]) self.assertQuery("iallocator", ["hail"]) - self.assertDryRun() self.rapi.AddResponse("1000") job_id = self.client.ReplaceInstanceDisks("instance-bar", - disks=[1], mode="replace_on_secondary", remote_node="foo-node", - dry_run=True) + disks=[1], mode="replace_on_secondary", remote_node="foo-node") self.assertEqual(1000, job_id) self.assertItems(["instance-bar"]) self.assertQuery("disks", ["1"]) self.assertQuery("remote_node", ["foo-node"]) - self.assertDryRun() self.rapi.AddResponse("5175") self.assertEqual(5175, self.client.ReplaceInstanceDisks("instance-moo")) @@ -863,11 +865,16 @@ class GanetiRapiClientTests(testutils.GanetiTestCase): self.rapi.AddResponse(serializer.DumpJson([rlib2._NODE_EVAC_RES1])) self.rapi.AddResponse("8888") - job_id = self.client.EvacuateNode("node-3", iallocator="hail", dry_run=True) + job_id = self.client.EvacuateNode("node-3", iallocator="hail", dry_run=True, + mode=constants.IALLOCATOR_NEVAC_ALL, + early_release=True) self.assertEqual(8888, job_id) self.assertItems(["node-3"]) - self.assertEqual(serializer.LoadJson(self.rapi.GetLastRequestData()), - { "iallocator": "hail", }) + self.assertEqual(serializer.LoadJson(self.rapi.GetLastRequestData()), { + "iallocator": "hail", + "mode": "all", + "early_release": True, + }) self.assertDryRun() self.assertRaises(client.GanetiApiError, @@ -881,34 +888,26 @@ class GanetiRapiClientTests(testutils.GanetiTestCase): "node-4", accept_old=False) self.assertEqual(self.rapi.CountPending(), 0) - self.rapi.AddResponse(serializer.DumpJson([])) - self.assertRaises(client.GanetiApiError, self.client.EvacuateNode, - "node-4", accept_old=True) - self.assertEqual(self.rapi.CountPending(), 0) - - self.rapi.AddResponse(serializer.DumpJson([])) - self.assertRaises(client.GanetiApiError, self.client.EvacuateNode, - "node-4", accept_old=True, primary=True) - self.assertEqual(self.rapi.CountPending(), 0) + for mode in [client.NODE_EVAC_PRI, client.NODE_EVAC_ALL]: + self.rapi.AddResponse(serializer.DumpJson([])) + self.assertRaises(client.GanetiApiError, self.client.EvacuateNode, + "node-4", accept_old=True, mode=mode) + self.assertEqual(self.rapi.CountPending(), 0) self.rapi.AddResponse(serializer.DumpJson([])) - self.assertRaises(client.GanetiApiError, self.client.EvacuateNode, - "node-4", accept_old=True, secondary=False) + self.rapi.AddResponse(serializer.DumpJson("21533")) + result = self.client.EvacuateNode("node-3", iallocator="hail", + dry_run=True, accept_old=True, + mode=client.NODE_EVAC_SEC, + early_release=True) + self.assertEqual(result, "21533") + self.assertItems(["node-3"]) + self.assertQuery("iallocator", ["hail"]) + self.assertQuery("early_release", ["1"]) + self.assertFalse(self.rapi.GetLastRequestData()) + self.assertDryRun() self.assertEqual(self.rapi.CountPending(), 0) - for sec in [True, None]: - self.rapi.AddResponse(serializer.DumpJson([])) - self.rapi.AddResponse(serializer.DumpJson([["res", "foo"]])) - result = self.client.EvacuateNode("node-3", iallocator="hail", - dry_run=True, accept_old=True, - primary=False, secondary=sec) - self.assertEqual(result, [["res", "foo"]]) - self.assertItems(["node-3"]) - self.assertQuery("iallocator", ["hail"]) - self.assertFalse(self.rapi.GetLastRequestData()) - self.assertDryRun() - self.assertEqual(self.rapi.CountPending(), 0) - def testMigrateNode(self): self.rapi.AddResponse(serializer.DumpJson([])) self.rapi.AddResponse("1111") diff --git a/test/ganeti.rapi.rlib2_unittest.py b/test/ganeti.rapi.rlib2_unittest.py index 6584c38e8cd4328569d30c84bed826cacbbd2d28..de82745567ae868a79cee2ec0f746a712bb5bc13 100755 --- a/test/ganeti.rapi.rlib2_unittest.py +++ b/test/ganeti.rapi.rlib2_unittest.py @@ -529,6 +529,14 @@ class TestParseInstanceReplaceDisksRequest(unittest.TestCase): self.assertFalse(hasattr(op, "iallocator")) self.assertFalse(hasattr(op, "disks")) + def testNoDisks(self): + self.assertRaises(http.HttpBadRequest, self.Parse, "inst20661", {}) + + for disks in [None, "", {}]: + self.assertRaises(http.HttpBadRequest, self.Parse, "inst20661", { + "disks": disks, + }) + def testWrong(self): self.assertRaises(http.HttpBadRequest, self.Parse, "inst", { "mode": constants.REPLACE_DISK_AUTO,