From e2fdc03b029130158b16ab65ebe3e2870448c05d Mon Sep 17 00:00:00 2001 From: Deepak C Shetty Date: Tue, 5 Aug 2014 12:11:56 +0000 Subject: [PATCH] Add support for glusterfs native protocol driver This patch adds basic support for glusterfs native protocol driver that uses cert based access type. A 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. Establishing the SSL trust between the instance and backend is done out of band of Manila. Partially-implements blueprint cert-based-access-type Change-Id: I229d481086478317f1c43635304a16794372f09d --- manila/share/api.py | 2 +- manila/share/drivers/glusterfs.py | 2 + manila/share/drivers/glusterfs_native.py | 166 ++++++++++++++++ manila/tests/test_share_api.py | 19 ++ manila/tests/test_share_glusterfs_native.py | 199 ++++++++++++++++++++ 5 files changed, 387 insertions(+), 1 deletion(-) create mode 100644 manila/share/drivers/glusterfs_native.py create mode 100644 manila/tests/test_share_glusterfs_native.py diff --git a/manila/share/api.py b/manila/share/api.py index 4203687bbe..7c5174da78 100644 --- a/manila/share/api.py +++ b/manila/share/api.py @@ -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) diff --git a/manila/share/drivers/glusterfs.py b/manila/share/drivers/glusterfs.py index 5f94ee57e8..666be3f653 100644 --- a/manila/share/drivers/glusterfs.py +++ b/manila/share/drivers/glusterfs.py @@ -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( diff --git a/manila/share/drivers/glusterfs_native.py b/manila/share/drivers/glusterfs_native.py new file mode 100644 index 0000000000..c4f55748db --- /dev/null +++ b/manila/share/drivers/glusterfs_native.py @@ -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 diff --git a/manila/tests/test_share_api.py b/manila/tests/test_share_api.py index 9ab9ef9e82..fc763fb543 100644 --- a/manila/tests/test_share_api.py +++ b/manila/tests/test_share_api.py @@ -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()) diff --git a/manila/tests/test_share_glusterfs_native.py b/manila/tests/test_share_glusterfs_native.py new file mode 100644 index 0000000000..ec9e83edf4 --- /dev/null +++ b/manila/tests/test_share_glusterfs_native.py @@ -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)