diff --git a/cinder/tests/unit/volume/drivers/test_veritas_cnfs.py b/cinder/tests/unit/volume/drivers/test_veritas_cnfs.py new file mode 100644 index 00000000000..2d5dc0ebce5 --- /dev/null +++ b/cinder/tests/unit/volume/drivers/test_veritas_cnfs.py @@ -0,0 +1,177 @@ +# Copyright (c) 2017 Veritas Technologies LLC +# 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. + + +import os + +import mock + +from cinder import context +from cinder import exception +from cinder import test +from cinder.tests.unit import fake_snapshot +from cinder.tests.unit import fake_volume +from cinder.volume import configuration as conf +from cinder.volume.drivers import veritas_cnfs as cnfs + + +class VeritasCNFSDriverTestCase(test.TestCase): + + """Test case for VeritasCNFS driver.""" + TEST_CNFS_SHARE = 'cnfs-host1:/share' + TEST_VOL_NM = 'volume-a6707cd3-348c-45cd-9524-255be0939b60' + TEST_SNAP_NM = 'snapshot-73368c68-1c0b-4027-ba8a-14629918945e' + TEST_VOL_SIZE = 1 + TEST_MNT_BASE = '/cnfs/share' + TEST_LOCAL_PATH = '/cnfs/share/mnt' + TEST_VOL_LOCAL_PATH = TEST_LOCAL_PATH + '/' + TEST_VOL_NM + TEST_SNAP_LOCAL_PATH = TEST_LOCAL_PATH + '/' + TEST_SNAP_NM + TEST_SPL_SNAP_LOCAL_PATH = TEST_SNAP_LOCAL_PATH + "::snap:vxfs:" + TEST_NFS_SHARES_CONFIG = '/etc/cinder/access_nfs_share' + TEST_NFS_MOUNT_OPTIONS_FAIL_NONE = '' + TEST_NFS_MOUNT_OPTIONS_FAIL_V4 = 'nfsvers=4' + TEST_NFS_MOUNT_OPTIONS_FAIL_V2 = 'nfsvers=2' + TEST_NFS_MOUNT_OPTIONS_PASS_V3 = 'nfsvers=3' + TEST_VOL_ID = 'a6707cd3-348c-45cd-9524-255be0939b60' + SNAPSHOT_ID = '73368c68-1c0b-4027-ba8a-14629918945e' + + def setUp(self): + super(VeritasCNFSDriverTestCase, self).setUp() + self.configuration = mock.Mock(conf.Configuration) + self.configuration.nfs_shares_config = self.TEST_NFS_SHARES_CONFIG + self.configuration.nfs_sparsed_volumes = True + self.configuration.nfs_mount_point_base = self.TEST_MNT_BASE + self.configuration.nfs_mount_options = (self. + TEST_NFS_MOUNT_OPTIONS_PASS_V3) + self.configuration.nfs_oversub_ratio = 1.0 + self.configuration.nfs_used_ratio = 0.95 + self.configuration.nfs_disk_util = 'df' + self.configuration.reserved_percentage = 0 + self.configuration.max_over_subscription_ratio = 20.0 + self.configuration.nas_secure_file_permissions = 'false' + self.configuration.nas_secure_file_operations = 'false' + self._loc = 'localhost:/share' + self.context = context.get_admin_context() + self.driver = cnfs.VeritasCNFSDriver(configuration=self.configuration) + + def test_throw_error_if_nfs_mount_options_not_configured(self): + """Fail if no nfs mount options are configured""" + drv = self.driver + none_opts = self.TEST_NFS_MOUNT_OPTIONS_FAIL_NONE + self.configuration.nfs_mount_options = none_opts + self.assertRaises( + exception.NfsException, drv.do_setup, context.RequestContext) + + def test_throw_error_if_nfs_mount_options_configured_with_NFSV2(self): + """Fail if nfs mount options is not nfsv4 """ + drv = self.driver + nfs_v2_opts = self.TEST_NFS_MOUNT_OPTIONS_FAIL_V2 + self.configuration.nfs_mount_options = nfs_v2_opts + self.assertRaises( + exception.NfsException, drv.do_setup, context.RequestContext) + + def test_throw_error_if_nfs_mount_options_configured_with_NFSV4(self): + """Fail if nfs mount options is not nfsv4 """ + drv = self.driver + nfs_v4_opts = self.TEST_NFS_MOUNT_OPTIONS_FAIL_V4 + self.configuration.nfs_mount_options = nfs_v4_opts + self.assertRaises( + exception.NfsException, drv.do_setup, context.RequestContext) + + @mock.patch.object(cnfs.VeritasCNFSDriver, '_get_local_volume_path') + @mock.patch.object(os.path, 'exists') + def test_do_clone_volume_success(self, m_exists, m_get_local_volume_path): + """test _do_clone_volume() when filesnap over nfs is supported""" + drv = self.driver + volume = fake_volume.fake_volume_obj(self.context, + provider_location=self._loc) + snapshot = fake_volume.fake_volume_obj(self.context) + with mock.patch.object(drv, '_execute'): + m_exists.return_value = True + drv._do_clone_volume(volume, volume.name, snapshot) + + @mock.patch.object(cnfs.VeritasCNFSDriver, '_get_local_volume_path') + @mock.patch.object(os.path, 'exists') + def test_do_clone_volume_fail(self, m_exists, m_get_local_volume_path): + """test _do_clone_volume() when filesnap over nfs is supported""" + drv = self.driver + volume = fake_volume.fake_volume_obj(self.context) + snapshot = fake_volume.fake_volume_obj(self.context) + with mock.patch.object(drv, '_execute'): + m_exists.return_value = False + self.assertRaises(exception.NfsException, drv._do_clone_volume, + volume, volume.name, snapshot) + + def assign_provider_loc(self, src_vol, tgt_vol): + tgt_vol.provider_location = src_vol.provider_location + + @mock.patch.object(cnfs.VeritasCNFSDriver, '_do_clone_volume') + def test_create_volume_from_snapshot(self, m_do_clone_volume): + """test create volume from snapshot""" + drv = self.driver + volume = fake_volume.fake_volume_obj(self.context) + snapshot = fake_volume.fake_volume_obj(self.context, + provider_location=self._loc) + volume.size = 10 + snapshot.volume_size = 10 + m_do_clone_volume(snapshot, snapshot.name, + volume).return_value = True + drv.create_volume_from_snapshot(volume, snapshot) + self.assertEqual(volume.provider_location, snapshot.provider_location) + + @mock.patch.object(cnfs.VeritasCNFSDriver, '_get_vol_by_id') + @mock.patch.object(cnfs.VeritasCNFSDriver, '_do_clone_volume') + def test_create_snapshot(self, m_do_clone_volume, m_get_vol_by_id): + """test create snapshot""" + drv = self.driver + volume = fake_volume.fake_volume_obj(context.get_admin_context(), + provider_location=self._loc) + snapshot = fake_snapshot.fake_snapshot_obj(context.get_admin_context()) + snapshot.volume = volume + m_get_vol_by_id.return_value = volume + m_do_clone_volume(snapshot, snapshot.name, + volume).return_value = True + drv.create_snapshot(snapshot) + self.assertEqual(volume.provider_location, snapshot.provider_location) + + @mock.patch.object(cnfs.VeritasCNFSDriver, '_ensure_share_mounted') + @mock.patch.object(cnfs.VeritasCNFSDriver, 'local_path') + def test_delete_snapshot(self, m_local_path, m_ensure_share_mounted): + """test delete snapshot""" + drv = self.driver + snapshot = fake_snapshot.fake_snapshot_obj(context.get_admin_context(), + provider_location=self._loc) + m_ensure_share_mounted(self._loc).AndReturn(None) + m_local_path(snapshot).AndReturn(self.TEST_SNAP_LOCAL_PATH) + with mock.patch.object(drv, '_execute'): + drv.delete_snapshot(snapshot) + + @mock.patch.object(cnfs.VeritasCNFSDriver, '_do_clone_volume') + @mock.patch.object(cnfs.VeritasCNFSDriver, 'local_path') + def test_create_volume_from_snapshot_greater_size(self, m_local_path, + m_do_clone_volume): + """test create volume from snapshot with greater volume size""" + drv = self.driver + volume = fake_volume.fake_volume_obj(self.context) + snapshot = fake_volume.fake_volume_obj(self.context, + provider_location=self._loc) + volume.size = 20 + snapshot.volume_size = 10 + m_do_clone_volume(snapshot, snapshot.name, + volume).return_value = True + m_local_path(volume).AndReturn(self.TEST_VOL_LOCAL_PATH) + with mock.patch.object(drv, '_execute'): + drv.create_volume_from_snapshot(volume, snapshot) + self.assertEqual(volume.provider_location, snapshot.provider_location) diff --git a/cinder/volume/drivers/veritas_cnfs.py b/cinder/volume/drivers/veritas_cnfs.py new file mode 100644 index 00000000000..f08782d8341 --- /dev/null +++ b/cinder/volume/drivers/veritas_cnfs.py @@ -0,0 +1,180 @@ +# Copyright (c) 2017 Veritas Technologies LLC +# 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. + +import os + +from oslo_log import log as logging +from oslo_utils import excutils + +from cinder import exception +from cinder.i18n import _ +from cinder import interface +from cinder.volume.drivers import nfs + +LOG = logging.getLogger(__name__) + + +@interface.volumedriver +class VeritasCNFSDriver(nfs.NfsDriver): + + """Veritas Clustered NFS based cinder driver + + 1.0.0 - Initial driver implementations for Kilo. + 1.0.1 - Liberty release driver not implemented. + Place holder for Liberty release in case we + need to support. + 1.0.2 - cinder.interface.volumedriver decorator. + Mitaka/Newton/Okata Release + 1.0.3 - Seperate create_cloned_volume() and + create_volume_from_snapshot () functionality. + Pike Release + Executes commands relating to Volumes. + """ + + VERSION = "1.0.3" + # ThirdPartySytems wiki page + CI_WIKI_NAME = "Veritas_Access_CI" + DRIVER_VOLUME_TYPE = 'nfs' + + def __init__(self, *args, **kwargs): + self._execute = None + self._context = None + super(VeritasCNFSDriver, self).__init__(*args, **kwargs) + + def do_setup(self, context): + self._context = context + super(VeritasCNFSDriver, self).do_setup(context) + opts = self.configuration.nfs_mount_options + if not opts or opts.find('vers=3') == -1 or ( + opts.find('nfsvers=3')) == -1: + msg = _("NFS is not configured to use NFSv3") + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + + def create_volume_from_snapshot(self, volume, snapshot): + """Creates a volume from snapshot.""" + LOG.debug('VeritasNFSDriver create_volume_from_snapshot called ' + 'volume_id = %(volume)s and snapshot_id = %(snapshot)s', + {'volume': volume.id, 'snapshot': snapshot.id}) + snap_name = snapshot.name + vol_size = volume.size + snap_size = snapshot.volume_size + self._do_clone_volume(snapshot, snap_name, volume) + volume.provider_location = snapshot.provider_location + + if vol_size != snap_size: + try: + self.extend_volume(volume, vol_size) + except exception.ExtendVolumeError as ex: + with excutils.save_and_reraise_exception(): + LOG.error('Failed to extend Volume: %s', ex.msg) + path = self.local_path(volume) + self._delete_file(path) + return {'provider_location': volume.provider_location} + + def _get_vol_by_id(self, volid): + vol = self.db.volume_get(self._context, volid) + return vol + + def _delete_file(self, path): + """Deletes file from disk and return result as boolean.""" + try: + LOG.debug('Deleting file at path %s', path) + self._execute('rm', '-f', path, run_as_root=True) + except OSError as ex: + LOG.warning('Exception during deleting %s', ex.strerror) + + def create_snapshot(self, snapshot): + """Create a snapshot of the volume.""" + src_vol_id = snapshot.volume_id + src_vol_name = snapshot.volume_name + src_vol = self._get_vol_by_id(src_vol_id) + self._do_clone_volume(src_vol, src_vol_name, snapshot) + snapshot.provider_location = src_vol.provider_location + LOG.debug("VeritasNFSDriver create_snapshot %r", + snapshot.provider_location) + return {'provider_location': snapshot.provider_location} + + def delete_snapshot(self, snapshot): + """Delete a snapshot.""" + if not snapshot.provider_location: + LOG.warning('Snapshot %s does not have provider_location ' + 'specified, skipping', snapshot.name) + return + self._ensure_share_mounted(snapshot.provider_location) + snap_path = self.local_path(snapshot) + self._delete_file(snap_path) + + def create_cloned_volume(self, volume, src_vref): + """Create a clone of the volume.""" + + LOG.debug('VeritasNFSDriver create_cloned_volume called ' + 'volume_id = %(volume)s and src_vol_id = %(src_vol_id)s', + {'volume': volume.id, 'src_vol_id': src_vref.id}) + src_vol_name = src_vref.name + vol_size = volume.size + src_vol_size = src_vref.size + self._do_clone_volume(src_vref, src_vol_name, volume) + volume.provider_location = src_vref.provider_location + + if vol_size != src_vol_size: + try: + self.extend_volume(volume, vol_size) + except exception.ExtendVolumeError as ex: + with excutils.save_and_reraise_exception(): + LOG.error('Failed to extend Volume: %s', ex.msg) + path = self.local_path(volume) + self._delete_file(path) + return {'provider_location': volume.provider_location} + + def _get_local_volume_path(self, provider_loc, vol_name): + mnt_path = self._get_mount_point_for_share(provider_loc) + vol_path = os.path.join(mnt_path, vol_name) + return vol_path + + def _do_clone_volume(self, src_vol, src_vol_name, tgt_vol): + cnfs_share = src_vol.provider_location + tgt_vol_name = tgt_vol.name + tgt_vol_path = self._get_local_volume_path(cnfs_share, tgt_vol_name) + src_vol_path = self._get_local_volume_path(cnfs_share, src_vol_name) + tgt_vol_path_spl = tgt_vol_path + "::snap:vxfs:" + self._execute('ln', src_vol_path, tgt_vol_path_spl, run_as_root=True) + LOG.debug("VeritasNFSDriver: do_clone_volume %(src_vol_path)s " + "%(tgt_vol_path)s %(tgt_vol_path_spl)s", + {'src_vol_path': src_vol_path, + 'tgt_vol_path_spl': tgt_vol_path_spl, + 'tgt_vol_path': tgt_vol_path}) + if not os.path.exists(tgt_vol_path): + self._execute('rm', '-f', tgt_vol_path_spl, run_as_root=True) + msg = _("Filesnap over NFS is not supported, " + "removing the ::snap:vxfs: file") + LOG.error(msg) + raise exception.NfsException(msg) + + def extend_volume(self, volume, size): + """Extend the volume to new size""" + path = self.local_path(volume) + self._execute('truncate', '-s', '%sG' % size, path, run_as_root=True) + LOG.debug("VeritasNFSDriver: extend_volume volume_id = %s", volume.id) + + def _update_volume_stats(self): + super(VeritasCNFSDriver, self)._update_volume_stats() + backend_name = self.configuration.safe_get('volume_backend_name') + res_percentage = self.configuration.safe_get('reserved_percentage') + self._stats["volume_backend_name"] = backend_name or 'VeritasCNFS' + self._stats["vendor_name"] = 'Veritas' + self._stats["reserved_percentage"] = res_percentage or 0 + self._stats["driver_version"] = self.VERSION + self._stats["storage_protocol"] = self.DRIVER_VOLUME_TYPE diff --git a/releasenotes/notes/veritas_access_driver-c73b2320ba9f46a8.yaml b/releasenotes/notes/veritas_access_driver-c73b2320ba9f46a8.yaml new file mode 100644 index 00000000000..739ff4f1a12 --- /dev/null +++ b/releasenotes/notes/veritas_access_driver-c73b2320ba9f46a8.yaml @@ -0,0 +1,3 @@ +--- +features: + - Added NFS based driver for Veritas Access.