cinder/cinder/tests/unit/test_hitachi_hnas_iscsi.py

577 lines
21 KiB
Python

# Copyright (c) 2014 Hitachi Data Systems, Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
"""
Self test for Hitachi Unified Storage (HUS-HNAS) platform.
"""
import os
import tempfile
import time
import mock
from oslo_concurrency import processutils as putils
import six
from cinder import exception
from cinder import test
from cinder.volume import configuration as conf
from cinder.volume.drivers.hitachi import hnas_iscsi as iscsi
from cinder.volume import volume_types
HNASCONF = """<?xml version="1.0" encoding="UTF-8" ?>
<config>
<hnas_cmd>ssc</hnas_cmd>
<chap_enabled>True</chap_enabled>
<mgmt_ip0>172.17.44.15</mgmt_ip0>
<username>supervisor</username>
<password>supervisor</password>
<svc_0>
<volume_type>default</volume_type>
<iscsi_ip>172.17.39.132</iscsi_ip>
<hdp>fs2</hdp>
</svc_0>
<svc_1>
<volume_type>silver</volume_type>
<iscsi_ip>172.17.39.133</iscsi_ip>
<hdp>fs2</hdp>
</svc_1>
</config>
"""
HNAS_WRONG_CONF1 = """<?xml version="1.0" encoding="UTF-8" ?>
<config>
<hnas_cmd>ssc</hnas_cmd>
<mgmt_ip0>172.17.44.15</mgmt_ip0>
<username>supervisor</username>
<password>supervisor</password>
<volume_type>default</volume_type>
<hdp>172.17.39.132:/cinder</hdp>
</svc_0>
</config>
"""
HNAS_WRONG_CONF2 = """<?xml version="1.0" encoding="UTF-8" ?>
<config>
<hnas_cmd>ssc</hnas_cmd>
<mgmt_ip0>172.17.44.15</mgmt_ip0>
<username>supervisor</username>
<password>supervisor</password>
<svc_0>
<volume_type>default</volume_type>
</svc_0>
<svc_1>
<volume_type>silver</volume_type>
</svc_1>
</config>
"""
# The following information is passed on to tests, when creating a volume
_VOLUME = {'name': 'testvol', 'volume_id': '1234567890', 'size': 128,
'volume_type': 'silver', 'volume_type_id': '1',
'provider_location': '83-68-96-AA-DA-5D.volume-2dfe280e-470a-4182'
'-afb8-1755025c35b8', 'id': 'abcdefg',
'host': 'host1@hnas-iscsi-backend#silver'}
class SimulatedHnasBackend(object):
"""Simulation Back end. Talks to HNAS."""
# these attributes are shared across object instances
start_lun = 0
init_index = 0
target_index = 0
hlun = 0
def __init__(self):
self.type = 'HNAS'
self.out = ''
self.volumes = []
# iSCSI connections
self.connections = []
def rename_existing_lu(self, cmd, ip0, user, pw, fslabel,
vol_name, vol_ref_name):
return 'Logical unit modified successfully.'
def get_existing_lu_info(self, cmd, ip0, user, pw, fslabel, lun):
out = "Name : volume-test \n\
Comment: \n\
Path : /.cinder/volume-test.iscsi \n\
Size : 20 GB \n\
File System : manage_iscsi_test \n\
File System Mounted : Yes \n\
Logical Unit Mounted: Yes"
return out
def deleteVolume(self, name):
volume = self.getVolume(name)
if volume:
self.volumes.remove(volume)
return True
else:
return False
def deleteVolumebyProvider(self, provider):
volume = self.getVolumebyProvider(provider)
if volume:
self.volumes.remove(volume)
return True
else:
return False
def getVolumes(self):
return self.volumes
def getVolume(self, name):
if self.volumes:
for volume in self.volumes:
if str(volume['name']) == name:
return volume
return None
def getVolumebyProvider(self, provider):
if self.volumes:
for volume in self.volumes:
if str(volume['provider_location']) == provider:
return volume
return None
def createVolume(self, name, provider, sizeMiB, comment):
new_vol = {'additionalStates': [],
'adminSpace': {'freeMiB': 0,
'rawReservedMiB': 384,
'reservedMiB': 128,
'usedMiB': 128},
'baseId': 115,
'copyType': 1,
'creationTime8601': '2012-10-22T16:37:57-07:00',
'creationTimeSec': 1350949077,
'failedStates': [],
'id': 115,
'provider_location': provider,
'name': name,
'comment': comment,
'provisioningType': 1,
'readOnly': False,
'sizeMiB': sizeMiB,
'state': 1,
'userSpace': {'freeMiB': 0,
'rawReservedMiB': 41984,
'reservedMiB': 31488,
'usedMiB': 31488},
'usrSpcAllocLimitPct': 0,
'usrSpcAllocWarningPct': 0,
'uuid': '1e7daee4-49f4-4d07-9ab8-2b6a4319e243',
'wwn': '50002AC00073383D'}
self.volumes.append(new_vol)
def create_lu(self, cmd, ip0, user, pw, hdp, size, name):
vol_id = name
_out = ("LUN: %d HDP: fs2 size: %s MB, is successfully created" %
(self.start_lun, size))
self.createVolume(name, vol_id, size, "create-lu")
self.start_lun += 1
return _out
def delete_lu(self, cmd, ip0, user, pw, hdp, lun):
_out = ""
id = "myID"
self.deleteVolumebyProvider(id + '.' + str(lun))
return _out
def create_dup(self, cmd, ip0, user, pw, src_lun, hdp, size, name):
_out = ("LUN: %s HDP: 9 size: %s MB, is successfully created" %
(self.start_lun, size))
id = name
self.createVolume(name, id + '.' + str(self.start_lun), size,
"create-dup")
self.start_lun += 1
return _out
def add_iscsi_conn(self, cmd, ip0, user, pw, lun, hdp,
port, iqn, initiator):
ctl = ""
conn = (self.hlun, lun, initiator, self.init_index, iqn,
self.target_index, ctl, port)
_out = ("H-LUN: %d mapped. LUN: %s, iSCSI Initiator: %s @ index: %d, \
and Target: %s @ index %d is successfully paired @ CTL: %s, \
Port: %s" % conn)
self.init_index += 1
self.target_index += 1
self.hlun += 1
self.connections.append(conn)
return _out
def del_iscsi_conn(self, cmd, ip0, user, pw, port, iqn, initiator):
self.connections.pop()
_out = ("H-LUN: successfully deleted from target")
return _out
def extend_vol(self, cmd, ip0, user, pw, hdp, lu, size, name):
_out = ("LUN: %s successfully extended to %s MB" % (lu, size))
id = name
self.out = _out
v = self.getVolumebyProvider(id + '.' + str(lu))
if v:
v['sizeMiB'] = size
return _out
def get_luns(self):
return len(self.alloc_lun)
def get_conns(self):
return len(self.connections)
def get_out(self):
return str(self.out)
def get_version(self, cmd, ver, ip0, user, pw):
self.out = "Array_ID: 18-48-A5-A1-80-13 (3080-G2) " \
"version: 11.2.3319.09 LU: 256" \
" RG: 0 RG_LU: 0 Utility_version: 11.1.3225.01"
return self.out
def get_iscsi_info(self, cmd, ip0, user, pw):
self.out = "CTL: 0 Port: 4 IP: 172.17.39.132 Port: 3260 Link: Up\n" \
"CTL: 1 Port: 5 IP: 172.17.39.133 Port: 3260 Link: Up"
return self.out
def get_hdp_info(self, cmd, ip0, user, pw, fslabel=None):
self.out = "HDP: 1024 272384 MB 33792 MB 12 % LUs: " \
"70 Normal fs1\n" \
"HDP: 1025 546816 MB 73728 MB 13 % LUs: 194 Normal fs2"
return self.out
def get_targetiqn(self, cmd, ip0, user, pw, id, hdp, secret):
self.out = """iqn.2013-08.cinderdomain:vs61.cindertarget"""
return self.out
def set_targetsecret(self, cmd, ip0, user, pw, target, hdp, secret):
self.out = """iqn.2013-08.cinderdomain:vs61.cindertarget"""
return self.out
def get_targetsecret(self, cmd, ip0, user, pw, target, hdp):
self.out = """wGkJhTpXaaYJ5Rv"""
return self.out
def get_evs(self, cmd, ip0, user, pw, fsid):
return '1'
def check_lu(self, cmd, ip0, user, pw, volume_name, hdp):
return True, 1, {'alias': 'cinder-default', 'secret': 'mysecret',
'iqn': 'iqn.1993-08.org.debian:01:11f90746eb2'}
def check_target(self, cmd, ip0, user, pw, hdp, target_alias):
return False, None
class HNASiSCSIDriverTest(test.TestCase):
"""Test HNAS iSCSI volume driver."""
def __init__(self, *args, **kwargs):
super(HNASiSCSIDriverTest, self).__init__(*args, **kwargs)
@mock.patch.object(iscsi, 'factory_bend')
def setUp(self, _factory_bend):
super(HNASiSCSIDriverTest, self).setUp()
self.backend = SimulatedHnasBackend()
_factory_bend.return_value = self.backend
self.config_file = tempfile.NamedTemporaryFile("w+", suffix='.xml')
self.addCleanup(self.config_file.close)
self.config_file.write(HNASCONF)
self.config_file.flush()
self.configuration = mock.Mock(spec=conf.Configuration)
self.configuration.hds_hnas_iscsi_config_file = self.config_file.name
self.configuration.hds_svc_iscsi_chap_enabled = True
self.driver = iscsi.HDSISCSIDriver(configuration=self.configuration)
self.driver.do_setup("")
def _create_volume(self):
loc = self.driver.create_volume(_VOLUME)
vol = _VOLUME.copy()
vol['provider_location'] = loc['provider_location']
return vol
@mock.patch('six.moves.builtins.open')
@mock.patch.object(os, 'access')
def test_read_config(self, m_access, m_open):
# Test exception when file is not found
m_access.return_value = False
m_open.return_value = six.StringIO(HNASCONF)
self.assertRaises(exception.NotFound, iscsi._read_config, '')
# Test exception when config file has parsing errors
# due to missing <svc> tag
m_access.return_value = True
m_open.return_value = six.StringIO(HNAS_WRONG_CONF1)
self.assertRaises(exception.ConfigNotFound, iscsi._read_config, '')
# Test exception when config file has parsing errors
# due to missing <hdp> tag
m_open.return_value = six.StringIO(HNAS_WRONG_CONF2)
self.configuration.hds_hnas_iscsi_config_file = ''
self.assertRaises(exception.ParameterNotFound, iscsi._read_config, '')
def test_create_volume(self):
loc = self.driver.create_volume(_VOLUME)
self.assertNotEqual(loc, None)
self.assertNotEqual(loc['provider_location'], None)
# cleanup
self.backend.deleteVolumebyProvider(loc['provider_location'])
def test_get_volume_stats(self):
stats = self.driver.get_volume_stats(True)
self.assertEqual("HDS", stats["vendor_name"])
self.assertEqual("iSCSI", stats["storage_protocol"])
self.assertEqual(2, len(stats['pools']))
def test_delete_volume(self):
vol = self._create_volume()
self.driver.delete_volume(vol)
# should not be deletable twice
prov_loc = self.backend.getVolumebyProvider(vol['provider_location'])
self.assertTrue(prov_loc is None)
def test_extend_volume(self):
vol = self._create_volume()
new_size = _VOLUME['size'] * 2
self.driver.extend_volume(vol, new_size)
# cleanup
self.backend.deleteVolumebyProvider(vol['provider_location'])
@mock.patch.object(iscsi.HDSISCSIDriver, '_id_to_vol')
def test_create_snapshot(self, m_id_to_vol):
vol = self._create_volume()
m_id_to_vol.return_value = vol
svol = vol.copy()
svol['volume_size'] = svol['size']
loc = self.driver.create_snapshot(svol)
self.assertNotEqual(loc, None)
svol['provider_location'] = loc['provider_location']
# cleanup
self.backend.deleteVolumebyProvider(svol['provider_location'])
self.backend.deleteVolumebyProvider(vol['provider_location'])
@mock.patch.object(iscsi.HDSISCSIDriver, '_id_to_vol')
def test_create_clone(self, m_id_to_vol):
src_vol = self._create_volume()
m_id_to_vol.return_value = src_vol
src_vol['volume_size'] = src_vol['size']
dst_vol = self._create_volume()
dst_vol['volume_size'] = dst_vol['size']
loc = self.driver.create_cloned_volume(dst_vol, src_vol)
self.assertNotEqual(loc, None)
# cleanup
self.backend.deleteVolumebyProvider(src_vol['provider_location'])
self.backend.deleteVolumebyProvider(loc['provider_location'])
@mock.patch.object(iscsi.HDSISCSIDriver, '_id_to_vol')
@mock.patch.object(iscsi.HDSISCSIDriver, 'extend_volume')
def test_create_clone_larger_size(self, m_extend_volume, m_id_to_vol):
src_vol = self._create_volume()
m_id_to_vol.return_value = src_vol
src_vol['volume_size'] = src_vol['size']
dst_vol = self._create_volume()
dst_vol['size'] = 256
dst_vol['volume_size'] = dst_vol['size']
loc = self.driver.create_cloned_volume(dst_vol, src_vol)
self.assertNotEqual(loc, None)
m_extend_volume.assert_called_once_with(dst_vol, 256)
# cleanup
self.backend.deleteVolumebyProvider(src_vol['provider_location'])
self.backend.deleteVolumebyProvider(loc['provider_location'])
@mock.patch.object(iscsi.HDSISCSIDriver, '_id_to_vol')
def test_delete_snapshot(self, m_id_to_vol):
svol = self._create_volume()
lun = svol['provider_location']
m_id_to_vol.return_value = svol
self.driver.delete_snapshot(svol)
self.assertTrue(self.backend.getVolumebyProvider(lun) is None)
def test_create_volume_from_snapshot(self):
svol = self._create_volume()
svol['volume_size'] = svol['size']
vol = self.driver.create_volume_from_snapshot(_VOLUME, svol)
self.assertNotEqual(vol, None)
# cleanup
self.backend.deleteVolumebyProvider(svol['provider_location'])
self.backend.deleteVolumebyProvider(vol['provider_location'])
@mock.patch.object(time, 'sleep')
@mock.patch.object(iscsi.HDSISCSIDriver, '_update_vol_location')
def test_initialize_connection(self, m_update_vol_location, m_sleep):
connector = {}
connector['initiator'] = 'iqn.1993-08.org.debian:01:11f90746eb2'
connector['host'] = 'dut_1.lab.hds.com'
vol = self._create_volume()
conn = self.driver.initialize_connection(vol, connector)
self.assertTrue('3260' in conn['data']['target_portal'])
self.assertTrue(type(conn['data']['target_lun']) is int)
self.backend.add_iscsi_conn = mock.MagicMock()
self.backend.add_iscsi_conn.side_effect = putils.ProcessExecutionError
self.assertRaises(exception.ISCSITargetAttachFailed,
self.driver.initialize_connection, vol, connector)
# cleanup
self.backend.deleteVolumebyProvider(vol['provider_location'])
@mock.patch.object(iscsi.HDSISCSIDriver, '_update_vol_location')
def test_terminate_connection(self, m_update_vol_location):
connector = {}
connector['initiator'] = 'iqn.1993-08.org.debian:01:11f90746eb2'
connector['host'] = 'dut_1.lab.hds.com'
vol = self._create_volume()
vol['provider_location'] = "portal," +\
connector['initiator'] +\
",18-48-A5-A1-80-13.0,ctl,port,hlun"
conn = self.driver.initialize_connection(vol, connector)
num_conn_before = self.backend.get_conns()
self.driver.terminate_connection(vol, conn)
num_conn_after = self.backend.get_conns()
self.assertNotEqual(num_conn_before, num_conn_after)
# cleanup
self.backend.deleteVolumebyProvider(vol['provider_location'])
@mock.patch.object(volume_types, 'get_volume_type_extra_specs',
return_value={'key': 'type', 'service_label': 'silver'})
def test_get_pool(self, m_ext_spec):
label = self.driver.get_pool(_VOLUME)
self.assertEqual('silver', label)
@mock.patch.object(time, 'sleep')
@mock.patch.object(iscsi.HDSISCSIDriver, '_update_vol_location')
def test_get_service_target(self, m_update_vol_location, m_sleep):
vol = _VOLUME.copy()
self.backend.check_lu = mock.MagicMock()
self.backend.check_target = mock.MagicMock()
# Test the case where volume is not already mapped - CHAP enabled
self.backend.check_lu.return_value = (False, 0, None)
self.backend.check_target.return_value = (False, None)
ret = self.driver._get_service_target(vol)
iscsi_ip, iscsi_port, ctl, svc_port, hdp, alias, secret = ret
self.assertEqual('evs1-tgt0', alias)
# Test the case where volume is not already mapped - CHAP disabled
self.driver.config['chap_enabled'] = 'False'
ret = self.driver._get_service_target(vol)
iscsi_ip, iscsi_port, ctl, svc_port, hdp, alias, secret = ret
self.assertEqual('evs1-tgt0', alias)
# Test the case where all targets are full
fake_tgt = {'alias': 'fake', 'luns': range(0, 32)}
self.backend.check_lu.return_value = (False, 0, None)
self.backend.check_target.return_value = (True, fake_tgt)
self.assertRaises(exception.NoMoreTargets,
self.driver._get_service_target, vol)
@mock.patch.object(iscsi.HDSISCSIDriver, '_get_service')
def test_unmanage(self, get_service):
get_service.return_value = ('fs2')
self.driver.unmanage(_VOLUME)
get_service.assert_called_once_with(_VOLUME)
def test_manage_existing_get_size(self):
vol = _VOLUME.copy()
existing_vol_ref = {'source-name': 'manage_iscsi_test/volume-test'}
out = self.driver.manage_existing_get_size(vol, existing_vol_ref)
self.assertEqual(20, out)
def test_manage_existing_get_size_error(self):
vol = _VOLUME.copy()
existing_vol_ref = {'source-name': 'invalid_FS/vol-not-found'}
self.assertRaises(exception.ManageExistingInvalidReference,
self.driver.manage_existing_get_size, vol,
existing_vol_ref)
def test_manage_existing_get_size_without_source_name(self):
vol = _VOLUME.copy()
existing_vol_ref = {
'source-id': 'bcc48c61-9691-4e5f-897c-793686093190'}
self.assertRaises(exception.ManageExistingInvalidReference,
self.driver.manage_existing_get_size, vol,
existing_vol_ref)
@mock.patch.object(volume_types, 'get_volume_type_extra_specs')
def test_manage_existing(self, m_get_extra_specs):
vol = _VOLUME.copy()
existing_vol_ref = {'source-name': 'fs2/volume-test'}
version = {'provider_location': '18-48-A5-A1-80-13.testvol'}
m_get_extra_specs.return_value = {'key': 'type',
'service_label': 'silver'}
out = self.driver.manage_existing(vol, existing_vol_ref)
m_get_extra_specs.assert_called_once_with('1')
self.assertEqual(version, out)
@mock.patch.object(volume_types, 'get_volume_type_extra_specs')
def test_manage_existing_invalid_pool(self, m_get_extra_specs):
vol = _VOLUME.copy()
existing_vol_ref = {'source-name': 'fs2/volume-test'}
m_get_extra_specs.return_value = {'key': 'type',
'service_label': 'gold'}
self.assertRaises(exception.ManageExistingVolumeTypeMismatch,
self.driver.manage_existing, vol, existing_vol_ref)
m_get_extra_specs.assert_called_once_with('1')
def test_manage_existing_invalid_volume_name(self):
vol = _VOLUME.copy()
existing_vol_ref = {'source-name': 'fs2/t/est_volume'}
self.assertRaises(exception.ManageExistingInvalidReference,
self.driver.manage_existing, vol, existing_vol_ref)
def test_manage_existing_without_volume_name(self):
vol = _VOLUME.copy()
existing_vol_ref = {'source-name': 'fs2/'}
self.assertRaises(exception.ManageExistingInvalidReference,
self.driver.manage_existing, vol, existing_vol_ref)
def test_manage_existing_with_FS_and_spaces(self):
vol = _VOLUME.copy()
existing_vol_ref = {'source-name': 'fs2/ '}
self.assertRaises(exception.ManageExistingInvalidReference,
self.driver.manage_existing, vol, existing_vol_ref)