Remove Dell EMC PS Series Driver
The Dell EMC PS Series driver is not supported anymore. Removing it. It was marked as deprecated in the Train release. Change-Id: Iceffcb80d31f62e93e394cae11f580c833587a32
This commit is contained in:
parent
3e67d8883b
commit
20924ae32c
@ -74,8 +74,6 @@ from cinder.volume.drivers.datera import datera_iscsi as \
|
||||
cinder_volume_drivers_datera_dateraiscsi
|
||||
from cinder.volume.drivers.dell_emc.powermax import common as \
|
||||
cinder_volume_drivers_dell_emc_powermax_common
|
||||
from cinder.volume.drivers.dell_emc import ps as \
|
||||
cinder_volume_drivers_dell_emc_ps
|
||||
from cinder.volume.drivers.dell_emc.sc import storagecenter_common as \
|
||||
cinder_volume_drivers_dell_emc_sc_storagecentercommon
|
||||
from cinder.volume.drivers.dell_emc.unity import driver as \
|
||||
@ -280,7 +278,6 @@ def list_opts():
|
||||
cinder_volume_driver.image_opts,
|
||||
cinder_volume_drivers_datera_dateraiscsi.d_opts,
|
||||
cinder_volume_drivers_dell_emc_powermax_common.powermax_opts,
|
||||
cinder_volume_drivers_dell_emc_ps.eqlx_opts,
|
||||
cinder_volume_drivers_dell_emc_sc_storagecentercommon.
|
||||
common_opts,
|
||||
cinder_volume_drivers_dell_emc_unity_driver.UNITY_OPTS,
|
||||
|
@ -1,606 +0,0 @@
|
||||
# Copyright (c) 2013-2017 Dell Inc, or its subsidiaries.
|
||||
# Copyright 2013 OpenStack Foundation
|
||||
#
|
||||
# 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 time
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
from eventlet import greenthread
|
||||
from oslo_concurrency import processutils
|
||||
import paramiko
|
||||
import six
|
||||
|
||||
from cinder import context
|
||||
from cinder import exception
|
||||
from cinder import ssh_utils
|
||||
from cinder import test
|
||||
from cinder import utils
|
||||
from cinder.volume import configuration as conf
|
||||
from cinder.volume.drivers.dell_emc import ps
|
||||
|
||||
|
||||
class PSSeriesISCSIDriverTestCase(test.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(PSSeriesISCSIDriverTestCase, self).setUp()
|
||||
self.configuration = mock.Mock(conf.Configuration)
|
||||
self.configuration.san_is_local = False
|
||||
self.configuration.san_ip = "10.0.0.1"
|
||||
self.configuration.san_login = "foo"
|
||||
self.configuration.san_password = "bar"
|
||||
self.configuration.san_ssh_port = 16022
|
||||
self.configuration.san_thin_provision = True
|
||||
self.configuration.san_private_key = 'foo'
|
||||
self.configuration.ssh_min_pool_conn = 1
|
||||
self.configuration.ssh_max_pool_conn = 5
|
||||
self.configuration.ssh_conn_timeout = 30
|
||||
self.configuration.eqlx_pool = 'non-default'
|
||||
self.configuration.eqlx_group_name = 'group-0'
|
||||
self.configuration.eqlx_cli_max_retries = 5
|
||||
|
||||
self.configuration.use_chap_auth = True
|
||||
self.configuration.chap_username = 'admin'
|
||||
self.configuration.chap_password = 'password'
|
||||
|
||||
self.configuration.max_over_subscription_ratio = 1.0
|
||||
|
||||
self.driver_stats_output = ['TotalCapacity: 111GB',
|
||||
'FreeSpace: 11GB',
|
||||
'VolumeReportedSpace: 80GB',
|
||||
'TotalVolumes: 100']
|
||||
self.cmd = 'this is dummy command'
|
||||
self._context = context.get_admin_context()
|
||||
self.driver = ps.PSSeriesISCSIDriver(
|
||||
configuration=self.configuration)
|
||||
self.volume_name = "fakevolume"
|
||||
self.volid = "fakeid"
|
||||
self.volume = {'name': self.volume_name,
|
||||
'display_name': 'fake_display_name'}
|
||||
self.connector = {
|
||||
'ip': '10.0.0.2',
|
||||
'initiator': 'iqn.1993-08.org.debian:01:2227dab76162',
|
||||
'host': 'fakehost'}
|
||||
self.access_record_output = [
|
||||
"ID Initiator Ipaddress AuthMethod UserName Apply-To",
|
||||
"--- --------------- ------------- ---------- ---------- --------",
|
||||
"1 iqn.1993-08.org.debian:01:222 *.*.*.* none both",
|
||||
" 7dab76162"]
|
||||
self.fake_access_id = '1'
|
||||
self.fake_iqn = 'iqn.2003-10.com.equallogic:group01:25366:fakev'
|
||||
self.fake_iqn_return = ['iSCSI target name is %s.' % self.fake_iqn]
|
||||
self.fake_volume_output = ["Size: 5GB",
|
||||
"iSCSI Name: %s" % self.fake_iqn,
|
||||
"Description: "]
|
||||
self.fake_volume_info = {'size': 5.0,
|
||||
'iSCSI_Name': self.fake_iqn}
|
||||
self.driver._group_ip = '10.0.1.6'
|
||||
self.properties = {
|
||||
'target_discovered': True,
|
||||
'target_portal': '%s:3260' % self.driver._group_ip,
|
||||
'target_iqn': self.fake_iqn,
|
||||
'volume_id': 1,
|
||||
'discard': True}
|
||||
self._model_update = {
|
||||
'provider_location': "%s:3260,1 %s 0" % (self.driver._group_ip,
|
||||
self.fake_iqn),
|
||||
'provider_auth': 'CHAP %s %s' % (
|
||||
self.configuration.chap_username,
|
||||
self.configuration.chap_password)
|
||||
}
|
||||
|
||||
def _fake_get_iscsi_properties(self, volume):
|
||||
return self.properties
|
||||
|
||||
def test_create_volume(self):
|
||||
volume = {'name': self.volume_name, 'size': 1}
|
||||
mock_attrs = {'args': ['volume', 'create', volume['name'],
|
||||
"%sG" % (volume['size']), 'pool',
|
||||
self.configuration.eqlx_pool,
|
||||
'thin-provision']}
|
||||
with mock.patch.object(self.driver,
|
||||
'_eql_execute') as mock_eql_execute:
|
||||
mock_eql_execute.configure_mock(**mock_attrs)
|
||||
mock_eql_execute.return_value = self.fake_iqn_return
|
||||
model_update = self.driver.create_volume(volume)
|
||||
self.assertEqual(self._model_update, model_update)
|
||||
|
||||
def test_delete_volume(self):
|
||||
volume = {'name': self.volume_name, 'size': 1}
|
||||
show_attrs = {'args': ['volume', 'select', volume['name'], 'show']}
|
||||
off_attrs = {'args': ['volume', 'select', volume['name'], 'offline']}
|
||||
delete_attrs = {'args': ['volume', 'delete', volume['name']]}
|
||||
with mock.patch.object(self.driver,
|
||||
'_eql_execute') as mock_eql_execute:
|
||||
mock_eql_execute.configure_mock(**show_attrs)
|
||||
mock_eql_execute.configure_mock(**off_attrs)
|
||||
mock_eql_execute.configure_mock(**delete_attrs)
|
||||
self.driver.delete_volume(volume)
|
||||
|
||||
def test_delete_absent_volume(self):
|
||||
volume = {'name': self.volume_name, 'size': 1, 'id': self.volid}
|
||||
mock_attrs = {'args': ['volume', 'select', volume['name'], 'show']}
|
||||
with mock.patch.object(self.driver,
|
||||
'_eql_execute') as mock_eql_execute:
|
||||
mock_eql_execute.configure_mock(**mock_attrs)
|
||||
mock_eql_execute.side_effect = processutils.ProcessExecutionError(
|
||||
stdout='% Error ..... does not exist.\n')
|
||||
self.driver.delete_volume(volume)
|
||||
|
||||
def test_ensure_export(self):
|
||||
volume = {'name': self.volume_name, 'size': 1}
|
||||
mock_attrs = {'args': ['volume', 'select', volume['name'], 'show']}
|
||||
with mock.patch.object(self.driver,
|
||||
'_eql_execute') as mock_eql_execute:
|
||||
mock_eql_execute.configure_mock(**mock_attrs)
|
||||
self.driver.ensure_export({}, volume)
|
||||
|
||||
def test_create_snapshot(self):
|
||||
snapshot = {'name': 'fakesnap', 'volume_name': 'fakevolume_name'}
|
||||
snap_name = 'fake_snap_name'
|
||||
with mock.patch.object(self.driver,
|
||||
'_eql_execute') as mock_eql_execute:
|
||||
mock_eql_execute.return_value = ['Snapshot name is %s' % snap_name]
|
||||
self.driver.create_snapshot(snapshot)
|
||||
|
||||
def test_create_volume_from_snapshot(self):
|
||||
snapshot = {'name': 'fakesnap', 'volume_name': 'fakevolume_name',
|
||||
'volume_size': '1'}
|
||||
volume = {'name': self.volume_name, 'size': '1'}
|
||||
mock_attrs = {'args': ['volume', 'select', snapshot['volume_name'],
|
||||
'snapshot', 'select', snapshot['name'],
|
||||
'clone', volume['name']]}
|
||||
|
||||
with mock.patch.object(self.driver,
|
||||
'_eql_execute') as mock_eql_execute:
|
||||
with mock.patch.object(self.driver,
|
||||
'extend_volume') as mock_extend_volume:
|
||||
mock_eql_execute.configure_mock(**mock_attrs)
|
||||
mock_eql_execute.return_value = self.fake_iqn_return
|
||||
mock_extend_volume.return_value = self.fake_iqn_return
|
||||
model_update = self.driver.create_volume_from_snapshot(
|
||||
volume, snapshot)
|
||||
self.assertEqual(self._model_update, model_update)
|
||||
self.assertFalse(self.driver.extend_volume.called)
|
||||
|
||||
def test_create_volume_from_snapshot_extend(self):
|
||||
snapshot = {'name': 'fakesnap', 'volume_name': 'fakevolume_name',
|
||||
'volume_size': '100'}
|
||||
volume = {'name': self.volume_name, 'size': '200'}
|
||||
mock_attrs = {'args': ['volume', 'select', snapshot['volume_name'],
|
||||
'snapshot', 'select', snapshot['name'],
|
||||
'clone', volume['name']]}
|
||||
|
||||
with mock.patch.object(self.driver,
|
||||
'_eql_execute') as mock_eql_execute:
|
||||
with mock.patch.object(self.driver,
|
||||
'extend_volume') as mock_extend_volume:
|
||||
mock_eql_execute.configure_mock(**mock_attrs)
|
||||
mock_eql_execute.return_value = self.fake_iqn_return
|
||||
mock_extend_volume.return_value = self.fake_iqn_return
|
||||
model_update = self.driver.create_volume_from_snapshot(
|
||||
volume, snapshot)
|
||||
self.assertEqual(self._model_update, model_update)
|
||||
self.assertTrue(self.driver.extend_volume.called)
|
||||
self.driver.extend_volume.assert_called_once_with(
|
||||
volume, volume['size'])
|
||||
|
||||
def test_create_cloned_volume(self):
|
||||
src_vref = {'name': 'fake_uuid', 'size': '1'}
|
||||
volume = {'name': self.volume_name, 'size': '1'}
|
||||
mock_attrs = {'args': ['volume', 'select', volume['name'],
|
||||
'multihost-access', 'enable']}
|
||||
with mock.patch.object(self.driver,
|
||||
'_eql_execute') as mock_eql_execute:
|
||||
with mock.patch.object(self.driver,
|
||||
'extend_volume') as mock_extend_volume:
|
||||
mock_eql_execute.configure_mock(**mock_attrs)
|
||||
mock_eql_execute.return_value = self.fake_iqn_return
|
||||
mock_extend_volume.return_value = self.fake_iqn_return
|
||||
model_update = self.driver.create_cloned_volume(
|
||||
volume, src_vref)
|
||||
self.assertEqual(self._model_update, model_update)
|
||||
self.assertFalse(self.driver.extend_volume.called)
|
||||
|
||||
def test_create_cloned_volume_extend(self):
|
||||
src_vref = {'name': 'fake_uuid', 'size': '100'}
|
||||
volume = {'name': self.volume_name, 'size': '200'}
|
||||
mock_attrs = {'args': ['volume', 'select', volume['name'],
|
||||
'multihost-access', 'enable']}
|
||||
with mock.patch.object(self.driver,
|
||||
'_eql_execute') as mock_eql_execute:
|
||||
with mock.patch.object(self.driver,
|
||||
'extend_volume') as mock_extend_volume:
|
||||
mock_eql_execute.configure_mock(**mock_attrs)
|
||||
mock_eql_execute.return_value = self.fake_iqn_return
|
||||
mock_extend_volume.return_value = self.fake_iqn_return
|
||||
cloned_vol = self.driver.create_cloned_volume(volume, src_vref)
|
||||
self.assertEqual(self._model_update, cloned_vol)
|
||||
self.assertTrue(self.driver.extend_volume.called)
|
||||
|
||||
def test_delete_snapshot(self):
|
||||
snapshot = {'name': 'fakesnap', 'volume_name': 'fakevolume_name'}
|
||||
mock_attrs = {'args': ['volume', 'select', snapshot['volume_name'],
|
||||
'snapshot', 'delete', snapshot['name']]}
|
||||
with mock.patch.object(self.driver,
|
||||
'_eql_execute') as mock_eql_execute:
|
||||
mock_eql_execute.configure_mock(**mock_attrs)
|
||||
self.driver.delete_snapshot(snapshot)
|
||||
|
||||
def test_delete_absent_snapshot(self):
|
||||
snapshot = {'name': 'fakesnap', 'volume_name': 'fakevolume_name'}
|
||||
mock_attrs = {'args': ['volume', 'select', snapshot['volume_name'],
|
||||
'snapshot', 'delete', snapshot['name']]}
|
||||
with mock.patch.object(self.driver,
|
||||
'_eql_execute') as mock_eql_execute:
|
||||
mock_eql_execute.configure_mock(**mock_attrs)
|
||||
mock_eql_execute.side_effect = processutils.ProcessExecutionError(
|
||||
stdout='% Error ..... does not exist.\n')
|
||||
self.driver.delete_snapshot(snapshot)
|
||||
|
||||
def test_extend_volume(self):
|
||||
new_size = '200'
|
||||
volume = {'name': self.volume_name, 'size': 100}
|
||||
mock_attrs = {'args': ['volume', 'select', volume['name'],
|
||||
'size', "%sG" % new_size]}
|
||||
with mock.patch.object(self.driver,
|
||||
'_eql_execute') as mock_eql_execute:
|
||||
mock_eql_execute.configure_mock(**mock_attrs)
|
||||
self.driver.extend_volume(volume, new_size)
|
||||
|
||||
def test_get_volume_info(self):
|
||||
attrs = ('volume', 'select', self.volume, 'show')
|
||||
with mock.patch.object(self.driver,
|
||||
'_eql_execute') as mock_eql_execute:
|
||||
mock_eql_execute.return_value = self.fake_volume_output
|
||||
data = self.driver._get_volume_info(self.volume)
|
||||
mock_eql_execute.assert_called_with(*attrs)
|
||||
self.assertEqual(self.fake_volume_info, data)
|
||||
|
||||
def test_get_volume_info_negative(self):
|
||||
with mock.patch.object(self.driver,
|
||||
'_eql_execute') as mock_eql_execute:
|
||||
mock_eql_execute.side_effect = processutils.ProcessExecutionError(
|
||||
stdout='% Error ..... does not exist.\n')
|
||||
self.assertRaises(exception.ManageExistingInvalidReference,
|
||||
self.driver._get_volume_info, self.volume_name)
|
||||
|
||||
def test_manage_existing(self):
|
||||
ref = {'source-name': self.volume_name}
|
||||
attrs = ('volume', 'select', self.volume_name,
|
||||
'multihost-access', 'enable')
|
||||
with mock.patch.object(self.driver,
|
||||
'_eql_execute') as mock_eql_execute:
|
||||
with mock.patch.object(self.driver,
|
||||
'_get_volume_info') as mock_volume_info:
|
||||
mock_volume_info.return_value = self.fake_volume_info
|
||||
mock_eql_execute.return_value = self.fake_iqn_return
|
||||
model_update = self.driver.manage_existing(self.volume, ref)
|
||||
mock_eql_execute.assert_called_with(*attrs)
|
||||
self.assertEqual(self._model_update, model_update)
|
||||
|
||||
def test_manage_existing_invalid_ref(self):
|
||||
ref = {}
|
||||
self.assertRaises(exception.InvalidInput,
|
||||
self.driver.manage_existing, self.volume, ref)
|
||||
|
||||
def test_manage_existing_get_size(self):
|
||||
ref = {'source-name': self.volume_name}
|
||||
with mock.patch.object(self.driver,
|
||||
'_eql_execute') as mock_eql_execute:
|
||||
mock_eql_execute.return_value = self.fake_volume_output
|
||||
size = self.driver.manage_existing_get_size(self.volume, ref)
|
||||
self.assertEqual(float('5.0'), size)
|
||||
|
||||
def test_manage_existing_get_size_invalid_ref(self):
|
||||
"""Error on manage with invalid reference."""
|
||||
ref = {}
|
||||
self.assertRaises(exception.InvalidInput,
|
||||
self.driver.manage_existing_get_size,
|
||||
self.volume, ref)
|
||||
|
||||
def test_unmanage(self):
|
||||
with mock.patch.object(self.driver,
|
||||
'_eql_execute') as mock_eql_execute:
|
||||
mock_eql_execute.return_value = None
|
||||
self.driver.unmanage(self.volume)
|
||||
|
||||
def test_initialize_connection(self):
|
||||
volume = {'name': self.volume_name}
|
||||
mock_attrs = {'args': ['volume', 'select', volume['name'], 'access',
|
||||
'create', 'initiator',
|
||||
self.connector['initiator'],
|
||||
'authmethod', 'chap',
|
||||
'username',
|
||||
self.configuration.chap_username]}
|
||||
with mock.patch.object(self.driver,
|
||||
'_eql_execute') as mock_eql_execute:
|
||||
with mock.patch.object(self.driver,
|
||||
'_get_iscsi_properties') as mock_iscsi:
|
||||
mock_eql_execute.configure_mock(**mock_attrs)
|
||||
mock_iscsi.return_value = self.properties
|
||||
iscsi_properties = self.driver.initialize_connection(
|
||||
volume, self.connector)
|
||||
self.assertEqual(self._fake_get_iscsi_properties(volume),
|
||||
iscsi_properties['data'])
|
||||
self.assertTrue(iscsi_properties['data']['discard'])
|
||||
|
||||
def test_terminate_connection(self):
|
||||
def my_side_effect(*args, **kwargs):
|
||||
if args[4] == 'show':
|
||||
return self.access_record_output
|
||||
else:
|
||||
return ''
|
||||
volume = {'name': self.volume_name}
|
||||
with mock.patch.object(self.driver,
|
||||
'_eql_execute') as mock_eql_execute:
|
||||
mock_eql_execute.side_effect = my_side_effect
|
||||
self.driver.terminate_connection(volume, self.connector)
|
||||
|
||||
def test_get_access_record(self):
|
||||
attrs = ('volume', 'select', self.volume['name'], 'access', 'show')
|
||||
with mock.patch.object(self.driver,
|
||||
'_eql_execute') as mock_eql_execute:
|
||||
mock_eql_execute.return_value = self.access_record_output
|
||||
data = self.driver._get_access_record(self.volume, self.connector)
|
||||
mock_eql_execute.assert_called_with(*attrs)
|
||||
self.assertEqual(self.fake_access_id, data)
|
||||
|
||||
def test_get_access_record_negative(self):
|
||||
attrs = ('volume', 'select', self.volume['name'], 'access', 'show')
|
||||
with mock.patch.object(self.driver,
|
||||
'_eql_execute') as mock_eql_execute:
|
||||
mock_eql_execute.return_value = []
|
||||
data = self.driver._get_access_record(self.volume, self.connector)
|
||||
mock_eql_execute.assert_called_with(*attrs)
|
||||
self.assertIsNone(data)
|
||||
|
||||
def test_do_setup(self):
|
||||
fake_group_ip = '10.1.2.3'
|
||||
|
||||
def my_side_effect(*args, **kwargs):
|
||||
if args[0] == 'grpparams':
|
||||
return ['Group-Ipaddress: %s' % fake_group_ip]
|
||||
else:
|
||||
return ''
|
||||
|
||||
with mock.patch.object(self.driver,
|
||||
'_eql_execute') as mock_eql_execute:
|
||||
mock_eql_execute.side_effect = my_side_effect
|
||||
self.driver.do_setup(self._context)
|
||||
self.assertEqual(fake_group_ip, self.driver._group_ip)
|
||||
|
||||
def test_update_volume_stats_thin(self):
|
||||
mock_attrs = {'args': ['pool', 'select',
|
||||
self.configuration.eqlx_pool, 'show']}
|
||||
self.configuration.san_thin_provision = True
|
||||
with mock.patch.object(self.driver,
|
||||
'_eql_execute') as mock_eql_execute:
|
||||
mock_eql_execute.configure_mock(**mock_attrs)
|
||||
mock_eql_execute.return_value = self.driver_stats_output
|
||||
self.driver._update_volume_stats()
|
||||
self.assert_volume_stats(self.driver._stats)
|
||||
|
||||
def test_update_volume_stats_thick(self):
|
||||
mock_attrs = {'args': ['pool', 'select',
|
||||
self.configuration.eqlx_pool, 'show']}
|
||||
self.configuration.san_thin_provision = False
|
||||
with mock.patch.object(self.driver,
|
||||
'_eql_execute') as mock_eql_execute:
|
||||
mock_eql_execute.configure_mock(**mock_attrs)
|
||||
mock_eql_execute.return_value = self.driver_stats_output
|
||||
self.driver._update_volume_stats()
|
||||
self.assert_volume_stats(self.driver._stats)
|
||||
|
||||
def test_get_volume_stats_thin(self):
|
||||
mock_attrs = {'args': ['pool', 'select',
|
||||
self.configuration.eqlx_pool, 'show']}
|
||||
self.configuration.san_thin_provision = True
|
||||
with mock.patch.object(self.driver,
|
||||
'_eql_execute') as mock_eql_execute:
|
||||
mock_eql_execute.configure_mock(**mock_attrs)
|
||||
mock_eql_execute.return_value = self.driver_stats_output
|
||||
stats = self.driver.get_volume_stats(refresh=True)
|
||||
self.assert_volume_stats(stats)
|
||||
|
||||
def test_get_volume_stats_thick(self):
|
||||
mock_attrs = {'args': ['pool', 'select',
|
||||
self.configuration.eqlx_pool, 'show']}
|
||||
self.configuration.san_thin_provision = False
|
||||
with mock.patch.object(self.driver,
|
||||
'_eql_execute') as mock_eql_execute:
|
||||
mock_eql_execute.configure_mock(**mock_attrs)
|
||||
mock_eql_execute.return_value = self.driver_stats_output
|
||||
stats = self.driver.get_volume_stats(refresh=True)
|
||||
self.assert_volume_stats(stats)
|
||||
|
||||
def assert_volume_stats(self, stats):
|
||||
thin_enabled = self.configuration.san_thin_provision
|
||||
self.assertEqual(float('111.0'), stats['total_capacity_gb'])
|
||||
self.assertEqual(float('11.0'), stats['free_capacity_gb'])
|
||||
self.assertEqual(100, stats['total_volumes'])
|
||||
|
||||
if thin_enabled:
|
||||
self.assertEqual(80.0, stats['provisioned_capacity_gb'])
|
||||
else:
|
||||
space = stats['total_capacity_gb'] - stats['free_capacity_gb']
|
||||
self.assertEqual(space, stats['provisioned_capacity_gb'])
|
||||
|
||||
self.assertEqual(thin_enabled, stats['thin_provisioning_support'])
|
||||
self.assertEqual(not thin_enabled,
|
||||
stats['thick_provisioning_support'])
|
||||
self.assertEqual('Dell EMC', stats['vendor_name'])
|
||||
self.assertFalse(stats['multiattach'])
|
||||
|
||||
def test_get_space_in_gb(self):
|
||||
self.assertEqual(123.0, self.driver._get_space_in_gb('123.0GB'))
|
||||
self.assertEqual(124.0, self.driver._get_space_in_gb('123.5GB'))
|
||||
self.assertEqual(123.0 * 1024, self.driver._get_space_in_gb('123.0TB'))
|
||||
self.assertEqual(1.0, self.driver._get_space_in_gb('1024.0MB'))
|
||||
self.assertEqual(2.0, self.driver._get_space_in_gb('1536.0MB'))
|
||||
|
||||
def test_get_output(self):
|
||||
|
||||
def _fake_recv(ignore_arg):
|
||||
return '%s> ' % self.configuration.eqlx_group_name
|
||||
|
||||
chan = mock.Mock(paramiko.Channel)
|
||||
mock_recv = self.mock_object(chan, 'recv')
|
||||
mock_recv.return_value = '%s> ' % self.configuration.eqlx_group_name
|
||||
self.assertEqual([_fake_recv(None)], self.driver._get_output(chan))
|
||||
|
||||
def test_get_prefixed_value(self):
|
||||
lines = ['Line1 passed', 'Line1 failed']
|
||||
prefix = ['Line1', 'Line2']
|
||||
expected_output = [' passed', None]
|
||||
self.assertEqual(expected_output[0],
|
||||
self.driver._get_prefixed_value(lines, prefix[0]))
|
||||
self.assertEqual(expected_output[1],
|
||||
self.driver._get_prefixed_value(lines, prefix[1]))
|
||||
|
||||
def test_ssh_execute(self):
|
||||
ssh = mock.Mock(paramiko.SSHClient)
|
||||
chan = mock.Mock(paramiko.Channel)
|
||||
transport = mock.Mock(paramiko.Transport)
|
||||
mock_get_output = self.mock_object(self.driver, '_get_output')
|
||||
self.mock_object(chan, 'invoke_shell')
|
||||
expected_output = ['NoError: test run']
|
||||
mock_get_output.return_value = expected_output
|
||||
ssh.get_transport.return_value = transport
|
||||
transport.open_session.return_value = chan
|
||||
chan.invoke_shell()
|
||||
chan.send('stty columns 255' + '\r')
|
||||
chan.send(self.cmd + '\r')
|
||||
chan.close()
|
||||
self.assertEqual(expected_output,
|
||||
self.driver._ssh_execute(ssh, self.cmd))
|
||||
|
||||
def test_ssh_execute_error(self):
|
||||
self.mock_object(self.driver, '_ssh_execute',
|
||||
side_effect=processutils.ProcessExecutionError)
|
||||
ssh = mock.Mock(paramiko.SSHClient)
|
||||
chan = mock.Mock(paramiko.Channel)
|
||||
transport = mock.Mock(paramiko.Transport)
|
||||
mock_get_output = self.mock_object(self.driver, '_get_output')
|
||||
self.mock_object(ssh, 'get_transport')
|
||||
self.mock_object(chan, 'invoke_shell')
|
||||
expected_output = ['Error: test run', '% Error']
|
||||
mock_get_output.return_value = expected_output
|
||||
ssh.get_transport().return_value = transport
|
||||
transport.open_session.return_value = chan
|
||||
chan.invoke_shell()
|
||||
chan.send('stty columns 255' + '\r')
|
||||
chan.send(self.cmd + '\r')
|
||||
chan.close()
|
||||
self.assertRaises(processutils.ProcessExecutionError,
|
||||
self.driver._ssh_execute, ssh, self.cmd)
|
||||
|
||||
@mock.patch.object(greenthread, 'sleep')
|
||||
def test_ensure_retries(self, _gt_sleep):
|
||||
num_attempts = 3
|
||||
self.driver.configuration.eqlx_cli_max_retries = num_attempts
|
||||
self.mock_object(self.driver, '_ssh_execute',
|
||||
side_effect=exception.VolumeBackendAPIException(
|
||||
"some error"))
|
||||
# mocks for calls in _run_ssh
|
||||
self.mock_object(utils, 'check_ssh_injection')
|
||||
self.mock_object(ssh_utils, 'SSHPool')
|
||||
|
||||
sshpool = ssh_utils.SSHPool("127.0.0.1", 22, 10,
|
||||
"test",
|
||||
password="test",
|
||||
min_size=1,
|
||||
max_size=1)
|
||||
self.driver.sshpool = mock.Mock(return_value=sshpool)
|
||||
ssh = mock.Mock(paramiko.SSHClient)
|
||||
self.driver.sshpool.item().__enter__ = mock.Mock(return_value=ssh)
|
||||
self.driver.sshpool.item().__exit__ = mock.Mock(return_value=False)
|
||||
# now call the execute
|
||||
self.assertRaises(exception.VolumeBackendAPIException,
|
||||
self.driver._eql_execute, "fake command")
|
||||
self.assertEqual(num_attempts + 1,
|
||||
self.driver._ssh_execute.call_count)
|
||||
|
||||
@mock.patch.object(greenthread, 'sleep')
|
||||
def test_ensure_connection_retries(self, _gt_sleep):
|
||||
num_attempts = 3
|
||||
self.driver.configuration.eqlx_cli_max_retries = num_attempts
|
||||
self.mock_object(self.driver, '_ssh_execute',
|
||||
side_effect=processutils.ProcessExecutionError(
|
||||
stdout='% Error ... some error.\n'))
|
||||
# mocks for calls in _run_ssh
|
||||
self.mock_object(utils, 'check_ssh_injection')
|
||||
self.mock_object(ssh_utils, 'SSHPool')
|
||||
|
||||
sshpool = ssh_utils.SSHPool("127.0.0.1", 22, 10,
|
||||
"test",
|
||||
password="test",
|
||||
min_size=1,
|
||||
max_size=1)
|
||||
self.driver.sshpool = mock.Mock(return_value=sshpool)
|
||||
ssh = mock.Mock(paramiko.SSHClient)
|
||||
self.driver.sshpool.item().__enter__ = mock.Mock(return_value=ssh)
|
||||
self.driver.sshpool.item().__exit__ = mock.Mock(return_value=False)
|
||||
# now call the execute
|
||||
self.assertRaises(exception.VolumeBackendAPIException,
|
||||
self.driver._eql_execute, "fake command")
|
||||
self.assertEqual(num_attempts + 1,
|
||||
self.driver._ssh_execute.call_count)
|
||||
|
||||
@unittest.skip("Skip until bug #1578986 is fixed")
|
||||
@mock.patch.object(greenthread, 'sleep')
|
||||
def test_ensure_retries_on_channel_timeout(self, _gt_sleep):
|
||||
num_attempts = 3
|
||||
self.driver.configuration.eqlx_cli_max_retries = num_attempts
|
||||
|
||||
# mocks for calls and objects in _run_ssh
|
||||
self.mock_object(utils, 'check_ssh_injection')
|
||||
self.mock_object(ssh_utils, 'SSHPool')
|
||||
|
||||
sshpool = ssh_utils.SSHPool("127.0.0.1", 22, 10,
|
||||
"test",
|
||||
password="test",
|
||||
min_size=1,
|
||||
max_size=1)
|
||||
self.driver.sshpool = mock.Mock(return_value=sshpool)
|
||||
ssh = mock.Mock(paramiko.SSHClient)
|
||||
self.driver.sshpool.item().__enter__ = mock.Mock(return_value=ssh)
|
||||
self.driver.sshpool.item().__exit__ = mock.Mock(return_value=False)
|
||||
# mocks for _ssh_execute and _get_output
|
||||
self.mock_object(self.driver, '_get_output',
|
||||
side_effect=exception.VolumeBackendAPIException(
|
||||
"some error"))
|
||||
# now call the execute
|
||||
with mock.patch('sys.stderr', new=six.StringIO()):
|
||||
self.assertRaises(exception.VolumeBackendAPIException,
|
||||
self.driver._eql_execute, "fake command")
|
||||
|
||||
self.assertEqual(num_attempts + 1, self.driver._get_output.call_count)
|
||||
|
||||
@unittest.skip("Skip until bug #1578986 is fixed")
|
||||
def test_with_timeout(self):
|
||||
@ps.with_timeout
|
||||
def no_timeout(cmd, *args, **kwargs):
|
||||
return 'no timeout'
|
||||
|
||||
@ps.with_timeout
|
||||
def w_timeout(cmd, *args, **kwargs):
|
||||
time.sleep(1)
|
||||
|
||||
self.assertEqual('no timeout', no_timeout('fake cmd'))
|
||||
self.assertRaises(exception.VolumeBackendAPIException,
|
||||
w_timeout, 'fake cmd', timeout=0.1)
|
||||
|
||||
def test_local_path(self):
|
||||
self.assertRaises(NotImplementedError, self.driver.local_path, '')
|
@ -1,717 +0,0 @@
|
||||
# Copyright (c) 2013-2017 Dell Inc, or its subsidiaries.
|
||||
# Copyright 2013 OpenStack Foundation
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""Volume driver for Dell EMC PS Series Storage."""
|
||||
|
||||
import functools
|
||||
import math
|
||||
import random
|
||||
|
||||
import eventlet
|
||||
from eventlet import greenthread
|
||||
import greenlet
|
||||
from oslo_concurrency import processutils
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log as logging
|
||||
from oslo_log import versionutils
|
||||
from oslo_utils import excutils
|
||||
from six.moves import range
|
||||
|
||||
from cinder import exception
|
||||
from cinder.i18n import _
|
||||
from cinder import interface
|
||||
from cinder import ssh_utils
|
||||
from cinder import utils
|
||||
from cinder.volume import configuration
|
||||
from cinder.volume.drivers import san
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
eqlx_opts = [
|
||||
cfg.StrOpt('eqlx_group_name',
|
||||
default='group-0',
|
||||
help='Group name to use for creating volumes. Defaults to '
|
||||
'"group-0".'),
|
||||
cfg.IntOpt('eqlx_cli_max_retries',
|
||||
min=0,
|
||||
default=5,
|
||||
help='Maximum retry count for reconnection. Default is 5.'),
|
||||
cfg.StrOpt('eqlx_pool',
|
||||
default='default',
|
||||
help='Pool in which volumes will be created. Defaults '
|
||||
'to "default".')
|
||||
]
|
||||
|
||||
|
||||
CONF = cfg.CONF
|
||||
CONF.register_opts(eqlx_opts, group=configuration.SHARED_CONF_GROUP)
|
||||
|
||||
|
||||
def with_timeout(f):
|
||||
@functools.wraps(f)
|
||||
def __inner(self, *args, **kwargs):
|
||||
timeout = kwargs.pop('timeout', None)
|
||||
gt = eventlet.spawn(f, self, *args, **kwargs)
|
||||
if timeout is None:
|
||||
return gt.wait()
|
||||
else:
|
||||
kill_thread = eventlet.spawn_after(timeout, gt.kill)
|
||||
try:
|
||||
res = gt.wait()
|
||||
except greenlet.GreenletExit:
|
||||
raise exception.VolumeBackendAPIException(
|
||||
data="Command timed out")
|
||||
else:
|
||||
kill_thread.cancel()
|
||||
return res
|
||||
|
||||
return __inner
|
||||
|
||||
|
||||
@interface.volumedriver
|
||||
class PSSeriesISCSIDriver(san.SanISCSIDriver):
|
||||
"""Implements commands for Dell EMC PS Series ISCSI management.
|
||||
|
||||
To enable the driver add the following line to the cinder configuration:
|
||||
volume_driver=cinder.volume.drivers.dell_emc.ps.PSSeriesISCSIDriver
|
||||
|
||||
Driver's prerequisites are:
|
||||
- a separate volume group set up and running on the SAN
|
||||
- SSH access to the SAN
|
||||
- a special user must be created which must be able to
|
||||
- create/delete volumes and snapshots;
|
||||
- clone snapshots into volumes;
|
||||
- modify volume access records;
|
||||
|
||||
The access credentials to the SAN are provided by means of the following
|
||||
flags:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
san_ip=<ip_address>
|
||||
san_login=<user name>
|
||||
san_password=<user password>
|
||||
san_private_key=<file containing SSH private key>
|
||||
|
||||
Thin provision of volumes is enabled by default, to disable it use:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
san_thin_provision=false
|
||||
|
||||
In order to use target CHAP authentication (which is disabled by default)
|
||||
SAN administrator must create a local CHAP user and specify the following
|
||||
flags for the driver:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
use_chap_auth=True
|
||||
chap_login=<chap_login>
|
||||
chap_password=<chap_password>
|
||||
|
||||
eqlx_group_name parameter actually represents the CLI prompt message
|
||||
without '>' ending. E.g. if prompt looks like 'group-0>', then the
|
||||
parameter must be set to 'group-0'
|
||||
|
||||
Version history:
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
1.0 - Initial driver
|
||||
1.1.0 - Misc fixes
|
||||
1.2.0 - Deprecated eqlx_cli_timeout infavor of ssh_conn_timeout
|
||||
1.3.0 - Added support for manage/unmanage volume
|
||||
1.4.0 - Removed deprecated options eqlx_cli_timeout, eqlx_use_chap,
|
||||
eqlx_chap_login, and eqlx_chap_password.
|
||||
1.4.1 - Rebranded driver to Dell EMC.
|
||||
1.4.2 - Enable report discard support.
|
||||
1.4.3 - Report total_volumes in volume stats
|
||||
1.4.4 - Fixed over-subscription ratio calculation
|
||||
1.4.5 - Optimize volume stats information parsing
|
||||
1.4.6 - Extend volume with no-snap option
|
||||
|
||||
"""
|
||||
|
||||
VERSION = "1.4.6"
|
||||
|
||||
# ThirdPartySytems wiki page
|
||||
CI_WIKI_NAME = "Dell_EMC_PS_Series_CI"
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(PSSeriesISCSIDriver, self).__init__(*args, **kwargs)
|
||||
self.configuration.append_config_values(eqlx_opts)
|
||||
self._group_ip = None
|
||||
self.sshpool = None
|
||||
|
||||
@staticmethod
|
||||
def get_driver_options():
|
||||
return eqlx_opts
|
||||
|
||||
def _get_output(self, chan):
|
||||
out = ''
|
||||
ending = '%s> ' % self.configuration.eqlx_group_name
|
||||
while out.find(ending) == -1:
|
||||
ret = chan.recv(102400)
|
||||
if len(ret) == 0:
|
||||
# According to paramiko.channel.Channel documentation, which
|
||||
# says "If a string of length zero is returned, the channel
|
||||
# stream has closed". So we can confirm that the PS server
|
||||
# has closed the connection.
|
||||
msg = _("The PS array has closed the connection.")
|
||||
LOG.error(msg)
|
||||
raise exception.VolumeBackendAPIException(data=msg)
|
||||
out += ret
|
||||
|
||||
LOG.debug("CLI output\n%s", out)
|
||||
return out.splitlines()
|
||||
|
||||
def _get_prefixed_value(self, lines, prefix):
|
||||
for line in lines:
|
||||
if line.startswith(prefix):
|
||||
return line[len(prefix):]
|
||||
return
|
||||
|
||||
@with_timeout
|
||||
def _ssh_execute(self, ssh, command, *arg, **kwargs):
|
||||
transport = ssh.get_transport()
|
||||
chan = transport.open_session()
|
||||
completed = False
|
||||
|
||||
try:
|
||||
chan.invoke_shell()
|
||||
|
||||
LOG.debug("Reading CLI MOTD")
|
||||
self._get_output(chan)
|
||||
|
||||
cmd = 'stty columns 255'
|
||||
LOG.debug("Setting CLI terminal width: '%s'", cmd)
|
||||
chan.send(cmd + '\r')
|
||||
out = self._get_output(chan)
|
||||
|
||||
LOG.debug("Sending CLI command: '%s'", command)
|
||||
chan.send(command + '\r')
|
||||
out = self._get_output(chan)
|
||||
|
||||
completed = True
|
||||
|
||||
if any(ln.startswith(('% Error', 'Error:')) for ln in out):
|
||||
desc = _("Error executing PS command")
|
||||
cmdout = '\n'.join(out)
|
||||
LOG.error(cmdout)
|
||||
raise processutils.ProcessExecutionError(
|
||||
stdout=cmdout, cmd=command, description=desc)
|
||||
return out
|
||||
finally:
|
||||
if not completed:
|
||||
LOG.debug("Timed out executing command: '%s'", command)
|
||||
chan.close()
|
||||
|
||||
def _run_ssh(self, cmd_list, attempts=1):
|
||||
utils.check_ssh_injection(cmd_list)
|
||||
command = ' '. join(cmd_list)
|
||||
|
||||
if not self.sshpool:
|
||||
password = self.configuration.san_password
|
||||
privatekey = self.configuration.san_private_key
|
||||
min_size = self.configuration.ssh_min_pool_conn
|
||||
max_size = self.configuration.ssh_max_pool_conn
|
||||
self.sshpool = ssh_utils.SSHPool(
|
||||
self.configuration.san_ip,
|
||||
self.configuration.san_ssh_port,
|
||||
self.configuration.ssh_conn_timeout,
|
||||
self.configuration.san_login,
|
||||
password=password,
|
||||
privatekey=privatekey,
|
||||
min_size=min_size,
|
||||
max_size=max_size)
|
||||
try:
|
||||
total_attempts = attempts
|
||||
with self.sshpool.item() as ssh:
|
||||
while attempts > 0:
|
||||
attempts -= 1
|
||||
try:
|
||||
LOG.info('PS-driver: executing "%s".', command)
|
||||
return self._ssh_execute(
|
||||
ssh, command,
|
||||
timeout=self.configuration.ssh_conn_timeout)
|
||||
except Exception:
|
||||
LOG.exception('Error running command.')
|
||||
greenthread.sleep(random.randint(20, 500) / 100.0)
|
||||
msg = (_("SSH Command failed after '%(total_attempts)r' "
|
||||
"attempts : '%(command)s'") %
|
||||
{'total_attempts': total_attempts - attempts,
|
||||
'command': command})
|
||||
raise exception.VolumeBackendAPIException(data=msg)
|
||||
|
||||
except Exception:
|
||||
with excutils.save_and_reraise_exception():
|
||||
LOG.error('Error running SSH command: "%s".', command)
|
||||
|
||||
def check_for_setup_error(self):
|
||||
super(PSSeriesISCSIDriver, self).check_for_setup_error()
|
||||
|
||||
def _eql_execute(self, *args, **kwargs):
|
||||
return self._run_ssh(
|
||||
args, attempts=self.configuration.eqlx_cli_max_retries + 1)
|
||||
|
||||
def _get_volume_data(self, lines):
|
||||
prefix = 'iSCSI target name is '
|
||||
target_name = self._get_prefixed_value(lines, prefix)[:-1]
|
||||
return self._get_model_update(target_name)
|
||||
|
||||
def _get_model_update(self, target_name):
|
||||
lun_id = "%s:%s,1 %s 0" % (self._group_ip, '3260', target_name)
|
||||
model_update = {}
|
||||
model_update['provider_location'] = lun_id
|
||||
if self.configuration.use_chap_auth:
|
||||
model_update['provider_auth'] = 'CHAP %s %s' % \
|
||||
(self.configuration.chap_username,
|
||||
self.configuration.chap_password)
|
||||
return model_update
|
||||
|
||||
def _get_space_in_gb(self, val):
|
||||
scale = 1.0
|
||||
part = 'GB'
|
||||
if val.endswith('MB'):
|
||||
scale = 1.0 / 1024
|
||||
part = 'MB'
|
||||
elif val.endswith('TB'):
|
||||
scale = 1.0 * 1024
|
||||
part = 'TB'
|
||||
return math.ceil(scale * float(val.partition(part)[0]))
|
||||
|
||||
def _update_volume_stats(self):
|
||||
"""Retrieve stats info from eqlx group."""
|
||||
|
||||
LOG.debug('Updating volume stats.')
|
||||
data = {}
|
||||
backend_name = "eqlx"
|
||||
if self.configuration:
|
||||
backend_name = self.configuration.safe_get('volume_backend_name')
|
||||
data["volume_backend_name"] = backend_name or 'eqlx'
|
||||
data["vendor_name"] = 'Dell EMC'
|
||||
data["driver_version"] = self.VERSION
|
||||
data["storage_protocol"] = 'iSCSI'
|
||||
|
||||
data['reserved_percentage'] = 0
|
||||
data['QoS_support'] = False
|
||||
|
||||
data['total_capacity_gb'] = None
|
||||
data['free_capacity_gb'] = None
|
||||
data['multiattach'] = False
|
||||
data['total_volumes'] = None
|
||||
|
||||
provisioned_capacity = None
|
||||
for line in self._eql_execute('pool', 'select',
|
||||
self.configuration.eqlx_pool, 'show'):
|
||||
if line.startswith('TotalCapacity:'):
|
||||
out_tup = line.rstrip().partition(' ')
|
||||
data['total_capacity_gb'] = self._get_space_in_gb(out_tup[-1])
|
||||
if line.startswith('FreeSpace:'):
|
||||
out_tup = line.rstrip().partition(' ')
|
||||
data['free_capacity_gb'] = self._get_space_in_gb(out_tup[-1])
|
||||
if line.startswith('VolumeReportedSpace:'):
|
||||
out_tup = line.rstrip().partition(' ')
|
||||
provisioned_capacity = self._get_space_in_gb(out_tup[-1])
|
||||
if line.startswith('TotalVolumes:'):
|
||||
out_tup = line.rstrip().partition(' ')
|
||||
data['total_volumes'] = int(out_tup[-1])
|
||||
# Terminate parsing once this data is found to improve performance
|
||||
if (data['total_capacity_gb'] and data['free_capacity_gb'] and
|
||||
provisioned_capacity and data['total_volumes']):
|
||||
break
|
||||
|
||||
global_capacity = data['total_capacity_gb']
|
||||
global_free = data['free_capacity_gb']
|
||||
|
||||
thin_enabled = self.configuration.san_thin_provision
|
||||
if not thin_enabled:
|
||||
provisioned_capacity = round(global_capacity - global_free, 2)
|
||||
|
||||
data['provisioned_capacity_gb'] = provisioned_capacity
|
||||
data['max_over_subscription_ratio'] = (
|
||||
self.configuration.max_over_subscription_ratio)
|
||||
data['thin_provisioning_support'] = thin_enabled
|
||||
data['thick_provisioning_support'] = not thin_enabled
|
||||
|
||||
self._stats = data
|
||||
|
||||
def _get_volume_info(self, volume_name):
|
||||
"""Get the volume details on the array"""
|
||||
command = ['volume', 'select', volume_name, 'show']
|
||||
try:
|
||||
data = {}
|
||||
for line in self._eql_execute(*command):
|
||||
if line.startswith('Size:'):
|
||||
out_tup = line.rstrip().partition(' ')
|
||||
data['size'] = self._get_space_in_gb(out_tup[-1])
|
||||
elif line.startswith('iSCSI Name:'):
|
||||
out_tup = line.rstrip().partition(': ')
|
||||
data['iSCSI_Name'] = out_tup[-1]
|
||||
return data
|
||||
except processutils.ProcessExecutionError:
|
||||
msg = (_("Volume does not exists %s.") % volume_name)
|
||||
LOG.error(msg)
|
||||
raise exception.ManageExistingInvalidReference(
|
||||
existing_ref=volume_name, reason=msg)
|
||||
|
||||
def _check_volume(self, volume):
|
||||
"""Check if the volume exists on the Array."""
|
||||
command = ['volume', 'select', volume['name'], 'show']
|
||||
try:
|
||||
self._eql_execute(*command)
|
||||
except processutils.ProcessExecutionError as err:
|
||||
with excutils.save_and_reraise_exception():
|
||||
if err.stdout.find('does not exist.\n') > -1:
|
||||
LOG.debug('Volume %s does not exist, '
|
||||
'it may have already been deleted',
|
||||
volume['name'])
|
||||
raise exception.VolumeNotFound(volume_id=volume['id'])
|
||||
|
||||
def _get_access_record(self, volume, connector):
|
||||
"""Returns access record id for the initiator"""
|
||||
try:
|
||||
out = self._eql_execute('volume', 'select', volume['name'],
|
||||
'access', 'show')
|
||||
return self._parse_connection(connector, out)
|
||||
except Exception:
|
||||
with excutils.save_and_reraise_exception():
|
||||
LOG.error('Failed to get access records '
|
||||
'to volume "%s".', volume['name'])
|
||||
|
||||
def _parse_connection(self, connector, out):
|
||||
"""Returns the correct connection id for the initiator.
|
||||
|
||||
This parses the cli output from the command
|
||||
'volume select <volumename> access show'
|
||||
and returns the correct connection id.
|
||||
"""
|
||||
lines = [line for line in out if line != '']
|
||||
# Every record has 2 lines
|
||||
for i in range(0, len(lines), 2):
|
||||
try:
|
||||
int(lines[i][0])
|
||||
# sanity check
|
||||
if len(lines[i + 1].split()) == 1:
|
||||
check = lines[i].split()[1] + lines[i + 1].strip()
|
||||
if connector['initiator'] == check:
|
||||
return lines[i].split()[0]
|
||||
except (IndexError, ValueError):
|
||||
pass # skip the line that is not a valid access record
|
||||
|
||||
return None
|
||||
|
||||
def do_setup(self, context):
|
||||
"""Disable cli confirmation and tune output format."""
|
||||
try:
|
||||
msg = _("The Dell PS driver is moving to maintenance mode "
|
||||
"in the S release and will be removed in T release.")
|
||||
versionutils.report_deprecated_feature(LOG, msg)
|
||||
|
||||
disabled_cli_features = ('confirmation', 'paging', 'events',
|
||||
'formatoutput')
|
||||
for feature in disabled_cli_features:
|
||||
self._eql_execute('cli-settings', feature, 'off')
|
||||
|
||||
for line in self._eql_execute('grpparams', 'show'):
|
||||
if line.startswith('Group-Ipaddress:'):
|
||||
out_tup = line.rstrip().partition(' ')
|
||||
self._group_ip = out_tup[-1]
|
||||
|
||||
LOG.info('PS-driver: Setup is complete, group IP is "%s".',
|
||||
self._group_ip)
|
||||
except Exception:
|
||||
with excutils.save_and_reraise_exception():
|
||||
LOG.error('Failed to setup the Dell EMC PS driver.')
|
||||
|
||||
def create_volume(self, volume):
|
||||
"""Create a volume."""
|
||||
try:
|
||||
cmd = ['volume', 'create',
|
||||
volume['name'], "%sG" % (volume['size'])]
|
||||
if self.configuration.eqlx_pool != 'default':
|
||||
cmd.append('pool')
|
||||
cmd.append(self.configuration.eqlx_pool)
|
||||
if self.configuration.san_thin_provision:
|
||||
cmd.append('thin-provision')
|
||||
out = self._eql_execute(*cmd)
|
||||
self.add_multihost_access(volume)
|
||||
return self._get_volume_data(out)
|
||||
except Exception:
|
||||
with excutils.save_and_reraise_exception():
|
||||
LOG.error('Failed to create volume "%s".', volume['name'])
|
||||
|
||||
def add_multihost_access(self, volume):
|
||||
"""Add multihost-access to a volume. Needed for live migration."""
|
||||
try:
|
||||
cmd = ['volume', 'select',
|
||||
volume['name'], 'multihost-access', 'enable']
|
||||
self._eql_execute(*cmd)
|
||||
except Exception:
|
||||
with excutils.save_and_reraise_exception():
|
||||
LOG.error('Failed to add multihost-access '
|
||||
'for volume "%s".',
|
||||
volume['name'])
|
||||
|
||||
def _set_volume_description(self, volume, description):
|
||||
"""Set the description of the volume"""
|
||||
try:
|
||||
cmd = ['volume', 'select',
|
||||
volume['name'], 'description', description]
|
||||
self._eql_execute(*cmd)
|
||||
except Exception:
|
||||
with excutils.save_and_reraise_exception():
|
||||
LOG.error('Failed to set description '
|
||||
'for volume "%s".',
|
||||
volume['name'])
|
||||
|
||||
def delete_volume(self, volume):
|
||||
"""Delete a volume."""
|
||||
try:
|
||||
self._check_volume(volume)
|
||||
self._eql_execute('volume', 'select', volume['name'], 'offline')
|
||||
self._eql_execute('volume', 'delete', volume['name'])
|
||||
except exception.VolumeNotFound:
|
||||
LOG.warning('Volume %s was not found while trying to delete it.',
|
||||
volume['name'])
|
||||
except Exception:
|
||||
with excutils.save_and_reraise_exception():
|
||||
LOG.error('Failed to delete volume "%s".', volume['name'])
|
||||
|
||||
def create_snapshot(self, snapshot):
|
||||
"""Create snapshot of existing volume on appliance."""
|
||||
try:
|
||||
out = self._eql_execute('volume', 'select',
|
||||
snapshot['volume_name'],
|
||||
'snapshot', 'create-now')
|
||||
prefix = 'Snapshot name is '
|
||||
snap_name = self._get_prefixed_value(out, prefix)
|
||||
self._eql_execute('volume', 'select', snapshot['volume_name'],
|
||||
'snapshot', 'rename', snap_name,
|
||||
snapshot['name'])
|
||||
except Exception:
|
||||
with excutils.save_and_reraise_exception():
|
||||
LOG.error('Failed to create snapshot of volume "%s".',
|
||||
snapshot['volume_name'])
|
||||
|
||||
def create_volume_from_snapshot(self, volume, snapshot):
|
||||
"""Create new volume from other volume's snapshot on appliance."""
|
||||
try:
|
||||
out = self._eql_execute('volume', 'select',
|
||||
snapshot['volume_name'], 'snapshot',
|
||||
'select', snapshot['name'],
|
||||
'clone', volume['name'])
|
||||
# Extend Volume if needed
|
||||
if out and volume['size'] > snapshot['volume_size']:
|
||||
self.extend_volume(volume, volume['size'])
|
||||
LOG.debug('Volume from snapshot %(name)s resized from '
|
||||
'%(current_size)sGB to %(new_size)sGB.',
|
||||
{'name': volume['name'],
|
||||
'current_size': snapshot['volume_size'],
|
||||
'new_size': volume['size']})
|
||||
|
||||
self.add_multihost_access(volume)
|
||||
return self._get_volume_data(out)
|
||||
except Exception:
|
||||
with excutils.save_and_reraise_exception():
|
||||
LOG.error('Failed to create volume from snapshot "%s".',
|
||||
snapshot['name'])
|
||||
|
||||
def create_cloned_volume(self, volume, src_vref):
|
||||
"""Creates a clone of the specified volume."""
|
||||
try:
|
||||
src_volume_name = src_vref['name']
|
||||
out = self._eql_execute('volume', 'select', src_volume_name,
|
||||
'clone', volume['name'])
|
||||
|
||||
# Extend Volume if needed
|
||||
if out and volume['size'] > src_vref['size']:
|
||||
self.extend_volume(volume, volume['size'])
|
||||
|
||||
self.add_multihost_access(volume)
|
||||
return self._get_volume_data(out)
|
||||
except Exception:
|
||||
with excutils.save_and_reraise_exception():
|
||||
LOG.error('Failed to create clone of volume "%s".',
|
||||
volume['name'])
|
||||
|
||||
def delete_snapshot(self, snapshot):
|
||||
"""Delete volume's snapshot."""
|
||||
try:
|
||||
self._eql_execute('volume', 'select', snapshot['volume_name'],
|
||||
'snapshot', 'delete', snapshot['name'])
|
||||
except processutils.ProcessExecutionError as err:
|
||||
if err.stdout.find('does not exist') > -1:
|
||||
LOG.debug('Snapshot %s could not be found.', snapshot['name'])
|
||||
except Exception:
|
||||
with excutils.save_and_reraise_exception():
|
||||
LOG.error('Failed to delete snapshot %(snap)s of '
|
||||
'volume %(vol)s.',
|
||||
{'snap': snapshot['name'],
|
||||
'vol': snapshot['volume_name']})
|
||||
|
||||
def initialize_connection(self, volume, connector):
|
||||
"""Restrict access to a volume."""
|
||||
try:
|
||||
connection_id = self._get_access_record(volume, connector)
|
||||
if connection_id is None:
|
||||
cmd = ['volume', 'select', volume['name'], 'access', 'create',
|
||||
'initiator', connector['initiator']]
|
||||
if self.configuration.use_chap_auth:
|
||||
cmd.extend(['authmethod', 'chap', 'username',
|
||||
self.configuration.chap_username])
|
||||
self._eql_execute(*cmd)
|
||||
|
||||
iscsi_properties = self._get_iscsi_properties(volume)
|
||||
iscsi_properties['discard'] = True
|
||||
return {
|
||||
'driver_volume_type': 'iscsi',
|
||||
'data': iscsi_properties
|
||||
}
|
||||
except Exception:
|
||||
with excutils.save_and_reraise_exception():
|
||||
LOG.error('Failed to initialize connection to volume "%s".',
|
||||
volume['name'])
|
||||
|
||||
def terminate_connection(self, volume, connector, force=False, **kwargs):
|
||||
"""Remove access restrictions from a volume."""
|
||||
try:
|
||||
connection_id = self._get_access_record(volume, connector)
|
||||
if connection_id is not None:
|
||||
self._eql_execute('volume', 'select', volume['name'],
|
||||
'access', 'delete', connection_id)
|
||||
except Exception:
|
||||
with excutils.save_and_reraise_exception():
|
||||
LOG.error('Failed to terminate connection to volume "%s".',
|
||||
volume['name'])
|
||||
|
||||
def create_export(self, context, volume, connector):
|
||||
"""Create an export of a volume.
|
||||
|
||||
Driver has nothing to do here for the volume has been exported
|
||||
already by the SAN, right after it's creation.
|
||||
"""
|
||||
pass
|
||||
|
||||
def ensure_export(self, context, volume):
|
||||
"""Ensure an export of a volume.
|
||||
|
||||
Driver has nothing to do here for the volume has been exported
|
||||
already by the SAN, right after it's creation. We will just make
|
||||
sure that the volume exists on the array and issue a warning.
|
||||
"""
|
||||
try:
|
||||
self._check_volume(volume)
|
||||
except exception.VolumeNotFound:
|
||||
LOG.warning('Volume %s is not found!, it may have been deleted.',
|
||||
volume['name'])
|
||||
except Exception:
|
||||
with excutils.save_and_reraise_exception():
|
||||
LOG.error('Failed to ensure export of volume "%s".',
|
||||
volume['name'])
|
||||
|
||||
def remove_export(self, context, volume):
|
||||
"""Remove an export of a volume.
|
||||
|
||||
Driver has nothing to do here for the volume has been exported
|
||||
already by the SAN, right after it's creation.
|
||||
Nothing to remove since there's nothing exported.
|
||||
"""
|
||||
pass
|
||||
|
||||
def extend_volume(self, volume, new_size):
|
||||
"""Extend the size of the volume."""
|
||||
try:
|
||||
self._eql_execute('volume', 'select', volume['name'],
|
||||
'size', "%sG" % new_size, 'no-snap')
|
||||
LOG.info('Volume %(name)s resized from '
|
||||
'%(current_size)sGB to %(new_size)sGB.',
|
||||
{'name': volume['name'],
|
||||
'current_size': volume['size'],
|
||||
'new_size': new_size})
|
||||
except Exception:
|
||||
with excutils.save_and_reraise_exception():
|
||||
LOG.error('Failed to extend_volume %(name)s from '
|
||||
'%(current_size)sGB to %(new_size)sGB.',
|
||||
{'name': volume['name'],
|
||||
'current_size': volume['size'],
|
||||
'new_size': new_size})
|
||||
|
||||
def _get_existing_volume_ref_name(self, ref):
|
||||
existing_volume_name = None
|
||||
if 'source-name' in ref:
|
||||
existing_volume_name = ref['source-name']
|
||||
elif 'source-id' in ref:
|
||||
existing_volume_name = ref['source-id']
|
||||
else:
|
||||
msg = _('Reference must contain source-id or source-name.')
|
||||
LOG.error(msg)
|
||||
raise exception.InvalidInput(reason=msg)
|
||||
|
||||
return existing_volume_name
|
||||
|
||||
def manage_existing(self, volume, existing_ref):
|
||||
"""Manage an existing volume on the backend storage."""
|
||||
existing_volume_name = self._get_existing_volume_ref_name(existing_ref)
|
||||
try:
|
||||
cmd = ['volume', 'rename',
|
||||
existing_volume_name, volume['name']]
|
||||
self._eql_execute(*cmd)
|
||||
self._set_volume_description(volume, '"OpenStack Managed"')
|
||||
self.add_multihost_access(volume)
|
||||
data = self._get_volume_info(volume['name'])
|
||||
updates = self._get_model_update(data['iSCSI_Name'])
|
||||
LOG.info("Backend volume %(back_vol)s renamed to "
|
||||
"%(vol)s and is now managed by cinder.",
|
||||
{'back_vol': existing_volume_name,
|
||||
'vol': volume['name']})
|
||||
return updates
|
||||
except Exception:
|
||||
with excutils.save_and_reraise_exception():
|
||||
LOG.error('Failed to manage volume "%s".', volume['name'])
|
||||
|
||||
def manage_existing_get_size(self, volume, existing_ref):
|
||||
"""Return size of volume to be managed by manage_existing.
|
||||
|
||||
When calculating the size, round up to the next GB.
|
||||
|
||||
:param volume: Cinder volume to manage
|
||||
:param existing_ref: Driver-specific information used to identify a
|
||||
volume
|
||||
"""
|
||||
existing_volume_name = self._get_existing_volume_ref_name(existing_ref)
|
||||
data = self._get_volume_info(existing_volume_name)
|
||||
return data['size']
|
||||
|
||||
def unmanage(self, volume):
|
||||
"""Removes the specified volume from Cinder management.
|
||||
|
||||
Does not delete the underlying backend storage object.
|
||||
|
||||
:param volume: Cinder volume to unmanage
|
||||
"""
|
||||
try:
|
||||
self._set_volume_description(volume, '"OpenStack UnManaged"')
|
||||
LOG.info("Virtual volume %(disp)s '%(vol)s' is no "
|
||||
"longer managed.",
|
||||
{'disp': volume['display_name'],
|
||||
'vol': volume['name']})
|
||||
except Exception:
|
||||
with excutils.save_and_reraise_exception():
|
||||
LOG.error('Failed to unmanage volume "%s".',
|
||||
volume['name'])
|
||||
|
||||
def local_path(self, volume):
|
||||
raise NotImplementedError()
|
@ -659,7 +659,7 @@ class SCCommonDriver(driver.ManageableVD,
|
||||
def ensure_export(self, context, volume):
|
||||
"""Ensure an export of a volume.
|
||||
|
||||
Per the eqlx driver we just make sure that the volume actually
|
||||
Per the sc driver we just make sure that the volume actually
|
||||
exists where we think it does.
|
||||
"""
|
||||
scvolume = None
|
||||
|
@ -10,7 +10,6 @@ Storage installation.
|
||||
|
||||
ts-cinder-config.rst
|
||||
ts-multipath-warn.rst
|
||||
ts-eql-volume-size.rst
|
||||
ts-HTTP-bad-req-in-cinder-vol-log.rst
|
||||
ts-duplicate-3par-host.rst
|
||||
ts-failed-attach-vol-after-detach.rst
|
||||
|
@ -1,223 +0,0 @@
|
||||
========================================================================
|
||||
Addressing discrepancies in reported volume sizes for EqualLogic storage
|
||||
========================================================================
|
||||
|
||||
Problem
|
||||
~~~~~~~
|
||||
|
||||
There is a discrepancy between both the actual volume size in EqualLogic
|
||||
(EQL) storage and the image size in the Image service, with what is
|
||||
reported to OpenStack database. This could lead to confusion
|
||||
if a user is creating volumes from an image that was uploaded from an EQL
|
||||
volume (through the Image service). The image size is slightly larger
|
||||
than the target volume size; this is because EQL size reporting accounts
|
||||
for additional storage used by EQL for internal volume metadata.
|
||||
|
||||
To reproduce the issue follow the steps in the following procedure.
|
||||
|
||||
This procedure assumes that the EQL array is provisioned, and that
|
||||
appropriate configuration settings have been included in
|
||||
``/etc/cinder/cinder.conf`` to connect to the EQL array.
|
||||
|
||||
Create a new volume. Note the ID and size of the volume. In the
|
||||
following example, the ID and size are
|
||||
``74cf9c04-4543-47ae-a937-a9b7c6c921e7`` and ``1``, respectively:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ openstack volume create volume1 --size 1
|
||||
|
||||
+---------------------+--------------------------------------+
|
||||
| Field | Value |
|
||||
+---------------------+--------------------------------------+
|
||||
| attachments | [] |
|
||||
| availability_zone | nova |
|
||||
| bootable | false |
|
||||
| consistencygroup_id | None |
|
||||
| created_at | 2016-12-06T11:33:30.957318 |
|
||||
| description | None |
|
||||
| encrypted | False |
|
||||
| id | 74cf9c04-4543-47ae-a937-a9b7c6c921e7 |
|
||||
| migration_status | None |
|
||||
| multiattach | False |
|
||||
| name | volume1 |
|
||||
| properties | |
|
||||
| replication_status | disabled |
|
||||
| size | 1 |
|
||||
| snapshot_id | None |
|
||||
| source_volid | None |
|
||||
| status | creating |
|
||||
| type | iscsi |
|
||||
| updated_at | None |
|
||||
| user_id | c36cec73b0e44876a4478b1e6cd749bb |
|
||||
+---------------------+--------------------------------------+
|
||||
|
||||
Verify the volume size on the EQL array by using its command-line
|
||||
interface.
|
||||
|
||||
The actual size (``VolReserve``) is 1.01GB. The EQL Group Manager
|
||||
should also report a volume size of 1.01GB:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
eql> volume select volume-74cf9c04-4543-47ae-a937-a9b7c6c921e7
|
||||
eql (volume_volume-74cf9c04-4543-47ae-a937-a9b7c6c921e7)> show
|
||||
_______________________________ Volume Information ________________________________
|
||||
Name: volume-74cf9c04-4543-47ae-a937-a9b7c6c921e7
|
||||
Size: 1GB
|
||||
VolReserve: 1.01GB
|
||||
VolReservelnUse: 0MB
|
||||
ReplReservelnUse: 0MB
|
||||
iSCSI Alias: volume-74cf9c04-4543-47ae-a937-a9b7c6c921e7
|
||||
iSCSI Name: iqn.2001-05.com.equallogic:0-8a0906-19f91850c-067000000b4532cl-volume-74cf9c04-4543-47ae-a937-a9b7c6c921e7
|
||||
ActualMembers: 1
|
||||
Snap-Warn: 10%
|
||||
Snap-Depletion: delete-oldest
|
||||
Description:
|
||||
Snap-Reserve: 100%
|
||||
Snap-Reserve-Avail: 100% (1.01GB)
|
||||
Permission: read-write
|
||||
DesiredStatus: online
|
||||
Status: online
|
||||
Connections: O
|
||||
Snapshots: O
|
||||
Bind:
|
||||
Type: not-replicated
|
||||
ReplicationReserveSpace: 0MB
|
||||
|
||||
Create a new image from this volume:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ openstack image create --volume volume1 \
|
||||
--disk-format raw --container-format bare image_from_volume1
|
||||
|
||||
+---------------------+--------------------------------------+
|
||||
| Field | Value |
|
||||
+---------------------+--------------------------------------+
|
||||
| container_format | bare |
|
||||
| disk_format | raw |
|
||||
| display_description | None |
|
||||
| id | 850fd393-a968-4259-9c65-6b495cba5209 |
|
||||
| image_id | 3020a21d-ba37-4495-8899-07fc201161b9 |
|
||||
| image_name | image_from_volume1 |
|
||||
| is_public | False |
|
||||
| protected | False |
|
||||
| size | 1 |
|
||||
| status | uploading |
|
||||
| updated_at | 2016-12-05T12:43:56.000000 |
|
||||
| volume_type | iscsi |
|
||||
+---------------------+--------------------------------------+
|
||||
|
||||
When you uploaded the volume in the previous step, the Image service
|
||||
reported the volume's size as ``1`` (GB). However, when using
|
||||
:command:`openstack image show` to show the image, the displayed size is
|
||||
1085276160 bytes, or roughly 1.01 GB:
|
||||
|
||||
+------------------+--------------------------------------+
|
||||
| Property | Value |
|
||||
+------------------+--------------------------------------+
|
||||
| checksum | cd573cfaace07e7949bc0c46028904ff |
|
||||
| container_format | bare |
|
||||
| created_at | 2016-12-06T11:39:06Z |
|
||||
| disk_format | raw |
|
||||
| id | 3020a21d-ba37-4495-8899-07fc201161b9 |
|
||||
| min_disk | 0 |
|
||||
| min_ram | 0 |
|
||||
| name | image_from_volume1 |
|
||||
| owner | 5669caad86a04256994cdf755df4d3c1 |
|
||||
| protected | False |
|
||||
| size | 1085276160 |
|
||||
| status | active |
|
||||
| tags | [] |
|
||||
| updated_at | 2016-12-06T11:39:24Z |
|
||||
| virtual_size | None |
|
||||
| visibility | private |
|
||||
+------------------+--------------------------------------+
|
||||
|
||||
|
||||
|
||||
Create a new volume using the previous image (``image_id 3020a21d-ba37-4495
|
||||
-8899-07fc201161b9`` in this example) as
|
||||
the source. Set the target volume size to 1GB; this is the size
|
||||
reported by the ``cinder`` tool when you uploaded the volume to the
|
||||
Image service:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ openstack volume create volume2 --size 1 --image 3020a21d-ba37-4495-8899-07fc201161b9
|
||||
ERROR: Invalid input received: Size of specified image 2 is larger
|
||||
than volume size 1. (HTTP 400) (Request-ID: req-4b9369c0-dec5-4e16-a114-c0cdl6bSd210)
|
||||
|
||||
The attempt to create a new volume based on the size reported by the
|
||||
``cinder`` tool will then fail.
|
||||
|
||||
Solution
|
||||
~~~~~~~~
|
||||
|
||||
To work around this problem, increase the target size of the new image
|
||||
to the next whole number. In the problem example, you created a 1GB
|
||||
volume to be used as volume-backed image, so a new volume using this
|
||||
volume-backed image should use a size of 2GB:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ openstack volume create volume2 --size 1 --image 3020a21d-ba37-4495-8899-07fc201161b9
|
||||
+---------------------+--------------------------------------+
|
||||
| Field | Value |
|
||||
+---------------------+--------------------------------------+
|
||||
| attachments | [] |
|
||||
| availability_zone | nova |
|
||||
| bootable | false |
|
||||
| consistencygroup_id | None |
|
||||
| created_at | 2016-12-06T11:49:06.031768 |
|
||||
| description | None |
|
||||
| encrypted | False |
|
||||
| id | a70d6305-f861-4382-84d8-c43128be0013 |
|
||||
| migration_status | None |
|
||||
| multiattach | False |
|
||||
| name | volume2 |
|
||||
| properties | |
|
||||
| replication_status | disabled |
|
||||
| size | 1 |
|
||||
| snapshot_id | None |
|
||||
| source_volid | None |
|
||||
| status | creating |
|
||||
| type | iscsi |
|
||||
| updated_at | None |
|
||||
| user_id | c36cec73b0e44876a4478b1e6cd749bb |
|
||||
+---------------------+--------------------------------------+
|
||||
|
||||
.. note::
|
||||
|
||||
The dashboard suggests a suitable size when you create a new volume
|
||||
based on a volume-backed image.
|
||||
|
||||
You can then check this new volume into the EQL array:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
eql> volume select volume-64e8eb18-d23f-437b-bcac-b352afa6843a
|
||||
eql (volume_volume-61e8eb18-d23f-437b-bcac-b352afa6843a)> show
|
||||
______________________________ Volume Information _______________________________
|
||||
Name: volume-64e8eb18-d23f-437b-bcac-b352afa6843a
|
||||
Size: 2GB
|
||||
VolReserve: 2.01GB
|
||||
VolReserveInUse: 1.01GB
|
||||
ReplReserveInUse: 0MB
|
||||
iSCSI Alias: volume-64e8eb18-d23f-437b-bcac-b352afa6843a
|
||||
iSCSI Name: iqn.2001-05.com.equallogic:0-8a0906-e3091850e-eae000000b7S32cl-volume-64e8eb18-d23f-437b-bcac-b3S2afa6Bl3a
|
||||
ActualMembers: 1
|
||||
Snap-Warn: 10%
|
||||
Snap-Depletion: delete-oldest
|
||||
Description:
|
||||
Snap-Reserve: 100%
|
||||
Snap-Reserve-Avail: 100% (2GB)
|
||||
Permission: read-write
|
||||
DesiredStatus: online
|
||||
Status: online
|
||||
Connections: 1
|
||||
Snapshots: O
|
||||
Bind:
|
||||
Type: not-replicated
|
||||
ReplicationReserveSpace: 0MB
|
@ -1,166 +0,0 @@
|
||||
=============================================
|
||||
Dell EMC PS Series volume driver (Deprecated)
|
||||
=============================================
|
||||
|
||||
The Dell PS Series (EqualLogic) volume driver interacts with configured PS
|
||||
Series arrays and supports various operations.
|
||||
|
||||
.. note::
|
||||
The Dell PS Series volume driver is moving into maintanence mode in S
|
||||
release and will be removed in the T release.
|
||||
|
||||
Supported operations
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
- Create, delete, attach, and detach volumes.
|
||||
- Create, list, and delete volume snapshots.
|
||||
- Clone a volume.
|
||||
|
||||
Configuration
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
The OpenStack Block Storage service supports:
|
||||
|
||||
- Multiple instances of Dell EqualLogic Groups or Dell EqualLogic Group
|
||||
Storage Pools and multiple pools on a single array.
|
||||
|
||||
- Multiple instances of Dell EqualLogic Groups or Dell EqualLogic Group
|
||||
Storage Pools or multiple pools on a single array.
|
||||
|
||||
The Dell EqualLogic volume driver's ability to access the EqualLogic Group is
|
||||
dependent upon the generic block storage driver's SSH settings in the
|
||||
``/etc/cinder/cinder.conf`` file (see
|
||||
:ref:`block-storage-sample-configuration-file` for reference).
|
||||
|
||||
.. config-table::
|
||||
:config-target: PS Series
|
||||
|
||||
cinder.volume.drivers.dell_emc.ps
|
||||
|
||||
Default (single-instance) configuration
|
||||
---------------------------------------
|
||||
|
||||
The following sample ``/etc/cinder/cinder.conf`` configuration lists the
|
||||
relevant settings for a typical Block Storage service using a single
|
||||
Dell EqualLogic Group:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[DEFAULT]
|
||||
# Required settings
|
||||
|
||||
volume_driver = cinder.volume.drivers.dell_emc.ps.PSSeriesISCSIDriver
|
||||
san_ip = IP_EQLX
|
||||
san_login = SAN_UNAME
|
||||
san_password = SAN_PW
|
||||
eqlx_group_name = EQLX_GROUP
|
||||
eqlx_pool = EQLX_POOL
|
||||
|
||||
# Optional settings
|
||||
|
||||
san_thin_provision = true|false
|
||||
use_chap_auth = true|false
|
||||
chap_username = EQLX_UNAME
|
||||
chap_password = EQLX_PW
|
||||
eqlx_cli_max_retries = 5
|
||||
san_ssh_port = 22
|
||||
ssh_conn_timeout = 30
|
||||
san_private_key = SAN_KEY_PATH
|
||||
ssh_min_pool_conn = 1
|
||||
ssh_max_pool_conn = 5
|
||||
|
||||
In this example, replace the following variables accordingly:
|
||||
|
||||
IP_EQLX
|
||||
The IP address used to reach the Dell EqualLogic Group through SSH.
|
||||
This field has no default value.
|
||||
|
||||
SAN_UNAME
|
||||
The user name to login to the Group manager via SSH at the
|
||||
``san_ip``. Default user name is ``grpadmin``.
|
||||
|
||||
SAN_PW
|
||||
The corresponding password of SAN_UNAME. Not used when
|
||||
``san_private_key`` is set. Default password is ``password``.
|
||||
|
||||
EQLX_GROUP
|
||||
The group to be used for a pool where the Block Storage service will
|
||||
create volumes and snapshots. Default group is ``group-0``.
|
||||
|
||||
EQLX_POOL
|
||||
The pool where the Block Storage service will create volumes and
|
||||
snapshots. Default pool is ``default``. This option cannot be used
|
||||
for multiple pools utilized by the Block Storage service on a single
|
||||
Dell EqualLogic Group.
|
||||
|
||||
EQLX_UNAME
|
||||
The CHAP login account for each volume in a pool, if
|
||||
``use_chap_auth`` is set to ``true``. Default account name is
|
||||
``chapadmin``.
|
||||
|
||||
EQLX_PW
|
||||
The corresponding password of EQLX_UNAME. The default password is
|
||||
randomly generated in hexadecimal, so you must set this password
|
||||
manually.
|
||||
|
||||
SAN_KEY_PATH (optional)
|
||||
The filename of the private key used for SSH authentication. This
|
||||
provides password-less login to the EqualLogic Group. Not used when
|
||||
``san_password`` is set. There is no default value.
|
||||
|
||||
In addition, enable thin provisioning for SAN volumes using the default
|
||||
``san_thin_provision = true`` setting.
|
||||
|
||||
Multiple back-end configuration
|
||||
-------------------------------
|
||||
|
||||
The following example shows the typical configuration for a Block
|
||||
Storage service that uses two Dell EqualLogic back ends:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
enabled_backends = backend1,backend2
|
||||
san_ssh_port = 22
|
||||
ssh_conn_timeout = 30
|
||||
san_thin_provision = true
|
||||
|
||||
[backend1]
|
||||
volume_driver = cinder.volume.drivers.dell_emc.ps.PSSeriesISCSIDriver
|
||||
volume_backend_name = backend1
|
||||
san_ip = IP_EQLX1
|
||||
san_login = SAN_UNAME
|
||||
san_password = SAN_PW
|
||||
eqlx_group_name = EQLX_GROUP
|
||||
eqlx_pool = EQLX_POOL
|
||||
|
||||
[backend2]
|
||||
volume_driver = cinder.volume.drivers.dell_emc.ps.PSSeriesISCSIDriver
|
||||
volume_backend_name = backend2
|
||||
san_ip = IP_EQLX2
|
||||
san_login = SAN_UNAME
|
||||
san_password = SAN_PW
|
||||
eqlx_group_name = EQLX_GROUP
|
||||
eqlx_pool = EQLX_POOL
|
||||
|
||||
In this example:
|
||||
|
||||
- Thin provisioning for SAN volumes is enabled
|
||||
(``san_thin_provision = true``). This is recommended when setting up
|
||||
Dell EqualLogic back ends.
|
||||
|
||||
- Each Dell EqualLogic back-end configuration (``[backend1]`` and
|
||||
``[backend2]``) has the same required settings as a single back-end
|
||||
configuration, with the addition of ``volume_backend_name``.
|
||||
|
||||
- The ``san_ssh_port`` option is set to its default value, 22. This
|
||||
option sets the port used for SSH.
|
||||
|
||||
- The ``ssh_conn_timeout`` option is also set to its default value, 30.
|
||||
This option sets the timeout in seconds for CLI commands over SSH.
|
||||
|
||||
- The ``IP_EQLX1`` and ``IP_EQLX2`` refer to the IP addresses used to
|
||||
reach the Dell EqualLogic Group of ``backend1`` and ``backend2``
|
||||
through SSH, respectively.
|
||||
|
||||
For information on configuring multiple back ends, see :doc:`Configure a
|
||||
multiple-storage back end </admin/blockstorage-multi-backend>`.
|
@ -24,9 +24,6 @@ title=Dell EMC XtremeIO Storage Driver (FC, iSCSI)
|
||||
[driver.dell_emc_powermax]
|
||||
title=Dell EMC PowerMax (2000, 8000) Storage Driver (iSCSI, FC)
|
||||
|
||||
[driver.dell_emc_ps]
|
||||
title=Dell EMC PS Series Storage Driver (iSCSI)
|
||||
|
||||
[driver.dell_emc_sc]
|
||||
title=Dell EMC SC Series Storage Driver (iSCSI, FC)
|
||||
|
||||
@ -186,7 +183,6 @@ notes=A vendor driver is considered supported if the vendor is
|
||||
isn't resolved before the end of the subsequent release.
|
||||
driver.datera=missing
|
||||
driver.dell_emc_powermax=complete
|
||||
driver.dell_emc_ps=complete
|
||||
driver.dell_emc_sc=complete
|
||||
driver.dell_emc_unity=complete
|
||||
driver.dell_emc_vmax_af=complete
|
||||
@ -245,7 +241,6 @@ notes=Cinder supports the ability to extend a volume that is attached to
|
||||
an instance, but not all drivers are able to do this.
|
||||
driver.datera=complete
|
||||
driver.dell_emc_powermax=complete
|
||||
driver.dell_emc_ps=complete
|
||||
driver.dell_emc_sc=complete
|
||||
driver.dell_emc_unity=complete
|
||||
driver.dell_emc_vmax_af=complete
|
||||
@ -304,7 +299,6 @@ notes=This is the ability to directly attach a snapshot to an
|
||||
instance like a volume.
|
||||
driver.datera=missing
|
||||
driver.dell_emc_powermax=missing
|
||||
driver.dell_emc_ps=missing
|
||||
driver.dell_emc_sc=missing
|
||||
driver.dell_emc_unity=complete
|
||||
driver.dell_emc_vmax_af=missing
|
||||
@ -366,7 +360,6 @@ notes=Vendor drivers that support Quality of Service (QoS) at the
|
||||
utilize frontend QoS via libvirt.
|
||||
driver.datera=complete
|
||||
driver.dell_emc_powermax=complete
|
||||
driver.dell_emc_ps=missing
|
||||
driver.dell_emc_sc=complete
|
||||
driver.dell_emc_unity=complete
|
||||
driver.dell_emc_vmax_af=complete
|
||||
@ -427,7 +420,6 @@ notes=Vendor drivers that support volume replication can report this
|
||||
to take advantage of Cinder's failover and failback commands.
|
||||
driver.datera=missing
|
||||
driver.dell_emc_powermax=complete
|
||||
driver.dell_emc_ps=missing
|
||||
driver.dell_emc_sc=complete
|
||||
driver.dell_emc_unity=complete
|
||||
driver.dell_emc_vmax_af=complete
|
||||
@ -489,7 +481,6 @@ notes=Vendor drivers that support consistency groups are able to
|
||||
creation of consistent snapshots across a group.
|
||||
driver.datera=missing
|
||||
driver.dell_emc_powermax=complete
|
||||
driver.dell_emc_ps=missing
|
||||
driver.dell_emc_sc=complete
|
||||
driver.dell_emc_unity=complete
|
||||
driver.dell_emc_vmax_af=complete
|
||||
@ -550,7 +541,6 @@ notes=If a volume driver supports thin provisioning it means that it
|
||||
'oversubscription'.
|
||||
driver.datera=missing
|
||||
driver.dell_emc_powermax=complete
|
||||
driver.dell_emc_ps=complete
|
||||
driver.dell_emc_sc=complete
|
||||
driver.dell_emc_unity=complete
|
||||
driver.dell_emc_vmax_af=complete
|
||||
@ -612,7 +602,6 @@ notes=Storage assisted volume migration is like host assisted volume
|
||||
functionality.
|
||||
driver.datera=missing
|
||||
driver.dell_emc_powermax=complete
|
||||
driver.dell_emc_ps=missing
|
||||
driver.dell_emc_sc=missing
|
||||
driver.dell_emc_unity=complete
|
||||
driver.dell_emc_vmax_af=complete
|
||||
@ -674,7 +663,6 @@ notes=Vendor drivers that report multi-attach support are able
|
||||
attach functionality otherwise data corruption may occur.
|
||||
driver.datera=missing
|
||||
driver.dell_emc_powermax=complete
|
||||
driver.dell_emc_ps=missing
|
||||
driver.dell_emc_sc=complete
|
||||
driver.dell_emc_unity=complete
|
||||
driver.dell_emc_vmax_af=complete
|
||||
@ -733,7 +721,6 @@ notes=Vendor drivers that implement the driver assisted function to revert a
|
||||
volume to the last snapshot taken.
|
||||
driver.datera=missing
|
||||
driver.dell_emc_powermax=complete
|
||||
driver.dell_emc_ps=missing
|
||||
driver.dell_emc_sc=missing
|
||||
driver.dell_emc_unity=complete
|
||||
driver.dell_emc_vmax_af=complete
|
||||
@ -796,7 +783,6 @@ notes=Vendor drivers that support running in an active/active
|
||||
a configuration.
|
||||
driver.datera=missing
|
||||
driver.dell_emc_powermax=missing
|
||||
driver.dell_emc_ps=missing
|
||||
driver.dell_emc_sc=missing
|
||||
driver.dell_emc_unity=missing
|
||||
driver.dell_emc_vmax_af=missing
|
||||
|
@ -1,6 +0,0 @@
|
||||
---
|
||||
deprecations:
|
||||
- The Dell EMC PS Series volume driver which supports
|
||||
Dell PS Series (EqualLogic) Storage is moving to
|
||||
maintenance mode in S Release and will be removed
|
||||
in T Release.
|
@ -1,3 +0,0 @@
|
||||
---
|
||||
features:
|
||||
- Added manage/unmanage volume support for Dell Equallogic driver.
|
@ -1,6 +0,0 @@
|
||||
---
|
||||
upgrade:
|
||||
- The EqualLogic driver is moved to the dell_emc directory and has been
|
||||
rebranded to its current Dell EMC PS Series name. The volume_driver
|
||||
entry in cinder.conf needs to be changed to
|
||||
``cinder.volume.drivers.dell_emc.ps.PSSeriesISCSIDriver``.
|
@ -1,5 +0,0 @@
|
||||
---
|
||||
fixes:
|
||||
- Dell EMC PS Series Driver code was creating duplicate ACL records during
|
||||
live migration. Fixes the initialize_connection code to not create access
|
||||
record for a host if one exists previously. This change fixes bug 1726591.
|
@ -1,5 +0,0 @@
|
||||
---
|
||||
fixes:
|
||||
- Dell EMC PS Series Driver was creating unmanaged snapshots
|
||||
when extending volumes. Fixed it by adding the missing
|
||||
no-snap parameter. This changes fixes bug 1720454.
|
@ -1,5 +0,0 @@
|
||||
---
|
||||
fixes:
|
||||
- Dell EMC PS Series Driver code reporting volume stats is now optimized
|
||||
to return the information earlier and accelerate the process. This change
|
||||
fixes bug 1661154.
|
@ -1,5 +0,0 @@
|
||||
---
|
||||
fixes:
|
||||
- |
|
||||
Dell EMC PS Driver stats report has been fixed, now reports the
|
||||
`provisioned_capacity_gb` properly. Fixes bug 1719659.
|
6
releasenotes/notes/ps-removedriver-5ba447c50f2474e7.yaml
Normal file
6
releasenotes/notes/ps-removedriver-5ba447c50f2474e7.yaml
Normal file
@ -0,0 +1,6 @@
|
||||
---
|
||||
upgrade:
|
||||
- |
|
||||
Dell EMC PS Series storage driver is not supported and
|
||||
removed starting from the Ussuri release. It was marked as
|
||||
deprecated in the Train release.
|
@ -1,4 +0,0 @@
|
||||
---
|
||||
features:
|
||||
- Dell EMC PS volume driver reports the total number
|
||||
of volumes on the backend in volume stats.
|
@ -1,15 +0,0 @@
|
||||
---
|
||||
upgrade:
|
||||
- |
|
||||
|
||||
Removing the Dell EqualLogic driver's deprecated configuration options.
|
||||
Please replace old options in your cinder.conf with the new one.
|
||||
|
||||
* Removed - ``eqlx_cli_timeout``
|
||||
* Replaced with - ``ssh_conn_timeout``
|
||||
* Removed - ``eqlx_use_chap``
|
||||
* Replaced with - ``use_chap_auth``
|
||||
* Removed - ``eqlx_chap_login``
|
||||
* Replaced with - ``chap_username``
|
||||
* Removed - ``eqlx_chap_password``
|
||||
* Replaced with - ``chap_password``
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user