Commit 58793040 authored by Santi Raffa's avatar Santi Raffa Committed by Thomas Thrainer
Browse files

Gluster: add GlusterVolume class



This commit teaches Gluster what a volume is and how to use it.
Signed-off-by: default avatarSanti Raffa <rsanti@google.com>
Signed-off-by: default avatarThomas Thrainer <thomasth@google.com>
Reviewed-by: default avatarThomas Thrainer <thomasth@google.com>
parent 8106dd64
......@@ -25,12 +25,252 @@ behaves essentially like a regular file system. Unlike RBD, there are no
special provisions for block device abstractions (yet).
"""
import logging
import os
import socket
from ganeti import utils
from ganeti import errors
from ganeti import netutils
from ganeti import constants
from ganeti.utils import io
from ganeti.storage import base
from ganeti.storage.filestorage import FileDeviceHelper
class GlusterVolume(object):
"""This class represents a Gluster volume.
Volumes are uniquely identified by:
- their IP address
- their port
- the volume name itself
Two GlusterVolume objects x, y with same IP address, port and volume name
are considered equal.
"""
def __init__(self, server_addr, port, volume, _run_cmd=utils.RunCmd):
"""Creates a Gluster volume object.
@type server_addr: str
@param server_addr: The address to connect to
@type port: int
@param port: The port to connect to (Gluster standard is 24007)
@type volume: str
@param volume: The gluster volume to use for storage.
"""
self.server_addr = server_addr
server_ip = netutils.Hostname.GetIP(self.server_addr)
self._server_ip = server_ip
port = netutils.ValidatePortNumber(port)
self._port = port
self._volume = volume
self.mount_point = io.PathJoin(constants.GLUSTER_MOUNTPOINT,
self._volume)
self._run_cmd = _run_cmd
@property
def server_ip(self):
return self._server_ip
@property
def port(self):
return self._port
@property
def volume(self):
return self._volume
def __eq__(self, other):
return (self.server_ip, self.port, self.volume) == \
(other.server_ip, other.port, other.volume)
def __repr__(self):
return """GlusterVolume("{ip}", {port}, "{volume}")""" \
.format(ip=self.server_ip, port=self.port, volume=self.volume)
def __hash__(self):
return (self.server_ip, self.port, self.volume).__hash__()
def _IsMounted(self):
"""Checks if we are mounted or not.
@rtype: bool
@return: True if this volume is mounted.
"""
if not os.path.exists(self.mount_point):
return False
return os.path.ismount(self.mount_point)
def _GuessMountFailReasons(self):
"""Try and give reasons why the mount might've failed.
@rtype: str
@return: A semicolon-separated list of problems found with the current setup
suitable for display to the user.
"""
reasons = []
# Does the mount point exist?
if not os.path.exists(self.mount_point):
reasons.append("%r: does not exist" % self.mount_point)
# Okay, it exists, but is it a directory?
elif not os.path.isdir(self.mount_point):
reasons.append("%r: not a directory" % self.mount_point)
# If, for some unfortunate reason, this folder exists before mounting:
#
# /var/run/ganeti/gluster/gv0/10.0.0.1:30000:gv0/
# '--------- cwd ------------'
#
# and you _are_ trying to mount the gluster volume gv0 on 10.0.0.1:30000,
# then the mount.glusterfs command parser gets confused and this command:
#
# mount -t glusterfs 10.0.0.1:30000:gv0 /var/run/ganeti/gluster/gv0
# '-- remote end --' '------ mountpoint -------'
#
# gets parsed instead like this:
#
# mount -t glusterfs 10.0.0.1:30000:gv0 /var/run/ganeti/gluster/gv0
# '-- mountpoint --' '----- syntax error ------'
#
# and if there _is_ a gluster server running locally at the default remote
# end, localhost:24007, then this is not a network error and therefore... no
# usage message gets printed out. All you get is a Byson parser error in the
# gluster log files about an unexpected token in line 1, "". (That's stdin.)
#
# Not that we rely on that output in any way whatsoever...
parser_confusing = io.PathJoin(self.mount_point,
self._GetFUSEMountString())
if os.path.exists(parser_confusing):
reasons.append("%r: please delete, rename or move." % parser_confusing)
# Let's try something else: can we connect to the server?
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
sock.connect((self.server_ip, self.port))
sock.close()
except socket.error as err:
reasons.append("%s:%d: %s" % (self.server_ip, self.port, err.strerror))
reasons.append("try running 'gluster volume info %s' on %s to ensure"
" it exists, it is started and it is using the tcp"
" transport" % (self.volume, self.server_ip))
return "; ".join(reasons)
def _GetFUSEMountString(self):
"""Return the string FUSE needs to mount this volume.
@rtype: str
"""
return "{ip}:{port}:{volume}" \
.format(ip=self.server_ip, port=self.port, volume=self.volume)
def GetKVMMountString(self, path):
"""Return the string KVM needs to use this volume.
@rtype: str
"""
ip = self.server_ip
if netutils.IPAddress.GetAddressFamily(ip) == socket.AF_INET6:
ip = "[%s]" % ip
return "gluster://{ip}:{port}/{volume}/{path}" \
.format(ip=ip, port=self.port, volume=self.volume, path=path)
def Mount(self):
"""Try and mount the volume. No-op if the volume is already mounted.
@raises BlockDeviceError: if the mount was unsuccessful
@rtype: context manager
@return: A simple context manager that lets you use this volume for
short lived operations like so::
with volume.mount():
# Do operations on volume
# Volume is now unmounted
"""
class _GlusterVolumeContextManager(object):
def __init__(self, volume):
self.volume = volume
def __enter__(self):
# We're already mounted.
return self
def __exit__(self, *exception_information):
self.volume.Unmount()
return False # do not swallow exceptions.
if self._IsMounted():
return _GlusterVolumeContextManager(self)
command = ["mount",
"-t", "glusterfs",
self._GetFUSEMountString(),
self.mount_point]
io.Makedirs(self.mount_point)
self._run_cmd(" ".join(command),
# Why set cwd? Because it's an area we control. If,
# for some unfortunate reason, this folder exists:
# "/%s/" % _GetFUSEMountString()
# ...then the gluster parser gets confused and treats
# _GetFUSEMountString() as your mount point and
# self.mount_point becomes a syntax error.
cwd=self.mount_point)
# mount.glusterfs exits with code 0 even after failure.
# https://bugzilla.redhat.com/show_bug.cgi?id=1031973
if not self._IsMounted():
reasons = self._GuessMountFailReasons()
if not reasons:
reasons = "%r failed." % (" ".join(command))
base.ThrowError("%r: mount failure: %s",
self.mount_point,
reasons)
return _GlusterVolumeContextManager(self)
def Unmount(self):
"""Try and unmount the volume.
Failures are logged but otherwise ignored.
@raises BlockDeviceError: if the volume was not mounted to begin with.
"""
if not self._IsMounted():
base.ThrowError("%r: should be mounted but isn't.", self.mount_point)
result = self._run_cmd(["umount",
self.mount_point])
if result.failed:
logging.warning("Failed to unmount %r from %r: %s",
self, self.mount_point, result.fail_reason)
class GlusterStorage(base.BlockDev):
"""File device using the Gluster backend.
......
......@@ -227,7 +227,7 @@ class TestFileDeviceHelper(testutils.GanetiTestCase):
def _Make(path, create_with_size=None, create_folders=False):
skip_checks = lambda path: None
if create_with_size:
return filestorage.FileDeviceHelper.Create(
return filestorage.FileDeviceHelper.CreateFile(
path, create_with_size, create_folders=create_folders,
_file_path_acceptance_fn=skip_checks
)
......
......@@ -20,7 +20,112 @@
"""Script for unittesting the ganeti.storage.gluster module"""
import os
import shutil
import tempfile
import unittest
import mock
from ganeti import errors
from ganeti.storage import filestorage
from ganeti.storage import gluster
from ganeti import utils
import testutils
class TestGlusterVolume(testutils.GanetiTestCase):
testAddrIpv = {4: "203.0.113.42",
6: "2001:DB8::74:65:28:6:69",
}
@staticmethod
def _MakeVolume(addr=None, port=9001,
run_cmd=NotImplemented,
vol_name="pinky"):
addr = addr if addr is not None else TestGlusterVolume.testAddrIpv[4]
return gluster.GlusterVolume(addr, port, vol_name, _run_cmd=run_cmd,
_mount_point="/invalid")
def setUp(self):
testutils.GanetiTestCase.setUp(self)
# Create some volumes.
self.vol_a = TestGlusterVolume._MakeVolume()
self.vol_a_clone = TestGlusterVolume._MakeVolume()
self.vol_b = TestGlusterVolume._MakeVolume(vol_name="pinker")
def testEquality(self):
self.assertEqual(self.vol_a, self.vol_a_clone)
def testInequality(self):
self.assertNotEqual(self.vol_a, self.vol_b)
def testHostnameResolution(self):
vol_1 = TestGlusterVolume._MakeVolume(addr="localhost")
self.assertEqual(vol_1.server_ip, "127.0.0.1")
self.assertRaises(errors.ResolverError, lambda: \
TestGlusterVolume._MakeVolume(addr="E_NOENT"))
def testKVMMountStrings(self):
# The only source of documentation I can find is:
# https://github.com/qemu/qemu/commit/8d6d89c
# This test gets as close as possible to the examples given there,
# within the limits of our implementation (no transport specification,
# no default port version).
vol_1 = TestGlusterVolume._MakeVolume(addr=TestGlusterVolume.testAddrIpv[4],
port=24007,
vol_name="testvol")
self.assertEqual(
vol_1.GetKVMMountString("dir/a.img"),
"gluster://203.0.113.42:24007/testvol/dir/a.img"
)
vol_2 = TestGlusterVolume._MakeVolume(addr=TestGlusterVolume.testAddrIpv[6],
port=24007,
vol_name="testvol")
self.assertEqual(
vol_2.GetKVMMountString("dir/a.img"),
"gluster://[2001:db8:0:74:65:28:6:69]:24007/testvol/dir/a.img"
)
vol_3 = TestGlusterVolume._MakeVolume(addr="localhost",
port=9001,
vol_name="testvol")
self.assertEqual(
vol_3.GetKVMMountString("dir/a.img"),
"gluster://127.0.0.1:9001/testvol/dir/a.img"
)
def testFUSEMountStrings(self):
vol_1 = TestGlusterVolume._MakeVolume(addr=TestGlusterVolume.testAddrIpv[4],
port=24007,
vol_name="testvol")
self.assertEqual(
vol_1._GetFUSEMountString(),
"203.0.113.42:24007:testvol"
)
vol_2 = TestGlusterVolume._MakeVolume(addr=TestGlusterVolume.testAddrIpv[6],
port=24007,
vol_name="testvol")
# This _ought_ to work. https://bugzilla.redhat.com/show_bug.cgi?id=764188
self.assertEqual(
vol_2._GetFUSEMountString(),
"2001:db8:0:74:65:28:6:69:24007:testvol"
)
vol_3 = TestGlusterVolume._MakeVolume(addr="localhost",
port=9001,
vol_name="testvol")
self.assertEqual(
vol_3._GetFUSEMountString(),
"127.0.0.1:9001:testvol"
)
if __name__ == "__main__":
testutils.GanetiTestProgram()
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment