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:
rajinir 2020-01-22 10:43:34 -06:00
parent 3e67d8883b
commit 20924ae32c
20 changed files with 7 additions and 9582 deletions

View File

@ -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,

View File

@ -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, '')

View File

@ -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()

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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>`.

View File

@ -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

View File

@ -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.

View File

@ -1,3 +0,0 @@
---
features:
- Added manage/unmanage volume support for Dell Equallogic driver.

View File

@ -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``.

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -1,5 +0,0 @@
---
fixes:
- |
Dell EMC PS Driver stats report has been fixed, now reports the
`provisioned_capacity_gb` properly. Fixes bug 1719659.

View 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.

View File

@ -1,4 +0,0 @@
---
features:
- Dell EMC PS volume driver reports the total number
of volumes on the backend in volume stats.

View File

@ -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