On Wed, Dec 4, 2013 at 2:50 PM, Santi Raffa <[email protected]> wrote:

> This commit teaches Gluster what a volume is and how to use it.
>
> Signed-off-by: Santi Raffa <[email protected]>
> ---
>  lib/storage/gluster.py                     | 226
> +++++++++++++++++++++++++++++
>  test/py/ganeti.storage.gluster_unittest.py |  94 ++++++++++++
>  2 files changed, 320 insertions(+)
>
> diff --git a/lib/storage/gluster.py b/lib/storage/gluster.py
> index 6cfdbee..45e3e60 100644
> --- a/lib/storage/gluster.py
> +++ b/lib/storage/gluster.py
> @@ -25,12 +25,238 @@ 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.__repr__().__hash__()
>

Please use the hash as proposed in my last comments.


> +
> +  def _IsMounted(self):
> +    """Checks if we are mounted or not.
> +
> +    @rtype: bool
> +    @return: True if this volume is mounted.
> +
> +    """
> +    self.server_addr = server_addr
> +
> +    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):
>

Please don't include hypervisor names in method names. It's better to look
at BlockDev.GetUserspaceAccessUri(...) and follow the naming scheme and
logic from there.


> +    """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
> +    """
> +
> +    if self._IsMounted():
> +      return
> +
> +    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)
> +
> +  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)
> +
> +  def __enter__(self):
> +    self.Mount()
> +    return self
> +
> +  def __exit__(self, *exception_information):
> +    self.Unmount()
> +
> +
>  class GlusterStorage(base.BlockDev):
>    """File device.
>
> diff --git a/test/py/ganeti.storage.gluster_unittest.py b/test/py/
> ganeti.storage.gluster_unittest.py
> index 6814cb9..bd31637 100644
> --- a/test/py/ganeti.storage.gluster_unittest.py
> +++ b/test/py/ganeti.storage.gluster_unittest.py
> @@ -20,7 +20,101 @@
>
>  """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):
>

Please use documentation IP addresses (http://tools.ietf.org/html/rfc5737)
throughout this tests.


> +
> +  @staticmethod
> +  def _MakeVolume(ipv=4, addr=None, port=9001,
> +                  run_cmd=NotImplemented,
> +                  vol_name="pinky"):
> +
> +    address = {4: "203.0.113.42",
> +               6: "001:DB8::74:65:28:6:69",
> +              }
> +
> +    return gluster.GlusterVolume(address[ipv] if not addr else addr,
> +                                 port,
> +                                 str(vol_name),
> +                                 _run_cmd=run_cmd
> +                                )
> +
> +  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="1.2.3.4",
> +                                         port=24007,
> +                                         vol_name="testvol")
> +    self.assertEqual(vol_1.GetKVMMountString("dir/a.img"),
> +                     "gluster://1.2.3.4:24007/testvol/dir/a.img")
> +
> +    vol_2 = TestGlusterVolume._MakeVolume(addr="1:2:3:4:5::8",
> +                                         port=24007,
> +                                         vol_name="testvol")
> +    self.assertEqual(vol_2.GetKVMMountString("dir/a.img"),
> +                     "gluster://[1:2:3:4:5::8]: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="1.2.3.4",
> +                                         port=24007,
> +                                         vol_name="testvol")
> +    self.assertEqual(vol_1._GetFUSEMountString(),
> +                     "1.2.3.4:24007:testvol")
> +
> +    vol_2 = TestGlusterVolume._MakeVolume(addr="1:2:3:4:5::8",
> +                                         port=24007,
> +                                         vol_name="testvol")
> +    # This _ought_ to work.
> https://bugzilla.redhat.com/show_bug.cgi?id=764188
> +    self.assertEqual(vol_2._GetFUSEMountString(),
> +                     "1:2:3:4:5::8: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()
> --
> 1.8.4.1
>
>


-- 
Thomas Thrainer | Software Engineer | [email protected] |

Google Germany GmbH
Dienerstr. 12
80331 München

Registergericht und -nummer: Hamburg, HRB 86891
Sitz der Gesellschaft: Hamburg
Geschäftsführer: Graham Law, Christine Elizabeth Flores

Reply via email to