diff --git a/lib/cmdlib.py b/lib/cmdlib.py index 8eeb471829db2b56a7e238e1eea98331de72db8e..51514d7e00dc8cdf38beb533e5a6c574499545d3 100644 --- a/lib/cmdlib.py +++ b/lib/cmdlib.py @@ -8822,6 +8822,66 @@ class LUQueryExports(NoHooksLU): return result +class LUPrepareExport(NoHooksLU): + """Prepares an instance for an export and returns useful information. + + """ + _OP_REQP = ["instance_name", "mode"] + REQ_BGL = False + + def CheckArguments(self): + """Check the arguments. + + """ + if self.op.mode not in constants.EXPORT_MODES: + raise errors.OpPrereqError("Invalid export mode %r" % self.op.mode, + errors.ECODE_INVAL) + + def ExpandNames(self): + self._ExpandAndLockInstance() + + def CheckPrereq(self): + """Check prerequisites. + + """ + instance_name = self.op.instance_name + + self.instance = self.cfg.GetInstanceInfo(instance_name) + assert self.instance is not None, \ + "Cannot retrieve locked instance %s" % self.op.instance_name + _CheckNodeOnline(self, self.instance.primary_node) + + self._cds = _GetClusterDomainSecret() + + def Exec(self, feedback_fn): + """Prepares an instance for an export. + + """ + instance = self.instance + + if self.op.mode == constants.EXPORT_MODE_REMOTE: + salt = utils.GenerateSecret(8) + + feedback_fn("Generating X509 certificate on %s" % instance.primary_node) + result = self.rpc.call_x509_cert_create(instance.primary_node, + constants.RIE_CERT_VALIDITY) + result.Raise("Can't create X509 key and certificate on %s" % result.node) + + (name, cert_pem) = result.payload + + cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, + cert_pem) + + return { + "handshake": masterd.instance.ComputeRemoteExportHandshake(self._cds), + "x509_key_name": (name, utils.Sha1Hmac(self._cds, name, salt=salt), + salt), + "x509_ca": utils.SignX509Certificate(cert, self._cds, salt), + } + + return None + + class LUExportInstance(LogicalUnit): """Export an instance to an image in the cluster. diff --git a/lib/constants.py b/lib/constants.py index 02e1a10ed1946fc3e0df1431d243e24b9ea7407d..df9a534fa264489bcd251f61ad1400649dd30d0f 100644 --- a/lib/constants.py +++ b/lib/constants.py @@ -336,6 +336,13 @@ LOCKS_APPEND = 'append' INSTANCE_CREATE = "create" INSTANCE_IMPORT = "import" +# Remote import/export handshake message and version +RIE_VERSION = 0 +RIE_HANDSHAKE = "Hi, I'm Ganeti" + +# Remote import/export certificate validity in seconds +RIE_CERT_VALIDITY = 24 * 60 * 60 + DISK_TEMPLATES = frozenset([DT_DISKLESS, DT_PLAIN, DT_DRBD8, DT_FILE]) diff --git a/lib/masterd/instance.py b/lib/masterd/instance.py index 7e95b0d5464d3fc99872f871075ff00338d9a71d..f48e01b13284ba51772be53ea7cae1c2c949209d 100644 --- a/lib/masterd/instance.py +++ b/lib/masterd/instance.py @@ -1105,3 +1105,50 @@ class ExportInstanceHelper: assert len(self._removed_snaps) == len(self._instance.disks) for idx in range(len(self._instance.disks)): self._RemoveSnapshot(idx) + + +def _GetImportExportHandshakeMessage(version): + """Returns the handshake message for a RIE protocol version. + + @type version: number + + """ + return "%s:%s" % (version, constants.RIE_HANDSHAKE) + + +def ComputeRemoteExportHandshake(cds): + """Computes the remote import/export handshake. + + @type cds: string + @param cds: Cluster domain secret + + """ + salt = utils.GenerateSecret(8) + msg = _GetImportExportHandshakeMessage(constants.RIE_VERSION) + return (constants.RIE_VERSION, utils.Sha1Hmac(cds, msg, salt=salt), salt) + + +def CheckRemoteExportHandshake(cds, handshake): + """Checks the handshake of a remote import/export. + + @type cds: string + @param cds: Cluster domain secret + @type handshake: sequence + @param handshake: Handshake sent by remote peer + + """ + try: + (version, hmac_digest, hmac_salt) = handshake + except (TypeError, ValueError), err: + return "Invalid data: %s" % err + + if not utils.VerifySha1Hmac(cds, _GetImportExportHandshakeMessage(version), + hmac_digest, salt=hmac_salt): + return "Hash didn't match, clusters don't share the same domain secret" + + if version != constants.RIE_VERSION: + return ("Clusters don't have the same remote import/export protocol" + " (local=%s, remote=%s)" % + (constants.RIE_VERSION, version)) + + return None diff --git a/lib/mcpu.py b/lib/mcpu.py index c601c51dc28aa326c1d0d2b635ac03d9126169ca..319878006a920d62de2d86af848661ac6a50c287 100644 --- a/lib/mcpu.py +++ b/lib/mcpu.py @@ -210,6 +210,7 @@ class Processor(object): opcodes.OpDiagnoseOS: cmdlib.LUDiagnoseOS, # exports lu opcodes.OpQueryExports: cmdlib.LUQueryExports, + opcodes.OpPrepareExport: cmdlib.LUPrepareExport, opcodes.OpExportInstance: cmdlib.LUExportInstance, opcodes.OpRemoveExport: cmdlib.LURemoveExport, # tags lu diff --git a/lib/opcodes.py b/lib/opcodes.py index c7d00be0550c056be2384867ff814927dd764efa..149dbabaa033e12f9eb6dc402c59410bbe49247e 100644 --- a/lib/opcodes.py +++ b/lib/opcodes.py @@ -653,6 +653,20 @@ class OpQueryExports(OpCode): __slots__ = ["nodes", "use_locking"] +class OpPrepareExport(OpCode): + """Prepares an instance export. + + @ivar instance_name: Instance name + @ivar mode: Export mode (one of L{constants.EXPORT_MODES}) + + """ + OP_ID = "OP_BACKUP_PREPARE" + OP_DSC_FIELD = "instance_name" + __slots__ = [ + "instance_name", "mode", + ] + + class OpExportInstance(OpCode): """Export an instance.""" OP_ID = "OP_BACKUP_EXPORT" diff --git a/test/ganeti.masterd.instance_unittest.py b/test/ganeti.masterd.instance_unittest.py index 05b0183ab5a5d98fdc37d0d60bd25717a99a2694..0c74defcf8871d9548df1c9882fa5f2d9506ba90 100755 --- a/test/ganeti.masterd.instance_unittest.py +++ b/test/ganeti.masterd.instance_unittest.py @@ -25,11 +25,13 @@ import os import sys import unittest +from ganeti import constants from ganeti import utils from ganeti import masterd from ganeti.masterd.instance import \ - ImportExportTimeouts, _TimeoutExpired, _DiskImportExportBase + ImportExportTimeouts, _TimeoutExpired, _DiskImportExportBase, \ + ComputeRemoteExportHandshake, CheckRemoteExportHandshake import testutils @@ -56,5 +58,33 @@ class TestMisc(unittest.TestCase): None, None, None, None, None, None, None) +class TestRieHandshake(unittest.TestCase): + def test(self): + cds = "cd-secret" + hs = ComputeRemoteExportHandshake(cds) + self.assertEqual(len(hs), 3) + self.assertEqual(hs[0], constants.RIE_VERSION) + + self.assertEqual(CheckRemoteExportHandshake(cds, hs), None) + + def testCheckErrors(self): + self.assert_(CheckRemoteExportHandshake(None, None)) + self.assert_(CheckRemoteExportHandshake("", "")) + self.assert_(CheckRemoteExportHandshake("", ("xyz", "foo"))) + + def testCheckWrongHash(self): + cds = "cd-secret999" + self.assert_(CheckRemoteExportHandshake(cds, (0, "fakehash", "xyz"))) + + def testCheckWrongVersion(self): + version = 14887 + self.assertNotEqual(version, constants.RIE_VERSION) + cds = "c28ac99" + salt = "a19cf8cc06" + msg = "%s:%s" % (version, constants.RIE_HANDSHAKE) + hs = (version, utils.Sha1Hmac(cds, msg, salt=salt), salt) + self.assert_(CheckRemoteExportHandshake(cds, hs)) + + if __name__ == "__main__": testutils.GanetiTestProgram()