diff --git a/Makefile.am b/Makefile.am index b3414da5aac3ccf44df065600f4daf9a0fb1a4eb..24d70bc40679998ca1ce8acb6011b068cef80cdb 100644 --- a/Makefile.am +++ b/Makefile.am @@ -421,7 +421,8 @@ storage_PYTHON = \ lib/storage/drbd.py \ lib/storage/drbd_info.py \ lib/storage/drbd_cmdgen.py \ - lib/storage/filestorage.py + lib/storage/filestorage.py \ + lib/storage/gluster.py rapi_PYTHON = \ lib/rapi/__init__.py \ @@ -1445,6 +1446,7 @@ python_tests = \ test/py/ganeti.storage.container_unittest.py \ test/py/ganeti.storage.drbd_unittest.py \ test/py/ganeti.storage.filestorage_unittest.py \ + test/py/ganeti.storage.gluster_unittest.py \ test/py/ganeti.tools.burnin_unittest.py \ test/py/ganeti.tools.ensure_dirs_unittest.py \ test/py/ganeti.tools.node_daemon_setup_unittest.py \ diff --git a/lib/cmdlib/instance.py b/lib/cmdlib/instance.py index 9117f8c1ba22cd9c48a6c47d1d28d103fa784031..4f512a4e68497a27df2467f10b7c441f90362cf7 100644 --- a/lib/cmdlib/instance.py +++ b/lib/cmdlib/instance.py @@ -849,7 +849,8 @@ class LUInstanceCreate(LogicalUnit): # build the full file storage dir path joinargs = [] - if self.op.disk_template == constants.DT_SHARED_FILE: + if self.op.disk_template in (constants.DT_SHARED_FILE, + constants.DT_GLUSTER): get_fsd_fn = self.cfg.GetSharedFileStorageDir else: get_fsd_fn = self.cfg.GetFileStorageDir @@ -1754,7 +1755,7 @@ class LUInstanceMove(LogicalUnit): for idx, dsk in enumerate(self.instance.disks): if dsk.dev_type not in (constants.DT_PLAIN, constants.DT_FILE, - constants.DT_SHARED_FILE): + constants.DT_SHARED_FILE, constants.DT_GLUSTER): raise errors.OpPrereqError("Instance disk %d has a complex layout," " cannot copy" % idx, errors.ECODE_STATE) diff --git a/lib/cmdlib/instance_storage.py b/lib/cmdlib/instance_storage.py index eb022ce09fcf90ad2d52f23674be10c150e6d9df..641b25383cc3c95016e009ef9eabe5f7a5359b26 100644 --- a/lib/cmdlib/instance_storage.py +++ b/lib/cmdlib/instance_storage.py @@ -285,6 +285,7 @@ def ComputeDiskSizePerVG(disk_template, disks): constants.DT_DRBD8: _compute(disks, constants.DRBD_META_SIZE), constants.DT_FILE: {}, constants.DT_SHARED_FILE: {}, + constants.DT_GLUSTER: {}, } if disk_template not in req_size_dict: diff --git a/lib/masterd/instance.py b/lib/masterd/instance.py index 8cac3a3af6e5159bbd64ca78277b4f220e4a297f..1f90e7e4f6b49808f7d75451f4b3568d3337555c 100644 --- a/lib/masterd/instance.py +++ b/lib/masterd/instance.py @@ -1648,6 +1648,7 @@ def ComputeDiskSize(disk_template, disks): sum(d[constants.IDISK_SIZE] + constants.DRBD_META_SIZE for d in disks), constants.DT_FILE: sum(d[constants.IDISK_SIZE] for d in disks), constants.DT_SHARED_FILE: sum(d[constants.IDISK_SIZE] for d in disks), + constants.DT_GLUSTER: sum(d[constants.IDISK_SIZE] for d in disks), constants.DT_BLOCK: 0, constants.DT_RBD: sum(d[constants.IDISK_SIZE] for d in disks), constants.DT_EXT: sum(d[constants.IDISK_SIZE] for d in disks), diff --git a/lib/objects.py b/lib/objects.py index ea88a9d6ee8cb4a88b3a9ae92ed79d04aa7aee6c..17d27bf03d40c798f9604a52eecff6aeabda750a 100644 --- a/lib/objects.py +++ b/lib/objects.py @@ -586,7 +586,8 @@ class Disk(ConfigObject): """ if self.dev_type in [constants.DT_PLAIN, constants.DT_FILE, constants.DT_BLOCK, constants.DT_RBD, - constants.DT_EXT, constants.DT_SHARED_FILE]: + constants.DT_EXT, constants.DT_SHARED_FILE, + constants.DT_GLUSTER]: result = [node_uuid] elif self.dev_type in constants.DTS_DRBD: result = [self.logical_id[0], self.logical_id[1]] @@ -663,7 +664,7 @@ class Disk(ConfigObject): """ if self.dev_type in (constants.DT_PLAIN, constants.DT_FILE, constants.DT_RBD, constants.DT_EXT, - constants.DT_SHARED_FILE): + constants.DT_SHARED_FILE, constants.DT_GLUSTER): self.size += amount elif self.dev_type == constants.DT_DRBD8: if self.children: diff --git a/lib/storage/bdev.py b/lib/storage/bdev.py index 8d0c35b460b520b80624086542db668fcd2c473d..075b24f1994823d5fc128dea3e3296d2c647ad81 100644 --- a/lib/storage/bdev.py +++ b/lib/storage/bdev.py @@ -39,6 +39,7 @@ from ganeti import serializer from ganeti.storage import base from ganeti.storage import drbd from ganeti.storage.filestorage import FileStorage +from ganeti.storage.gluster import GlusterStorage class RbdShowmappedJsonError(Exception): @@ -1667,6 +1668,7 @@ DEV_MAP = { constants.DT_EXT: ExtStorageDevice, constants.DT_FILE: FileStorage, constants.DT_SHARED_FILE: FileStorage, + constants.DT_GLUSTER: GlusterStorage, } """Map disk types to disk type classes. diff --git a/lib/storage/gluster.py b/lib/storage/gluster.py new file mode 100644 index 0000000000000000000000000000000000000000..e90fdacecb4e0383bc49252c6e5ac6cf283a31e0 --- /dev/null +++ b/lib/storage/gluster.py @@ -0,0 +1,158 @@ +# +# + +# Copyright (C) 2013 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. + +"""Gluster storage class. + +This class is very similar to FileStorage, given that Gluster when mounted +behaves essentially like a regular file system. Unlike RBD, there are no +special provisions for block device abstractions (yet). + +""" +from ganeti import errors + +from ganeti.storage import base +from ganeti.storage.filestorage import FileDeviceHelper + + +class GlusterStorage(base.BlockDev): + """File device using the Gluster backend. + + This class represents a file storage backend device stored on Gluster. The + system administrator must mount the Gluster device himself at boot time before + Ganeti is run. + + The unique_id for the file device is a (file_driver, file_path) tuple. + + """ + def __init__(self, unique_id, children, size, params, dyn_params): + """Initalizes a file device backend. + + """ + if children: + base.ThrowError("Invalid setup for file device") + super(GlusterStorage, self).__init__(unique_id, children, size, params, + dyn_params) + if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2: + raise ValueError("Invalid configuration data %s" % str(unique_id)) + self.driver = unique_id[0] + self.dev_path = unique_id[1] + + self.file = FileDeviceHelper(self.dev_path) + + self.Attach() + + def Assemble(self): + """Assemble the device. + + Checks whether the file device exists, raises BlockDeviceError otherwise. + + """ + assert self.attached, "Gluster file assembled without being attached" + self.file.Exists(assert_exists=True) + + def Shutdown(self): + """Shutdown the device. + + """ + + self.file = None + self.dev_path = None + self.attached = False + + def Open(self, force=False): + """Make the device ready for I/O. + + This is a no-op for the file type. + + """ + assert self.attached, "Gluster file opened without being attached" + + def Close(self): + """Notifies that the device will no longer be used for I/O. + + This is a no-op for the file type. + """ + pass + + def Remove(self): + """Remove the file backing the block device. + + @rtype: boolean + @return: True if the removal was successful + + """ + return self.file.Remove() + + def Rename(self, new_id): + """Renames the file. + + """ + # TODO: implement rename for file-based storage + base.ThrowError("Rename is not supported for Gluster storage") + + def Grow(self, amount, dryrun, backingstore, excl_stor): + """Grow the file + + @param amount: the amount (in mebibytes) to grow with + + """ + self.file.Grow(amount, dryrun, backingstore, excl_stor) + + def Attach(self): + """Attach to an existing file. + + Check if this file already exists. + + @rtype: boolean + @return: True if file exists + + """ + self.attached = self.file.Exists() + return self.attached + + def GetActualSize(self): + """Return the actual disk size. + + @note: the device needs to be active when this is called + + """ + return self.file.Size() + + @classmethod + def Create(cls, unique_id, children, size, spindles, params, excl_stor, + dyn_params): + """Create a new file. + + @param size: the size of file in MiB + + @rtype: L{bdev.FileStorage} + @return: an instance of FileStorage + + """ + if excl_stor: + raise errors.ProgrammerError("FileStorage device requested with" + " exclusive_storage") + if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2: + raise ValueError("Invalid configuration data %s" % str(unique_id)) + + dev_path = unique_id[1] + + FileDeviceHelper.Create(dev_path, size) + return GlusterStorage(unique_id, children, size, params, dyn_params) diff --git a/lib/tools/burnin.py b/lib/tools/burnin.py index 190b9fe93652bf4b3548e3f1dc8877db8f9773b4..1863a1bd9c9f0ee71958ccb2b2bef024397d7dc3 100755 --- a/lib/tools/burnin.py +++ b/lib/tools/burnin.py @@ -61,6 +61,7 @@ _SINGLE_NODE_DISK_TEMPLATES = compat.UniqueFrozenset([ constants.DT_SHARED_FILE, constants.DT_EXT, constants.DT_RBD, + constants.DT_GLUSTER ]) _SUPPORTED_DISK_TEMPLATES = compat.UniqueFrozenset([ @@ -71,6 +72,7 @@ _SUPPORTED_DISK_TEMPLATES = compat.UniqueFrozenset([ constants.DT_PLAIN, constants.DT_RBD, constants.DT_SHARED_FILE, + constants.DT_GLUSTER ]) #: Disk templates for which import/export is tested @@ -78,6 +80,7 @@ _IMPEXP_DISK_TEMPLATES = (_SUPPORTED_DISK_TEMPLATES - frozenset([ constants.DT_DISKLESS, constants.DT_FILE, constants.DT_SHARED_FILE, + constants.DT_GLUSTER ])) diff --git a/lib/utils/storage.py b/lib/utils/storage.py index 7cba91f1c5ad42c9ba5d98487d2112ca2a3067f4..bf569534278b01e57b13ad566d15279942dd2390 100644 --- a/lib/utils/storage.py +++ b/lib/utils/storage.py @@ -91,6 +91,8 @@ def _GetDefaultStorageUnitForDiskTemplate(cfg, disk_template): return (storage_type, cluster.file_storage_dir) elif disk_template == constants.DT_SHARED_FILE: return (storage_type, cluster.shared_file_storage_dir) + elif disk_template == constants.DT_GLUSTER: + return (storage_type, constants.GLUSTER_MOUNTPOINT) else: return (storage_type, None) diff --git a/src/Ganeti/Constants.hs b/src/Ganeti/Constants.hs index 6641be6fa95557363f2a9e5d8e26d2fc5f5b4715..da2f6408549bbb130cf4c268505f9283e59a28ad 100644 --- a/src/Ganeti/Constants.hs +++ b/src/Ganeti/Constants.hs @@ -789,6 +789,9 @@ dtRbd = Types.diskTemplateToRaw DTRbd dtExt :: String dtExt = Types.diskTemplateToRaw DTExt +dtGluster :: String +dtGluster = Types.diskTemplateToRaw DTGluster + -- | This is used to order determine the default disk template when -- the list of enabled disk templates is inferred from the current -- state of the cluster. This only happens on an upgrade from a @@ -797,7 +800,8 @@ dtExt = Types.diskTemplateToRaw DTExt diskTemplatePreference :: [String] diskTemplatePreference = map Types.diskTemplateToRaw - [DTBlock, DTDiskless, DTDrbd8, DTExt, DTFile, DTPlain, DTRbd, DTSharedFile] + [DTBlock, DTDiskless, DTDrbd8, DTExt, DTFile, + DTPlain, DTRbd, DTSharedFile, DTGluster] diskTemplates :: FrozenSet String diskTemplates = ConstantUtils.mkSet $ map Types.diskTemplateToRaw [minBound..] @@ -818,7 +822,8 @@ mapDiskTemplateStorageType = (DTFile, StorageFile), (DTDiskless, StorageDiskless), (DTPlain, StorageLvmVg), - (DTRbd, StorageRados)] + (DTRbd, StorageRados), + (DTGluster, StorageFile)] -- | The set of network-mirrored disk templates dtsIntMirror :: FrozenSet String @@ -828,21 +833,22 @@ dtsIntMirror = ConstantUtils.mkSet [dtDrbd8] dtsExtMirror :: FrozenSet String dtsExtMirror = ConstantUtils.mkSet $ - map Types.diskTemplateToRaw [DTDiskless, DTBlock, DTExt, DTSharedFile, DTRbd] + map Types.diskTemplateToRaw + [DTDiskless, DTBlock, DTExt, DTSharedFile, DTRbd, DTGluster] -- | The set of non-lvm-based disk templates dtsNotLvm :: FrozenSet String dtsNotLvm = ConstantUtils.mkSet $ map Types.diskTemplateToRaw - [DTSharedFile, DTDiskless, DTBlock, DTExt, DTFile, DTRbd] + [DTSharedFile, DTDiskless, DTBlock, DTExt, DTFile, DTRbd, DTGluster] -- | The set of disk templates which can be grown dtsGrowable :: FrozenSet String dtsGrowable = ConstantUtils.mkSet $ map Types.diskTemplateToRaw - [DTSharedFile, DTDrbd8, DTPlain, DTExt, DTFile, DTRbd] + [DTSharedFile, DTDrbd8, DTPlain, DTExt, DTFile, DTRbd, DTGluster] -- | The set of disk templates that allow adoption dtsMayAdopt :: FrozenSet String @@ -860,7 +866,8 @@ dtsMirrored = dtsIntMirror `ConstantUtils.union` dtsExtMirror -- | The set of file based disk templates dtsFilebased :: FrozenSet String dtsFilebased = - ConstantUtils.mkSet $ map Types.diskTemplateToRaw [DTSharedFile, DTFile] + ConstantUtils.mkSet $ map Types.diskTemplateToRaw + [DTSharedFile, DTFile, DTGluster] -- | The set of disk templates that can be moved by copying -- @@ -878,7 +885,7 @@ dtsExclStorage = ConstantUtils.mkSet $ map Types.diskTemplateToRaw [DTPlain] dtsNoFreeSpaceCheck :: FrozenSet String dtsNoFreeSpaceCheck = ConstantUtils.mkSet $ - map Types.diskTemplateToRaw [DTExt, DTSharedFile, DTFile, DTRbd] + map Types.diskTemplateToRaw [DTExt, DTSharedFile, DTFile, DTRbd, DTGluster] dtsBlock :: FrozenSet String dtsBlock = @@ -3763,6 +3770,7 @@ diskLdDefaults = , (ldpAccess, PyValueEx diskKernelspace) ]) , (DTSharedFile, Map.empty) + , (DTGluster, Map.empty) ] diskDtDefaults :: Map DiskTemplate (Map String PyValueEx) @@ -3795,6 +3803,7 @@ diskDtDefaults = , (rbdAccess, PyValueEx diskKernelspace) ]) , (DTSharedFile, Map.empty) + , (DTGluster, Map.empty) ] niccDefaults :: Map String PyValueEx @@ -4577,3 +4586,9 @@ errorsEcodeAll = jstoreJobsPerArchiveDirectory :: Int jstoreJobsPerArchiveDirectory = 10000 + +-- * Gluster settings + +-- | Where Ganeti should manage Gluster volume mountpoints +glusterMountpoint :: String +glusterMountpoint = "/var/run/ganeti/gluster" diff --git a/src/Ganeti/HTools/Cluster.hs b/src/Ganeti/HTools/Cluster.hs index 054a8dc57827fd6a9241652502640724b7e29c7f..88067ab4c14cccf40491ff1c0a26a7047a6a281c 100644 --- a/src/Ganeti/HTools/Cluster.hs +++ b/src/Ganeti/HTools/Cluster.hs @@ -982,6 +982,12 @@ nodeEvacInstance nl il mode inst@(Instance.Instance failOnSecondaryChange mode dt >> evacOneNodeOnly nl il inst gdx avail_nodes +nodeEvacInstance nl il mode inst@(Instance.Instance + {Instance.diskTemplate = dt@DTGluster}) + gdx avail_nodes = + failOnSecondaryChange mode dt >> + evacOneNodeOnly nl il inst gdx avail_nodes + nodeEvacInstance nl il ChangePrimary inst@(Instance.Instance {Instance.diskTemplate = DTDrbd8}) _ _ = diff --git a/src/Ganeti/HTools/Instance.hs b/src/Ganeti/HTools/Instance.hs index 5416425fcb632c91c460b880258e4e8405691ed4..92e414d6c176b9b2c07f09634a8ecabd764d1c2c 100644 --- a/src/Ganeti/HTools/Instance.hs +++ b/src/Ganeti/HTools/Instance.hs @@ -157,6 +157,7 @@ movableDiskTemplates = [ T.DTDrbd8 , T.DTBlock , T.DTSharedFile + , T.DTGluster , T.DTRbd , T.DTExt ] diff --git a/src/Ganeti/HTools/Types.hs b/src/Ganeti/HTools/Types.hs index 141164db4cf2fea15d3c4e664b8f48dbbc867abc..fd4fabc5e1f86834d9d0fa66f0796ef0da827695 100644 --- a/src/Ganeti/HTools/Types.hs +++ b/src/Ganeti/HTools/Types.hs @@ -140,6 +140,7 @@ templateMirrorType DTBlock = MirrorExternal templateMirrorType DTDrbd8 = MirrorInternal templateMirrorType DTRbd = MirrorExternal templateMirrorType DTExt = MirrorExternal +templateMirrorType DTGluster = MirrorExternal -- | The resource spec type. data RSpec = RSpec diff --git a/src/Ganeti/Objects.hs b/src/Ganeti/Objects.hs index ce8241e85e60dd3b1762ba23cfd769812722b261..78aa78d559bcd26cd50c96ad19f03cf33f1c72f0 100644 --- a/src/Ganeti/Objects.hs +++ b/src/Ganeti/Objects.hs @@ -353,6 +353,13 @@ decodeDLId obj lid = do path' <- readJSON path return $ LIDSharedFile driver' path' _ -> fail "Can't read logical_id for shared file type" + DTGluster -> + case lid of + JSArray [driver, path] -> do + driver' <- readJSON driver + path' <- readJSON path + return $ LIDSharedFile driver' path' + _ -> fail "Can't read logical_id for shared file type" DTBlock -> case lid of JSArray [driver, path] -> do diff --git a/src/Ganeti/Storage/Utils.hs b/src/Ganeti/Storage/Utils.hs index 64fd19091feb760c2a842fbda0ce1972bf6c56ed..ef0df37c95e1c61ae0a6ede895212999dc4dc5a7 100644 --- a/src/Ganeti/Storage/Utils.hs +++ b/src/Ganeti/Storage/Utils.hs @@ -31,6 +31,7 @@ module Ganeti.Storage.Utils import Ganeti.Config import Ganeti.Objects import Ganeti.Types +import Ganeti.Constants import qualified Ganeti.Types as T import Control.Monad diff --git a/src/Ganeti/Types.hs b/src/Ganeti/Types.hs index 94b190a959410e73308bdd246d324e720654d577..3200336dcd1a8e879aca50c7b26464672978003e 100644 --- a/src/Ganeti/Types.hs +++ b/src/Ganeti/Types.hs @@ -297,6 +297,7 @@ $(THH.declareLADT ''String "DiskTemplate" , ("DTDrbd8", "drbd") , ("DTRbd", "rbd") , ("DTExt", "ext") + , ("DTGluster", "gluster") ]) $(THH.makeJSONInstance ''DiskTemplate) @@ -530,6 +531,7 @@ diskTemplateToStorageType DTPlain = StorageLvmVg diskTemplateToStorageType DTRbd = StorageRados diskTemplateToStorageType DTDiskless = StorageDiskless diskTemplateToStorageType DTBlock = StorageBlock +diskTemplateToStorageType DTGluster = StorageFile -- | Equips a raw storage unit with its parameters addParamsToStorageUnit :: SPExclusiveStorage -> StorageUnitRaw -> StorageUnit diff --git a/test/py/ganeti.objects_unittest.py b/test/py/ganeti.objects_unittest.py index ff8a299d92e6fcaa6d977cd149621cf1463e324d..1fcee13a48530fc47f4be9d61ff5d65e34c6f5b1 100755 --- a/test/py/ganeti.objects_unittest.py +++ b/test/py/ganeti.objects_unittest.py @@ -746,7 +746,7 @@ class TestDisk(unittest.TestCase): def testUpgradeConfigDevTypeLegacyUnchanged(self): dev_types = [constants.DT_FILE, constants.DT_SHARED_FILE, constants.DT_BLOCK, constants.DT_EXT, - constants.DT_RBD] + constants.DT_RBD, constants.DT_GLUSTER] for dev_type in dev_types: disk = objects.Disk() disk.dev_type = dev_type diff --git a/test/py/ganeti.storage.gluster_unittest.py b/test/py/ganeti.storage.gluster_unittest.py new file mode 100644 index 0000000000000000000000000000000000000000..6814cb900984325926b46c76131d4920aa2c4da5 --- /dev/null +++ b/test/py/ganeti.storage.gluster_unittest.py @@ -0,0 +1,26 @@ +#!/usr/bin/python +# + +# Copyright (C) 2013 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 ganeti.storage.gluster module""" + +import testutils + +if __name__ == "__main__": + testutils.GanetiTestProgram()