From 05e733b4a872c1e689f50eb7cd7166aaafe30ae9 Mon Sep 17 00:00:00 2001 From: Michael Hanselmann <hansmi@google.com> Date: Thu, 20 Sep 2012 18:47:28 +0200 Subject: [PATCH] Add new module for virtual clusters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This module will take care of managing paths for virtual clusters. Unittests are included (100% coverage). Signed-off-by: Michael Hanselmann <hansmi@google.com> Reviewed-by: RenΓ© Nussbaumer <rn@google.com> --- Makefile.am | 2 + lib/vcluster.py | 244 +++++++++++++++++++++++++++++++ test/ganeti.vcluster_unittest.py | 237 ++++++++++++++++++++++++++++++ 3 files changed, 483 insertions(+) create mode 100644 lib/vcluster.py create mode 100755 test/ganeti.vcluster_unittest.py diff --git a/Makefile.am b/Makefile.am index 58daa7a58..0cd3cfab7 100644 --- a/Makefile.am +++ b/Makefile.am @@ -240,6 +240,7 @@ pkgpython_PYTHON = \ lib/ssh.py \ lib/storage.py \ lib/uidpool.py \ + lib/vcluster.py \ lib/workerpool.py client_PYTHON = \ @@ -913,6 +914,7 @@ python_tests = \ test/ganeti.utils.wrapper_unittest.py \ test/ganeti.utils.x509_unittest.py \ test/ganeti.utils_unittest.py \ + test/ganeti.vcluster_unittest.py \ test/ganeti.workerpool_unittest.py \ test/qa.qa_config_unittest.py \ test/cfgupgrade_unittest.py \ diff --git a/lib/vcluster.py b/lib/vcluster.py new file mode 100644 index 000000000..40b67776e --- /dev/null +++ b/lib/vcluster.py @@ -0,0 +1,244 @@ +# +# + +# Copyright (C) 2012 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. + + +"""Module containing utilities for virtual clusters. + + +""" + +import os + +from ganeti import compat + + +_VIRT_PATH_PREFIX = "/###-VIRTUAL-PATH-###," +_ROOTDIR_ENVNAME = "GANETI_ROOTDIR" +_HOSTNAME_ENVNAME = "GANETI_HOSTNAME" + + +def _GetRootDirectory(envname): + """Retrieves root directory from an environment variable. + + @type envname: string + @param envname: Environment variable name + @rtype: string + @return: Root directory (can be empty) + + """ + path = os.getenv(envname) + + if path: + if not os.path.isabs(path): + raise RuntimeError("Root directory in '%s' must be absolute: %s" % + (envname, path)) + return os.path.normpath(path) + + return "" + + +def _GetHostname(envname): + """Retrieves virtual hostname from an environment variable. + + @type envname: string + @param envname: Environment variable name + @rtype: string + @return: Host name (can be empty) + + """ + return os.getenv(envname, default="") + + +def _CheckHostname(hostname): + """Very basic check for hostnames. + + @type hostname: string + @param hostname: Hostname + + """ + if os.path.basename(hostname) != hostname: + raise RuntimeError("Hostname '%s' can not be used for a file system" + " path" % hostname) + + +def _PreparePaths(rootdir, hostname): + """Checks if the root directory and hostname are acceptable. + + @type rootdir: string + @param rootdir: Root directory (from environment) + @type hostname: string + @param hostname: Hostname (from environment) + @rtype: tuple; (string, string, string or None) + @return: Tuple containing cluster-global root directory, node root directory + and virtual hostname + + """ + if bool(rootdir) ^ bool(hostname): + raise RuntimeError("Both root directory and hostname must be specified" + " using the environment variables %s and %s" % + (_ROOTDIR_ENVNAME, _HOSTNAME_ENVNAME)) + + if rootdir: + assert rootdir == os.path.normpath(rootdir) + + _CheckHostname(hostname) + + if os.path.basename(rootdir) != hostname: + raise RuntimeError("Last component of root directory ('%s') must match" + " hostname ('%s')" % (rootdir, hostname)) + + return (os.path.dirname(rootdir), rootdir, hostname) + else: + return ("", "", None) + + +(_VIRT_BASEDIR, _VIRT_NODEROOT, _VIRT_HOSTNAME) = \ + _PreparePaths(_GetRootDirectory(_ROOTDIR_ENVNAME), + _GetHostname(_HOSTNAME_ENVNAME)) + + +assert (compat.all([_VIRT_BASEDIR, _VIRT_NODEROOT, _VIRT_HOSTNAME]) or + not compat.any([_VIRT_BASEDIR, _VIRT_NODEROOT, _VIRT_HOSTNAME])) + + +def GetVirtualHostname(): + """Returns the virtual hostname. + + @rtype: string or L{None} + + """ + return _VIRT_HOSTNAME + + +def _MakeNodeRoot(base, node_name): + """Appends a node name to the base directory. + + """ + _CheckHostname(node_name) + return os.path.normpath("%s/%s" % (base, node_name)) + + +def ExchangeNodeRoot(node_name, filename, + _basedir=_VIRT_BASEDIR, _noderoot=_VIRT_NODEROOT): + """Replaces the node-specific root directory in a path. + + Replaces it with the root directory for another node. + + """ + if _basedir: + pure = _RemoveNodePrefix(filename, _noderoot=_noderoot) + result = "%s/%s" % (_MakeNodeRoot(_basedir, node_name), pure) + else: + result = filename + + return os.path.normpath(result) + + +def EnvironmentForHost(hostname, _basedir=_VIRT_BASEDIR): + """Returns the environment variables for a host. + + """ + if _basedir: + return { + _ROOTDIR_ENVNAME: _MakeNodeRoot(_basedir, hostname), + _HOSTNAME_ENVNAME: hostname, + } + else: + return {} + + +def AddNodePrefix(path, _noderoot=_VIRT_NODEROOT): + """Adds a node-specific prefix to a path in a virtual cluster. + + Returned path includes user-specified root directory if specified in + environment. + + """ + assert os.path.isabs(path) + + if _noderoot: + result = "%s/%s" % (_noderoot, path) + else: + result = path + + assert os.path.isabs(result) + + return os.path.normpath(result) + + +def _RemoveNodePrefix(path, _noderoot=_VIRT_NODEROOT): + """Removes the node-specific prefix from a path. + + """ + assert os.path.isabs(path) + + norm_path = os.path.normpath(path) + + if _noderoot: + # Make sure path is actually below node root + norm_root = os.path.normpath(_noderoot) + root_with_sep = "%s%s" % (norm_root, os.sep) + prefix = os.path.commonprefix([root_with_sep, norm_path]) + + if prefix == root_with_sep: + result = norm_path[len(norm_root):] + else: + raise RuntimeError("Path '%s' is not below node root '%s'" % + (path, _noderoot)) + else: + result = norm_path + + assert os.path.isabs(result) + + return result + + +def MakeVirtualPath(path, _noderoot=_VIRT_NODEROOT): + """Virtualizes a path. + + A path is "virtualized" by stripping it of its node-specific directory and + prepending a prefix (L{_VIRT_PATH_PREFIX}). Use L{LocalizeVirtualPath} to + undo the process. + + """ + assert os.path.isabs(path) + + if _noderoot: + return _VIRT_PATH_PREFIX + _RemoveNodePrefix(path, _noderoot=_noderoot) + else: + return path + + +def LocalizeVirtualPath(path, _noderoot=_VIRT_NODEROOT): + """Localizes a virtual path. + + A "virtualized" path consists of a prefix (L{LocalizeVirtualPath}) and a + local path. This function adds the node-specific directory to the local path. + + """ + assert os.path.isabs(path) + + if _noderoot: + if path.startswith(_VIRT_PATH_PREFIX): + return AddNodePrefix(path[len(_VIRT_PATH_PREFIX):], _noderoot=_noderoot) + else: + raise RuntimeError("Path '%s' is not a virtual path" % path) + else: + return path diff --git a/test/ganeti.vcluster_unittest.py b/test/ganeti.vcluster_unittest.py new file mode 100755 index 000000000..9808fe46b --- /dev/null +++ b/test/ganeti.vcluster_unittest.py @@ -0,0 +1,237 @@ +#!/usr/bin/python +# + +# Copyright (C) 2012 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 testing ganeti.vcluster""" + +import os +import unittest + +from ganeti import utils +from ganeti import compat +from ganeti import vcluster + +import testutils + + +_ENV_DOES_NOT_EXIST = "GANETI_TEST_DOES_NOT_EXIST" +_ENV_TEST = "GANETI_TESTVAR" + + +class _EnvVarTest(testutils.GanetiTestCase): + def setUp(self): + testutils.GanetiTestCase.setUp(self) + + os.environ.pop(_ENV_DOES_NOT_EXIST, None) + os.environ.pop(_ENV_TEST, None) + + +class TestGetRootDirectory(_EnvVarTest): + def test(self): + assert os.getenv(_ENV_TEST) is None + + self.assertEqual(vcluster._GetRootDirectory(_ENV_DOES_NOT_EXIST), "") + self.assertEqual(vcluster._GetRootDirectory(_ENV_TEST), "") + + # Absolute path + os.environ[_ENV_TEST] = "/tmp/xy11" + self.assertEqual(vcluster._GetRootDirectory(_ENV_TEST), "/tmp/xy11") + + # Relative path + os.environ[_ENV_TEST] = "foobar" + self.assertRaises(RuntimeError, vcluster._GetRootDirectory, _ENV_TEST) + + + +class TestGetHostname(_EnvVarTest): + def test(self): + assert os.getenv(_ENV_TEST) is None + + self.assertEqual(vcluster._GetRootDirectory(_ENV_DOES_NOT_EXIST), "") + self.assertEqual(vcluster._GetRootDirectory(_ENV_TEST), "") + + os.environ[_ENV_TEST] = "some.host.example.com" + self.assertEqual(vcluster._GetHostname(_ENV_TEST), "some.host.example.com") + + +class TestCheckHostname(_EnvVarTest): + def test(self): + for i in ["/", "/tmp"]: + self.assertRaises(RuntimeError, vcluster._CheckHostname, i) + + +class TestPreparePaths(_EnvVarTest): + def testInvalidParameters(self): + self.assertRaises(RuntimeError, vcluster._PreparePaths, + None, "host.example.com") + self.assertRaises(RuntimeError, vcluster._PreparePaths, + "/tmp/", "") + + def testNonNormalizedRootDir(self): + self.assertRaises(AssertionError, vcluster._PreparePaths, + "/tmp////xyz//", "host.example.com") + + def testInvalidHostname(self): + self.assertRaises(RuntimeError, vcluster._PreparePaths, "/tmp", "/") + + def testPathHostnameMismatch(self): + self.assertRaises(RuntimeError, vcluster._PreparePaths, + "/tmp/host.example.com", "server.example.com") + + def testNoVirtCluster(self): + for i in ["", None]: + self.assertEqual(vcluster._PreparePaths(i, i), ("", "", None)) + + def testVirtCluster(self): + self.assertEqual(vcluster._PreparePaths("/tmp/host.example.com", + "host.example.com"), + ("/tmp", "/tmp/host.example.com", "host.example.com")) + + +class TestMakeNodeRoot(unittest.TestCase): + def test(self): + self.assertRaises(RuntimeError, vcluster._MakeNodeRoot, "/tmp", "/") + + for i in ["/tmp", "/tmp/", "/tmp///"]: + self.assertEqual(vcluster._MakeNodeRoot(i, "other.example.com"), + "/tmp/other.example.com") + + +class TestEnvironmentForHost(unittest.TestCase): + def test(self): + self.assertEqual(vcluster.EnvironmentForHost("host.example.com", + _basedir=None), + {}) + for i in ["host.example.com", "other.example.com"]: + self.assertEqual(vcluster.EnvironmentForHost(i, _basedir="/tmp"), { + vcluster._ROOTDIR_ENVNAME: "/tmp/%s" % i, + vcluster._HOSTNAME_ENVNAME: i, + }) + + +class TestExchangeNodeRoot(unittest.TestCase): + def test(self): + result = vcluster.ExchangeNodeRoot("node1.example.com", "/tmp/file", + _basedir=None, _noderoot=None) + self.assertEqual(result, "/tmp/file") + + self.assertRaises(RuntimeError, vcluster.ExchangeNodeRoot, + "node1.example.com", "/tmp/node1.example.com", + _basedir="/tmp", + _noderoot="/tmp/nodeZZ.example.com") + + result = vcluster.ExchangeNodeRoot("node2.example.com", + "/tmp/node1.example.com/file", + _basedir="/tmp", + _noderoot="/tmp/node1.example.com") + self.assertEqual(result, "/tmp/node2.example.com/file") + + +class TestAddNodePrefix(unittest.TestCase): + def testRelativePath(self): + self.assertRaises(AssertionError, vcluster.AddNodePrefix, + "foobar", _noderoot=None) + + def testRelativeNodeRoot(self): + self.assertRaises(AssertionError, vcluster.AddNodePrefix, + "/tmp", _noderoot="foobar") + + def test(self): + path = vcluster.AddNodePrefix("/file/path", + _noderoot="/tmp/node1.example.com/") + self.assertEqual(path, "/tmp/node1.example.com/file/path") + + self.assertEqual(vcluster.AddNodePrefix("/file/path", _noderoot=""), + "/file/path") + + +class TestRemoveNodePrefix(unittest.TestCase): + def testRelativePath(self): + self.assertRaises(AssertionError, vcluster._RemoveNodePrefix, + "foobar", _noderoot=None) + + def testOutsideNodeRoot(self): + self.assertRaises(RuntimeError, vcluster._RemoveNodePrefix, + "/file/path", _noderoot="/tmp/node1.example.com") + self.assertRaises(RuntimeError, vcluster._RemoveNodePrefix, + "/tmp/xyzfile", _noderoot="/tmp/xyz") + + def test(self): + path = vcluster._RemoveNodePrefix("/tmp/node1.example.com/file/path", + _noderoot="/tmp/node1.example.com") + self.assertEqual(path, "/file/path") + + path = vcluster._RemoveNodePrefix("/file/path", _noderoot=None) + self.assertEqual(path, "/file/path") + + +class TestMakeVirtualPath(unittest.TestCase): + def testRelativePath(self): + self.assertRaises(AssertionError, vcluster.MakeVirtualPath, + "foobar", _noderoot=None) + + def testOutsideNodeRoot(self): + self.assertRaises(RuntimeError, vcluster.MakeVirtualPath, + "/file/path", _noderoot="/tmp/node1.example.com") + + def testWithNodeRoot(self): + path = vcluster.MakeVirtualPath("/tmp/node1.example.com/tmp/file", + _noderoot="/tmp/node1.example.com") + self.assertEqual(path, "%s/tmp/file" % vcluster._VIRT_PATH_PREFIX) + + def testNormal(self): + self.assertEqual(vcluster.MakeVirtualPath("/tmp/file", _noderoot=None), + "/tmp/file") + + +class TestLocalizeVirtualPath(unittest.TestCase): + def testWrongPrefix(self): + self.assertRaises(RuntimeError, vcluster.LocalizeVirtualPath, + "/tmp/some/path", _noderoot="/tmp/node1.example.com") + + def testCorrectPrefixRelativePath(self): + self.assertRaises(AssertionError, vcluster.LocalizeVirtualPath, + vcluster._VIRT_PATH_PREFIX + "foobar", + _noderoot="/tmp/node1.example.com") + + def testWithNodeRoot(self): + lvp = vcluster.LocalizeVirtualPath + + virtpath1 = "%s/tmp/file" % vcluster._VIRT_PATH_PREFIX + virtpath2 = "%s////tmp////file" % vcluster._VIRT_PATH_PREFIX + + for i in [virtpath1, virtpath2]: + result = lvp(i, _noderoot="/tmp/node1.example.com") + self.assertEqual(result, "/tmp/node1.example.com/tmp/file") + + def testNormal(self): + self.assertEqual(vcluster.LocalizeVirtualPath("/tmp/file", _noderoot=None), + "/tmp/file") + + +class TestVirtualPathPrefix(unittest.TestCase): + def test(self): + self.assertTrue(os.path.isabs(vcluster._VIRT_PATH_PREFIX)) + self.assertEqual(os.path.normcase(vcluster._VIRT_PATH_PREFIX), + vcluster._VIRT_PATH_PREFIX) + + +if __name__ == "__main__": + testutils.GanetiTestProgram() -- GitLab