blob: b352d61d1bc382c003cd71683ef6b336fa0aee24 [file] [log] [blame]
#
#
# Copyright (C) 2013 Google Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# 1. Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
# TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""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).
"""
import logging
import os
import socket
from ganeti import utils
from ganeti import errors
from ganeti import netutils
from ganeti import constants
from ganeti import ssconf
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,
_mount_point=None):
"""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
if _mount_point: # tests
self.mount_point = _mount_point
else:
self.mount_point = ssconf.SimpleStore().GetGlusterStorageDir()
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 "-o server-port={port} {ip}:/{volume}" \
.format(port=self.port, ip=self.server_ip, 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.
This class represents a file storage backend device stored on Gluster. Ganeti
mounts and unmounts the Gluster devices automatically.
The unique_id for the file device is a (file_driver, file_path) tuple.
"""
def __init__(self, unique_id, children, size, params, dyn_params, **kwargs):
"""Initalizes a file device backend.
"""
if children:
base.ThrowError("Invalid setup for file device")
try:
self.driver, self.path = unique_id
except ValueError: # wrong number of arguments
raise ValueError("Invalid configuration data %s" % repr(unique_id))
server_addr = params[constants.GLUSTER_HOST]
port = params[constants.GLUSTER_PORT]
volume = params[constants.GLUSTER_VOLUME]
self.volume = GlusterVolume(server_addr, port, volume)
self.full_path = io.PathJoin(self.volume.mount_point, self.path)
self.file = None
super(GlusterStorage, self).__init__(unique_id, children, size,
params, dyn_params, **kwargs)
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, exclusive=True):
"""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
"""
with self.volume.Mount():
self.file = FileDeviceHelper(self.full_path)
if self.file.Remove():
self.file = None
return True
else:
return False
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
"""
try:
self.volume.Mount()
self.file = FileDeviceHelper(self.full_path)
self.dev_path = self.full_path
except Exception as err:
self.volume.Unmount()
raise err
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()
def GetUserspaceAccessUri(self, hypervisor):
"""Generate KVM userspace URIs to be used as `-drive file` settings.
@see: L{BlockDev.GetUserspaceAccessUri}
@see: https://github.com/qemu/qemu/commit/8d6d89cb63c57569864ecdeb84d3a1c2eb
"""
if hypervisor == constants.HT_KVM:
return self.volume.GetKVMMountString(self.path)
else:
base.ThrowError("Hypervisor %s doesn't support Gluster userspace access" %
hypervisor)
@classmethod
def Create(cls, unique_id, children, size, spindles, params, excl_stor,
dyn_params, **kwargs):
"""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))
full_path = unique_id[1]
server_addr = params[constants.GLUSTER_HOST]
port = params[constants.GLUSTER_PORT]
volume = params[constants.GLUSTER_VOLUME]
volume_obj = GlusterVolume(server_addr, port, volume)
full_path = io.PathJoin(volume_obj.mount_point, full_path)
# Possible optimization: defer actual creation to first Attach, rather
# than mounting and unmounting here, then remounting immediately after.
with volume_obj.Mount():
FileDeviceHelper.CreateFile(full_path, size, create_folders=True)
return GlusterStorage(unique_id, children, size, params, dyn_params,
**kwargs)