Merge "Add support for glusterfs native protocol driver"
This commit is contained in:
commit
92f84aa038
|
@ -92,7 +92,7 @@ class API(base.Base):
|
|||
|
||||
# TODO(rushiagr): Find a suitable place to keep all the allowed
|
||||
# share types so that it becomes easier to add one
|
||||
if share_proto.lower() not in ['nfs', 'cifs']:
|
||||
if share_proto.lower() not in ['nfs', 'cifs', 'glusterfs']:
|
||||
msg = (_("Invalid share type provided: %s") % share_proto)
|
||||
raise exception.InvalidInput(reason=msg)
|
||||
|
||||
|
|
|
@ -113,7 +113,9 @@ class GlusterfsShareDriver(driver.ExecuteMixin, driver.ShareDriver):
|
|||
raise
|
||||
|
||||
self._ensure_gluster_vol_mounted()
|
||||
self._setup_gluster_vol()
|
||||
|
||||
def _setup_gluster_vol(self):
|
||||
# exporting the whole volume must be prohibited
|
||||
# to not to defeat access control
|
||||
args, kw = self.gluster_address.make_gluster_args(
|
||||
|
|
|
@ -0,0 +1,166 @@
|
|||
# Copyright (c) 2014 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.
|
||||
|
||||
""" GlusterFS native protocol (glusterfs) driver for shares.
|
||||
|
||||
Manila share is a GlusterFS volume. Unlike the generic driver, this
|
||||
does not use service VM approach. Instances directly talk with the
|
||||
GlusterFS backend storage pool. Instance use the 'glusterfs' protocol
|
||||
to mount the GlusterFS share. Access to the share is allowed via
|
||||
SSL Certificates. Only the share which has the SSL trust established
|
||||
with the GlusterFS backend can mount and hence use the share.
|
||||
"""
|
||||
|
||||
|
||||
from manila import exception
|
||||
from manila.openstack.common import log as logging
|
||||
from manila.share.drivers import glusterfs
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
CLIENT_SSL = 'client.ssl'
|
||||
SERVER_SSL = 'server.ssl'
|
||||
AUTH_SSL_ALLOW = 'auth.ssl-allow'
|
||||
ACCESS_TYPE_CERT = 'cert'
|
||||
|
||||
|
||||
class GlusterfsNativeShareDriver(glusterfs.GlusterfsShareDriver):
|
||||
|
||||
def _setup_gluster_vol(self):
|
||||
super(GlusterfsNativeShareDriver, self)._setup_gluster_vol()
|
||||
|
||||
# Enable gluster volume for SSL access.
|
||||
# This applies for both service mount and instance mount(s).
|
||||
|
||||
# TODO(deepakcs): Once gluster support dual-access, we can limit
|
||||
# service mount to non-ssl access.
|
||||
gargs, gkw = self.gluster_address.make_gluster_args(
|
||||
'volume', 'set', self.gluster_address.volume,
|
||||
CLIENT_SSL, 'on')
|
||||
try:
|
||||
self._execute(*gargs, **gkw)
|
||||
except exception.ProcessExecutionError as exc:
|
||||
LOG.error(_("Error in gluster volume set during volume setup."
|
||||
"Volume: %(volname)s, Option: %(option)s, "
|
||||
"Error: %(error)s"),
|
||||
{'volname': self.gluster_address.volume,
|
||||
'option': CLIENT_SSL, 'error': exc.stderr})
|
||||
raise
|
||||
gargs, gkw = self.gluster_address.make_gluster_args(
|
||||
'volume', 'set', self.gluster_address.volume,
|
||||
SERVER_SSL, 'on')
|
||||
try:
|
||||
self._execute(*gargs, **gkw)
|
||||
except exception.ProcessExecutionError as exc:
|
||||
LOG.error(_("Error in gluster volume set during volume setup."
|
||||
"Volume: %(volname)s, Option: %(option)s, "
|
||||
"Error: %(error)s"),
|
||||
{'volname': self.gluster_address.volume,
|
||||
'option': SERVER_SSL, 'error': exc.stderr})
|
||||
raise
|
||||
|
||||
def create_share(self, ctx, share, share_server=None):
|
||||
"""Create a share using GlusterFS volume.
|
||||
|
||||
1 Manila share = 1 GlusterFS volume. Ensure that the
|
||||
GlusterFS volume is properly setup to be consumed as
|
||||
a share.
|
||||
"""
|
||||
|
||||
# Handle the case where create is called after delete share
|
||||
try:
|
||||
self._setup_gluster_vol()
|
||||
except exception.ProcessExecutionError:
|
||||
LOG.error(_("Unable to create share %s"), (share['name'],))
|
||||
raise
|
||||
|
||||
# TODO(deepakcs): Add validation for gluster mount being present
|
||||
# (decorator maybe)
|
||||
|
||||
# For native protocol, the export_location should be of the form:
|
||||
# server:/volname
|
||||
export_location = self.gluster_address.export
|
||||
|
||||
LOG.info(_("export_location sent back from create_share: %s"),
|
||||
(export_location,))
|
||||
return export_location
|
||||
|
||||
def delete_share(self, context, share, share_server=None):
|
||||
"""Delete a share on the GlusterFS volume.
|
||||
|
||||
1 Manila share = 1 GlusterFS volume. Ensure that the
|
||||
GlusterFS volume is reset back to its original state.
|
||||
"""
|
||||
# Get the gluster volume back to its original state
|
||||
|
||||
gargs, gkw = self.gluster_address.make_gluster_args(
|
||||
'volume', 'reset', self.gluster_address.volume)
|
||||
try:
|
||||
self._execute(*gargs, **gkw)
|
||||
except exception.ProcessExecutionError as exc:
|
||||
LOG.error(_("Error in gluster volume reset during delete share."
|
||||
"Volume: %(volname)s, Error: %(error)s"),
|
||||
{'volname': self.gluster_address.volume,
|
||||
'error': exc.stderr})
|
||||
raise
|
||||
|
||||
def allow_access(self, context, share, access, share_server=None):
|
||||
"""Allow access to a share using certs.
|
||||
|
||||
Add the SSL CN (Common Name) that's allowed to access the server.
|
||||
"""
|
||||
|
||||
if access['access_type'] != ACCESS_TYPE_CERT:
|
||||
raise exception.InvalidShareAccess(_("Only 'cert' access type "
|
||||
"allowed"))
|
||||
|
||||
gargs, gkw = self.gluster_address.make_gluster_args(
|
||||
'volume', 'set', self.gluster_address.volume,
|
||||
AUTH_SSL_ALLOW, access['access_to'])
|
||||
try:
|
||||
self._execute(*gargs, **gkw)
|
||||
except exception.ProcessExecutionError as exc:
|
||||
LOG.error(_("Error in gluster volume set during allow access."
|
||||
"Volume: %(volname)s, Option: %(option)s, "
|
||||
"access_to: %(access_to)s, Error: %(error)s"),
|
||||
{'volname': self.gluster_address.volume,
|
||||
'option': AUTH_SSL_ALLOW,
|
||||
'access_to': access['access_to'], 'error': exc.stderr})
|
||||
raise
|
||||
|
||||
def deny_access(self, context, share, access, share_server=None):
|
||||
"""Deny access to a share that's using cert based auth.
|
||||
|
||||
Remove the SSL CN (Common Name) that's allowed to access the server.
|
||||
"""
|
||||
|
||||
if access['access_type'] != ACCESS_TYPE_CERT:
|
||||
raise exception.InvalidShareAccess(_("Only 'cert' access type "
|
||||
"allowed for access "
|
||||
"removal."))
|
||||
|
||||
gargs, gkw = self.gluster_address.make_gluster_args(
|
||||
'volume', 'reset', self.gluster_address.volume,
|
||||
AUTH_SSL_ALLOW)
|
||||
try:
|
||||
self._execute(*gargs, **gkw)
|
||||
except exception.ProcessExecutionError as exc:
|
||||
LOG.error(_("Error in gluster volume reset during deny access."
|
||||
"Volume: %(volname)s, Option: %(option)s, "
|
||||
"Error: %(error)s"),
|
||||
{'volname': self.gluster_address.volume,
|
||||
'option': AUTH_SSL_ALLOW, 'error': exc.stderr})
|
||||
raise
|
|
@ -310,6 +310,25 @@ class ShareAPITestCase(test.TestCase):
|
|||
db_driver.share_create.assert_called_once_with(
|
||||
self.context, options)
|
||||
|
||||
def test_create_glusterfs(self):
|
||||
date = datetime.datetime(1, 1, 1, 1, 1, 1)
|
||||
self.mock_utcnow.return_value = date
|
||||
share = fake_share('fakeid',
|
||||
user_id=self.context.user_id,
|
||||
project_id=self.context.project_id,
|
||||
status='creating')
|
||||
options = share.copy()
|
||||
for name in ('id', 'export_location', 'host', 'launched_at',
|
||||
'terminated_at'):
|
||||
options.pop(name, None)
|
||||
with mock.patch.object(db_driver, 'share_create',
|
||||
mock.Mock(return_value=share)):
|
||||
options.update(share_proto='glusterfs')
|
||||
self.api.create(self.context, 'glusterfs', '1', 'fakename',
|
||||
'fakedesc', availability_zone='fakeaz')
|
||||
db_driver.share_create.assert_called_once_with(
|
||||
self.context, options)
|
||||
|
||||
@mock.patch.object(quota.QUOTAS, 'reserve',
|
||||
mock.Mock(return_value='reservation'))
|
||||
@mock.patch.object(quota.QUOTAS, 'commit', mock.Mock())
|
||||
|
|
|
@ -0,0 +1,199 @@
|
|||
# Copyright (c) 2014 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.
|
||||
|
||||
""" GlusterFS native protocol (glusterfs) driver for shares.
|
||||
|
||||
Test cases for GlusterFS native protocol driver.
|
||||
"""
|
||||
|
||||
|
||||
import mock
|
||||
from oslo.config import cfg
|
||||
|
||||
from manila import context
|
||||
from manila import exception
|
||||
from manila.share import configuration as config
|
||||
from manila.share.drivers import glusterfs_native
|
||||
from manila import test
|
||||
from manila.tests.db import fakes as db_fakes
|
||||
from manila.tests import fake_utils
|
||||
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
gluster_address_attrs = {
|
||||
'export': '127.0.0.1:/testvol',
|
||||
'host': '127.0.0.1',
|
||||
'qualified': 'testuser@127.0.0.1:/testvol',
|
||||
'remote_user': 'testuser',
|
||||
'volume': 'testvol',
|
||||
}
|
||||
|
||||
|
||||
def fake_share(**kwargs):
|
||||
share = {
|
||||
'id': 'fakeid',
|
||||
'name': 'fakename',
|
||||
'size': 1,
|
||||
'share_proto': 'glusterfs',
|
||||
'export_location': '127.0.0.1:/mnt/glusterfs/testvol',
|
||||
}
|
||||
share.update(kwargs)
|
||||
return db_fakes.FakeModel(share)
|
||||
|
||||
|
||||
class GlusterfsNativeShareDriverTestCase(test.TestCase):
|
||||
"""Tests GlusterfsNativeShareDriver."""
|
||||
|
||||
def setUp(self):
|
||||
super(GlusterfsNativeShareDriverTestCase, self).setUp()
|
||||
fake_utils.stub_out_utils_execute(self.stubs)
|
||||
self._execute = fake_utils.fake_execute
|
||||
self._context = context.get_admin_context()
|
||||
|
||||
CONF.set_default('glusterfs_mount_point_base', '/mnt/glusterfs')
|
||||
CONF.set_default('reserved_share_percentage', 50)
|
||||
|
||||
self.fake_conf = config.Configuration(None)
|
||||
self._db = mock.Mock()
|
||||
self._driver = glusterfs_native.GlusterfsNativeShareDriver(
|
||||
self._db, execute=self._execute,
|
||||
configuration=self.fake_conf)
|
||||
self._driver.gluster_address = mock.Mock(**gluster_address_attrs)
|
||||
self.share = fake_share()
|
||||
|
||||
self.addCleanup(fake_utils.fake_execute_set_repliers, [])
|
||||
self.addCleanup(fake_utils.fake_execute_clear_log)
|
||||
|
||||
def test_create_share(self):
|
||||
self._driver._setup_gluster_vol = mock.Mock()
|
||||
|
||||
expected = gluster_address_attrs['export']
|
||||
actual = self._driver.create_share(self._context, self.share)
|
||||
|
||||
self.assertTrue(self._driver._setup_gluster_vol.called)
|
||||
self.assertEqual(actual, expected)
|
||||
|
||||
def test_create_share_error(self):
|
||||
self._driver._setup_gluster_vol = mock.Mock()
|
||||
self._driver._setup_gluster_vol.side_effect = (
|
||||
exception.ProcessExecutionError)
|
||||
|
||||
self.assertRaises(exception.ProcessExecutionError,
|
||||
self._driver.create_share, self._context, self.share)
|
||||
|
||||
def test_delete_share(self):
|
||||
self._driver.gluster_address = mock.Mock(
|
||||
make_gluster_args=mock.Mock(return_value=(('true',), {})))
|
||||
|
||||
self._driver.delete_share(self._context, self.share)
|
||||
|
||||
self.assertTrue(self._driver.gluster_address.make_gluster_args.called)
|
||||
self.assertEqual(
|
||||
self._driver.gluster_address.make_gluster_args.call_args[0][1],
|
||||
'reset')
|
||||
|
||||
def test_delete_share_error(self):
|
||||
self._driver.gluster_address = mock.Mock(
|
||||
make_gluster_args=mock.Mock(return_value=(('true',), {})))
|
||||
|
||||
def exec_runner(*ignore_args, **ignore_kw):
|
||||
raise exception.ProcessExecutionError
|
||||
|
||||
expected_exec = ['true']
|
||||
fake_utils.fake_execute_set_repliers([(expected_exec[0], exec_runner)])
|
||||
|
||||
self.assertRaises(exception.ProcessExecutionError,
|
||||
self._driver.delete_share, self._context, self.share)
|
||||
|
||||
def test_allow_access(self):
|
||||
self._driver.gluster_address = mock.Mock(
|
||||
make_gluster_args=mock.Mock(return_value=(('true',), {})))
|
||||
access = {'access_type': 'cert', 'access_to': 'client.example.com'}
|
||||
|
||||
self._driver.allow_access(self._context, self.share, access)
|
||||
|
||||
self.assertTrue(self._driver.gluster_address.make_gluster_args.called)
|
||||
self.assertEqual(
|
||||
self._driver.gluster_address.make_gluster_args.call_args[0][1],
|
||||
'set')
|
||||
self.assertEqual(
|
||||
self._driver.gluster_address.make_gluster_args.call_args[0][-2],
|
||||
'auth.ssl-allow')
|
||||
self.assertEqual(
|
||||
self._driver.gluster_address.make_gluster_args.call_args[0][-1],
|
||||
access['access_to'])
|
||||
|
||||
def test_allow_access_error(self):
|
||||
# Invalid access type
|
||||
access = {'access_type': 'invalid', 'access_to': 'client.example.com'}
|
||||
|
||||
self.assertRaises(exception.InvalidShareAccess,
|
||||
self._driver.allow_access, self._context, self.share,
|
||||
access)
|
||||
|
||||
# ProcessExecutionError
|
||||
self._driver.gluster_address = mock.Mock(
|
||||
make_gluster_args=mock.Mock(return_value=(('true',), {})))
|
||||
access = {'access_type': 'cert', 'access_to': 'client.example.com'}
|
||||
|
||||
def exec_runner(*ignore_args, **ignore_kw):
|
||||
raise exception.ProcessExecutionError
|
||||
|
||||
expected_exec = ['true']
|
||||
fake_utils.fake_execute_set_repliers([(expected_exec[0], exec_runner)])
|
||||
|
||||
self.assertRaises(exception.ProcessExecutionError,
|
||||
self._driver.allow_access, self._context, self.share,
|
||||
access)
|
||||
|
||||
def test_deny_access(self):
|
||||
self._driver.gluster_address = mock.Mock(
|
||||
make_gluster_args=mock.Mock(return_value=(('true',), {})))
|
||||
access = {'access_type': 'cert', 'access_to': 'client.example.com'}
|
||||
|
||||
self._driver.deny_access(self._context, self.share, access)
|
||||
|
||||
self.assertTrue(self._driver.gluster_address.make_gluster_args.called)
|
||||
self.assertEqual(
|
||||
self._driver.gluster_address.make_gluster_args.call_args[0][1],
|
||||
'reset')
|
||||
self.assertEqual(
|
||||
self._driver.gluster_address.make_gluster_args.call_args[0][-1],
|
||||
'auth.ssl-allow')
|
||||
|
||||
def test_deny_access_error(self):
|
||||
# Invalid access type
|
||||
access = {'access_type': 'invalid', 'access_to': 'client.example.com'}
|
||||
|
||||
self.assertRaises(exception.InvalidShareAccess,
|
||||
self._driver.deny_access, self._context, self.share,
|
||||
access)
|
||||
|
||||
# ProcessExecutionError
|
||||
self._driver.gluster_address = mock.Mock(
|
||||
make_gluster_args=mock.Mock(return_value=(('true',), {})))
|
||||
access = {'access_type': 'cert', 'access_to': 'client.example.com'}
|
||||
|
||||
def exec_runner(*ignore_args, **ignore_kw):
|
||||
raise exception.ProcessExecutionError
|
||||
|
||||
expected_exec = ['true']
|
||||
fake_utils.fake_execute_set_repliers([(expected_exec[0], exec_runner)])
|
||||
|
||||
self.assertRaises(exception.ProcessExecutionError,
|
||||
self._driver.deny_access, self._context, self.share,
|
||||
access)
|
Loading…
Reference in New Issue