manila/manila/share/drivers/glusterfs/common.py

290 lines
11 KiB
Python

# Copyright (c) 2015 Red Hat, Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""Common GlussterFS routines."""
import re
import xml.etree.cElementTree as etree
from oslo_config import cfg
from oslo_log import log
import six
from manila import exception
from manila.i18n import _
from manila.i18n import _LE
from manila.i18n import _LW
from manila.share.drivers.ganesha import utils as ganesha_utils
LOG = log.getLogger(__name__)
glusterfs_common_opts = [
cfg.StrOpt('glusterfs_server_password',
secret=True,
deprecated_name='glusterfs_native_server_password',
help='Remote GlusterFS server node\'s login password. '
'This is not required if '
'\'glusterfs_path_to_private_key\' is '
'configured.'),
cfg.StrOpt('glusterfs_path_to_private_key',
deprecated_name='glusterfs_native_path_to_private_key',
help='Path of Manila host\'s private SSH key file.'),
]
CONF = cfg.CONF
CONF.register_opts(glusterfs_common_opts)
class GlusterManager(object):
"""Interface with a GlusterFS volume."""
scheme = re.compile('\A(?:(?P<user>[^:@/]+)@)?'
'(?P<host>[^:@/]+)'
'(?::/(?P<volume>[^/]+)(?P<path>/.*)?)?\Z')
@classmethod
def parse(cls, address):
"""Parse address string into component dict."""
m = cls.scheme.search(address)
if not m:
raise exception.GlusterfsException(
_('Invalid gluster address %s.') % address)
return m.groupdict()
def __getattr__(self, attr):
if attr in self.components:
return self.components[attr]
raise AttributeError("'%(typ)s' object has no attribute '%(attr)s'" %
{'typ': type(self).__name__, 'attr': attr})
def __init__(self, address, execf=None, path_to_private_key=None,
remote_server_password=None, requires={}):
"""Initialize a GlusterManager instance.
:param address: the Gluster URI (either string of
[<user>@]<host>[:/<volume>[/<path>]] format or
component dict with "user", "host", "volume",
"path" keys).
:param execf: executor function for management commands.
:param path_to_private_key: path to private ssh key of remote server.
:param remote_server_password: ssh password for remote server.
:param requires: a dict mapping some of the component names to
either True or False; having it specified,
respectively, the presence or absence of the
given component in the uri will be enforced.
"""
self.components = (address if isinstance(address, dict) else
self.parse(address))
for k, v in six.iteritems(requires):
if v is None:
continue
if (self.components.get(k) is not None) != v:
raise exception.GlusterfsException(
_('Invalid gluster address %s.') % address)
self.path_to_private_key = path_to_private_key
self.remote_server_password = remote_server_password
if execf:
self.gluster_call = self.make_gluster_call(execf)
@property
def host_access(self):
return '@'.join(filter(None, (self.user, self.host)))
def _build_uri(self, base):
u = base
for sep, comp in ((':/', 'volume'), ('', 'path')):
if self.components[comp] is None:
break
u = sep.join((u, self.components[comp]))
return u
@property
def qualified(self):
return self._build_uri(self.host_access)
@property
def export(self):
if self.volume:
return self._build_uri(self.host)
def make_gluster_call(self, execf):
"""Execute a Gluster command locally or remotely."""
if self.user:
gluster_execf = ganesha_utils.SSHExecutor(
self.host, 22, None, self.user,
password=self.remote_server_password,
privatekey=self.path_to_private_key)
else:
gluster_execf = ganesha_utils.RootExecutor(execf)
return lambda *args, **kwargs: gluster_execf(*(('gluster',) + args),
**kwargs)
def get_gluster_vol_option(self, option):
"""Get the value of an option set on a GlusterFS volume."""
args = ('--xml', 'volume', 'info', self.volume)
try:
out, err = self.gluster_call(*args)
except exception.ProcessExecutionError as exc:
LOG.error(_LE("Error retrieving volume info: %s"), exc.stderr)
raise exception.GlusterfsException("gluster %s failed" %
' '.join(args))
if not out:
raise exception.GlusterfsException(
'gluster volume info %s: no data received' %
self.volume
)
vix = etree.fromstring(out)
if int(vix.find('./volInfo/volumes/count').text) != 1:
raise exception.InvalidShare('Volume name ambiguity')
for e in vix.findall(".//option"):
o, v = (e.find(a).text for a in ('name', 'value'))
if o == option:
return v
def get_gluster_version(self):
"""Retrieve GlusterFS version.
:returns: version (as tuple of strings, example: ('3', '6', '0beta2'))
"""
try:
out, err = self.gluster_call('--version')
except exception.ProcessExecutionError as exc:
raise exception.GlusterfsException(
_("'gluster version' failed on server "
"%(server)s: %(message)s") %
{'server': self.host, 'message': six.text_type(exc)})
try:
owords = out.split()
if owords[0] != 'glusterfs':
raise RuntimeError
vers = owords[1].split('.')
# provoke an exception if vers does not start with two numerals
int(vers[0])
int(vers[1])
except Exception:
raise exception.GlusterfsException(
_("Cannot parse version info obtained from server "
"%(server)s, version info: %(info)s") %
{'server': self.host, 'info': out})
return vers
def check_gluster_version(self, minvers):
"""Retrieve and check GlusterFS version.
:param minvers: minimum version to require
(given as tuple of integers, example: (3, 6))
"""
vers = self.get_gluster_version()
if self.numreduct(vers) < minvers:
raise exception.GlusterfsException(_(
"Unsupported GlusterFS version %(version)s on server "
"%(server)s, minimum requirement: %(minvers)s") % {
'server': self.host,
'version': '.'.join(vers),
'minvers': '.'.join(six.text_type(c) for c in minvers)})
@staticmethod
def numreduct(vers):
"""The numeric reduct of a tuple of strings.
That is, applying an integer conversion map on the longest
initial segment of vers which consists of numerals.
"""
numvers = []
for c in vers:
try:
numvers.append(int(c))
except ValueError:
break
return tuple(numvers)
def _mount_gluster_vol(execute, gluster_export, mount_path, ensure=False):
"""Mount a GlusterFS volume at the specified mount path.
:param execute: command exectution function
:param gluster_export: GlusterFS export to mount
:param mount_path: path to mount at
:param ensure: boolean to allow remounting a volume with a warning
"""
execute('mkdir', '-p', mount_path)
command = ['mount', '-t', 'glusterfs', gluster_export, mount_path]
try:
execute(*command, run_as_root=True)
except exception.ProcessExecutionError as exc:
if ensure and 'already mounted' in exc.stderr:
LOG.warn(_LW("%s is already mounted"), gluster_export)
else:
raise exception.GlusterfsException(
'Unable to mount Gluster volume'
)
def _umount_gluster_vol(execute, mount_path):
"""Unmount a GlusterFS volume at the specified mount path.
:param execute: command exectution function
:param mount_path: path where volume is mounted
"""
try:
execute('umount', mount_path, run_as_root=True)
except exception.ProcessExecutionError as exc:
msg = (_("Unable to unmount gluster volume. "
"mount_dir: %(mount_path)s, Error: %(error)s") %
{'mount_path': mount_path, 'error': exc.stderr})
LOG.error(msg)
raise exception.GlusterfsException(msg)
def _restart_gluster_vol(gluster_mgr):
"""Restart a GlusterFS volume through its manager.
:param gluster_mgr: GlusterManager instance
"""
try:
# TODO(csaba): '--mode=script' ensures that the Gluster CLI runs in
# script mode. This seems unnecessary as the Gluster CLI is
# expected to run in non-interactive mode when the stdin is not
# a terminal, as is the case below. But on testing, found the
# behaviour of Gluster-CLI to be the contrary. Need to investigate
# this odd-behaviour of Gluster-CLI.
gluster_mgr.gluster_call(
'volume', 'stop', gluster_mgr.volume, '--mode=script')
except exception.ProcessExecutionError as exc:
msg = (_("Error stopping gluster volume. "
"Volume: %(volname)s, Error: %(error)s") %
{'volname': gluster_mgr.volume, 'error': exc.stderr})
LOG.error(msg)
raise exception.GlusterfsException(msg)
try:
gluster_mgr.gluster_call(
'volume', 'start', gluster_mgr.volume)
except exception.ProcessExecutionError as exc:
msg = (_("Error starting gluster volume. "
"Volume: %(volname)s, Error: %(error)s") %
{'volname': gluster_mgr.volume, 'error': exc.stderr})
LOG.error(msg)
raise exception.GlusterfsException(msg)