Remove Hitachi volume drivers
Hitachi has decided to provide their drivers out of tree [1]. The drivers were marked as unsupported in-tree in the Pike release and are now being removed. [1] http://lists.openstack.org/pipermail/openstack/2017-March/018812.html Change-Id: I23867aa98f68298beb5db4558c66c1ffd4e7d6f1 Closes-bug: #1652864 Closes-bug: #1671966 Closes-bug: #1677688 Closes-bug: #1677923 Closes-bug: #1645738
This commit is contained in:
parent
df8ec82a18
commit
55d726e5c3
@ -1128,45 +1128,6 @@ class InvalidGroupSnapshotStatus(Invalid):
|
|||||||
message = _("Invalid GroupSnapshot Status: %(reason)s")
|
message = _("Invalid GroupSnapshot Status: %(reason)s")
|
||||||
|
|
||||||
|
|
||||||
# Hitachi Block Storage Driver
|
|
||||||
class HBSDError(VolumeDriverException):
|
|
||||||
message = _("HBSD error occurs.")
|
|
||||||
|
|
||||||
|
|
||||||
class HBSDCmdError(HBSDError):
|
|
||||||
|
|
||||||
def __init__(self, message=None, ret=None, err=None):
|
|
||||||
self.ret = ret
|
|
||||||
self.stderr = err
|
|
||||||
|
|
||||||
super(HBSDCmdError, self).__init__(message=message)
|
|
||||||
|
|
||||||
|
|
||||||
class HBSDBusy(HBSDError):
|
|
||||||
message = "Device or resource is busy."
|
|
||||||
|
|
||||||
|
|
||||||
class HBSDNotFound(NotFound):
|
|
||||||
message = _("Storage resource could not be found.")
|
|
||||||
|
|
||||||
|
|
||||||
class HBSDVolumeIsBusy(VolumeIsBusy):
|
|
||||||
message = _("Volume %(volume_name)s is busy.")
|
|
||||||
|
|
||||||
|
|
||||||
# Hitachi VSP Driver
|
|
||||||
class VSPError(VolumeDriverException):
|
|
||||||
message = _("VSP error occurred. %(message)s")
|
|
||||||
|
|
||||||
|
|
||||||
class VSPBusy(VSPError):
|
|
||||||
message = _("Device or resource is busy.")
|
|
||||||
|
|
||||||
|
|
||||||
class VSPNotSupported(VSPError):
|
|
||||||
message = _("The function on the storage is not supported.")
|
|
||||||
|
|
||||||
|
|
||||||
# Datera driver
|
# Datera driver
|
||||||
class DateraAPIException(VolumeBackendAPIException):
|
class DateraAPIException(VolumeBackendAPIException):
|
||||||
message = _("Bad response from Datera API")
|
message = _("Bad response from Datera API")
|
||||||
@ -1297,11 +1258,6 @@ class NotSupportedOperation(Invalid):
|
|||||||
code = 405
|
code = 405
|
||||||
|
|
||||||
|
|
||||||
# Hitachi HNAS drivers
|
|
||||||
class HNASConnError(VolumeDriverException):
|
|
||||||
message = "%(message)s"
|
|
||||||
|
|
||||||
|
|
||||||
# NexentaStor driver exception
|
# NexentaStor driver exception
|
||||||
class NexentaException(VolumeDriverException):
|
class NexentaException(VolumeDriverException):
|
||||||
message = "%(message)s"
|
message = "%(message)s"
|
||||||
|
@ -99,26 +99,6 @@ from cinder.volume.drivers.fujitsu import eternus_dx_common as \
|
|||||||
from cinder.volume.drivers.fusionstorage import dsware as \
|
from cinder.volume.drivers.fusionstorage import dsware as \
|
||||||
cinder_volume_drivers_fusionstorage_dsware
|
cinder_volume_drivers_fusionstorage_dsware
|
||||||
from cinder.volume.drivers import hgst as cinder_volume_drivers_hgst
|
from cinder.volume.drivers import hgst as cinder_volume_drivers_hgst
|
||||||
from cinder.volume.drivers.hitachi import hbsd_common as \
|
|
||||||
cinder_volume_drivers_hitachi_hbsdcommon
|
|
||||||
from cinder.volume.drivers.hitachi import hbsd_fc as \
|
|
||||||
cinder_volume_drivers_hitachi_hbsdfc
|
|
||||||
from cinder.volume.drivers.hitachi import hbsd_horcm as \
|
|
||||||
cinder_volume_drivers_hitachi_hbsdhorcm
|
|
||||||
from cinder.volume.drivers.hitachi import hbsd_iscsi as \
|
|
||||||
cinder_volume_drivers_hitachi_hbsdiscsi
|
|
||||||
from cinder.volume.drivers.hitachi import hnas_nfs as \
|
|
||||||
cinder_volume_drivers_hitachi_hnasnfs
|
|
||||||
from cinder.volume.drivers.hitachi import hnas_utils as \
|
|
||||||
cinder_volume_drivers_hitachi_hnasutils
|
|
||||||
from cinder.volume.drivers.hitachi import vsp_common as \
|
|
||||||
cinder_volume_drivers_hitachi_vspcommon
|
|
||||||
from cinder.volume.drivers.hitachi import vsp_fc as \
|
|
||||||
cinder_volume_drivers_hitachi_vspfc
|
|
||||||
from cinder.volume.drivers.hitachi import vsp_horcm as \
|
|
||||||
cinder_volume_drivers_hitachi_vsphorcm
|
|
||||||
from cinder.volume.drivers.hitachi import vsp_iscsi as \
|
|
||||||
cinder_volume_drivers_hitachi_vspiscsi
|
|
||||||
from cinder.volume.drivers.hpe import hpe_3par_common as \
|
from cinder.volume.drivers.hpe import hpe_3par_common as \
|
||||||
cinder_volume_drivers_hpe_hpe3parcommon
|
cinder_volume_drivers_hpe_hpe3parcommon
|
||||||
from cinder.volume.drivers.hpe import hpe_lefthand_iscsi as \
|
from cinder.volume.drivers.hpe import hpe_lefthand_iscsi as \
|
||||||
@ -296,16 +276,6 @@ def list_opts():
|
|||||||
FJ_ETERNUS_DX_OPT_opts,
|
FJ_ETERNUS_DX_OPT_opts,
|
||||||
cinder_volume_drivers_fusionstorage_dsware.volume_opts,
|
cinder_volume_drivers_fusionstorage_dsware.volume_opts,
|
||||||
cinder_volume_drivers_hgst.hgst_opts,
|
cinder_volume_drivers_hgst.hgst_opts,
|
||||||
cinder_volume_drivers_hitachi_hbsdcommon.volume_opts,
|
|
||||||
cinder_volume_drivers_hitachi_hbsdfc.volume_opts,
|
|
||||||
cinder_volume_drivers_hitachi_hbsdhorcm.volume_opts,
|
|
||||||
cinder_volume_drivers_hitachi_hbsdiscsi.volume_opts,
|
|
||||||
cinder_volume_drivers_hitachi_hnasnfs.NFS_OPTS,
|
|
||||||
cinder_volume_drivers_hitachi_hnasutils.drivers_common_opts,
|
|
||||||
cinder_volume_drivers_hitachi_vspcommon.common_opts,
|
|
||||||
cinder_volume_drivers_hitachi_vspfc.fc_opts,
|
|
||||||
cinder_volume_drivers_hitachi_vsphorcm.horcm_opts,
|
|
||||||
cinder_volume_drivers_hitachi_vspiscsi.iscsi_opts,
|
|
||||||
cinder_volume_drivers_hpe_hpe3parcommon.hpe3par_opts,
|
cinder_volume_drivers_hpe_hpe3parcommon.hpe3par_opts,
|
||||||
cinder_volume_drivers_hpe_hpelefthandiscsi.hpelefthand_opts,
|
cinder_volume_drivers_hpe_hpelefthandiscsi.hpelefthand_opts,
|
||||||
cinder_volume_drivers_huawei_huaweidriver.huawei_opts,
|
cinder_volume_drivers_huawei_huaweidriver.huawei_opts,
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -1,618 +0,0 @@
|
|||||||
# Copyright (C) 2014, Hitachi, Ltd.
|
|
||||||
#
|
|
||||||
# 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 Block Storage Driver
|
|
||||||
"""
|
|
||||||
|
|
||||||
import mock
|
|
||||||
|
|
||||||
from cinder import exception
|
|
||||||
from cinder import test
|
|
||||||
from cinder.volume import configuration as conf
|
|
||||||
from cinder.volume.drivers.hitachi import hbsd_basiclib
|
|
||||||
from cinder.volume.drivers.hitachi import hbsd_common
|
|
||||||
from cinder.volume.drivers.hitachi import hbsd_fc
|
|
||||||
from cinder.volume.drivers.hitachi import hbsd_snm2
|
|
||||||
|
|
||||||
|
|
||||||
def _exec_hsnm(*args, **kargs):
|
|
||||||
return HBSDSNM2FCDriverTest.hsnm_vals.get(args)
|
|
||||||
|
|
||||||
|
|
||||||
def _exec_hsnm_get_lu_ret_err(*args, **kargs):
|
|
||||||
return HBSDSNM2FCDriverTest.hsnm_get_lu_ret_err.get(args)
|
|
||||||
|
|
||||||
|
|
||||||
def _exec_hsnm_get_lu_vol_type_err(*args, **kargs):
|
|
||||||
return HBSDSNM2FCDriverTest.hsnm_get_lu_vol_type_err.get(args)
|
|
||||||
|
|
||||||
|
|
||||||
def _exec_hsnm_get_lu_dppool_err(*args, **kargs):
|
|
||||||
return HBSDSNM2FCDriverTest.hsnm_get_lu_dppool_err.get(args)
|
|
||||||
|
|
||||||
|
|
||||||
def _exec_hsnm_get_lu_size_err(*args, **kargs):
|
|
||||||
return HBSDSNM2FCDriverTest.hsnm_get_lu_size_err.get(args)
|
|
||||||
|
|
||||||
|
|
||||||
def _exec_hsnm_get_lu_num_port_err(*args, **kargs):
|
|
||||||
return HBSDSNM2FCDriverTest.hsnm_get_lu_num_port_err.get(args)
|
|
||||||
|
|
||||||
|
|
||||||
class HBSDSNM2FCDriverTest(test.TestCase):
|
|
||||||
"""Test HBSDSNM2FCDriver."""
|
|
||||||
|
|
||||||
audppool_result = " DP RAID \
|
|
||||||
Current Utilization Current Over Replication\
|
|
||||||
Available Current Replication Rotational \
|
|
||||||
\
|
|
||||||
Stripe \
|
|
||||||
Needing Preparation\n\
|
|
||||||
Pool Tier Mode Level Total Capacity Consumed Capacity \
|
|
||||||
Percent Provisioning Percent Capacity \
|
|
||||||
Utilization Percent Type Speed Encryption Status \
|
|
||||||
\
|
|
||||||
Reconstruction Progress Size Capacity\n\
|
|
||||||
30 Disable 1( 1D+1D) 532.0 GB 2.0 GB \
|
|
||||||
1% 24835% 532.0 GB \
|
|
||||||
1% SAS 10000rpm N/A Normal \
|
|
||||||
N/A \
|
|
||||||
256KB 0.0 GB"
|
|
||||||
|
|
||||||
aureplicationlocal_result = "Pair Name LUN Pair \
|
|
||||||
LUN Status Copy Type Group \
|
|
||||||
Point-in-Time MU Number\n\
|
|
||||||
0 10 0 Split( 99%) \
|
|
||||||
ShadowImage ---:Ungrouped N/A\
|
|
||||||
"
|
|
||||||
|
|
||||||
auluref_result = " Stripe RAID DP Tier \
|
|
||||||
RAID Rotational Number\n\
|
|
||||||
LU Capacity Size Group Pool Mode Level Type\
|
|
||||||
Speed of Paths Status\n\
|
|
||||||
0 2097152 blocks 256KB 0 0 Enable 0 Normal"
|
|
||||||
|
|
||||||
auluref_result1 = " Stripe RAID DP Tier \
|
|
||||||
RAID Rotational Number\n\
|
|
||||||
LU Capacity Size Group Pool Mode Level Type\
|
|
||||||
Speed of Paths Status\n\
|
|
||||||
0 2097152 blocks 256KB 0 0 Enable 0 DUMMY"
|
|
||||||
|
|
||||||
auhgwwn_result = "Port 00 Host Group Security ON\n Detected WWN\n \
|
|
||||||
Name Port Name Host Group\n\
|
|
||||||
HBSD-00 10000000C97BCE7A 001:HBSD-01\n\
|
|
||||||
Assigned WWN\n Name Port Name \
|
|
||||||
Host Group\n abcdefg 10000000C97BCE7A \
|
|
||||||
001:HBSD-01"
|
|
||||||
|
|
||||||
aufibre1_result = "Port Information\n\
|
|
||||||
Port Address\n CTL Port\
|
|
||||||
Node Name Port Name Setting Current\n 0 0 \
|
|
||||||
50060E801053C2E0 50060E801053C2E0 0000EF 272700"
|
|
||||||
|
|
||||||
auhgmap_result = "Mapping Mode = ON\nPort Group \
|
|
||||||
H-LUN LUN\n 00 001:HBSD-00 0 1000"
|
|
||||||
|
|
||||||
hsnm_vals = {
|
|
||||||
('audppool', '-unit None -refer -g'): [0, "%s" % audppool_result, ""],
|
|
||||||
('aureplicationlocal',
|
|
||||||
'-unit None -create -si -pvol 1 -svol 1 -compsplit -pace normal'):
|
|
||||||
[0, "", ""],
|
|
||||||
('aureplicationlocal',
|
|
||||||
'-unit None -create -si -pvol 3 -svol 1 -compsplit -pace normal'):
|
|
||||||
[1, "", ""],
|
|
||||||
('aureplicationlocal', '-unit None -refer -pvol 1'):
|
|
||||||
[0, "%s" % aureplicationlocal_result, ""],
|
|
||||||
('aureplicationlocal', '-unit None -refer -pvol 3'):
|
|
||||||
[1, "", "DMEC002015"],
|
|
||||||
('aureplicationlocal', '-unit None -refer -svol 3'):
|
|
||||||
[1, "", "DMEC002015"],
|
|
||||||
('aureplicationlocal', '-unit None -simplex -si -pvol 1 -svol 0'):
|
|
||||||
[0, "", ""],
|
|
||||||
('auluchgsize', '-unit None -lu 1 -size 256g'):
|
|
||||||
[0, "", ""],
|
|
||||||
('auludel', '-unit None -lu 1 -f'): [0, 0, ""],
|
|
||||||
('auludel', '-unit None -lu 3 -f'): [1, 0, ""],
|
|
||||||
('auluadd', '-unit None -lu 1 -dppoolno 30 -size 128g'): [0, 0, ""],
|
|
||||||
('auluadd', '-unit None -lu 1 -dppoolno 30 -size 256g'): [1, "", ""],
|
|
||||||
('auluref', '-unit None'): [0, "%s" % auluref_result, ""],
|
|
||||||
('auluref', '-unit None -lu 0'): [0, "%s" % auluref_result, ""],
|
|
||||||
('auhgmap', '-unit None -add 0 0 1 1 1'): [0, 0, ""],
|
|
||||||
('auhgwwn', '-unit None -refer'): [0, "%s" % auhgwwn_result, ""],
|
|
||||||
('aufibre1', '-unit None -refer'): [0, "%s" % aufibre1_result, ""],
|
|
||||||
('auhgmap', '-unit None -refer'): [0, "%s" % auhgmap_result, ""]}
|
|
||||||
|
|
||||||
auluref_ret_err = "Stripe RAID DP Tier \
|
|
||||||
RAID Rotational Number\n\
|
|
||||||
LU Capacity Size Group Pool Mode Level Type\
|
|
||||||
Speed of Paths Status\n\
|
|
||||||
0 2097152 blocks 256KB 0 0 Enable 0 Normal"
|
|
||||||
|
|
||||||
hsnm_get_lu_ret_err = {
|
|
||||||
('auluref', '-unit None -lu 0'): [1, "%s" % auluref_ret_err, ""],
|
|
||||||
}
|
|
||||||
|
|
||||||
auluref_vol_type_err = "Stripe RAID DP Tier \
|
|
||||||
RAID Rotational Number\n\
|
|
||||||
LU Capacity Size Group Pool Mode Level Type\
|
|
||||||
Speed of Paths Status\n\
|
|
||||||
0 2097152 blocks 256KB 0 0 Enable 0 DUMMY"
|
|
||||||
|
|
||||||
hsnm_get_lu_vol_type_err = {
|
|
||||||
('auluref', '-unit None -lu 0'):
|
|
||||||
[0, "%s" % auluref_vol_type_err, ""],
|
|
||||||
}
|
|
||||||
|
|
||||||
auluref_dppool_err = "Stripe RAID DP Tier \
|
|
||||||
RAID Rotational Number\n\
|
|
||||||
LU Capacity Size Group Pool Mode Level Type\
|
|
||||||
Speed of Paths Status\n\
|
|
||||||
0 2097152 blocks 256KB 0 N/A Enable 0 Normal"
|
|
||||||
|
|
||||||
hsnm_get_lu_dppool_err = {
|
|
||||||
('auluref', '-unit None -lu 0'):
|
|
||||||
[0, "%s" % auluref_dppool_err, ""],
|
|
||||||
}
|
|
||||||
|
|
||||||
auluref_size_err = "Stripe RAID DP Tier \
|
|
||||||
RAID Rotational Number\n\
|
|
||||||
LU Capacity Size Group Pool Mode Level Type\
|
|
||||||
Speed of Paths Status\n\
|
|
||||||
0 2097151 blocks 256KB N/A 0 Enable 0 Normal"
|
|
||||||
hsnm_get_lu_size_err = {
|
|
||||||
('auluref', '-unit None -lu 0'): [0, "%s" % auluref_size_err, ""],
|
|
||||||
}
|
|
||||||
|
|
||||||
auluref_num_port_err = "Stripe RAID DP Tier \
|
|
||||||
RAID Rotational Number\n\
|
|
||||||
LU Capacity Size Group Pool Mode Level Type\
|
|
||||||
Speed of Paths Status\n\
|
|
||||||
0 2097152 blocks 256KB 0 0 Enable 1 Normal"
|
|
||||||
|
|
||||||
hsnm_get_lu_num_port_err = {
|
|
||||||
('auluref', '-unit None -lu 0'): [0, "%s" % auluref_num_port_err, ""],
|
|
||||||
}
|
|
||||||
|
|
||||||
# The following information is passed on to tests, when creating a volume
|
|
||||||
|
|
||||||
_VOLUME = {'size': 128, 'volume_type': None, 'source_volid': '0',
|
|
||||||
'provider_location': '1', 'name': 'test',
|
|
||||||
'id': 'abcdefg', 'snapshot_id': '0', 'status': 'available'}
|
|
||||||
|
|
||||||
test_volume = {'name': 'test_volume', 'size': 128,
|
|
||||||
'id': 'test-volume-0',
|
|
||||||
'provider_location': '1', 'status': 'available'}
|
|
||||||
|
|
||||||
test_volume_larger = {'name': 'test_volume', 'size': 256,
|
|
||||||
'id': 'test-volume-0',
|
|
||||||
'provider_location': '1', 'status': 'available'}
|
|
||||||
|
|
||||||
test_volume_error = {'name': 'test_volume_error', 'size': 256,
|
|
||||||
'id': 'test-volume-error',
|
|
||||||
'provider_location': '3', 'status': 'available'}
|
|
||||||
|
|
||||||
test_volume_error1 = {'name': 'test_volume_error', 'size': 128,
|
|
||||||
'id': 'test-volume-error',
|
|
||||||
'provider_location': None, 'status': 'available'}
|
|
||||||
|
|
||||||
test_volume_error2 = {'name': 'test_volume_error', 'size': 256,
|
|
||||||
'id': 'test-volume-error',
|
|
||||||
'provider_location': '1', 'status': 'available'}
|
|
||||||
|
|
||||||
test_volume_error3 = {'name': 'test_volume3', 'size': 128,
|
|
||||||
'id': 'test-volume3',
|
|
||||||
'volume_metadata': [{'key': 'type',
|
|
||||||
'value': 'V-VOL'}],
|
|
||||||
'provider_location': '1', 'status': 'available'}
|
|
||||||
|
|
||||||
test_volume_error4 = {'name': 'test_volume4', 'size': 128,
|
|
||||||
'id': 'test-volume2',
|
|
||||||
'provider_location': '3', 'status': 'available'}
|
|
||||||
|
|
||||||
test_snapshot = {'volume_name': 'test', 'size': 128,
|
|
||||||
'volume_size': 128, 'name': 'test-snap',
|
|
||||||
'volume_id': 0, 'id': 'test-snap-0', 'volume': _VOLUME,
|
|
||||||
'provider_location': '1', 'status': 'available'}
|
|
||||||
|
|
||||||
test_snapshot_error2 = {'volume_name': 'test', 'size': 128,
|
|
||||||
'volume_size': 128, 'name': 'test-snap',
|
|
||||||
'volume_id': 0, 'id': 'test-snap-0',
|
|
||||||
'volume': test_volume_error,
|
|
||||||
'provider_location': None, 'status': 'available'}
|
|
||||||
|
|
||||||
UNIT_NAME = 'HUS110_91122819'
|
|
||||||
test_existing_ref = {'ldev': '0', 'unit_name': UNIT_NAME}
|
|
||||||
test_existing_none_ldev_ref = {'ldev': None, 'unit_name': UNIT_NAME}
|
|
||||||
test_existing_invalid_ldev_ref = {'ldev': 'AAA', 'unit_name': UNIT_NAME}
|
|
||||||
test_existing_no_ldev_ref = {'unit_name': UNIT_NAME}
|
|
||||||
test_existing_none_unit_ref = {'ldev': '0', 'unit_name': None}
|
|
||||||
test_existing_invalid_unit_ref = {'ldev': '0', 'unit_name': 'Dummy'}
|
|
||||||
test_existing_no_unit_ref = {'ldev': '0'}
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super(HBSDSNM2FCDriverTest, self).__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
super(HBSDSNM2FCDriverTest, self).setUp()
|
|
||||||
self._setup_config()
|
|
||||||
self._setup_driver()
|
|
||||||
|
|
||||||
def _setup_config(self):
|
|
||||||
self.configuration = mock.Mock(conf.Configuration)
|
|
||||||
self.configuration.hitachi_pool_id = 30
|
|
||||||
self.configuration.hitachi_target_ports = "00"
|
|
||||||
self.configuration.hitachi_debug_level = 0
|
|
||||||
self.configuration.hitachi_serial_number = "None"
|
|
||||||
self.configuration.hitachi_unit_name = "None"
|
|
||||||
self.configuration.hitachi_group_request = False
|
|
||||||
self.configuration.hitachi_zoning_request = False
|
|
||||||
self.configuration.config_group = "None"
|
|
||||||
self.configuration.hitachi_ldev_range = [0, 100]
|
|
||||||
self.configuration.hitachi_default_copy_method = 'SI'
|
|
||||||
self.configuration.hitachi_copy_check_interval = 1
|
|
||||||
self.configuration.hitachi_copy_speed = 3
|
|
||||||
|
|
||||||
def _setup_driver(self):
|
|
||||||
self.driver = hbsd_fc.HBSDFCDriver(
|
|
||||||
configuration=self.configuration)
|
|
||||||
context = None
|
|
||||||
db = None
|
|
||||||
self.driver.common = hbsd_common.HBSDCommon(
|
|
||||||
self.configuration, self.driver, context, db)
|
|
||||||
self.driver.common.command = hbsd_snm2.HBSDSNM2(self.configuration)
|
|
||||||
self.driver.common.pair_flock = \
|
|
||||||
self.driver.common.command.set_pair_flock()
|
|
||||||
self.driver.common.horcmgr_flock = \
|
|
||||||
self.driver.common.command.set_horcmgr_flock()
|
|
||||||
self.driver.do_setup_status.set()
|
|
||||||
|
|
||||||
# API test cases
|
|
||||||
@mock.patch.object(hbsd_basiclib, 'get_process_lock')
|
|
||||||
@mock.patch.object(hbsd_common.HBSDCommon, 'get_volume_metadata')
|
|
||||||
@mock.patch.object(hbsd_snm2.HBSDSNM2, 'exec_hsnm', side_effect=_exec_hsnm)
|
|
||||||
def test_create_volume(self, arg1, arg2, arg3):
|
|
||||||
"""test create_volume."""
|
|
||||||
ret = self.driver.create_volume(self._VOLUME)
|
|
||||||
vol = self._VOLUME.copy()
|
|
||||||
vol['provider_location'] = ret['provider_location']
|
|
||||||
self.assertEqual('1', vol['provider_location'])
|
|
||||||
|
|
||||||
@mock.patch.object(hbsd_basiclib, 'get_process_lock')
|
|
||||||
@mock.patch.object(hbsd_common.HBSDCommon, 'get_volume_metadata')
|
|
||||||
@mock.patch.object(hbsd_snm2.HBSDSNM2, 'exec_hsnm', side_effect=_exec_hsnm)
|
|
||||||
def test_create_volume_error(self, arg1, arg2, arg3):
|
|
||||||
"""test create_volume."""
|
|
||||||
self.assertRaises(exception.HBSDCmdError,
|
|
||||||
self.driver.create_volume,
|
|
||||||
self.test_volume_error)
|
|
||||||
|
|
||||||
@mock.patch.object(hbsd_basiclib, 'get_process_lock')
|
|
||||||
@mock.patch.object(hbsd_snm2.HBSDSNM2, 'exec_hsnm', side_effect=_exec_hsnm)
|
|
||||||
def test_get_volume_stats(self, arg1, arg2):
|
|
||||||
"""test get_volume_stats."""
|
|
||||||
stats = self.driver.get_volume_stats(True)
|
|
||||||
self.assertEqual('Hitachi', stats['vendor_name'])
|
|
||||||
|
|
||||||
@mock.patch.object(hbsd_basiclib, 'get_process_lock')
|
|
||||||
@mock.patch.object(hbsd_snm2.HBSDSNM2, 'exec_hsnm', side_effect=_exec_hsnm)
|
|
||||||
def test_get_volume_stats_error(self, arg1, arg2):
|
|
||||||
"""test get_volume_stats."""
|
|
||||||
self.configuration.hitachi_pool_id = 29
|
|
||||||
stats = self.driver.get_volume_stats(True)
|
|
||||||
self.assertEqual({}, stats)
|
|
||||||
self.configuration.hitachi_pool_id = 30
|
|
||||||
|
|
||||||
@mock.patch.object(hbsd_basiclib, 'get_process_lock')
|
|
||||||
@mock.patch.object(hbsd_snm2.HBSDSNM2, 'exec_hsnm', side_effect=_exec_hsnm)
|
|
||||||
def test_extend_volume(self, arg1, arg2):
|
|
||||||
"""test extend_volume."""
|
|
||||||
self.driver.extend_volume(self._VOLUME, 256)
|
|
||||||
|
|
||||||
@mock.patch.object(hbsd_basiclib, 'get_process_lock')
|
|
||||||
@mock.patch.object(hbsd_snm2.HBSDSNM2, 'exec_hsnm', side_effect=_exec_hsnm)
|
|
||||||
def test_extend_volume_error(self, arg1, arg2):
|
|
||||||
"""test extend_volume."""
|
|
||||||
self.assertRaises(exception.HBSDError, self.driver.extend_volume,
|
|
||||||
self.test_volume_error3, 256)
|
|
||||||
|
|
||||||
@mock.patch.object(hbsd_basiclib, 'get_process_lock')
|
|
||||||
@mock.patch.object(hbsd_snm2.HBSDSNM2, 'exec_hsnm', side_effect=_exec_hsnm)
|
|
||||||
def test_delete_volume(self, arg1, arg2):
|
|
||||||
"""test delete_volume."""
|
|
||||||
self.driver.delete_volume(self._VOLUME)
|
|
||||||
|
|
||||||
@mock.patch.object(hbsd_basiclib, 'get_process_lock')
|
|
||||||
@mock.patch.object(hbsd_snm2.HBSDSNM2, 'exec_hsnm', side_effect=_exec_hsnm)
|
|
||||||
def test_delete_volume_error(self, arg1, arg2):
|
|
||||||
"""test delete_volume."""
|
|
||||||
self.assertRaises(exception.HBSDCmdError,
|
|
||||||
self.driver.delete_volume,
|
|
||||||
self.test_volume_error4)
|
|
||||||
|
|
||||||
@mock.patch.object(hbsd_basiclib, 'get_process_lock')
|
|
||||||
@mock.patch.object(hbsd_common.HBSDCommon, 'get_snapshot_metadata',
|
|
||||||
return_value={'dummy_snapshot_meta': 'snapshot_meta'})
|
|
||||||
@mock.patch.object(hbsd_common.HBSDCommon, 'get_volume_metadata',
|
|
||||||
return_value={'dummy_volume_meta': 'meta'})
|
|
||||||
@mock.patch.object(hbsd_common.HBSDCommon, 'get_volume',
|
|
||||||
return_value=_VOLUME)
|
|
||||||
@mock.patch.object(hbsd_snm2.HBSDSNM2, 'exec_hsnm', side_effect=_exec_hsnm)
|
|
||||||
def test_create_snapshot(self, arg1, arg2, arg3, arg4, arg5):
|
|
||||||
"""test create_snapshot."""
|
|
||||||
ret = self.driver.create_volume(self._VOLUME)
|
|
||||||
ret = self.driver.create_snapshot(self.test_snapshot)
|
|
||||||
self.assertEqual('1', ret['provider_location'])
|
|
||||||
|
|
||||||
@mock.patch.object(hbsd_basiclib, 'get_process_lock')
|
|
||||||
@mock.patch.object(hbsd_common.HBSDCommon, 'get_snapshot_metadata',
|
|
||||||
return_value={'dummy_snapshot_meta': 'snapshot_meta'})
|
|
||||||
@mock.patch.object(hbsd_common.HBSDCommon, 'get_volume_metadata',
|
|
||||||
return_value={'dummy_volume_meta': 'meta'})
|
|
||||||
@mock.patch.object(hbsd_common.HBSDCommon, 'get_volume',
|
|
||||||
return_value=test_volume_error)
|
|
||||||
@mock.patch.object(hbsd_snm2.HBSDSNM2, 'exec_hsnm', side_effect=_exec_hsnm)
|
|
||||||
def test_create_snapshot_error(self, arg1, arg2, arg3, arg4, arg5):
|
|
||||||
"""test create_snapshot."""
|
|
||||||
self.assertRaises(exception.HBSDCmdError,
|
|
||||||
self.driver.create_snapshot,
|
|
||||||
self.test_snapshot_error2)
|
|
||||||
|
|
||||||
@mock.patch.object(hbsd_basiclib, 'get_process_lock')
|
|
||||||
@mock.patch.object(hbsd_snm2.HBSDSNM2, 'exec_hsnm', side_effect=_exec_hsnm)
|
|
||||||
def test_delete_snapshot(self, arg1, arg2):
|
|
||||||
"""test delete_snapshot."""
|
|
||||||
self.driver.delete_snapshot(self.test_snapshot)
|
|
||||||
return
|
|
||||||
|
|
||||||
@mock.patch.object(hbsd_basiclib, 'get_process_lock')
|
|
||||||
@mock.patch.object(hbsd_snm2.HBSDSNM2, 'exec_hsnm', side_effect=_exec_hsnm)
|
|
||||||
def test_delete_snapshot_error(self, arg1, arg2):
|
|
||||||
"""test delete_snapshot."""
|
|
||||||
self.driver.delete_snapshot(self.test_snapshot_error2)
|
|
||||||
return
|
|
||||||
|
|
||||||
@mock.patch.object(hbsd_basiclib, 'get_process_lock')
|
|
||||||
@mock.patch.object(hbsd_common.HBSDCommon, 'get_volume_metadata',
|
|
||||||
return_value={'dummy_volume_meta': 'meta'})
|
|
||||||
@mock.patch.object(hbsd_snm2.HBSDSNM2, 'exec_hsnm', side_effect=_exec_hsnm)
|
|
||||||
def test_create_volume_from_snapshot(self, arg1, arg2, arg3):
|
|
||||||
"""test create_volume_from_snapshot."""
|
|
||||||
vol = self.driver.create_volume_from_snapshot(self._VOLUME,
|
|
||||||
self.test_snapshot)
|
|
||||||
self.assertIsNotNone(vol)
|
|
||||||
return
|
|
||||||
|
|
||||||
@mock.patch.object(hbsd_basiclib, 'get_process_lock')
|
|
||||||
@mock.patch.object(hbsd_common.HBSDCommon, 'get_volume_metadata',
|
|
||||||
return_value={'dummy_volume_meta': 'meta'})
|
|
||||||
@mock.patch.object(hbsd_snm2.HBSDSNM2, 'exec_hsnm', side_effect=_exec_hsnm)
|
|
||||||
def test_create_volume_from_snapshot_error(self, arg1, arg2, arg3):
|
|
||||||
"""test create_volume_from_snapshot."""
|
|
||||||
self.assertRaises(exception.HBSDError,
|
|
||||||
self.driver.create_volume_from_snapshot,
|
|
||||||
self.test_volume_error2, self.test_snapshot)
|
|
||||||
return
|
|
||||||
|
|
||||||
@mock.patch.object(hbsd_common.HBSDCommon, 'get_volume_metadata',
|
|
||||||
return_value={'dummy_volume_meta': 'meta'})
|
|
||||||
@mock.patch.object(hbsd_common.HBSDCommon, 'get_volume',
|
|
||||||
return_value=_VOLUME)
|
|
||||||
@mock.patch.object(hbsd_snm2.HBSDSNM2, 'exec_hsnm', side_effect=_exec_hsnm)
|
|
||||||
@mock.patch.object(hbsd_basiclib, 'get_process_lock')
|
|
||||||
def test_create_cloned_volume(self, arg1, arg2, arg3, arg4):
|
|
||||||
"""test create_cloned_volume."""
|
|
||||||
vol = self.driver.create_cloned_volume(self._VOLUME,
|
|
||||||
self.test_volume)
|
|
||||||
self.assertIsNotNone(vol)
|
|
||||||
return
|
|
||||||
|
|
||||||
@mock.patch.object(hbsd_common.HBSDCommon, 'get_volume_metadata',
|
|
||||||
return_value={'dummy_volume_meta': 'meta'})
|
|
||||||
@mock.patch.object(hbsd_common.HBSDCommon, 'get_volume',
|
|
||||||
return_value=_VOLUME)
|
|
||||||
@mock.patch.object(hbsd_common.HBSDCommon, 'extend_volume')
|
|
||||||
@mock.patch.object(hbsd_snm2.HBSDSNM2, 'exec_hsnm', side_effect=_exec_hsnm)
|
|
||||||
@mock.patch.object(hbsd_basiclib, 'get_process_lock')
|
|
||||||
def test_create_cloned_volume_larger(self, arg1, arg2, arg3, arg4, arg5):
|
|
||||||
"""test create_cloned_volume."""
|
|
||||||
vol = self.driver.create_cloned_volume(self.test_volume_larger,
|
|
||||||
self._VOLUME)
|
|
||||||
self.assertIsNotNone(vol)
|
|
||||||
arg3.assert_called_once_with(self.test_volume_larger,
|
|
||||||
self.test_volume_larger['size'])
|
|
||||||
return
|
|
||||||
|
|
||||||
@mock.patch.object(hbsd_common.HBSDCommon, 'get_volume_metadata',
|
|
||||||
return_value={'dummy_volume_meta': 'meta'})
|
|
||||||
@mock.patch.object(hbsd_common.HBSDCommon, 'get_volume',
|
|
||||||
return_value=test_volume_error1)
|
|
||||||
@mock.patch.object(hbsd_snm2.HBSDSNM2, 'exec_hsnm', side_effect=_exec_hsnm)
|
|
||||||
@mock.patch.object(hbsd_basiclib, 'get_process_lock')
|
|
||||||
def test_create_cloned_volume_error(self, arg1, arg2, arg3, arg4):
|
|
||||||
"""test create_cloned_volume."""
|
|
||||||
self.assertRaises(exception.HBSDError,
|
|
||||||
self.driver.create_cloned_volume,
|
|
||||||
self._VOLUME, self.test_volume_error1)
|
|
||||||
return
|
|
||||||
|
|
||||||
@mock.patch.object(hbsd_basiclib, 'get_process_lock')
|
|
||||||
@mock.patch.object(hbsd_snm2.HBSDSNM2, 'exec_hsnm', side_effect=_exec_hsnm)
|
|
||||||
def test_initialize_connection(self, arg1, arg2):
|
|
||||||
"""test initialize connection."""
|
|
||||||
connector = {'wwpns': '0x100000', 'ip': '0xc0a80100'}
|
|
||||||
rc = self.driver.initialize_connection(self._VOLUME, connector)
|
|
||||||
self.assertEqual('fibre_channel', rc['driver_volume_type'])
|
|
||||||
self.assertEqual(['50060E801053C2E0'], rc['data']['target_wwn'])
|
|
||||||
self.assertEqual(1, rc['data']['target_lun'])
|
|
||||||
return
|
|
||||||
|
|
||||||
@mock.patch.object(hbsd_basiclib, 'get_process_lock')
|
|
||||||
@mock.patch.object(hbsd_snm2.HBSDSNM2, 'exec_hsnm', side_effect=_exec_hsnm)
|
|
||||||
def test_initialize_connection_error(self, arg1, arg2):
|
|
||||||
"""test initialize connection."""
|
|
||||||
connector = {'wwpns': 'x', 'ip': '0xc0a80100'}
|
|
||||||
self.assertRaises(exception.HBSDError,
|
|
||||||
self.driver.initialize_connection,
|
|
||||||
self._VOLUME, connector)
|
|
||||||
return
|
|
||||||
|
|
||||||
@mock.patch.object(hbsd_snm2.HBSDSNM2, 'exec_hsnm', side_effect=_exec_hsnm)
|
|
||||||
def test_terminate_connection(self, arg1):
|
|
||||||
"""test terminate connection."""
|
|
||||||
connector = {'wwpns': '0x100000', 'ip': '0xc0a80100'}
|
|
||||||
rc = self.driver.terminate_connection(self._VOLUME, connector)
|
|
||||||
self.assertEqual('fibre_channel', rc['driver_volume_type'])
|
|
||||||
self.assertEqual(['50060E801053C2E0'], rc['data']['target_wwn'])
|
|
||||||
return
|
|
||||||
|
|
||||||
@mock.patch.object(hbsd_snm2.HBSDSNM2, 'exec_hsnm', side_effect=_exec_hsnm)
|
|
||||||
def test_terminate_connection_error(self, arg1):
|
|
||||||
"""test terminate connection."""
|
|
||||||
connector = {'ip': '0xc0a80100'}
|
|
||||||
self.assertRaises(exception.HBSDError,
|
|
||||||
self.driver.terminate_connection,
|
|
||||||
self._VOLUME, connector)
|
|
||||||
return
|
|
||||||
|
|
||||||
@mock.patch.object(hbsd_basiclib, 'get_process_lock')
|
|
||||||
@mock.patch.object(hbsd_snm2.HBSDSNM2, 'exec_hsnm', side_effect=_exec_hsnm)
|
|
||||||
def test_manage_existing(self, arg1, arg2):
|
|
||||||
rc = self.driver.manage_existing(self._VOLUME, self.test_existing_ref)
|
|
||||||
self.assertEqual(0, rc['provider_location'])
|
|
||||||
|
|
||||||
@mock.patch.object(hbsd_basiclib, 'get_process_lock')
|
|
||||||
@mock.patch.object(hbsd_snm2.HBSDSNM2, 'exec_hsnm', side_effect=_exec_hsnm)
|
|
||||||
@mock.patch.object(hbsd_common.HBSDCommon, '_update_volume_metadata')
|
|
||||||
def test_manage_existing_get_size(self, arg1, arg2, arg3):
|
|
||||||
self.configuration.hitachi_unit_name = self.UNIT_NAME
|
|
||||||
size = self.driver.manage_existing_get_size(self._VOLUME,
|
|
||||||
self.test_existing_ref)
|
|
||||||
self.assertEqual(1, size)
|
|
||||||
|
|
||||||
@mock.patch.object(hbsd_basiclib, 'get_process_lock')
|
|
||||||
@mock.patch.object(hbsd_snm2.HBSDSNM2, 'exec_hsnm', side_effect=_exec_hsnm)
|
|
||||||
@mock.patch.object(hbsd_common.HBSDCommon, '_update_volume_metadata')
|
|
||||||
def test_manage_existing_get_size_none_ldev(self, arg1, arg2, arg3):
|
|
||||||
self.configuration.hitachi_unit_name = self.UNIT_NAME
|
|
||||||
self.assertRaises(exception.ManageExistingInvalidReference,
|
|
||||||
self.driver.manage_existing_get_size, self._VOLUME,
|
|
||||||
self.test_existing_none_ldev_ref)
|
|
||||||
|
|
||||||
@mock.patch.object(hbsd_basiclib, 'get_process_lock')
|
|
||||||
@mock.patch.object(hbsd_snm2.HBSDSNM2, 'exec_hsnm', side_effect=_exec_hsnm)
|
|
||||||
@mock.patch.object(hbsd_common.HBSDCommon, '_update_volume_metadata')
|
|
||||||
def test_manage_existing_get_size_invalid_ldev_ref(self, arg1, arg2, arg3):
|
|
||||||
self.configuration.hitachi_unit_name = self.UNIT_NAME
|
|
||||||
self.assertRaises(exception.ManageExistingInvalidReference,
|
|
||||||
self.driver.manage_existing_get_size, self._VOLUME,
|
|
||||||
self.test_existing_invalid_ldev_ref)
|
|
||||||
|
|
||||||
@mock.patch.object(hbsd_basiclib, 'get_process_lock')
|
|
||||||
@mock.patch.object(hbsd_snm2.HBSDSNM2, 'exec_hsnm', side_effect=_exec_hsnm)
|
|
||||||
@mock.patch.object(hbsd_common.HBSDCommon, '_update_volume_metadata')
|
|
||||||
def test_manage_existing_get_size_no_ldev_ref(self, arg1, arg2, arg3):
|
|
||||||
self.configuration.hitachi_unit_name = self.UNIT_NAME
|
|
||||||
self.assertRaises(exception.ManageExistingInvalidReference,
|
|
||||||
self.driver.manage_existing_get_size, self._VOLUME,
|
|
||||||
self.test_existing_no_ldev_ref)
|
|
||||||
|
|
||||||
@mock.patch.object(hbsd_basiclib, 'get_process_lock')
|
|
||||||
@mock.patch.object(hbsd_snm2.HBSDSNM2, 'exec_hsnm', side_effect=_exec_hsnm)
|
|
||||||
@mock.patch.object(hbsd_common.HBSDCommon, '_update_volume_metadata')
|
|
||||||
def test_manage_existing_get_size_none_unit_ref(self, arg1, arg2, arg3):
|
|
||||||
self.configuration.hitachi_unit_name = self.UNIT_NAME
|
|
||||||
self.assertRaises(exception.ManageExistingInvalidReference,
|
|
||||||
self.driver.manage_existing_get_size, self._VOLUME,
|
|
||||||
self.test_existing_none_unit_ref)
|
|
||||||
|
|
||||||
@mock.patch.object(hbsd_basiclib, 'get_process_lock')
|
|
||||||
@mock.patch.object(hbsd_snm2.HBSDSNM2, 'exec_hsnm', side_effect=_exec_hsnm)
|
|
||||||
@mock.patch.object(hbsd_common.HBSDCommon, '_update_volume_metadata')
|
|
||||||
def test_manage_existing_get_size_invalid_unit_ref(self, arg1, arg2, arg3):
|
|
||||||
self.configuration.hitachi_unit_name = self.UNIT_NAME
|
|
||||||
self.assertRaises(exception.ManageExistingInvalidReference,
|
|
||||||
self.driver.manage_existing_get_size, self._VOLUME,
|
|
||||||
self.test_existing_invalid_unit_ref)
|
|
||||||
|
|
||||||
@mock.patch.object(hbsd_basiclib, 'get_process_lock')
|
|
||||||
@mock.patch.object(hbsd_snm2.HBSDSNM2, 'exec_hsnm', side_effect=_exec_hsnm)
|
|
||||||
@mock.patch.object(hbsd_common.HBSDCommon, '_update_volume_metadata')
|
|
||||||
def test_manage_existing_get_size_no_unit_ref(self, arg1, arg2, arg3):
|
|
||||||
self.configuration.hitachi_unit_name = self.UNIT_NAME
|
|
||||||
self.assertRaises(exception.ManageExistingInvalidReference,
|
|
||||||
self.driver.manage_existing_get_size, self._VOLUME,
|
|
||||||
self.test_existing_no_unit_ref)
|
|
||||||
|
|
||||||
@mock.patch.object(hbsd_basiclib, 'get_process_lock')
|
|
||||||
@mock.patch.object(hbsd_snm2.HBSDSNM2, 'exec_hsnm',
|
|
||||||
side_effect=_exec_hsnm_get_lu_ret_err)
|
|
||||||
@mock.patch.object(hbsd_common.HBSDCommon, '_update_volume_metadata')
|
|
||||||
def test_manage_existing_get_size_ret_err(self, arg1, arg2, arg3):
|
|
||||||
self.configuration.hitachi_unit_name = self.UNIT_NAME
|
|
||||||
self.assertRaises(exception.ManageExistingInvalidReference,
|
|
||||||
self.driver.manage_existing_get_size, self._VOLUME,
|
|
||||||
self.test_existing_ref)
|
|
||||||
|
|
||||||
@mock.patch.object(hbsd_basiclib, 'get_process_lock')
|
|
||||||
@mock.patch.object(hbsd_snm2.HBSDSNM2, 'exec_hsnm',
|
|
||||||
side_effect=_exec_hsnm_get_lu_vol_type_err)
|
|
||||||
@mock.patch.object(hbsd_common.HBSDCommon, '_update_volume_metadata')
|
|
||||||
def test_manage_existing_get_lu_vol_type_err(self, arg1, arg2, arg3):
|
|
||||||
self.configuration.hitachi_unit_name = self.UNIT_NAME
|
|
||||||
self.assertRaises(exception.ManageExistingInvalidReference,
|
|
||||||
self.driver.manage_existing_get_size, self._VOLUME,
|
|
||||||
self.test_existing_ref)
|
|
||||||
|
|
||||||
@mock.patch.object(hbsd_basiclib, 'get_process_lock')
|
|
||||||
@mock.patch.object(hbsd_snm2.HBSDSNM2, 'exec_hsnm',
|
|
||||||
side_effect=_exec_hsnm_get_lu_dppool_err)
|
|
||||||
@mock.patch.object(hbsd_common.HBSDCommon, '_update_volume_metadata')
|
|
||||||
def test_manage_existing_get_lu_dppool_err(self, arg1, arg2, arg3):
|
|
||||||
self.configuration.hitachi_unit_name = self.UNIT_NAME
|
|
||||||
self.assertRaises(exception.ManageExistingInvalidReference,
|
|
||||||
self.driver.manage_existing_get_size, self._VOLUME,
|
|
||||||
self.test_existing_ref)
|
|
||||||
|
|
||||||
@mock.patch.object(hbsd_basiclib, 'get_process_lock')
|
|
||||||
@mock.patch.object(hbsd_snm2.HBSDSNM2, 'exec_hsnm',
|
|
||||||
side_effect=_exec_hsnm_get_lu_size_err)
|
|
||||||
@mock.patch.object(hbsd_common.HBSDCommon, '_update_volume_metadata')
|
|
||||||
def test_manage_existing_get_lu_size_err(self, arg1, arg2, arg3):
|
|
||||||
self.configuration.hitachi_unit_name = self.UNIT_NAME
|
|
||||||
self.assertRaises(exception.ManageExistingInvalidReference,
|
|
||||||
self.driver.manage_existing_get_size, self._VOLUME,
|
|
||||||
self.test_existing_ref)
|
|
||||||
|
|
||||||
@mock.patch.object(hbsd_basiclib, 'get_process_lock')
|
|
||||||
@mock.patch.object(hbsd_snm2.HBSDSNM2, 'exec_hsnm',
|
|
||||||
side_effect=_exec_hsnm_get_lu_num_port_err)
|
|
||||||
@mock.patch.object(hbsd_common.HBSDCommon, '_update_volume_metadata')
|
|
||||||
def test_manage_existing_get_lu_num_port_err(self, arg1, arg2, arg3):
|
|
||||||
self.configuration.hitachi_unit_name = self.UNIT_NAME
|
|
||||||
self.assertRaises(exception.ManageExistingInvalidReference,
|
|
||||||
self.driver.manage_existing_get_size, self._VOLUME,
|
|
||||||
self.test_existing_ref)
|
|
||||||
|
|
||||||
@mock.patch.object(hbsd_basiclib, 'get_process_lock')
|
|
||||||
@mock.patch.object(hbsd_snm2.HBSDSNM2, 'exec_hsnm', side_effect=_exec_hsnm)
|
|
||||||
def test_unmanage(self, arg1, arg2):
|
|
||||||
self.driver.unmanage(self._VOLUME)
|
|
||||||
|
|
||||||
@mock.patch.object(hbsd_basiclib, 'get_process_lock')
|
|
||||||
@mock.patch.object(hbsd_snm2.HBSDSNM2, 'exec_hsnm', side_effect=_exec_hsnm)
|
|
||||||
def test_unmanage_busy(self, arg1, arg2):
|
|
||||||
self.assertRaises(exception.HBSDVolumeIsBusy,
|
|
||||||
self.driver.unmanage, self.test_volume_error3)
|
|
@ -1,607 +0,0 @@
|
|||||||
# Copyright (C) 2014, Hitachi, Ltd.
|
|
||||||
#
|
|
||||||
# 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 Block Storage Driver
|
|
||||||
"""
|
|
||||||
|
|
||||||
import mock
|
|
||||||
|
|
||||||
from cinder import exception
|
|
||||||
from cinder import test
|
|
||||||
from cinder import utils
|
|
||||||
from cinder.volume import configuration as conf
|
|
||||||
from cinder.volume.drivers.hitachi import hbsd_basiclib
|
|
||||||
from cinder.volume.drivers.hitachi import hbsd_common
|
|
||||||
from cinder.volume.drivers.hitachi import hbsd_iscsi
|
|
||||||
from cinder.volume.drivers.hitachi import hbsd_snm2
|
|
||||||
|
|
||||||
|
|
||||||
def _exec_hsnm(*args, **kargs):
|
|
||||||
return HBSDSNM2ISCSIDriverTest.hsnm_vals.get(args)
|
|
||||||
|
|
||||||
|
|
||||||
def _exec_hsnm_init(*args, **kargs):
|
|
||||||
return HBSDSNM2ISCSIDriverTest.hsnm_vals_init.get(args)
|
|
||||||
|
|
||||||
|
|
||||||
class HBSDSNM2ISCSIDriverTest(test.TestCase):
|
|
||||||
"""Test HBSDSNM2ISCSIDriver."""
|
|
||||||
|
|
||||||
audppool_result = " DP RAID \
|
|
||||||
Current Utilization Current Over Replication\
|
|
||||||
Available Current Replication Rotational \
|
|
||||||
\
|
|
||||||
Stripe \
|
|
||||||
Needing Preparation\n\
|
|
||||||
Pool Tier Mode Level Total Capacity Consumed Capacity \
|
|
||||||
Percent Provisioning Percent Capacity \
|
|
||||||
Utilization Percent Type Speed Encryption Status \
|
|
||||||
\
|
|
||||||
Reconstruction Progress Size Capacity\n\
|
|
||||||
30 Disable 1( 1D+1D) 532.0 GB 2.0 GB \
|
|
||||||
1% 24835% 532.0 GB \
|
|
||||||
1% SAS 10000rpm N/A Normal \
|
|
||||||
N/A \
|
|
||||||
256KB 0.0 GB"
|
|
||||||
|
|
||||||
aureplicationlocal_result = "Pair Name LUN Pair \
|
|
||||||
LUN Status Copy Type Group \
|
|
||||||
Point-in-Time MU Number\n\
|
|
||||||
0 10 0 Split( 99%) \
|
|
||||||
ShadowImage ---:Ungrouped N/A\
|
|
||||||
"
|
|
||||||
|
|
||||||
auluref_result = " Stripe RAID DP Tier \
|
|
||||||
RAID Rotational Number\n\
|
|
||||||
LU Capacity Size Group Pool Mode Level Type\
|
|
||||||
Speed of Paths Status\n\
|
|
||||||
0 2097152 blocks 256KB 0 0 Enable 0 Normal"
|
|
||||||
|
|
||||||
auhgwwn_result = "Port 00 Host Group Security ON\n Detected WWN\n \
|
|
||||||
Name Port Name Host Group\n\
|
|
||||||
HBSD-00 10000000C97BCE7A 001:HBSD-01\n\
|
|
||||||
Assigned WWN\n Name Port Name \
|
|
||||||
Host Group\n abcdefg 10000000C97BCE7A \
|
|
||||||
001:HBSD-01"
|
|
||||||
|
|
||||||
autargetini_result = "Port 00 Target Security ON\n\
|
|
||||||
Target Name \
|
|
||||||
iSCSI Name\n\
|
|
||||||
001:HBSD-01 \
|
|
||||||
iqn"
|
|
||||||
|
|
||||||
autargetini_result2 = "Port 00 Target Security ON\n\
|
|
||||||
Target Name \
|
|
||||||
iSCSI Name"
|
|
||||||
|
|
||||||
autargetmap_result = "Mapping Mode = ON\n\
|
|
||||||
Port Target H-LUN LUN\n\
|
|
||||||
00 001:HBSD-01 0 1000"
|
|
||||||
|
|
||||||
auiscsi_result = "Port 00\n\
|
|
||||||
Port Number : 3260\n\
|
|
||||||
Keep Alive Timer[sec.] : 60\n\
|
|
||||||
MTU : 1500\n\
|
|
||||||
Transfer Rate : 1Gbps\n\
|
|
||||||
Link Status : Link Up\n\
|
|
||||||
Ether Address : 00:00:87:33:D1:3E\n\
|
|
||||||
IPv4\n\
|
|
||||||
IPv4 Address : 192.168.0.1\n\
|
|
||||||
IPv4 Subnet Mask : 255.255.252.0\n\
|
|
||||||
IPv4 Default Gateway : 0.0.0.0\n\
|
|
||||||
IPv6 Status : Disable\n\
|
|
||||||
Connecting Hosts : 0\n\
|
|
||||||
Result : Normal\n\
|
|
||||||
VLAN Status : Disable\n\
|
|
||||||
VLAN ID : N/A\n\
|
|
||||||
Header Digest : Enable\n\
|
|
||||||
Data Digest : Enable\n\
|
|
||||||
Window Scale : Disable"
|
|
||||||
|
|
||||||
autargetdef_result = "Port 00\n\
|
|
||||||
Authentication Mutual\n\
|
|
||||||
Target Method CHAP Algorithm \
|
|
||||||
Authentication\n\
|
|
||||||
001:T000 None --- ---\n\
|
|
||||||
User Name : ---\n\
|
|
||||||
iSCSI Name : iqn-target"
|
|
||||||
|
|
||||||
hsnm_vals = {
|
|
||||||
('audppool', '-unit None -refer -g'): [0, "%s" % audppool_result, ""],
|
|
||||||
('aureplicationlocal',
|
|
||||||
'-unit None -create -si -pvol 1 -svol 1 -compsplit -pace normal'):
|
|
||||||
[0, "", ""],
|
|
||||||
('aureplicationlocal',
|
|
||||||
'-unit None -create -si -pvol 3 -svol 1 -compsplit -pace normal'):
|
|
||||||
[1, "", ""],
|
|
||||||
('aureplicationlocal', '-unit None -refer -pvol 1'):
|
|
||||||
[0, "%s" % aureplicationlocal_result, ""],
|
|
||||||
('aureplicationlocal', '-unit None -refer -pvol 3'):
|
|
||||||
[1, "", "DMEC002015"],
|
|
||||||
('aureplicationlocal', '-unit None -refer -svol 3'):
|
|
||||||
[1, "", "DMEC002015"],
|
|
||||||
('aureplicationlocal', '-unit None -simplex -si -pvol 1 -svol 0'):
|
|
||||||
[0, "", ""],
|
|
||||||
('aureplicationlocal', '-unit None -simplex -si -pvol 1 -svol 1'):
|
|
||||||
[1, "", ""],
|
|
||||||
('auluchgsize', '-unit None -lu 1 -size 256g'):
|
|
||||||
[0, "", ""],
|
|
||||||
('auludel', '-unit None -lu 1 -f'): [0, "", ""],
|
|
||||||
('auludel', '-unit None -lu 3 -f'): [1, "", ""],
|
|
||||||
('auluadd', '-unit None -lu 1 -dppoolno 30 -size 128g'): [0, "", ""],
|
|
||||||
('auluadd', '-unit None -lu 1 -dppoolno 30 -size 256g'): [1, "", ""],
|
|
||||||
('auluref', '-unit None'): [0, "%s" % auluref_result, ""],
|
|
||||||
('auluref', '-unit None -lu 0'): [0, "%s" % auluref_result, ""],
|
|
||||||
('autargetmap', '-unit None -add 0 0 1 1 1'): [0, "", ""],
|
|
||||||
('autargetmap', '-unit None -add 0 0 0 0 1'): [0, "", ""],
|
|
||||||
('autargetini', '-unit None -refer'):
|
|
||||||
[0, "%s" % autargetini_result, ""],
|
|
||||||
('autargetini', '-unit None -add 0 0 -tno 0 -iname iqn'):
|
|
||||||
[0, "", ""],
|
|
||||||
('autargetmap', '-unit None -refer'):
|
|
||||||
[0, "%s" % autargetmap_result, ""],
|
|
||||||
('autargetdef',
|
|
||||||
'-unit None -add 0 0 -tno 0 -talias HBSD-0.0.0.0 -iname iqn.target \
|
|
||||||
-authmethod None'):
|
|
||||||
[0, "", ""],
|
|
||||||
('autargetdef', '-unit None -add 0 0 -tno 0 -talias HBSD-0.0.0.0 \
|
|
||||||
-iname iqnX.target -authmethod None'):
|
|
||||||
[1, "", ""],
|
|
||||||
('autargetopt', '-unit None -set 0 0 -talias HBSD-0.0.0.0 \
|
|
||||||
-ReportFullPortalList enable'):
|
|
||||||
[0, "", ""],
|
|
||||||
('auiscsi', '-unit None -refer'): [0, "%s" % auiscsi_result, ""],
|
|
||||||
('autargetdef', '-unit None -refer'):
|
|
||||||
[0, "%s" % autargetdef_result, ""]}
|
|
||||||
|
|
||||||
hsnm_vals_init = {
|
|
||||||
('audppool', '-unit None -refer -g'): [0, "%s" % audppool_result, ""],
|
|
||||||
('aureplicationlocal',
|
|
||||||
'-unit None -create -si -pvol 1 -svol 1 -compsplit -pace normal'):
|
|
||||||
[0, 0, ""],
|
|
||||||
('aureplicationlocal', '-unit None -refer -pvol 1'):
|
|
||||||
[0, "%s" % aureplicationlocal_result, ""],
|
|
||||||
('aureplicationlocal', '-unit None -simplex -si -pvol 1 -svol 0'):
|
|
||||||
[0, 0, ""],
|
|
||||||
('auluchgsize', '-unit None -lu 1 -size 256g'):
|
|
||||||
[0, 0, ""],
|
|
||||||
('auludel', '-unit None -lu 1 -f'): [0, "", ""],
|
|
||||||
('auluadd', '-unit None -lu 1 -dppoolno 30 -size 128g'): [0, "", ""],
|
|
||||||
('auluref', '-unit None'): [0, "%s" % auluref_result, ""],
|
|
||||||
('autargetmap', '-unit None -add 0 0 1 1 1'): [0, "", ""],
|
|
||||||
('autargetmap', '-unit None -add 0 0 0 0 1'): [0, "", ""],
|
|
||||||
('autargetini', '-unit None -refer'):
|
|
||||||
[0, "%s" % autargetini_result2, ""],
|
|
||||||
('autargetini', '-unit None -add 0 0 -tno 0 -iname iqn'):
|
|
||||||
[0, "", ""],
|
|
||||||
('autargetmap', '-unit None -refer'):
|
|
||||||
[0, "%s" % autargetmap_result, ""],
|
|
||||||
('autargetdef',
|
|
||||||
'-unit None -add 0 0 -tno 0 -talias HBSD-0.0.0.0 -iname iqn.target \
|
|
||||||
-authmethod None'):
|
|
||||||
[0, "", ""],
|
|
||||||
('autargetopt', '-unit None -set 0 0 -talias HBSD-0.0.0.0 \
|
|
||||||
-ReportFullPortalList enable'):
|
|
||||||
[0, "", ""],
|
|
||||||
('auiscsi', '-unit None -refer'): [0, "%s" % auiscsi_result, ""],
|
|
||||||
('autargetdef', '-unit None -refer'):
|
|
||||||
[0, "%s" % autargetdef_result, ""],
|
|
||||||
('auman', '-help'):
|
|
||||||
[0, "Version 27.50", ""]}
|
|
||||||
|
|
||||||
# The following information is passed on to tests, when creating a volume
|
|
||||||
|
|
||||||
_VOLUME = {'size': 128, 'volume_type': None, 'source_volid': '0',
|
|
||||||
'provider_location': '1', 'name': 'test',
|
|
||||||
'id': 'abcdefg', 'snapshot_id': '0', 'status': 'available'}
|
|
||||||
|
|
||||||
test_volume = {'name': 'test_volume', 'size': 128,
|
|
||||||
'id': 'test-volume-0',
|
|
||||||
'provider_location': '1', 'status': 'available'}
|
|
||||||
|
|
||||||
test_volume_larger = {'name': 'test_volume', 'size': 256,
|
|
||||||
'id': 'test-volume-0',
|
|
||||||
'provider_location': '1', 'status': 'available'}
|
|
||||||
|
|
||||||
test_volume_error = {'name': 'test_volume_error', 'size': 256,
|
|
||||||
'id': 'test-volume-error',
|
|
||||||
'provider_location': '3', 'status': 'available'}
|
|
||||||
|
|
||||||
test_volume_error1 = {'name': 'test_volume_error', 'size': 128,
|
|
||||||
'id': 'test-volume-error',
|
|
||||||
'provider_location': None, 'status': 'available'}
|
|
||||||
|
|
||||||
test_volume_error2 = {'name': 'test_volume_error', 'size': 256,
|
|
||||||
'id': 'test-volume-error',
|
|
||||||
'provider_location': '1', 'status': 'available'}
|
|
||||||
|
|
||||||
test_volume_error3 = {'name': 'test_volume3', 'size': 128,
|
|
||||||
'id': 'test-volume3',
|
|
||||||
'volume_metadata': [{'key': 'type',
|
|
||||||
'value': 'V-VOL'}],
|
|
||||||
'provider_location': '1', 'status': 'available'}
|
|
||||||
|
|
||||||
test_volume_error4 = {'name': 'test_volume4', 'size': 128,
|
|
||||||
'id': 'test-volume2',
|
|
||||||
'provider_location': '3', 'status': 'available'}
|
|
||||||
|
|
||||||
test_snapshot = {'volume_name': 'test', 'size': 128,
|
|
||||||
'volume_size': 128, 'name': 'test-snap',
|
|
||||||
'volume_id': 0, 'id': 'test-snap-0', 'volume': _VOLUME,
|
|
||||||
'provider_location': '1', 'status': 'available'}
|
|
||||||
|
|
||||||
test_snapshot_error2 = {'volume_name': 'test', 'size': 128,
|
|
||||||
'volume_size': 128, 'name': 'test-snap',
|
|
||||||
'volume_id': 0, 'id': 'test-snap-0',
|
|
||||||
'volume': test_volume_error,
|
|
||||||
'provider_location': None, 'status': 'available'}
|
|
||||||
|
|
||||||
UNIT_NAME = 'HUS110_91122819'
|
|
||||||
test_existing_ref = {'ldev': '0', 'unit_name': UNIT_NAME}
|
|
||||||
test_existing_none_ldev_ref = {'ldev': None, 'unit_name': UNIT_NAME}
|
|
||||||
test_existing_invalid_ldev_ref = {'ldev': 'AAA', 'unit_name': UNIT_NAME}
|
|
||||||
test_existing_no_ldev_ref = {'unit_name': UNIT_NAME}
|
|
||||||
test_existing_none_unit_ref = {'ldev': '0', 'unit_name': None}
|
|
||||||
test_existing_invalid_unit_ref = {'ldev': '0', 'unit_name': 'Dummy'}
|
|
||||||
test_existing_no_unit_ref = {'ldev': '0'}
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super(HBSDSNM2ISCSIDriverTest, self).__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
@mock.patch.object(utils, 'brick_get_connector_properties',
|
|
||||||
return_value={'ip': '0.0.0.0',
|
|
||||||
'initiator': 'iqn'})
|
|
||||||
@mock.patch.object(hbsd_basiclib, 'get_process_lock')
|
|
||||||
@mock.patch.object(hbsd_snm2.HBSDSNM2, 'exec_hsnm',
|
|
||||||
side_effect=_exec_hsnm_init)
|
|
||||||
@mock.patch.object(utils, 'execute',
|
|
||||||
return_value=['', ''])
|
|
||||||
def setUp(self, args1, arg2, arg3, arg4):
|
|
||||||
super(HBSDSNM2ISCSIDriverTest, self).setUp()
|
|
||||||
self._setup_config()
|
|
||||||
self._setup_driver()
|
|
||||||
self.driver.check_param()
|
|
||||||
self.driver.common.create_lock_file()
|
|
||||||
self.driver.common.command.connect_storage()
|
|
||||||
self.driver.max_hostgroups = \
|
|
||||||
self.driver.common.command.get_max_hostgroups()
|
|
||||||
self.driver.add_hostgroup()
|
|
||||||
self.driver.output_param_to_log()
|
|
||||||
self.driver.do_setup_status.set()
|
|
||||||
|
|
||||||
def _setup_config(self):
|
|
||||||
self.configuration = mock.Mock(conf.Configuration)
|
|
||||||
self.configuration.hitachi_pool_id = 30
|
|
||||||
self.configuration.hitachi_thin_pool_id = 31
|
|
||||||
self.configuration.hitachi_target_ports = "00"
|
|
||||||
self.configuration.hitachi_debug_level = 0
|
|
||||||
self.configuration.hitachi_serial_number = None
|
|
||||||
self.configuration.hitachi_unit_name = "None"
|
|
||||||
self.configuration.hitachi_group_request = True
|
|
||||||
self.configuration.hitachi_group_range = "0-1"
|
|
||||||
self.configuration.config_group = "None"
|
|
||||||
self.configuration.hitachi_ldev_range = "0-100"
|
|
||||||
self.configuration.hitachi_default_copy_method = 'FULL'
|
|
||||||
self.configuration.hitachi_copy_check_interval = 1
|
|
||||||
self.configuration.hitachi_async_copy_check_interval = 1
|
|
||||||
self.configuration.hitachi_copy_speed = 3
|
|
||||||
self.configuration.hitachi_auth_method = None
|
|
||||||
self.configuration.hitachi_auth_user = "HBSD-CHAP-user"
|
|
||||||
self.configuration.hitachi_auth_password = "HBSD-CHAP-password"
|
|
||||||
self.configuration.hitachi_add_chap_user = "False"
|
|
||||||
|
|
||||||
def _setup_driver(self):
|
|
||||||
self.driver = hbsd_iscsi.HBSDISCSIDriver(
|
|
||||||
configuration=self.configuration)
|
|
||||||
context = None
|
|
||||||
db = None
|
|
||||||
self.driver.common = hbsd_common.HBSDCommon(
|
|
||||||
self.configuration, self.driver, context, db)
|
|
||||||
self.driver.common.command = hbsd_snm2.HBSDSNM2(self.configuration)
|
|
||||||
self.driver.common.horcmgr_flock = \
|
|
||||||
self.driver.common.command.set_horcmgr_flock()
|
|
||||||
|
|
||||||
# API test cases
|
|
||||||
@mock.patch.object(hbsd_basiclib, 'get_process_lock')
|
|
||||||
@mock.patch.object(hbsd_common.HBSDCommon, 'get_volume_metadata')
|
|
||||||
@mock.patch.object(hbsd_snm2.HBSDSNM2, 'exec_hsnm', side_effect=_exec_hsnm)
|
|
||||||
def test_create_volume(self, arg1, arg2, arg3):
|
|
||||||
"""test create_volume."""
|
|
||||||
ret = self.driver.create_volume(self._VOLUME)
|
|
||||||
vol = self._VOLUME.copy()
|
|
||||||
vol['provider_location'] = ret['provider_location']
|
|
||||||
self.assertEqual('1', vol['provider_location'])
|
|
||||||
|
|
||||||
@mock.patch.object(hbsd_basiclib, 'get_process_lock')
|
|
||||||
@mock.patch.object(hbsd_common.HBSDCommon, 'get_volume_metadata')
|
|
||||||
@mock.patch.object(hbsd_snm2.HBSDSNM2, 'exec_hsnm', side_effect=_exec_hsnm)
|
|
||||||
def test_create_volume_error(self, arg1, arg2, arg3):
|
|
||||||
"""test create_volume."""
|
|
||||||
self.assertRaises(exception.HBSDCmdError,
|
|
||||||
self.driver.create_volume,
|
|
||||||
self.test_volume_error)
|
|
||||||
|
|
||||||
@mock.patch.object(hbsd_basiclib, 'get_process_lock')
|
|
||||||
@mock.patch.object(hbsd_snm2.HBSDSNM2, 'exec_hsnm', side_effect=_exec_hsnm)
|
|
||||||
def test_get_volume_stats(self, arg1, arg2):
|
|
||||||
"""test get_volume_stats."""
|
|
||||||
stats = self.driver.get_volume_stats(True)
|
|
||||||
self.assertEqual('Hitachi', stats['vendor_name'])
|
|
||||||
|
|
||||||
@mock.patch.object(hbsd_basiclib, 'get_process_lock')
|
|
||||||
@mock.patch.object(hbsd_snm2.HBSDSNM2, 'exec_hsnm', side_effect=_exec_hsnm)
|
|
||||||
def test_get_volume_stats_error(self, arg1, arg2):
|
|
||||||
"""test get_volume_stats."""
|
|
||||||
self.configuration.hitachi_pool_id = 29
|
|
||||||
stats = self.driver.get_volume_stats(True)
|
|
||||||
self.assertEqual({}, stats)
|
|
||||||
self.configuration.hitachi_pool_id = 30
|
|
||||||
|
|
||||||
@mock.patch.object(hbsd_basiclib, 'get_process_lock')
|
|
||||||
@mock.patch.object(hbsd_snm2.HBSDSNM2, 'exec_hsnm', side_effect=_exec_hsnm)
|
|
||||||
def test_extend_volume(self, arg1, arg2):
|
|
||||||
"""test extend_volume."""
|
|
||||||
self.driver.extend_volume(self._VOLUME, 256)
|
|
||||||
|
|
||||||
@mock.patch.object(hbsd_basiclib, 'get_process_lock')
|
|
||||||
@mock.patch.object(hbsd_snm2.HBSDSNM2, 'exec_hsnm', side_effect=_exec_hsnm)
|
|
||||||
def test_extend_volume_error(self, arg1, arg2):
|
|
||||||
"""test extend_volume."""
|
|
||||||
self.assertRaises(exception.HBSDError, self.driver.extend_volume,
|
|
||||||
self.test_volume_error3, 256)
|
|
||||||
|
|
||||||
@mock.patch.object(hbsd_basiclib, 'get_process_lock')
|
|
||||||
@mock.patch.object(hbsd_snm2.HBSDSNM2, 'exec_hsnm', side_effect=_exec_hsnm)
|
|
||||||
def test_delete_volume(self, arg1, arg2):
|
|
||||||
"""test delete_volume."""
|
|
||||||
self.driver.delete_volume(self._VOLUME)
|
|
||||||
|
|
||||||
@mock.patch.object(hbsd_basiclib, 'get_process_lock')
|
|
||||||
@mock.patch.object(hbsd_snm2.HBSDSNM2, 'exec_hsnm', side_effect=_exec_hsnm)
|
|
||||||
def test_delete_volume_error(self, arg1, arg2):
|
|
||||||
"""test delete_volume."""
|
|
||||||
self.assertRaises(exception.HBSDCmdError,
|
|
||||||
self.driver.delete_volume,
|
|
||||||
self.test_volume_error4)
|
|
||||||
|
|
||||||
@mock.patch.object(hbsd_basiclib, 'get_process_lock')
|
|
||||||
@mock.patch.object(hbsd_common.HBSDCommon, 'get_snapshot_metadata',
|
|
||||||
return_value={'dummy_snapshot_meta': 'snapshot_meta'})
|
|
||||||
@mock.patch.object(hbsd_common.HBSDCommon, 'get_volume_metadata',
|
|
||||||
return_value={'dummy_volume_meta': 'meta'})
|
|
||||||
@mock.patch.object(hbsd_common.HBSDCommon, 'get_volume',
|
|
||||||
return_value=_VOLUME)
|
|
||||||
@mock.patch.object(hbsd_snm2.HBSDSNM2, 'exec_hsnm', side_effect=_exec_hsnm)
|
|
||||||
def test_create_snapshot(self, arg1, arg2, arg3, arg4, arg5):
|
|
||||||
"""test create_snapshot."""
|
|
||||||
ret = self.driver.create_volume(self._VOLUME)
|
|
||||||
ret = self.driver.create_snapshot(self.test_snapshot)
|
|
||||||
self.assertEqual('1', ret['provider_location'])
|
|
||||||
|
|
||||||
@mock.patch.object(hbsd_basiclib, 'get_process_lock')
|
|
||||||
@mock.patch.object(hbsd_common.HBSDCommon, 'get_snapshot_metadata',
|
|
||||||
return_value={'dummy_snapshot_meta': 'snapshot_meta'})
|
|
||||||
@mock.patch.object(hbsd_common.HBSDCommon, 'get_volume_metadata',
|
|
||||||
return_value={'dummy_volume_meta': 'meta'})
|
|
||||||
@mock.patch.object(hbsd_common.HBSDCommon, 'get_volume',
|
|
||||||
return_value=test_volume_error)
|
|
||||||
@mock.patch.object(hbsd_snm2.HBSDSNM2, 'exec_hsnm', side_effect=_exec_hsnm)
|
|
||||||
def test_create_snapshot_error(self, arg1, arg2, arg3, arg4, arg5):
|
|
||||||
"""test create_snapshot."""
|
|
||||||
self.assertRaises(exception.HBSDCmdError,
|
|
||||||
self.driver.create_snapshot,
|
|
||||||
self.test_snapshot_error2)
|
|
||||||
|
|
||||||
@mock.patch.object(hbsd_basiclib, 'get_process_lock')
|
|
||||||
@mock.patch.object(hbsd_snm2.HBSDSNM2, 'exec_hsnm', side_effect=_exec_hsnm)
|
|
||||||
def test_delete_snapshot(self, arg1, arg2):
|
|
||||||
"""test delete_snapshot."""
|
|
||||||
self.driver.delete_snapshot(self.test_snapshot)
|
|
||||||
return
|
|
||||||
|
|
||||||
@mock.patch.object(hbsd_basiclib, 'get_process_lock')
|
|
||||||
@mock.patch.object(hbsd_snm2.HBSDSNM2, 'exec_hsnm', side_effect=_exec_hsnm)
|
|
||||||
def test_delete_snapshot_error(self, arg1, arg2):
|
|
||||||
"""test delete_snapshot."""
|
|
||||||
self.driver.delete_snapshot(self.test_snapshot_error2)
|
|
||||||
return
|
|
||||||
|
|
||||||
@mock.patch.object(hbsd_basiclib, 'get_process_lock')
|
|
||||||
@mock.patch.object(hbsd_common.HBSDCommon, 'get_volume_metadata',
|
|
||||||
return_value={'dummy_volume_meta': 'meta'})
|
|
||||||
@mock.patch.object(hbsd_snm2.HBSDSNM2, 'exec_hsnm', side_effect=_exec_hsnm)
|
|
||||||
def test_create_volume_from_snapshot(self, arg1, arg2, arg3):
|
|
||||||
"""test create_volume_from_snapshot."""
|
|
||||||
vol = self.driver.create_volume_from_snapshot(self._VOLUME,
|
|
||||||
self.test_snapshot)
|
|
||||||
self.assertIsNotNone(vol)
|
|
||||||
return
|
|
||||||
|
|
||||||
@mock.patch.object(hbsd_basiclib, 'get_process_lock')
|
|
||||||
@mock.patch.object(hbsd_common.HBSDCommon, 'get_volume_metadata',
|
|
||||||
return_value={'dummy_volume_meta': 'meta'})
|
|
||||||
@mock.patch.object(hbsd_snm2.HBSDSNM2, 'exec_hsnm', side_effect=_exec_hsnm)
|
|
||||||
def test_create_volume_from_snapshot_error(self, arg1, arg2, arg3):
|
|
||||||
"""test create_volume_from_snapshot."""
|
|
||||||
self.assertRaises(exception.HBSDError,
|
|
||||||
self.driver.create_volume_from_snapshot,
|
|
||||||
self.test_volume_error2, self.test_snapshot)
|
|
||||||
return
|
|
||||||
|
|
||||||
@mock.patch.object(hbsd_common.HBSDCommon, 'get_volume_metadata',
|
|
||||||
return_value={'dummy_volume_meta': 'meta'})
|
|
||||||
@mock.patch.object(hbsd_common.HBSDCommon, 'get_volume',
|
|
||||||
return_value=_VOLUME)
|
|
||||||
@mock.patch.object(hbsd_snm2.HBSDSNM2, 'exec_hsnm', side_effect=_exec_hsnm)
|
|
||||||
@mock.patch.object(hbsd_basiclib, 'get_process_lock')
|
|
||||||
def test_create_cloned_volume(self, arg1, arg2, arg3, arg4):
|
|
||||||
"""test create_cloned_volume."""
|
|
||||||
vol = self.driver.create_cloned_volume(self._VOLUME,
|
|
||||||
self.test_snapshot)
|
|
||||||
self.assertIsNotNone(vol)
|
|
||||||
return
|
|
||||||
|
|
||||||
@mock.patch.object(hbsd_common.HBSDCommon, 'get_volume_metadata',
|
|
||||||
return_value={'dummy_volume_meta': 'meta'})
|
|
||||||
@mock.patch.object(hbsd_common.HBSDCommon, 'get_volume',
|
|
||||||
return_value=_VOLUME)
|
|
||||||
@mock.patch.object(hbsd_common.HBSDCommon, 'extend_volume')
|
|
||||||
@mock.patch.object(hbsd_snm2.HBSDSNM2, 'exec_hsnm', side_effect=_exec_hsnm)
|
|
||||||
@mock.patch.object(hbsd_basiclib, 'get_process_lock')
|
|
||||||
def test_create_cloned_volume_larger(self, arg1, arg2, arg3, arg4, arg5):
|
|
||||||
"""test create_cloned_volume."""
|
|
||||||
vol = self.driver.create_cloned_volume(self.test_volume_larger,
|
|
||||||
self._VOLUME)
|
|
||||||
self.assertIsNotNone(vol)
|
|
||||||
arg3.assert_called_once_with(self.test_volume_larger,
|
|
||||||
self.test_volume_larger['size'])
|
|
||||||
return
|
|
||||||
|
|
||||||
@mock.patch.object(hbsd_common.HBSDCommon, 'get_volume_metadata',
|
|
||||||
return_value={'dummy_volume_meta': 'meta'})
|
|
||||||
@mock.patch.object(hbsd_common.HBSDCommon, 'get_volume',
|
|
||||||
return_value=test_volume_error1)
|
|
||||||
@mock.patch.object(hbsd_snm2.HBSDSNM2, 'exec_hsnm', side_effect=_exec_hsnm)
|
|
||||||
@mock.patch.object(hbsd_basiclib, 'get_process_lock')
|
|
||||||
def test_create_cloned_volume_error(self, arg1, arg2, arg3, arg4):
|
|
||||||
"""test create_cloned_volume."""
|
|
||||||
self.assertRaises(exception.HBSDError,
|
|
||||||
self.driver.create_cloned_volume,
|
|
||||||
self._VOLUME, self.test_volume_error1)
|
|
||||||
return
|
|
||||||
|
|
||||||
@mock.patch.object(hbsd_basiclib, 'get_process_lock')
|
|
||||||
@mock.patch.object(hbsd_snm2.HBSDSNM2, 'exec_hsnm', side_effect=_exec_hsnm)
|
|
||||||
def test_initialize_connection(self, arg1, arg2):
|
|
||||||
"""test initialize connection."""
|
|
||||||
connector = {
|
|
||||||
'wwpns': '0x100000', 'ip': '0.0.0.0', 'initiator':
|
|
||||||
'iqn'}
|
|
||||||
rc = self.driver.initialize_connection(self._VOLUME, connector)
|
|
||||||
self.assertEqual('iscsi', rc['driver_volume_type'])
|
|
||||||
self.assertEqual('iqn-target', rc['data']['target_iqn'])
|
|
||||||
self.assertEqual(1, rc['data']['target_lun'])
|
|
||||||
return
|
|
||||||
|
|
||||||
@mock.patch.object(hbsd_basiclib, 'get_process_lock')
|
|
||||||
@mock.patch.object(hbsd_snm2.HBSDSNM2, 'exec_hsnm', side_effect=_exec_hsnm)
|
|
||||||
def test_initialize_connection_error(self, arg1, arg2):
|
|
||||||
"""test initialize connection."""
|
|
||||||
connector = {
|
|
||||||
'wwpns': '0x100000', 'ip': '0.0.0.0', 'initiator':
|
|
||||||
'iqnX'}
|
|
||||||
self.assertRaises(exception.HBSDError,
|
|
||||||
self.driver.initialize_connection,
|
|
||||||
self._VOLUME, connector)
|
|
||||||
return
|
|
||||||
|
|
||||||
@mock.patch.object(hbsd_snm2.HBSDSNM2, 'exec_hsnm', side_effect=_exec_hsnm)
|
|
||||||
def test_terminate_connection(self, arg1):
|
|
||||||
"""test terminate connection."""
|
|
||||||
connector = {
|
|
||||||
'wwpns': '0x100000', 'ip': '0.0.0.0', 'initiator':
|
|
||||||
'iqn'}
|
|
||||||
self.driver.terminate_connection(self._VOLUME, connector)
|
|
||||||
return
|
|
||||||
|
|
||||||
@mock.patch.object(hbsd_snm2.HBSDSNM2, 'exec_hsnm', side_effect=_exec_hsnm)
|
|
||||||
def test_terminate_connection_error(self, arg1):
|
|
||||||
"""test terminate connection."""
|
|
||||||
connector = {'ip': '0.0.0.0'}
|
|
||||||
self.assertRaises(exception.HBSDError,
|
|
||||||
self.driver.terminate_connection,
|
|
||||||
self._VOLUME, connector)
|
|
||||||
return
|
|
||||||
|
|
||||||
@mock.patch.object(hbsd_basiclib, 'get_process_lock')
|
|
||||||
@mock.patch.object(hbsd_snm2.HBSDSNM2, 'exec_hsnm', side_effect=_exec_hsnm)
|
|
||||||
def test_manage_existing(self, arg1, arg2):
|
|
||||||
rc = self.driver.manage_existing(self._VOLUME, self.test_existing_ref)
|
|
||||||
self.assertEqual(0, rc['provider_location'])
|
|
||||||
|
|
||||||
@mock.patch.object(hbsd_basiclib, 'get_process_lock')
|
|
||||||
@mock.patch.object(hbsd_snm2.HBSDSNM2, 'exec_hsnm', side_effect=_exec_hsnm)
|
|
||||||
@mock.patch.object(hbsd_common.HBSDCommon, '_update_volume_metadata')
|
|
||||||
def test_manage_existing_get_size(self, arg1, arg2, arg3):
|
|
||||||
self.configuration.hitachi_unit_name = self.UNIT_NAME
|
|
||||||
size = self.driver.manage_existing_get_size(self._VOLUME,
|
|
||||||
self.test_existing_ref)
|
|
||||||
self.assertEqual(1, size)
|
|
||||||
|
|
||||||
@mock.patch.object(hbsd_basiclib, 'get_process_lock')
|
|
||||||
@mock.patch.object(hbsd_snm2.HBSDSNM2, 'exec_hsnm', side_effect=_exec_hsnm)
|
|
||||||
@mock.patch.object(hbsd_common.HBSDCommon, '_update_volume_metadata')
|
|
||||||
def test_manage_existing_get_size_none_ldev(self, arg1, arg2, arg3):
|
|
||||||
self.configuration.hitachi_unit_name = self.UNIT_NAME
|
|
||||||
self.assertRaises(exception.ManageExistingInvalidReference,
|
|
||||||
self.driver.manage_existing_get_size, self._VOLUME,
|
|
||||||
self.test_existing_none_ldev_ref)
|
|
||||||
|
|
||||||
@mock.patch.object(hbsd_basiclib, 'get_process_lock')
|
|
||||||
@mock.patch.object(hbsd_snm2.HBSDSNM2, 'exec_hsnm', side_effect=_exec_hsnm)
|
|
||||||
@mock.patch.object(hbsd_common.HBSDCommon, '_update_volume_metadata')
|
|
||||||
def test_manage_existing_get_size_invalid_ldev_ref(self, arg1, arg2, arg3):
|
|
||||||
self.configuration.hitachi_unit_name = self.UNIT_NAME
|
|
||||||
self.assertRaises(exception.ManageExistingInvalidReference,
|
|
||||||
self.driver.manage_existing_get_size, self._VOLUME,
|
|
||||||
self.test_existing_invalid_ldev_ref)
|
|
||||||
|
|
||||||
@mock.patch.object(hbsd_basiclib, 'get_process_lock')
|
|
||||||
@mock.patch.object(hbsd_snm2.HBSDSNM2, 'exec_hsnm', side_effect=_exec_hsnm)
|
|
||||||
@mock.patch.object(hbsd_common.HBSDCommon, '_update_volume_metadata')
|
|
||||||
def test_manage_existing_get_size_no_ldev_ref(self, arg1, arg2, arg3):
|
|
||||||
self.configuration.hitachi_unit_name = self.UNIT_NAME
|
|
||||||
self.assertRaises(exception.ManageExistingInvalidReference,
|
|
||||||
self.driver.manage_existing_get_size, self._VOLUME,
|
|
||||||
self.test_existing_no_ldev_ref)
|
|
||||||
|
|
||||||
@mock.patch.object(hbsd_basiclib, 'get_process_lock')
|
|
||||||
@mock.patch.object(hbsd_snm2.HBSDSNM2, 'exec_hsnm', side_effect=_exec_hsnm)
|
|
||||||
@mock.patch.object(hbsd_common.HBSDCommon, '_update_volume_metadata')
|
|
||||||
def test_manage_existing_get_size_none_unit_ref(self, arg1, arg2, arg3):
|
|
||||||
self.configuration.hitachi_unit_name = self.UNIT_NAME
|
|
||||||
self.assertRaises(exception.ManageExistingInvalidReference,
|
|
||||||
self.driver.manage_existing_get_size, self._VOLUME,
|
|
||||||
self.test_existing_none_unit_ref)
|
|
||||||
|
|
||||||
@mock.patch.object(hbsd_basiclib, 'get_process_lock')
|
|
||||||
@mock.patch.object(hbsd_snm2.HBSDSNM2, 'exec_hsnm', side_effect=_exec_hsnm)
|
|
||||||
@mock.patch.object(hbsd_common.HBSDCommon, '_update_volume_metadata')
|
|
||||||
def test_manage_existing_get_size_invalid_unit_ref(self, arg1, arg2, arg3):
|
|
||||||
self.configuration.hitachi_unit_name = self.UNIT_NAME
|
|
||||||
self.assertRaises(exception.ManageExistingInvalidReference,
|
|
||||||
self.driver.manage_existing_get_size, self._VOLUME,
|
|
||||||
self.test_existing_invalid_unit_ref)
|
|
||||||
|
|
||||||
@mock.patch.object(hbsd_basiclib, 'get_process_lock')
|
|
||||||
@mock.patch.object(hbsd_snm2.HBSDSNM2, 'exec_hsnm', side_effect=_exec_hsnm)
|
|
||||||
@mock.patch.object(hbsd_common.HBSDCommon, '_update_volume_metadata')
|
|
||||||
def test_manage_existing_get_size_no_unit_ref(self, arg1, arg2, arg3):
|
|
||||||
self.configuration.hitachi_unit_name = self.UNIT_NAME
|
|
||||||
self.assertRaises(exception.ManageExistingInvalidReference,
|
|
||||||
self.driver.manage_existing_get_size, self._VOLUME,
|
|
||||||
self.test_existing_no_unit_ref)
|
|
||||||
|
|
||||||
@mock.patch.object(hbsd_basiclib, 'get_process_lock')
|
|
||||||
@mock.patch.object(hbsd_snm2.HBSDSNM2, 'exec_hsnm', side_effect=_exec_hsnm)
|
|
||||||
def test_unmanage(self, arg1, arg2):
|
|
||||||
self.driver.unmanage(self._VOLUME)
|
|
||||||
|
|
||||||
@mock.patch.object(hbsd_basiclib, 'get_process_lock')
|
|
||||||
@mock.patch.object(hbsd_snm2.HBSDSNM2, 'exec_hsnm', side_effect=_exec_hsnm)
|
|
||||||
def test_unmanage_busy(self, arg1, arg2):
|
|
||||||
self.assertRaises(exception.HBSDVolumeIsBusy,
|
|
||||||
self.driver.unmanage, self.test_volume_error3)
|
|
@ -1,519 +0,0 @@
|
|||||||
# 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.
|
|
||||||
#
|
|
||||||
|
|
||||||
import mock
|
|
||||||
import os
|
|
||||||
import paramiko
|
|
||||||
import time
|
|
||||||
|
|
||||||
from oslo_concurrency import processutils as putils
|
|
||||||
|
|
||||||
from cinder import exception
|
|
||||||
from cinder import test
|
|
||||||
from cinder import utils
|
|
||||||
from cinder.volume.drivers.hitachi import hnas_backend
|
|
||||||
|
|
||||||
|
|
||||||
evsfs_list = "\n\
|
|
||||||
FS ID FS Label FS Permanent ID EVS ID EVS Label\n\
|
|
||||||
----- ----------- ------------------ ------ ---------\n\
|
|
||||||
1026 gold 0xaadee0e035cfc0b7 1 EVS-Manila\n\
|
|
||||||
1029 test_hdp 0xaadee09634acfcac 1 EVS-Manila\n\
|
|
||||||
1030 fs-cinder 0xaadfcf742fba644e 2 EVS-Cinder\n\
|
|
||||||
1031 cinder2 0xaadfcf7e0769a6bc 3 EVS-Test\n\
|
|
||||||
1024 fs02-husvm 0xaac8715e2e9406cd 3 EVS-Test\n\
|
|
||||||
\n"
|
|
||||||
|
|
||||||
cluster_getmac = "cluster MAC: 83-68-96-AA-DA-5D"
|
|
||||||
|
|
||||||
version = "\n\
|
|
||||||
Model: HNAS 4040 \n\n\
|
|
||||||
Software: 11.2.3319.14 (built 2013-09-19 12:34:24+01:00) \n\n\
|
|
||||||
Hardware: NAS Platform (M2SEKW1339109) \n\n\
|
|
||||||
board MMB1 \n\
|
|
||||||
mmb 11.2.3319.14 release (2013-09-19 12:34:24+01:00)\n\n\
|
|
||||||
board MFB1 \n\
|
|
||||||
mfb1hw MB v0883 WL v002F TD v002F FD v002F TC v0059 \
|
|
||||||
RY v0059 TY v0059 IC v0059 WF v00E2 FS v00E2 OS v00E2 \
|
|
||||||
WD v00E2 DI v001A FC v0002 \n\
|
|
||||||
Serial no B1339745 (Thu Jan 1 00:00:50 2009) \n\n\
|
|
||||||
board MCP \n\
|
|
||||||
Serial no B1339109 (Thu Jan 1 00:00:49 2009) \n\
|
|
||||||
\n"
|
|
||||||
|
|
||||||
evsipaddr = "\n\
|
|
||||||
EVS Type Label IP Address Mask Port \n\
|
|
||||||
---------- --------------- ------------------ --------------- ------\n\
|
|
||||||
admin hnas4040 192.0.2.2 255.255.255.0 eth1 \n\
|
|
||||||
admin hnas4040 172.24.44.15 255.255.255.0 eth0 \n\
|
|
||||||
evs 1 EVSTest1 172.24.44.20 255.255.255.0 ag1 \n\
|
|
||||||
evs 1 EVSTest1 10.0.0.20 255.255.255.0 ag1 \n\
|
|
||||||
evs 2 EVSTest2 172.24.44.21 255.255.255.0 ag1 \n\
|
|
||||||
\n"
|
|
||||||
|
|
||||||
df_f = "\n\
|
|
||||||
ID Label EVS Size Used Snapshots Deduped Avail \
|
|
||||||
Thin ThinSize ThinAvail FS Type\n\
|
|
||||||
---- ---------- --- ------ ------------ --------- ------- ------------ \
|
|
||||||
---- -------- --------- --------------------\n\
|
|
||||||
1025 fs-cinder 2 250 GB 21.4 GB (9%) 0 B (0%) NA 228 GB (91%) \
|
|
||||||
No 32 KB,WFS-2,128 DSBs\n\
|
|
||||||
\n"
|
|
||||||
|
|
||||||
df_f_tb = "\n\
|
|
||||||
ID Label EVS Size Used Snapshots Deduped Avail \
|
|
||||||
Thin ThinSize ThinAvail FS Type\n\
|
|
||||||
---- ---------- --- ------ ------------ --------- ------- ------------ \
|
|
||||||
---- -------- --------- --------------------\n\
|
|
||||||
1025 fs-cinder 2 250 TB 21.4 TB (9%) 0 B (0%) NA 228 TB (91%) \
|
|
||||||
No 32 KB,WFS-2,128 DSBs\n\
|
|
||||||
\n"
|
|
||||||
|
|
||||||
nfs_export = "\n\
|
|
||||||
Export name: /export01-husvm \n\
|
|
||||||
Export path: /export01-husvm \n\
|
|
||||||
File system label: fs-cinder \n\
|
|
||||||
File system size: 250 GB \n\
|
|
||||||
File system free space: 228 GB \n\
|
|
||||||
File system state: \n\
|
|
||||||
formatted = Yes \n\
|
|
||||||
mounted = Yes \n\
|
|
||||||
failed = No \n\
|
|
||||||
thin provisioned = No \n\
|
|
||||||
Access snapshots: Yes \n\
|
|
||||||
Display snapshots: Yes \n\
|
|
||||||
Read Caching: Disabled \n\
|
|
||||||
Disaster recovery setting: \n\
|
|
||||||
Recovered = No \n\
|
|
||||||
Transfer setting = Use file system default \n\n\
|
|
||||||
Export configuration: \n\
|
|
||||||
127.0.0.1 \n\
|
|
||||||
\n"
|
|
||||||
|
|
||||||
df_f_single_evs = "\n\
|
|
||||||
ID Label Size Used Snapshots Deduped Avail \
|
|
||||||
Thin ThinSize ThinAvail FS Type\n\
|
|
||||||
---- ---------- ------ ------------ --------- ------- ------------ \
|
|
||||||
---- -------- --------- --------------------\n\
|
|
||||||
1025 fs-cinder 250 GB 21.4 GB (9%) 0 B (0%) NA 228 GB (91%) \
|
|
||||||
No 32 KB,WFS-2,128 DSBs\n\
|
|
||||||
\n"
|
|
||||||
|
|
||||||
nfs_export_tb = "\n\
|
|
||||||
Export name: /export01-husvm \n\
|
|
||||||
Export path: /export01-husvm \n\
|
|
||||||
File system label: fs-cinder \n\
|
|
||||||
File system size: 250 TB \n\
|
|
||||||
File system free space: 228 TB \n\
|
|
||||||
\n"
|
|
||||||
|
|
||||||
nfs_export_not_available = "\n\
|
|
||||||
Export name: /export01-husvm \n\
|
|
||||||
Export path: /export01-husvm \n\
|
|
||||||
File system label: fs-cinder \n\
|
|
||||||
*** not available *** \n\
|
|
||||||
\n"
|
|
||||||
|
|
||||||
evs_list = "\n\
|
|
||||||
Node EVS ID Type Label Enabled Status IP Address Port \n\
|
|
||||||
---- ------ ------- --------------- ------- ------ ------------------- ---- \n\
|
|
||||||
1 Cluster hnas4040 Yes Online 192.0.2.200 eth1 \n\
|
|
||||||
1 0 Admin hnas4040 Yes Online 192.0.2.2 eth1 \n\
|
|
||||||
172.24.44.15 eth0 \n\
|
|
||||||
172.24.49.101 ag2 \n\
|
|
||||||
1 1 Service EVS-Manila Yes Online 172.24.49.32 ag2 \n\
|
|
||||||
172.24.48.32 ag4 \n\
|
|
||||||
1 2 Service EVS-Cinder Yes Online 172.24.49.21 ag2 \n\
|
|
||||||
1 3 Service EVS-Test Yes Online 192.168.100.100 ag2 \n\
|
|
||||||
\n"
|
|
||||||
|
|
||||||
lu_list = "Name : cinder-lu \n\
|
|
||||||
Comment: \n\
|
|
||||||
Path : /.cinder/cinder-lu \n\
|
|
||||||
Size : 2 GB \n\
|
|
||||||
File System : fs-cinder \n\
|
|
||||||
File System Mounted : YES \n\
|
|
||||||
Logical Unit Mounted: No"
|
|
||||||
|
|
||||||
lu_list_tb = "Name : test-lu \n\
|
|
||||||
Comment: \n\
|
|
||||||
Path : /.cinder/test-lu \n\
|
|
||||||
Size : 2 TB \n\
|
|
||||||
File System : fs-cinder \n\
|
|
||||||
File System Mounted : YES \n\
|
|
||||||
Logical Unit Mounted: No"
|
|
||||||
|
|
||||||
hnas_fs_list = "%(l1)s\n\n%(l2)s\n\n " % {'l1': lu_list,
|
|
||||||
'l2': lu_list_tb}
|
|
||||||
|
|
||||||
add_targetsecret = "Target created successfully."
|
|
||||||
|
|
||||||
backend_opts = {'mgmt_ip0': '0.0.0.0',
|
|
||||||
'cluster_admin_ip0': None,
|
|
||||||
'ssh_port': '22',
|
|
||||||
'username': 'supervisor',
|
|
||||||
'password': 'supervisor',
|
|
||||||
'ssh_private_key': 'test_key'}
|
|
||||||
|
|
||||||
target_chap_disable = "\n\
|
|
||||||
Alias : cinder-default \n\
|
|
||||||
Globally unique name: iqn.2014-12.10.10.10.10:evstest1.cinder-default \n\
|
|
||||||
Comment : \n\
|
|
||||||
Secret : \n\
|
|
||||||
Authentication : Disabled \n\
|
|
||||||
Logical units : No logical units. \n\
|
|
||||||
\n\
|
|
||||||
LUN Logical Unit \n\
|
|
||||||
---- -------------------------------- \n\
|
|
||||||
0 cinder-lu \n\
|
|
||||||
1 volume-99da7ae7-1e7f-4d57-8bf... \n\
|
|
||||||
\n\
|
|
||||||
Access configuration: \n\
|
|
||||||
"
|
|
||||||
|
|
||||||
file_clone_stat = "Clone: /nfs_cinder/cinder-lu \n\
|
|
||||||
SnapshotFile: FileHandle[00000000004010000d20116826ffffffffffffff] \n\
|
|
||||||
\n\
|
|
||||||
SnapshotFile: FileHandle[00000000004029000d81f26826ffffffffffffff] \n\
|
|
||||||
"
|
|
||||||
|
|
||||||
file_clone_stat_snap_file1 = "\
|
|
||||||
FileHandle[00000000004010000d20116826ffffffffffffff] \n\n\
|
|
||||||
References: \n\
|
|
||||||
Clone: /nfs_cinder/cinder-lu \n\
|
|
||||||
Clone: /nfs_cinder/snapshot-lu-1 \n\
|
|
||||||
Clone: /nfs_cinder/snapshot-lu-2 \n\
|
|
||||||
"
|
|
||||||
|
|
||||||
file_clone_stat_snap_file2 = "\
|
|
||||||
FileHandle[00000000004010000d20116826ffffffffffffff] \n\n\
|
|
||||||
References: \n\
|
|
||||||
Clone: /nfs_cinder/volume-not-used \n\
|
|
||||||
Clone: /nfs_cinder/snapshot-1 \n\
|
|
||||||
Clone: /nfs_cinder/snapshot-2 \n\
|
|
||||||
"
|
|
||||||
|
|
||||||
not_a_clone = "\
|
|
||||||
file-clone-stat: failed to get predecessor snapshot-files: File is not a clone"
|
|
||||||
|
|
||||||
file_relatives =\
|
|
||||||
[' /nfs_cinder/snapshot-lu-1 ',
|
|
||||||
' /nfs_cinder/snapshot-lu-2 ',
|
|
||||||
' /nfs_cinder/volume-not-used ',
|
|
||||||
' /nfs_cinder/snapshot-1 ',
|
|
||||||
' /nfs_cinder/snapshot-2 ']
|
|
||||||
|
|
||||||
|
|
||||||
class HDSHNASBackendTest(test.TestCase):
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super(HDSHNASBackendTest, self).__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
super(HDSHNASBackendTest, self).setUp()
|
|
||||||
self.hnas_backend = hnas_backend.HNASSSHBackend(backend_opts)
|
|
||||||
|
|
||||||
def test_run_cmd(self):
|
|
||||||
self.mock_object(os.path, 'isfile', return_value=True)
|
|
||||||
self.mock_object(utils, 'execute')
|
|
||||||
self.mock_object(time, 'sleep')
|
|
||||||
self.mock_object(paramiko, 'SSHClient')
|
|
||||||
self.mock_object(paramiko.RSAKey, 'from_private_key_file')
|
|
||||||
self.mock_object(putils, 'ssh_execute',
|
|
||||||
return_value=(df_f, ''))
|
|
||||||
|
|
||||||
out, err = self.hnas_backend._run_cmd('ssh', '0.0.0.0',
|
|
||||||
'supervisor', 'supervisor',
|
|
||||||
'df', '-a')
|
|
||||||
|
|
||||||
self.assertIn('fs-cinder', out)
|
|
||||||
self.assertIn('WFS-2,128 DSBs', out)
|
|
||||||
|
|
||||||
def test_run_cmd_retry_exception(self):
|
|
||||||
self.hnas_backend.cluster_admin_ip0 = '172.24.44.11'
|
|
||||||
|
|
||||||
exceptions = [putils.ProcessExecutionError(stderr='Connection reset'),
|
|
||||||
putils.ProcessExecutionError(stderr='Failed to establish'
|
|
||||||
' SSC connection'),
|
|
||||||
putils.ProcessExecutionError(stderr='Connection reset'),
|
|
||||||
putils.ProcessExecutionError(stderr='Connection reset'),
|
|
||||||
putils.ProcessExecutionError(stderr='Connection reset')]
|
|
||||||
|
|
||||||
self.mock_object(os.path, 'isfile',
|
|
||||||
return_value=True)
|
|
||||||
self.mock_object(utils, 'execute')
|
|
||||||
self.mock_object(time, 'sleep')
|
|
||||||
self.mock_object(paramiko, 'SSHClient')
|
|
||||||
self.mock_object(paramiko.RSAKey, 'from_private_key_file')
|
|
||||||
self.mock_object(putils, 'ssh_execute',
|
|
||||||
side_effect=exceptions)
|
|
||||||
|
|
||||||
self.assertRaises(exception.HNASConnError, self.hnas_backend._run_cmd,
|
|
||||||
'ssh', '0.0.0.0', 'supervisor', 'supervisor', 'df',
|
|
||||||
'-a')
|
|
||||||
|
|
||||||
def test_run_cmd_exception_without_retry(self):
|
|
||||||
self.mock_object(os.path, 'isfile',
|
|
||||||
return_value=True)
|
|
||||||
self.mock_object(utils, 'execute')
|
|
||||||
self.mock_object(time, 'sleep')
|
|
||||||
self.mock_object(paramiko, 'SSHClient')
|
|
||||||
self.mock_object(paramiko.RSAKey, 'from_private_key_file')
|
|
||||||
self.mock_object(putils, 'ssh_execute',
|
|
||||||
side_effect=putils.ProcessExecutionError(
|
|
||||||
stderr='Error'))
|
|
||||||
|
|
||||||
self.assertRaises(putils.ProcessExecutionError,
|
|
||||||
self.hnas_backend._run_cmd, 'ssh', '0.0.0.0',
|
|
||||||
'supervisor', 'supervisor', 'df', '-a')
|
|
||||||
|
|
||||||
def test_get_version(self):
|
|
||||||
expected_out = {
|
|
||||||
'hardware': 'NAS Platform (M2SEKW1339109)',
|
|
||||||
'mac': '83-68-96-AA-DA-5D',
|
|
||||||
'version': '11.2.3319.14',
|
|
||||||
'model': 'HNAS 4040',
|
|
||||||
'serial': 'B1339745'
|
|
||||||
}
|
|
||||||
|
|
||||||
self.mock_object(self.hnas_backend, '_run_cmd',
|
|
||||||
side_effect=[(cluster_getmac, ''), (version, '')])
|
|
||||||
|
|
||||||
out = self.hnas_backend.get_version()
|
|
||||||
|
|
||||||
self.assertEqual(expected_out, out)
|
|
||||||
|
|
||||||
def test_get_evs(self):
|
|
||||||
self.mock_object(self.hnas_backend, '_run_cmd',
|
|
||||||
return_value=(evsfs_list, ''))
|
|
||||||
|
|
||||||
out = self.hnas_backend.get_evs('fs-cinder')
|
|
||||||
|
|
||||||
self.assertEqual('2', out)
|
|
||||||
|
|
||||||
def test_get_export_list(self):
|
|
||||||
self.mock_object(self.hnas_backend, '_run_cmd',
|
|
||||||
side_effect=[(nfs_export, ''),
|
|
||||||
(evsfs_list, ''),
|
|
||||||
(evs_list, '')])
|
|
||||||
|
|
||||||
out = self.hnas_backend.get_export_list()
|
|
||||||
|
|
||||||
self.assertEqual('fs-cinder', out[0]['fs'])
|
|
||||||
self.assertEqual(250.0, out[0]['size'])
|
|
||||||
self.assertEqual(228.0, out[0]['free'])
|
|
||||||
self.assertEqual('/export01-husvm', out[0]['path'])
|
|
||||||
|
|
||||||
def test_get_export_list_data_not_available(self):
|
|
||||||
self.mock_object(self.hnas_backend, '_run_cmd',
|
|
||||||
side_effect=[(nfs_export_not_available, ''),
|
|
||||||
(evsfs_list, ''),
|
|
||||||
(evs_list, '')])
|
|
||||||
|
|
||||||
out = self.hnas_backend.get_export_list()
|
|
||||||
|
|
||||||
self.assertEqual('fs-cinder', out[0]['fs'])
|
|
||||||
self.assertEqual('/export01-husvm', out[0]['path'])
|
|
||||||
self.assertEqual(-1, out[0]['size'])
|
|
||||||
self.assertEqual(-1, out[0]['free'])
|
|
||||||
|
|
||||||
def test_get_export_list_tb(self):
|
|
||||||
size = float(250 * 1024)
|
|
||||||
free = float(228 * 1024)
|
|
||||||
self.mock_object(self.hnas_backend, '_run_cmd',
|
|
||||||
side_effect=[(nfs_export_tb, ''),
|
|
||||||
(evsfs_list, ''),
|
|
||||||
(evs_list, '')])
|
|
||||||
|
|
||||||
out = self.hnas_backend.get_export_list()
|
|
||||||
|
|
||||||
self.assertEqual('fs-cinder', out[0]['fs'])
|
|
||||||
self.assertEqual(size, out[0]['size'])
|
|
||||||
self.assertEqual(free, out[0]['free'])
|
|
||||||
self.assertEqual('/export01-husvm', out[0]['path'])
|
|
||||||
|
|
||||||
def test_file_clone(self):
|
|
||||||
path1 = '/.cinder/path1'
|
|
||||||
path2 = '/.cinder/path2'
|
|
||||||
|
|
||||||
self.mock_object(self.hnas_backend, '_run_cmd',
|
|
||||||
return_value=(evsfs_list, ''))
|
|
||||||
|
|
||||||
self.hnas_backend.file_clone('fs-cinder', path1, path2)
|
|
||||||
|
|
||||||
calls = [mock.call('evsfs', 'list'), mock.call('console-context',
|
|
||||||
'--evs', '2',
|
|
||||||
'file-clone-create',
|
|
||||||
'-f', 'fs-cinder',
|
|
||||||
path1, path2)]
|
|
||||||
self.hnas_backend._run_cmd.assert_has_calls(calls, any_order=False)
|
|
||||||
|
|
||||||
def test_file_clone_wrong_fs(self):
|
|
||||||
self.mock_object(self.hnas_backend, '_run_cmd',
|
|
||||||
return_value=(evsfs_list, ''))
|
|
||||||
|
|
||||||
self.assertRaises(exception.InvalidParameterValue,
|
|
||||||
self.hnas_backend.file_clone, 'fs-fake', 'src',
|
|
||||||
'dst')
|
|
||||||
|
|
||||||
def test_get_evs_info(self):
|
|
||||||
expected_out = {'evs_number': '1'}
|
|
||||||
expected_out2 = {'evs_number': '2'}
|
|
||||||
|
|
||||||
self.mock_object(self.hnas_backend, '_run_cmd',
|
|
||||||
return_value=(evsipaddr, ''))
|
|
||||||
|
|
||||||
out = self.hnas_backend.get_evs_info()
|
|
||||||
|
|
||||||
self.hnas_backend._run_cmd.assert_called_with('evsipaddr', '-l')
|
|
||||||
self.assertEqual(expected_out, out['10.0.0.20'])
|
|
||||||
self.assertEqual(expected_out, out['172.24.44.20'])
|
|
||||||
self.assertEqual(expected_out2, out['172.24.44.21'])
|
|
||||||
|
|
||||||
def test_get_fs_info(self):
|
|
||||||
self.mock_object(self.hnas_backend, '_run_cmd',
|
|
||||||
side_effect=[(df_f, ''), (evsfs_list, ''),
|
|
||||||
(hnas_fs_list, '')])
|
|
||||||
|
|
||||||
out = self.hnas_backend.get_fs_info('fs-cinder')
|
|
||||||
|
|
||||||
self.assertEqual('2', out['evs_id'])
|
|
||||||
self.assertEqual('fs-cinder', out['label'])
|
|
||||||
self.assertEqual('228', out['available_size'])
|
|
||||||
self.assertEqual('250', out['total_size'])
|
|
||||||
self.assertEqual(0, out['provisioned_capacity'])
|
|
||||||
|
|
||||||
def test_get_fs_empty_return(self):
|
|
||||||
self.mock_object(self.hnas_backend, '_run_cmd',
|
|
||||||
return_value=('Not mounted', ''))
|
|
||||||
|
|
||||||
out = self.hnas_backend.get_fs_info('fs-cinder')
|
|
||||||
self.assertEqual({}, out)
|
|
||||||
|
|
||||||
def test_get_fs_info_single_evs(self):
|
|
||||||
self.mock_object(self.hnas_backend, '_run_cmd',
|
|
||||||
side_effect=[(df_f_single_evs, ''), (evsfs_list, ''),
|
|
||||||
(hnas_fs_list, '')])
|
|
||||||
|
|
||||||
out = self.hnas_backend.get_fs_info('fs-cinder')
|
|
||||||
|
|
||||||
self.assertEqual('fs-cinder', out['label'])
|
|
||||||
self.assertEqual('228', out['available_size'])
|
|
||||||
self.assertEqual('250', out['total_size'])
|
|
||||||
self.assertEqual(0, out['provisioned_capacity'])
|
|
||||||
|
|
||||||
def test_get_fs_tb(self):
|
|
||||||
available_size = float(228 * 1024 ** 2)
|
|
||||||
total_size = float(250 * 1024 ** 2)
|
|
||||||
|
|
||||||
self.mock_object(self.hnas_backend, '_run_cmd',
|
|
||||||
side_effect=[(df_f_tb, ''), (evsfs_list, ''),
|
|
||||||
(hnas_fs_list, '')])
|
|
||||||
|
|
||||||
out = self.hnas_backend.get_fs_info('fs-cinder')
|
|
||||||
|
|
||||||
self.assertEqual('fs-cinder', out['label'])
|
|
||||||
self.assertEqual(str(available_size), out['available_size'])
|
|
||||||
self.assertEqual(str(total_size), out['total_size'])
|
|
||||||
self.assertEqual(0, out['provisioned_capacity'])
|
|
||||||
|
|
||||||
def test_get_fs_single_evs_tb(self):
|
|
||||||
available_size = float(228 * 1024 ** 2)
|
|
||||||
total_size = float(250 * 1024 ** 2)
|
|
||||||
|
|
||||||
self.mock_object(self.hnas_backend, '_run_cmd',
|
|
||||||
side_effect=[(df_f_tb, ''), (evsfs_list, ''),
|
|
||||||
(hnas_fs_list, '')])
|
|
||||||
|
|
||||||
out = self.hnas_backend.get_fs_info('fs-cinder')
|
|
||||||
|
|
||||||
self.assertEqual('fs-cinder', out['label'])
|
|
||||||
self.assertEqual(str(available_size), out['available_size'])
|
|
||||||
self.assertEqual(str(total_size), out['total_size'])
|
|
||||||
self.assertEqual(0, out['provisioned_capacity'])
|
|
||||||
|
|
||||||
def test_get_cloned_file_relatives(self):
|
|
||||||
self.mock_object(self.hnas_backend, '_run_cmd',
|
|
||||||
side_effect=[(evsfs_list, ''), (file_clone_stat, ''),
|
|
||||||
(file_clone_stat_snap_file1, ''),
|
|
||||||
(file_clone_stat_snap_file2, '')])
|
|
||||||
out = self.hnas_backend.get_cloned_file_relatives('cinder-lu',
|
|
||||||
'fs-cinder')
|
|
||||||
self.assertEqual(file_relatives, out)
|
|
||||||
self.hnas_backend._run_cmd.assert_called_with('console-context',
|
|
||||||
'--evs', '2',
|
|
||||||
'file-clone-stat-'
|
|
||||||
'snapshot-file',
|
|
||||||
'-f', 'fs-cinder',
|
|
||||||
'00000000004029000d81'
|
|
||||||
'f26826ffffffffffffff]')
|
|
||||||
|
|
||||||
def test_get_cloned_file_relatives_not_clone_except(self):
|
|
||||||
exc = putils.ProcessExecutionError(stderr='File is not a clone')
|
|
||||||
self.mock_object(self.hnas_backend, '_run_cmd',
|
|
||||||
side_effect=[(evsfs_list, ''), exc])
|
|
||||||
|
|
||||||
self.assertRaises(exception.ManageExistingInvalidReference,
|
|
||||||
self.hnas_backend.get_cloned_file_relatives,
|
|
||||||
'cinder-lu', 'fs-cinder', True)
|
|
||||||
|
|
||||||
def test_get_cloned_file_relatives_not_clone_no_except(self):
|
|
||||||
exc = putils.ProcessExecutionError(stderr='File is not a clone')
|
|
||||||
self.mock_object(self.hnas_backend, '_run_cmd',
|
|
||||||
side_effect=[(evsfs_list, ''), exc])
|
|
||||||
|
|
||||||
out = self.hnas_backend.get_cloned_file_relatives('cinder-lu',
|
|
||||||
'fs-cinder')
|
|
||||||
|
|
||||||
self.assertEqual([], out)
|
|
||||||
|
|
||||||
def test_check_snapshot_parent_true(self):
|
|
||||||
self.mock_object(self.hnas_backend, '_run_cmd',
|
|
||||||
side_effect=[(evsfs_list, ''),
|
|
||||||
(file_clone_stat, ''),
|
|
||||||
(file_clone_stat_snap_file1, ''),
|
|
||||||
(file_clone_stat_snap_file2, '')])
|
|
||||||
out = self.hnas_backend.check_snapshot_parent('cinder-lu',
|
|
||||||
'snapshot-lu-1',
|
|
||||||
'fs-cinder')
|
|
||||||
|
|
||||||
self.assertTrue(out)
|
|
||||||
|
|
||||||
def test_check_snapshot_parent_false(self):
|
|
||||||
self.mock_object(self.hnas_backend, '_run_cmd',
|
|
||||||
side_effect=[(evsfs_list, ''),
|
|
||||||
(file_clone_stat, ''),
|
|
||||||
(file_clone_stat_snap_file1, ''),
|
|
||||||
(file_clone_stat_snap_file2, '')])
|
|
||||||
out = self.hnas_backend.check_snapshot_parent('cinder-lu',
|
|
||||||
'snapshot-lu-3',
|
|
||||||
'fs-cinder')
|
|
||||||
|
|
||||||
self.assertFalse(out)
|
|
||||||
|
|
||||||
def test_get_export_path(self):
|
|
||||||
export_out = '/export01-husvm'
|
|
||||||
|
|
||||||
self.mock_object(self.hnas_backend, '_run_cmd',
|
|
||||||
side_effect=[(evsfs_list, ''), (nfs_export, '')])
|
|
||||||
|
|
||||||
out = self.hnas_backend.get_export_path(export_out, 'fs-cinder')
|
|
||||||
|
|
||||||
self.assertEqual(export_out, out)
|
|
||||||
self.hnas_backend._run_cmd.assert_called_with('console-context',
|
|
||||||
'--evs', '2',
|
|
||||||
'nfs-export', 'list',
|
|
||||||
export_out)
|
|
@ -1,834 +0,0 @@
|
|||||||
# 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.
|
|
||||||
#
|
|
||||||
|
|
||||||
import mock
|
|
||||||
import os
|
|
||||||
|
|
||||||
from oslo_concurrency import processutils as putils
|
|
||||||
import socket
|
|
||||||
|
|
||||||
from cinder import context
|
|
||||||
from cinder import exception
|
|
||||||
from cinder.image import image_utils
|
|
||||||
from cinder import test
|
|
||||||
from cinder.tests.unit import fake_constants as fake
|
|
||||||
from cinder.tests.unit import fake_snapshot
|
|
||||||
from cinder.tests.unit import fake_volume
|
|
||||||
from cinder import utils
|
|
||||||
from cinder.volume import configuration as conf
|
|
||||||
from cinder.volume.drivers.hitachi import hnas_backend as backend
|
|
||||||
from cinder.volume.drivers.hitachi import hnas_nfs as nfs
|
|
||||||
from cinder.volume.drivers.hitachi import hnas_utils
|
|
||||||
from cinder.volume.drivers import nfs as base_nfs
|
|
||||||
from cinder.volume import utils as vutils
|
|
||||||
|
|
||||||
_VOLUME = {'name': 'cinder-volume',
|
|
||||||
'id': fake.VOLUME_ID,
|
|
||||||
'size': 128,
|
|
||||||
'host': 'host1@hnas-nfs-backend#default',
|
|
||||||
'volume_type': 'default',
|
|
||||||
'provider_location': 'hnas'}
|
|
||||||
|
|
||||||
_SNAPSHOT = {
|
|
||||||
'name': 'snapshot-51dd4-8d8a-4aa9-9176-086c9d89e7fc',
|
|
||||||
'id': fake.SNAPSHOT_ID,
|
|
||||||
'size': 128,
|
|
||||||
'volume_type': None,
|
|
||||||
'provider_location': 'hnas',
|
|
||||||
'volume_size': 128,
|
|
||||||
'volume': _VOLUME,
|
|
||||||
'volume_name': _VOLUME['name'],
|
|
||||||
'host': 'host1@hnas-iscsi-backend#silver',
|
|
||||||
'volume_type_id': fake.VOLUME_TYPE_ID,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class HNASNFSDriverTest(test.TestCase):
|
|
||||||
"""Test HNAS NFS volume driver."""
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super(HNASNFSDriverTest, self).__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
def instantiate_snapshot(self, snap):
|
|
||||||
snap = snap.copy()
|
|
||||||
snap['volume'] = fake_volume.fake_volume_obj(
|
|
||||||
None, **snap['volume'])
|
|
||||||
snapshot = fake_snapshot.fake_snapshot_obj(
|
|
||||||
None, expected_attrs=['volume'], **snap)
|
|
||||||
return snapshot
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
super(HNASNFSDriverTest, self).setUp()
|
|
||||||
self.context = context.get_admin_context()
|
|
||||||
|
|
||||||
self.volume = fake_volume.fake_volume_obj(
|
|
||||||
self.context,
|
|
||||||
**_VOLUME)
|
|
||||||
|
|
||||||
self.snapshot = self.instantiate_snapshot(_SNAPSHOT)
|
|
||||||
|
|
||||||
self.volume_type = fake_volume.fake_volume_type_obj(
|
|
||||||
None,
|
|
||||||
**{'name': 'silver'}
|
|
||||||
)
|
|
||||||
self.clone = fake_volume.fake_volume_obj(
|
|
||||||
None,
|
|
||||||
**{'id': fake.VOLUME2_ID,
|
|
||||||
'size': 128,
|
|
||||||
'host': 'host1@hnas-nfs-backend#default',
|
|
||||||
'volume_type': 'default',
|
|
||||||
'provider_location': 'hnas'})
|
|
||||||
|
|
||||||
# xml parsed from utils
|
|
||||||
self.parsed_xml = {
|
|
||||||
'username': 'supervisor',
|
|
||||||
'password': 'supervisor',
|
|
||||||
'hnas_cmd': 'ssc',
|
|
||||||
'ssh_port': '22',
|
|
||||||
'services': {
|
|
||||||
'default': {
|
|
||||||
'hdp': '172.24.49.21:/fs-cinder',
|
|
||||||
'pool_name': 'default',
|
|
||||||
'label': 'svc_0',
|
|
||||||
'ctl': '1',
|
|
||||||
'export': {
|
|
||||||
'fs': 'fs-cinder',
|
|
||||||
'path': '/export-cinder/volume'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'cluster_admin_ip0': None,
|
|
||||||
'ssh_private_key': None,
|
|
||||||
'chap_enabled': 'True',
|
|
||||||
'mgmt_ip0': '172.17.44.15',
|
|
||||||
'ssh_enabled': None
|
|
||||||
}
|
|
||||||
|
|
||||||
self.configuration = mock.Mock(spec=conf.Configuration)
|
|
||||||
self.configuration.hds_hnas_nfs_config_file = 'fake.xml'
|
|
||||||
|
|
||||||
self.mock_object(hnas_utils, 'read_cinder_conf',
|
|
||||||
return_value=self.parsed_xml)
|
|
||||||
|
|
||||||
self.configuration = mock.Mock(spec=conf.Configuration)
|
|
||||||
self.configuration.max_over_subscription_ratio = 20.0
|
|
||||||
self.configuration.reserved_percentage = 0
|
|
||||||
self.configuration.hds_hnas_nfs_config_file = 'fake_config.xml'
|
|
||||||
self.configuration.nfs_shares_config = 'fake_nfs_share.xml'
|
|
||||||
self.configuration.num_shell_tries = 2
|
|
||||||
self.configuration.nfs_mount_point_base = '%state_path/mnt'
|
|
||||||
self.configuration.nfs_mount_options = None
|
|
||||||
|
|
||||||
self.driver = nfs.HNASNFSDriver(configuration=self.configuration)
|
|
||||||
|
|
||||||
def test_check_pool_and_share_no_default_configured(self):
|
|
||||||
nfs_shares = '172.24.49.21:/fs-cinder'
|
|
||||||
|
|
||||||
self.mock_object(hnas_utils, 'get_pool', return_value='default')
|
|
||||||
|
|
||||||
self.driver.config['services'] = {
|
|
||||||
'silver': {
|
|
||||||
'hdp': 'fs3',
|
|
||||||
'iscsi_ip': '172.17.39.133',
|
|
||||||
'iscsi_port': '3260',
|
|
||||||
'port': '22',
|
|
||||||
'volume_type': 'silver',
|
|
||||||
'label': 'svc_1',
|
|
||||||
'evs': '2',
|
|
||||||
'tgt': {
|
|
||||||
'alias': 'iscsi-test',
|
|
||||||
'secret': 'itEpgB5gPefGhW2'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.assertRaises(exception.ManageExistingVolumeTypeMismatch,
|
|
||||||
self.driver._check_pool_and_share, self.volume,
|
|
||||||
nfs_shares)
|
|
||||||
|
|
||||||
def test_check_pool_and_share_mismatch_exception(self):
|
|
||||||
# passing a share that does not exists in config should raise an
|
|
||||||
# exception
|
|
||||||
nfs_shares = '172.24.49.21:/nfs_share'
|
|
||||||
|
|
||||||
self.mock_object(hnas_utils, 'get_pool', return_value='default')
|
|
||||||
|
|
||||||
self.assertRaises(exception.ManageExistingVolumeTypeMismatch,
|
|
||||||
self.driver._check_pool_and_share, self.volume,
|
|
||||||
nfs_shares)
|
|
||||||
|
|
||||||
def test_check_pool_and_share_type_mismatch_exception(self):
|
|
||||||
nfs_shares = '172.24.49.21:/fs-cinder'
|
|
||||||
self.volume.host = 'host1@hnas-nfs-backend#gold'
|
|
||||||
|
|
||||||
# returning a pool different from 'default' should raise an exception
|
|
||||||
self.mock_object(hnas_utils, 'get_pool', return_value='default')
|
|
||||||
|
|
||||||
self.assertRaises(exception.ManageExistingVolumeTypeMismatch,
|
|
||||||
self.driver._check_pool_and_share, self.volume,
|
|
||||||
nfs_shares)
|
|
||||||
|
|
||||||
def test_do_setup(self):
|
|
||||||
version_info = {
|
|
||||||
'mac': '83-68-96-AA-DA-5D',
|
|
||||||
'model': 'HNAS 4040',
|
|
||||||
'version': '12.4.3924.11',
|
|
||||||
'hardware': 'NAS Platform',
|
|
||||||
'serial': 'B1339109',
|
|
||||||
}
|
|
||||||
export_list = [
|
|
||||||
{'fs': 'fs-cinder',
|
|
||||||
'name': '/fs-cinder',
|
|
||||||
'free': 228.0,
|
|
||||||
'path': '/fs-cinder',
|
|
||||||
'evs': ['172.24.49.21'],
|
|
||||||
'size': 250.0}
|
|
||||||
]
|
|
||||||
|
|
||||||
showmount = "Export list for 172.24.49.21: \n\
|
|
||||||
/fs-cinder * \n\
|
|
||||||
/shares/9bcf0bcc-8cc8-437e38bcbda9 127.0.0.1,10.1.0.5,172.24.44.141 \n\
|
|
||||||
"
|
|
||||||
|
|
||||||
self.mock_object(backend.HNASSSHBackend, 'get_version',
|
|
||||||
return_value=version_info)
|
|
||||||
self.mock_object(self.driver, '_load_shares_config')
|
|
||||||
self.mock_object(backend.HNASSSHBackend, 'get_export_list',
|
|
||||||
return_value=export_list)
|
|
||||||
self.mock_object(self.driver, '_execute', return_value=(showmount, ''))
|
|
||||||
|
|
||||||
self.driver.do_setup(None)
|
|
||||||
|
|
||||||
self.driver._execute.assert_called_with('showmount', '-e',
|
|
||||||
'172.24.49.21')
|
|
||||||
self.assertTrue(backend.HNASSSHBackend.get_export_list.called)
|
|
||||||
|
|
||||||
def test_do_setup_execute_exception(self):
|
|
||||||
version_info = {
|
|
||||||
'mac': '83-68-96-AA-DA-5D',
|
|
||||||
'model': 'HNAS 4040',
|
|
||||||
'version': '12.4.3924.11',
|
|
||||||
'hardware': 'NAS Platform',
|
|
||||||
'serial': 'B1339109',
|
|
||||||
}
|
|
||||||
|
|
||||||
export_list = [
|
|
||||||
{'fs': 'fs-cinder',
|
|
||||||
'name': '/fs-cinder',
|
|
||||||
'free': 228.0,
|
|
||||||
'path': '/fs-cinder',
|
|
||||||
'evs': ['172.24.49.21'],
|
|
||||||
'size': 250.0}
|
|
||||||
]
|
|
||||||
|
|
||||||
self.mock_object(backend.HNASSSHBackend, 'get_version',
|
|
||||||
return_value=version_info)
|
|
||||||
self.mock_object(self.driver, '_load_shares_config')
|
|
||||||
self.mock_object(backend.HNASSSHBackend, 'get_export_list',
|
|
||||||
return_value=export_list)
|
|
||||||
self.mock_object(self.driver, '_execute',
|
|
||||||
side_effect=putils.ProcessExecutionError)
|
|
||||||
|
|
||||||
self.assertRaises(putils.ProcessExecutionError, self.driver.do_setup,
|
|
||||||
None)
|
|
||||||
|
|
||||||
def test_do_setup_missing_export(self):
|
|
||||||
version_info = {
|
|
||||||
'mac': '83-68-96-AA-DA-5D',
|
|
||||||
'model': 'HNAS 4040',
|
|
||||||
'version': '12.4.3924.11',
|
|
||||||
'hardware': 'NAS Platform',
|
|
||||||
'serial': 'B1339109',
|
|
||||||
}
|
|
||||||
export_list = [
|
|
||||||
{'fs': 'fs-cinder',
|
|
||||||
'name': '/wrong-fs',
|
|
||||||
'free': 228.0,
|
|
||||||
'path': '/fs-cinder',
|
|
||||||
'evs': ['172.24.49.21'],
|
|
||||||
'size': 250.0}
|
|
||||||
]
|
|
||||||
|
|
||||||
showmount = "Export list for 172.24.49.21: \n\
|
|
||||||
/fs-cinder * \n\
|
|
||||||
"
|
|
||||||
|
|
||||||
self.mock_object(backend.HNASSSHBackend, 'get_version',
|
|
||||||
return_value=version_info)
|
|
||||||
self.mock_object(self.driver, '_load_shares_config')
|
|
||||||
self.mock_object(backend.HNASSSHBackend, 'get_export_list',
|
|
||||||
return_value=export_list)
|
|
||||||
self.mock_object(self.driver, '_execute', return_value=(showmount, ''))
|
|
||||||
|
|
||||||
self.assertRaises(exception.InvalidParameterValue,
|
|
||||||
self.driver.do_setup, None)
|
|
||||||
|
|
||||||
def test_create_volume(self):
|
|
||||||
self.mock_object(self.driver, '_ensure_shares_mounted')
|
|
||||||
self.mock_object(self.driver, '_do_create_volume')
|
|
||||||
|
|
||||||
out = self.driver.create_volume(self.volume)
|
|
||||||
|
|
||||||
self.assertEqual('172.24.49.21:/fs-cinder', out['provider_location'])
|
|
||||||
self.assertTrue(self.driver._ensure_shares_mounted.called)
|
|
||||||
|
|
||||||
def test_create_volume_exception(self):
|
|
||||||
# pool 'original' doesnt exists in services
|
|
||||||
self.volume.host = 'host1@hnas-nfs-backend#original'
|
|
||||||
|
|
||||||
self.mock_object(self.driver, '_ensure_shares_mounted')
|
|
||||||
|
|
||||||
self.assertRaises(exception.ParameterNotFound,
|
|
||||||
self.driver.create_volume, self.volume)
|
|
||||||
|
|
||||||
def test_create_cloned_volume(self):
|
|
||||||
self.volume.size = 150
|
|
||||||
|
|
||||||
self.mock_object(self.driver, 'extend_volume')
|
|
||||||
self.mock_object(backend.HNASSSHBackend, 'file_clone')
|
|
||||||
|
|
||||||
out = self.driver.create_cloned_volume(self.volume, self.clone)
|
|
||||||
|
|
||||||
self.assertEqual('hnas', out['provider_location'])
|
|
||||||
|
|
||||||
def test_create_cloned_volume_invalid_volume_type(self):
|
|
||||||
self.volume.volume_type_id = fake.VOLUME_TYPE_ID
|
|
||||||
self.clone.volume_type_id = fake.VOLUME_TYPE2_ID
|
|
||||||
|
|
||||||
self.mock_object(self.driver, 'extend_volume')
|
|
||||||
self.mock_object(backend.HNASSSHBackend, 'file_clone')
|
|
||||||
|
|
||||||
self.assertRaises(exception.InvalidVolumeType,
|
|
||||||
self.driver.create_cloned_volume, self.volume,
|
|
||||||
self.clone)
|
|
||||||
|
|
||||||
def test_get_volume_stats(self):
|
|
||||||
self.driver.pools = [{'pool_name': 'default',
|
|
||||||
'service_label': 'default',
|
|
||||||
'fs': '172.24.49.21:/easy-stack'},
|
|
||||||
{'pool_name': 'cinder_svc',
|
|
||||||
'service_label': 'cinder_svc',
|
|
||||||
'fs': '172.24.49.26:/MNT-CinderTest2'}]
|
|
||||||
|
|
||||||
self.mock_object(self.driver, '_update_volume_stats')
|
|
||||||
self.mock_object(self.driver, '_get_capacity_info',
|
|
||||||
return_value=(150, 50, 100))
|
|
||||||
|
|
||||||
out = self.driver.get_volume_stats()
|
|
||||||
|
|
||||||
self.assertEqual('6.0.0', out['driver_version'])
|
|
||||||
self.assertEqual('Hitachi', out['vendor_name'])
|
|
||||||
self.assertEqual('NFS', out['storage_protocol'])
|
|
||||||
|
|
||||||
def test_create_volume_from_snapshot(self):
|
|
||||||
expected_out = {'provider_location': 'hnas'}
|
|
||||||
|
|
||||||
self.mock_object(self.driver, '_file_not_present',
|
|
||||||
mock.Mock(return_value=False))
|
|
||||||
self.mock_object(backend.HNASSSHBackend, 'file_clone')
|
|
||||||
result = self.driver.create_volume_from_snapshot(self.volume,
|
|
||||||
self.snapshot)
|
|
||||||
|
|
||||||
self.assertEqual(expected_out, result)
|
|
||||||
|
|
||||||
def test_create_volume_from_snapshot_legacy(self):
|
|
||||||
expected_out = {'provider_location': 'hnas'}
|
|
||||||
|
|
||||||
self.mock_object(self.driver, '_file_not_present',
|
|
||||||
mock.Mock(return_value=True))
|
|
||||||
self.mock_object(backend.HNASSSHBackend, 'file_clone')
|
|
||||||
result = self.driver.create_volume_from_snapshot(self.volume,
|
|
||||||
self.snapshot)
|
|
||||||
|
|
||||||
self.assertEqual(expected_out, result)
|
|
||||||
|
|
||||||
def test_create_snapshot(self):
|
|
||||||
expected_out = {'provider_location': 'hnas'}
|
|
||||||
self.mock_object(backend.HNASSSHBackend, 'file_clone')
|
|
||||||
result = self.driver.create_snapshot(self.snapshot)
|
|
||||||
|
|
||||||
self.assertEqual(expected_out, result)
|
|
||||||
|
|
||||||
def test_delete_snapshot(self):
|
|
||||||
nfs_mount = "/opt/stack/data/cinder/mnt/"
|
|
||||||
path = nfs_mount + self.driver._get_snapshot_name(self.snapshot)
|
|
||||||
|
|
||||||
self.mock_object(self.driver, '_file_not_present',
|
|
||||||
mock.Mock(return_value=False))
|
|
||||||
|
|
||||||
self.mock_object(self.driver, '_get_file_path',
|
|
||||||
mock.Mock(return_value=path))
|
|
||||||
self.mock_object(self.driver, '_execute')
|
|
||||||
|
|
||||||
self.driver.delete_snapshot(self.snapshot)
|
|
||||||
|
|
||||||
self.driver._execute.assert_called_with('rm', path, run_as_root=True)
|
|
||||||
|
|
||||||
def test_delete_snapshot_legacy(self):
|
|
||||||
nfs_mount = "/opt/stack/data/cinder/mnt/"
|
|
||||||
legacy_path = nfs_mount + self.snapshot.name
|
|
||||||
|
|
||||||
self.mock_object(self.driver, '_file_not_present',
|
|
||||||
mock.Mock(return_value=True))
|
|
||||||
self.mock_object(self.driver, '_file_not_present',
|
|
||||||
mock.Mock(return_value=False))
|
|
||||||
self.mock_object(self.driver, '_get_file_path',
|
|
||||||
mock.Mock(return_value=legacy_path))
|
|
||||||
self.mock_object(self.driver, '_execute')
|
|
||||||
|
|
||||||
self.driver.delete_snapshot(self.snapshot)
|
|
||||||
|
|
||||||
self.driver._execute.assert_called_with('rm', legacy_path,
|
|
||||||
run_as_root=True)
|
|
||||||
|
|
||||||
def test_extend_volume(self):
|
|
||||||
share_mount_point = '/fs-cinder'
|
|
||||||
data = image_utils.imageutils.QemuImgInfo
|
|
||||||
data.virtual_size = 200 * 1024 ** 3
|
|
||||||
|
|
||||||
self.mock_object(self.driver, '_get_mount_point_for_share',
|
|
||||||
return_value=share_mount_point)
|
|
||||||
self.mock_object(image_utils, 'qemu_img_info', return_value=data)
|
|
||||||
|
|
||||||
self.driver.extend_volume(self.volume, 200)
|
|
||||||
|
|
||||||
self.driver._get_mount_point_for_share.assert_called_with('hnas')
|
|
||||||
|
|
||||||
def test_extend_volume_resizing_exception(self):
|
|
||||||
share_mount_point = '/fs-cinder'
|
|
||||||
data = image_utils.imageutils.QemuImgInfo
|
|
||||||
data.virtual_size = 2048 ** 3
|
|
||||||
|
|
||||||
self.mock_object(self.driver, '_get_mount_point_for_share',
|
|
||||||
return_value=share_mount_point)
|
|
||||||
self.mock_object(image_utils, 'qemu_img_info', return_value=data)
|
|
||||||
|
|
||||||
self.mock_object(image_utils, 'resize_image')
|
|
||||||
|
|
||||||
self.assertRaises(exception.InvalidResults,
|
|
||||||
self.driver.extend_volume, self.volume, 200)
|
|
||||||
|
|
||||||
def test_manage_existing(self):
|
|
||||||
self.driver._mounted_shares = ['172.24.49.21:/fs-cinder']
|
|
||||||
existing_vol_ref = {'source-name': '172.24.49.21:/fs-cinder'}
|
|
||||||
|
|
||||||
self.mock_object(os.path, 'isfile', return_value=True)
|
|
||||||
self.mock_object(self.driver, '_get_mount_point_for_share',
|
|
||||||
return_value='/fs-cinder/cinder-volume')
|
|
||||||
self.mock_object(utils, 'resolve_hostname',
|
|
||||||
return_value='172.24.49.21')
|
|
||||||
self.mock_object(self.driver, '_ensure_shares_mounted')
|
|
||||||
self.mock_object(self.driver, '_execute')
|
|
||||||
|
|
||||||
out = self.driver.manage_existing(self.volume, existing_vol_ref)
|
|
||||||
|
|
||||||
loc = {'provider_location': '172.24.49.21:/fs-cinder'}
|
|
||||||
self.assertEqual(loc, out)
|
|
||||||
|
|
||||||
os.path.isfile.assert_called_once_with('/fs-cinder/cinder-volume/')
|
|
||||||
self.driver._get_mount_point_for_share.assert_called_once_with(
|
|
||||||
'172.24.49.21:/fs-cinder')
|
|
||||||
utils.resolve_hostname.assert_called_with('172.24.49.21')
|
|
||||||
self.driver._ensure_shares_mounted.assert_called_once_with()
|
|
||||||
|
|
||||||
def test_manage_existing_name_matches(self):
|
|
||||||
self.driver._mounted_shares = ['172.24.49.21:/fs-cinder']
|
|
||||||
existing_vol_ref = {'source-name': '172.24.49.21:/fs-cinder'}
|
|
||||||
|
|
||||||
self.mock_object(self.driver, '_get_share_mount_and_vol_from_vol_ref',
|
|
||||||
return_value=('172.24.49.21:/fs-cinder',
|
|
||||||
'/mnt/silver',
|
|
||||||
self.volume.name))
|
|
||||||
|
|
||||||
out = self.driver.manage_existing(self.volume, existing_vol_ref)
|
|
||||||
|
|
||||||
loc = {'provider_location': '172.24.49.21:/fs-cinder'}
|
|
||||||
self.assertEqual(loc, out)
|
|
||||||
|
|
||||||
def test_manage_existing_exception(self):
|
|
||||||
existing_vol_ref = {'source-name': '172.24.49.21:/fs-cinder'}
|
|
||||||
|
|
||||||
self.mock_object(self.driver, '_get_share_mount_and_vol_from_vol_ref',
|
|
||||||
return_value=('172.24.49.21:/fs-cinder',
|
|
||||||
'/mnt/silver',
|
|
||||||
'cinder-volume'))
|
|
||||||
self.mock_object(self.driver, '_execute',
|
|
||||||
side_effect=putils.ProcessExecutionError)
|
|
||||||
|
|
||||||
self.assertRaises(exception.VolumeBackendAPIException,
|
|
||||||
self.driver.manage_existing, self.volume,
|
|
||||||
existing_vol_ref)
|
|
||||||
|
|
||||||
def test_manage_existing_missing_source_name(self):
|
|
||||||
# empty source-name should raise an exception
|
|
||||||
existing_vol_ref = {}
|
|
||||||
|
|
||||||
self.assertRaises(exception.ManageExistingInvalidReference,
|
|
||||||
self.driver.manage_existing, self.volume,
|
|
||||||
existing_vol_ref)
|
|
||||||
|
|
||||||
def test_manage_existing_already_managed(self):
|
|
||||||
self.driver._mounted_shares = ['172.24.49.21:/fs-cinder']
|
|
||||||
existing_vol_ref = {'source-name': '172.24.49.21:/fs-cinder'}
|
|
||||||
expected_size = 1
|
|
||||||
|
|
||||||
self.mock_object(self.driver, '_ensure_shares_mounted')
|
|
||||||
self.mock_object(base_nfs.NfsDriver, '_get_mount_point_for_share',
|
|
||||||
return_value='/mnt/silver')
|
|
||||||
self.mock_object(os.path, 'isfile', return_value=True)
|
|
||||||
self.mock_object(utils, 'get_file_size', return_value=expected_size)
|
|
||||||
|
|
||||||
self.mock_object(vutils, 'check_already_managed_volume',
|
|
||||||
return_value=True)
|
|
||||||
|
|
||||||
self.assertRaises(exception.ManageExistingAlreadyManaged,
|
|
||||||
self.driver.manage_existing, self.volume,
|
|
||||||
existing_vol_ref)
|
|
||||||
|
|
||||||
def test_manage_existing_missing_volume_in_backend(self):
|
|
||||||
self.driver._mounted_shares = ['172.24.49.21:/fs-cinder']
|
|
||||||
existing_vol_ref = {'source-name': '172.24.49.21:/fs-cinder'}
|
|
||||||
|
|
||||||
self.mock_object(self.driver, '_ensure_shares_mounted')
|
|
||||||
self.mock_object(utils, 'resolve_hostname',
|
|
||||||
side_effect=['172.24.49.21', '172.24.49.22'])
|
|
||||||
|
|
||||||
self.assertRaises(exception.ManageExistingInvalidReference,
|
|
||||||
self.driver.manage_existing, self.volume,
|
|
||||||
existing_vol_ref)
|
|
||||||
|
|
||||||
def test_manage_existing_get_size(self):
|
|
||||||
existing_vol_ref = {
|
|
||||||
'source-name': '172.24.49.21:/fs-cinder/cinder-volume',
|
|
||||||
}
|
|
||||||
self.driver._mounted_shares = ['172.24.49.21:/fs-cinder']
|
|
||||||
expected_size = 1
|
|
||||||
|
|
||||||
self.mock_object(self.driver, '_ensure_shares_mounted')
|
|
||||||
self.mock_object(utils, 'resolve_hostname',
|
|
||||||
return_value='172.24.49.21')
|
|
||||||
self.mock_object(base_nfs.NfsDriver, '_get_mount_point_for_share',
|
|
||||||
return_value='/mnt/silver')
|
|
||||||
self.mock_object(os.path, 'isfile', return_value=True)
|
|
||||||
self.mock_object(utils, 'get_file_size', return_value=expected_size)
|
|
||||||
|
|
||||||
out = self.driver.manage_existing_get_size(self.volume,
|
|
||||||
existing_vol_ref)
|
|
||||||
|
|
||||||
self.assertEqual(1, out)
|
|
||||||
utils.get_file_size.assert_called_once_with(
|
|
||||||
'/mnt/silver/cinder-volume')
|
|
||||||
utils.resolve_hostname.assert_called_with('172.24.49.21')
|
|
||||||
|
|
||||||
def test_manage_existing_get_size_exception(self):
|
|
||||||
existing_vol_ref = {
|
|
||||||
'source-name': '172.24.49.21:/fs-cinder/cinder-volume',
|
|
||||||
}
|
|
||||||
self.driver._mounted_shares = ['172.24.49.21:/fs-cinder']
|
|
||||||
|
|
||||||
self.mock_object(self.driver, '_get_share_mount_and_vol_from_vol_ref',
|
|
||||||
return_value=('172.24.49.21:/fs-cinder',
|
|
||||||
'/mnt/silver',
|
|
||||||
'cinder-volume'))
|
|
||||||
|
|
||||||
self.assertRaises(exception.VolumeBackendAPIException,
|
|
||||||
self.driver.manage_existing_get_size, self.volume,
|
|
||||||
existing_vol_ref)
|
|
||||||
|
|
||||||
def test_manage_existing_get_size_resolving_hostname_exception(self):
|
|
||||||
existing_vol_ref = {
|
|
||||||
'source-name': '172.24.49.21:/fs-cinder/cinder-volume',
|
|
||||||
}
|
|
||||||
|
|
||||||
self.driver._mounted_shares = ['172.24.49.21:/fs-cinder']
|
|
||||||
|
|
||||||
self.mock_object(self.driver, '_ensure_shares_mounted')
|
|
||||||
self.mock_object(utils, 'resolve_hostname',
|
|
||||||
side_effect=socket.gaierror)
|
|
||||||
|
|
||||||
self.assertRaises(socket.gaierror,
|
|
||||||
self.driver.manage_existing_get_size, self.volume,
|
|
||||||
existing_vol_ref)
|
|
||||||
|
|
||||||
def test_unmanage(self):
|
|
||||||
path = '/opt/stack/cinder/mnt/826692dfaeaf039b1f4dcc1dacee2c2e'
|
|
||||||
vol_str = 'volume-' + self.volume.id
|
|
||||||
vol_path = os.path.join(path, vol_str)
|
|
||||||
new_path = os.path.join(path, 'unmanage-' + vol_str)
|
|
||||||
|
|
||||||
self.mock_object(self.driver, '_get_mount_point_for_share',
|
|
||||||
return_value=path)
|
|
||||||
self.mock_object(self.driver, '_execute')
|
|
||||||
|
|
||||||
self.driver.unmanage(self.volume)
|
|
||||||
|
|
||||||
self.driver._execute.assert_called_with('mv', vol_path, new_path,
|
|
||||||
run_as_root=False,
|
|
||||||
check_exit_code=True)
|
|
||||||
self.driver._get_mount_point_for_share.assert_called_with(
|
|
||||||
self.volume.provider_location)
|
|
||||||
|
|
||||||
def test_unmanage_volume_exception(self):
|
|
||||||
path = '/opt/stack/cinder/mnt/826692dfaeaf039b1f4dcc1dacee2c2e'
|
|
||||||
|
|
||||||
self.mock_object(self.driver, '_get_mount_point_for_share',
|
|
||||||
return_value=path)
|
|
||||||
self.mock_object(self.driver, '_execute', side_effect=ValueError)
|
|
||||||
|
|
||||||
self.driver.unmanage(self.volume)
|
|
||||||
|
|
||||||
def test_manage_existing_snapshot(self):
|
|
||||||
nfs_share = "172.24.49.21:/fs-cinder"
|
|
||||||
nfs_mount = "/opt/stack/data/cinder/mnt/" + fake.SNAPSHOT_ID
|
|
||||||
path = "unmanage-%s.%s" % (self.snapshot.volume.name, self.snapshot.id)
|
|
||||||
loc = {'provider_location': '172.24.49.21:/fs-cinder'}
|
|
||||||
existing_ref = {'source-name': '172.24.49.21:/fs-cinder/'
|
|
||||||
+ fake.SNAPSHOT_ID}
|
|
||||||
|
|
||||||
self.mock_object(self.driver, '_get_share_mount_and_vol_from_vol_ref',
|
|
||||||
return_value=(nfs_share, nfs_mount, path))
|
|
||||||
self.mock_object(backend.HNASSSHBackend, 'check_snapshot_parent',
|
|
||||||
return_value=True)
|
|
||||||
self.mock_object(self.driver, '_execute')
|
|
||||||
self.mock_object(backend.HNASSSHBackend, 'get_export_path',
|
|
||||||
return_value='fs-cinder')
|
|
||||||
|
|
||||||
out = self.driver.manage_existing_snapshot(self.snapshot,
|
|
||||||
existing_ref)
|
|
||||||
|
|
||||||
self.assertEqual(loc, out)
|
|
||||||
|
|
||||||
def test_manage_existing_snapshot_legacy(self):
|
|
||||||
nfs_share = "172.24.49.21:/fs-cinder"
|
|
||||||
nfs_mount = "/opt/stack/data/cinder/mnt/" + fake.SNAPSHOT_ID
|
|
||||||
path = "unmanage-snapshot-%s" % self.snapshot.id
|
|
||||||
loc = {'provider_location': '172.24.49.21:/fs-cinder'}
|
|
||||||
existing_ref = {
|
|
||||||
'source-name': '172.24.49.21:/fs-cinder/' + fake.SNAPSHOT_ID}
|
|
||||||
|
|
||||||
self.mock_object(self.driver, '_get_share_mount_and_vol_from_vol_ref',
|
|
||||||
return_value=(nfs_share, nfs_mount, path))
|
|
||||||
self.mock_object(backend.HNASSSHBackend, 'check_snapshot_parent',
|
|
||||||
return_value=True)
|
|
||||||
self.mock_object(self.driver, '_execute')
|
|
||||||
self.mock_object(backend.HNASSSHBackend, 'get_export_path',
|
|
||||||
return_value='fs-cinder')
|
|
||||||
|
|
||||||
out = self.driver.manage_existing_snapshot(self.snapshot, existing_ref)
|
|
||||||
|
|
||||||
self.assertEqual(loc, out)
|
|
||||||
|
|
||||||
def test_manage_existing_snapshot_not_parent_exception(self):
|
|
||||||
nfs_share = "172.24.49.21:/fs-cinder"
|
|
||||||
nfs_mount = "/opt/stack/data/cinder/mnt/" + fake.SNAPSHOT_ID
|
|
||||||
path = "unmanage-%s.%s" % (fake.VOLUME_ID, self.snapshot.id)
|
|
||||||
|
|
||||||
existing_ref = {'source-name': '172.24.49.21:/fs-cinder/'
|
|
||||||
+ fake.SNAPSHOT_ID}
|
|
||||||
|
|
||||||
self.mock_object(self.driver, '_get_share_mount_and_vol_from_vol_ref',
|
|
||||||
return_value=(nfs_share, nfs_mount, path))
|
|
||||||
self.mock_object(backend.HNASSSHBackend, 'check_snapshot_parent',
|
|
||||||
return_value=False)
|
|
||||||
self.mock_object(backend.HNASSSHBackend, 'get_export_path',
|
|
||||||
return_value='fs-cinder')
|
|
||||||
|
|
||||||
self.assertRaises(exception.ManageExistingInvalidReference,
|
|
||||||
self.driver.manage_existing_snapshot, self.snapshot,
|
|
||||||
existing_ref)
|
|
||||||
|
|
||||||
def test_manage_existing_snapshot_get_size(self):
|
|
||||||
existing_ref = {
|
|
||||||
'source-name': '172.24.49.21:/fs-cinder/cinder-snapshot',
|
|
||||||
}
|
|
||||||
self.driver._mounted_shares = ['172.24.49.21:/fs-cinder']
|
|
||||||
expected_size = 1
|
|
||||||
|
|
||||||
self.mock_object(self.driver, '_ensure_shares_mounted')
|
|
||||||
self.mock_object(utils, 'resolve_hostname',
|
|
||||||
return_value='172.24.49.21')
|
|
||||||
self.mock_object(base_nfs.NfsDriver, '_get_mount_point_for_share',
|
|
||||||
return_value='/mnt/silver')
|
|
||||||
self.mock_object(os.path, 'isfile', return_value=True)
|
|
||||||
self.mock_object(utils, 'get_file_size', return_value=expected_size)
|
|
||||||
|
|
||||||
out = self.driver.manage_existing_snapshot_get_size(
|
|
||||||
self.snapshot, existing_ref)
|
|
||||||
|
|
||||||
self.assertEqual(1, out)
|
|
||||||
utils.get_file_size.assert_called_once_with(
|
|
||||||
'/mnt/silver/cinder-snapshot')
|
|
||||||
utils.resolve_hostname.assert_called_with('172.24.49.21')
|
|
||||||
|
|
||||||
def test_unmanage_snapshot(self):
|
|
||||||
path = '/opt/stack/cinder/mnt/826692dfaeaf039b1f4dcc1dacee2c2e'
|
|
||||||
snapshot_name = "%s.%s" % (self.snapshot.volume.name, self.snapshot.id)
|
|
||||||
old_path = os.path.join(path, snapshot_name)
|
|
||||||
new_path = os.path.join(path, 'unmanage-' + snapshot_name)
|
|
||||||
|
|
||||||
self.mock_object(self.driver, '_get_mount_point_for_share',
|
|
||||||
return_value=path)
|
|
||||||
self.mock_object(self.driver, '_execute')
|
|
||||||
|
|
||||||
self.driver.unmanage_snapshot(self.snapshot)
|
|
||||||
|
|
||||||
self.driver._execute.assert_called_with('mv', old_path, new_path,
|
|
||||||
run_as_root=False,
|
|
||||||
check_exit_code=True)
|
|
||||||
self.driver._get_mount_point_for_share.assert_called_with(
|
|
||||||
self.snapshot.provider_location)
|
|
||||||
|
|
||||||
def test_get_manageable_volumes_not_safe(self):
|
|
||||||
manageable_vol = [{'cinder_id': '1e5177e7-95e5-4a0f-b170-e45f4b469f6a',
|
|
||||||
'extra_info': None,
|
|
||||||
'reason_not_safe': 'already managed',
|
|
||||||
'reference': {
|
|
||||||
'source-name':
|
|
||||||
'172.24.49.21:/fs-cinder/volume-1e5177e7-'
|
|
||||||
'95e5-4a0f-b170-e45f4b469f6a'},
|
|
||||||
'safe_to_manage': False,
|
|
||||||
'size': 128}]
|
|
||||||
|
|
||||||
rsrc = [self.volume]
|
|
||||||
path = '/opt/stack/cinder/mnt/826692dfaeaf039b1f4dcc1dacee2c2e'
|
|
||||||
self.mock_object(base_nfs.NfsDriver, '_get_mount_point_for_share',
|
|
||||||
return_value=path)
|
|
||||||
vols_exp = [self.volume.name]
|
|
||||||
self.mock_object(self.driver, '_get_volumes_from_export',
|
|
||||||
return_value=vols_exp)
|
|
||||||
self.mock_object(self.driver, '_get_file_size',
|
|
||||||
return_value=self.volume.size)
|
|
||||||
|
|
||||||
out = self.driver._get_manageable_resource_info(
|
|
||||||
rsrc, "volume", None, 1000, 0, ['reference'], ['desc'])
|
|
||||||
|
|
||||||
self.driver._get_volumes_from_export.assert_called_with(
|
|
||||||
'172.24.49.21:/fs-cinder')
|
|
||||||
self.driver._get_file_size.assert_called_with('%s/%s' % (
|
|
||||||
path, self.volume.name))
|
|
||||||
self.driver._get_mount_point_for_share(self.volume.provider_location)
|
|
||||||
|
|
||||||
self.assertEqual(out, manageable_vol)
|
|
||||||
|
|
||||||
def test_get_manageable_volumes(self):
|
|
||||||
manageable_vol = [{
|
|
||||||
'cinder_id': '1e5177e7-95e5-4a0f-b170-e45f4b469f6a',
|
|
||||||
'extra_info': None,
|
|
||||||
'reason_not_safe': 'already managed',
|
|
||||||
'reference': {
|
|
||||||
'source-name': '172.24.49.21:/fs-cinder/'
|
|
||||||
'volume-1e5177e7-95e5-4a0f-b170-e45f4b469f6a'},
|
|
||||||
'safe_to_manage': False,
|
|
||||||
'size': 128}]
|
|
||||||
|
|
||||||
rsrc = [self.volume]
|
|
||||||
path = '/opt/stack/cinder/mnt/826692dfaeaf039b1f4dcc1dacee2c2e'
|
|
||||||
self.mock_object(base_nfs.NfsDriver, '_get_mount_point_for_share',
|
|
||||||
return_value=path)
|
|
||||||
vols_exp = [fake.VOLUME_NAME]
|
|
||||||
self.mock_object(self.driver, '_get_volumes_from_export',
|
|
||||||
return_value=vols_exp)
|
|
||||||
self.mock_object(self.driver, '_get_file_size',
|
|
||||||
return_value=self.volume.size)
|
|
||||||
|
|
||||||
out = self.driver._get_manageable_resource_info(rsrc, "volume", None,
|
|
||||||
1000, 0, ['reference'],
|
|
||||||
['desc'])
|
|
||||||
|
|
||||||
self.driver._get_volumes_from_export.assert_called_with(
|
|
||||||
'172.24.49.21:/fs-cinder')
|
|
||||||
self.driver._get_file_size.assert_called_with(
|
|
||||||
'%s/%s' % (path, self.volume.name))
|
|
||||||
self.driver._get_mount_point_for_share(self.volume.provider_location)
|
|
||||||
|
|
||||||
self.assertEqual(out, manageable_vol)
|
|
||||||
|
|
||||||
def test_get_manageable_snapshots(self):
|
|
||||||
manageable_snap = [{
|
|
||||||
'cinder_id': '253b2878-ec60-4793-ad19-e65496ec7aab',
|
|
||||||
'extra_info': None,
|
|
||||||
'reason_not_safe': 'already managed',
|
|
||||||
'reference': {
|
|
||||||
'source-name': '172.24.49.21:/fs-cinder/'
|
|
||||||
'snapshot-253b2878-ec60-4793-'
|
|
||||||
'ad19-e65496ec7aab'},
|
|
||||||
'safe_to_manage': False,
|
|
||||||
'size': 128,
|
|
||||||
'source_reference': {'id': '1'}}]
|
|
||||||
|
|
||||||
rsrc = [self.snapshot]
|
|
||||||
path = '/opt/stack/cinder/mnt/826692dfaeaf039b1f4dcc1dacee2c2e'
|
|
||||||
self.mock_object(base_nfs.NfsDriver, '_get_mount_point_for_share',
|
|
||||||
return_value=path)
|
|
||||||
vols_exp = [fake.SNAPSHOT_NAME]
|
|
||||||
self.mock_object(self.driver, '_get_volumes_from_export',
|
|
||||||
return_value=vols_exp)
|
|
||||||
self.mock_object(self.driver, '_get_file_size',
|
|
||||||
return_value=self.volume.size)
|
|
||||||
self.mock_object(backend.HNASSSHBackend, 'get_cloned_file_relatives',
|
|
||||||
return_value=[' /nfs_cinder/volume-1',
|
|
||||||
'/nfs_cinder/snapshot2'])
|
|
||||||
|
|
||||||
out = self.driver._get_manageable_resource_info(rsrc, "snapshot", None,
|
|
||||||
1000, 0, ['reference'],
|
|
||||||
['desc'])
|
|
||||||
|
|
||||||
self.driver._get_volumes_from_export.assert_called_with(
|
|
||||||
'172.24.49.21:/fs-cinder')
|
|
||||||
self.driver._get_file_size.assert_called_with(
|
|
||||||
'%s/%s' % (path, self.snapshot.name))
|
|
||||||
self.driver._get_mount_point_for_share(self.snapshot.provider_location)
|
|
||||||
|
|
||||||
self.assertEqual(out, manageable_snap)
|
|
||||||
|
|
||||||
def test_get_manageable_snapshots_unknown_origin(self):
|
|
||||||
manageable_snap = [{
|
|
||||||
'cinder_id': '253b2878-ec60-4793-ad19-e65496ec7aab',
|
|
||||||
'extra_info': 'Could not determine the volume that owns '
|
|
||||||
'the snapshot',
|
|
||||||
'reason_not_safe': 'already managed',
|
|
||||||
'reference': {
|
|
||||||
'source-name': '172.24.49.21:/fs-cinder/'
|
|
||||||
'snapshot-253b2878-ec60-4793-'
|
|
||||||
'ad19-e65496ec7aab'},
|
|
||||||
'safe_to_manage': False,
|
|
||||||
'size': 128,
|
|
||||||
'source_reference': {'id': 'unknown'}}]
|
|
||||||
|
|
||||||
rsrc = [self.snapshot]
|
|
||||||
path = '/opt/stack/cinder/mnt/826692dfaeaf039b1f4dcc1dacee2c2e'
|
|
||||||
self.mock_object(base_nfs.NfsDriver, '_get_mount_point_for_share',
|
|
||||||
return_value=path)
|
|
||||||
vols_exp = [fake.SNAPSHOT_NAME]
|
|
||||||
self.mock_object(self.driver, '_get_volumes_from_export',
|
|
||||||
return_value=vols_exp)
|
|
||||||
self.mock_object(self.driver, '_get_file_size',
|
|
||||||
return_value=self.volume.size)
|
|
||||||
self.mock_object(backend.HNASSSHBackend, 'get_cloned_file_relatives',
|
|
||||||
return_value=[' /nfs_cinder/volume-1',
|
|
||||||
' /nfs_cinder/volume-2',
|
|
||||||
'/nfs_cinder/snapshot2'])
|
|
||||||
|
|
||||||
out = self.driver._get_manageable_resource_info(rsrc, "snapshot", None,
|
|
||||||
1000, 0, ['reference'],
|
|
||||||
['desc'])
|
|
||||||
|
|
||||||
self.driver._get_volumes_from_export.assert_called_with(
|
|
||||||
'172.24.49.21:/fs-cinder')
|
|
||||||
self.driver._get_mount_point_for_share(self.snapshot.provider_location)
|
|
||||||
self.driver._get_file_size.assert_called_with('%s/%s' % (
|
|
||||||
path, self.snapshot.name))
|
|
||||||
self.assertEqual(out, manageable_snap)
|
|
@ -1,305 +0,0 @@
|
|||||||
# Copyright (c) 2016 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.
|
|
||||||
#
|
|
||||||
|
|
||||||
import ddt
|
|
||||||
import os
|
|
||||||
|
|
||||||
from xml.etree import ElementTree as ETree
|
|
||||||
|
|
||||||
from cinder import context
|
|
||||||
from cinder import exception
|
|
||||||
from cinder import test
|
|
||||||
from cinder.tests.unit import fake_constants
|
|
||||||
from cinder.tests.unit import fake_volume
|
|
||||||
from cinder.volume import configuration as conf
|
|
||||||
from cinder.volume.drivers.hitachi import hnas_utils
|
|
||||||
from cinder.volume import volume_types
|
|
||||||
|
|
||||||
_VOLUME = {'name': 'cinder-volume',
|
|
||||||
'id': fake_constants.VOLUME_ID,
|
|
||||||
'size': 128,
|
|
||||||
'host': 'host1@hnas-nfs-backend#default',
|
|
||||||
'volume_type': 'default',
|
|
||||||
'provider_location': 'hnas'}
|
|
||||||
|
|
||||||
service_parameters = ['volume_type', 'hdp']
|
|
||||||
optional_parameters = ['ssc_cmd', 'cluster_admin_ip0']
|
|
||||||
|
|
||||||
config_from_cinder_conf = {
|
|
||||||
'username': 'supervisor',
|
|
||||||
'fs': {'easy-stack': 'easy-stack',
|
|
||||||
'silver': 'silver'},
|
|
||||||
'ssh_port': 22,
|
|
||||||
'cluster_admin_ip0': None,
|
|
||||||
'ssh_private_key': None,
|
|
||||||
'mgmt_ip0': '172.24.44.15',
|
|
||||||
'ssc_cmd': 'ssc',
|
|
||||||
'services': {
|
|
||||||
'default': {
|
|
||||||
'label': u'svc_0',
|
|
||||||
'pool_name': 'default',
|
|
||||||
'hdp': 'easy-stack'},
|
|
||||||
'FS-CinderDev1': {
|
|
||||||
'label': u'svc_1',
|
|
||||||
'pool_name': 'FS-CinderDev1',
|
|
||||||
'hdp': 'silver'}},
|
|
||||||
'password': 'supervisor'}
|
|
||||||
|
|
||||||
valid_XML_str = '''
|
|
||||||
<config>
|
|
||||||
<mgmt_ip0>172.24.44.15</mgmt_ip0>
|
|
||||||
<username>supervisor</username>
|
|
||||||
<password>supervisor</password>
|
|
||||||
<ssh_enabled>False</ssh_enabled>
|
|
||||||
<ssh_private_key>/home/ubuntu/.ssh/id_rsa</ssh_private_key>
|
|
||||||
<svc_0>
|
|
||||||
<volume_type>default</volume_type>
|
|
||||||
<hdp>easy-stack</hdp>
|
|
||||||
</svc_0>
|
|
||||||
<svc_1>
|
|
||||||
<volume_type>silver</volume_type>
|
|
||||||
<hdp>FS-CinderDev1</hdp>
|
|
||||||
</svc_1>
|
|
||||||
</config>
|
|
||||||
'''
|
|
||||||
|
|
||||||
XML_no_authentication = '''
|
|
||||||
<config>
|
|
||||||
<mgmt_ip0>172.24.44.15</mgmt_ip0>
|
|
||||||
<username>supervisor</username>
|
|
||||||
<ssh_enabled>False</ssh_enabled>
|
|
||||||
</config>
|
|
||||||
'''
|
|
||||||
|
|
||||||
XML_empty_authentication_param = '''
|
|
||||||
<config>
|
|
||||||
<mgmt_ip0>172.24.44.15</mgmt_ip0>
|
|
||||||
<username>supervisor</username>
|
|
||||||
<password></password>
|
|
||||||
<ssh_enabled>False</ssh_enabled>
|
|
||||||
<ssh_private_key></ssh_private_key>
|
|
||||||
<svc_0>
|
|
||||||
<volume_type>default</volume_type>
|
|
||||||
<hdp>easy-stack</hdp>
|
|
||||||
</svc_0>
|
|
||||||
</config>
|
|
||||||
'''
|
|
||||||
|
|
||||||
# missing mgmt_ip0
|
|
||||||
XML_without_mandatory_params = '''
|
|
||||||
<config>
|
|
||||||
<username>supervisor</username>
|
|
||||||
<password>supervisor</password>
|
|
||||||
<ssh_enabled>False</ssh_enabled>
|
|
||||||
<svc_0>
|
|
||||||
<volume_type>default</volume_type>
|
|
||||||
<hdp>easy-stack</hdp>
|
|
||||||
</svc_0>
|
|
||||||
</config>
|
|
||||||
'''
|
|
||||||
|
|
||||||
XML_no_services_configured = '''
|
|
||||||
<config>
|
|
||||||
<mgmt_ip0>172.24.44.15</mgmt_ip0>
|
|
||||||
<username>supervisor</username>
|
|
||||||
<password>supervisor</password>
|
|
||||||
<ssh_port>10</ssh_port>
|
|
||||||
<ssh_enabled>False</ssh_enabled>
|
|
||||||
<ssh_private_key>/home/ubuntu/.ssh/id_rsa</ssh_private_key>
|
|
||||||
</config>
|
|
||||||
'''
|
|
||||||
|
|
||||||
parsed_xml = {'username': 'supervisor', 'password': 'supervisor',
|
|
||||||
'ssc_cmd': 'ssc', 'ssh_port': 22,
|
|
||||||
'fs': {'easy-stack': 'easy-stack',
|
|
||||||
'FS-CinderDev1': 'FS-CinderDev1'},
|
|
||||||
'cluster_admin_ip0': None,
|
|
||||||
'ssh_private_key': '/home/ubuntu/.ssh/id_rsa',
|
|
||||||
'services': {
|
|
||||||
'default': {'hdp': 'easy-stack', 'pool_name': 'default',
|
|
||||||
'label': 'svc_0'},
|
|
||||||
'silver': {'hdp': 'FS-CinderDev1', 'pool_name': 'silver',
|
|
||||||
'label': 'svc_1'}},
|
|
||||||
'mgmt_ip0': '172.24.44.15'}
|
|
||||||
|
|
||||||
valid_XML_etree = ETree.XML(valid_XML_str)
|
|
||||||
invalid_XML_etree_no_authentication = ETree.XML(XML_no_authentication)
|
|
||||||
invalid_XML_etree_empty_parameter = ETree.XML(XML_empty_authentication_param)
|
|
||||||
invalid_XML_etree_no_mandatory_params = ETree.XML(XML_without_mandatory_params)
|
|
||||||
invalid_XML_etree_no_service = ETree.XML(XML_no_services_configured)
|
|
||||||
|
|
||||||
|
|
||||||
@ddt.ddt
|
|
||||||
class HNASUtilsTest(test.TestCase):
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super(HNASUtilsTest, self).__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
super(HNASUtilsTest, self).setUp()
|
|
||||||
|
|
||||||
self.fake_conf = conf.Configuration(hnas_utils.drivers_common_opts,
|
|
||||||
conf.SHARED_CONF_GROUP)
|
|
||||||
|
|
||||||
self.override_config('hnas_username', 'supervisor',
|
|
||||||
conf.SHARED_CONF_GROUP)
|
|
||||||
self.override_config('hnas_password', 'supervisor',
|
|
||||||
conf.SHARED_CONF_GROUP)
|
|
||||||
self.override_config('hnas_mgmt_ip0', '172.24.44.15',
|
|
||||||
conf.SHARED_CONF_GROUP)
|
|
||||||
self.override_config('hnas_svc0_pool_name', 'default',
|
|
||||||
conf.SHARED_CONF_GROUP)
|
|
||||||
self.override_config('hnas_svc0_hdp', 'easy-stack',
|
|
||||||
conf.SHARED_CONF_GROUP)
|
|
||||||
self.override_config('hnas_svc1_pool_name', 'FS-CinderDev1',
|
|
||||||
conf.SHARED_CONF_GROUP)
|
|
||||||
self.override_config('hnas_svc1_hdp', 'silver',
|
|
||||||
conf.SHARED_CONF_GROUP)
|
|
||||||
|
|
||||||
self.context = context.get_admin_context()
|
|
||||||
self.volume = fake_volume.fake_volume_obj(self.context, **_VOLUME)
|
|
||||||
self.volume_type = (fake_volume.fake_volume_type_obj(None, **{
|
|
||||||
'id': fake_constants.VOLUME_TYPE_ID, 'name': 'silver'}))
|
|
||||||
|
|
||||||
def test_read_xml_config(self):
|
|
||||||
self.mock_object(os, 'access', return_value=True)
|
|
||||||
self.mock_object(ETree, 'parse', return_value=ETree.ElementTree)
|
|
||||||
self.mock_object(ETree.ElementTree, 'getroot',
|
|
||||||
return_value=valid_XML_etree)
|
|
||||||
|
|
||||||
xml_path = 'xml_file_found'
|
|
||||||
out = hnas_utils.read_xml_config(xml_path,
|
|
||||||
service_parameters,
|
|
||||||
optional_parameters)
|
|
||||||
|
|
||||||
self.assertEqual(parsed_xml, out)
|
|
||||||
|
|
||||||
def test_read_xml_config_parser_error(self):
|
|
||||||
xml_file = 'hnas_nfs.xml'
|
|
||||||
self.mock_object(os, 'access', return_value=True)
|
|
||||||
self.mock_object(ETree, 'parse', side_effect=ETree.ParseError)
|
|
||||||
|
|
||||||
self.assertRaises(exception.ConfigNotFound, hnas_utils.read_xml_config,
|
|
||||||
xml_file, service_parameters, optional_parameters)
|
|
||||||
|
|
||||||
def test_read_xml_config_not_found(self):
|
|
||||||
self.mock_object(os, 'access', return_value=False)
|
|
||||||
|
|
||||||
xml_path = 'xml_file_not_found'
|
|
||||||
self.assertRaises(exception.NotFound, hnas_utils.read_xml_config,
|
|
||||||
xml_path, service_parameters, optional_parameters)
|
|
||||||
|
|
||||||
def test_read_xml_config_without_services_configured(self):
|
|
||||||
xml_file = 'hnas_nfs.xml'
|
|
||||||
|
|
||||||
self.mock_object(os, 'access', return_value=True)
|
|
||||||
self.mock_object(ETree, 'parse', return_value=ETree.ElementTree)
|
|
||||||
self.mock_object(ETree.ElementTree, 'getroot',
|
|
||||||
return_value=invalid_XML_etree_no_service)
|
|
||||||
|
|
||||||
self.assertRaises(exception.ParameterNotFound,
|
|
||||||
hnas_utils.read_xml_config, xml_file,
|
|
||||||
service_parameters, optional_parameters)
|
|
||||||
|
|
||||||
def test_read_xml_config_empty_authentication_parameter(self):
|
|
||||||
xml_file = 'hnas_nfs.xml'
|
|
||||||
|
|
||||||
self.mock_object(os, 'access', return_value=True)
|
|
||||||
self.mock_object(ETree, 'parse', return_value=ETree.ElementTree)
|
|
||||||
self.mock_object(ETree.ElementTree, 'getroot',
|
|
||||||
return_value=invalid_XML_etree_empty_parameter)
|
|
||||||
|
|
||||||
self.assertRaises(exception.ParameterNotFound,
|
|
||||||
hnas_utils.read_xml_config, xml_file,
|
|
||||||
service_parameters, optional_parameters)
|
|
||||||
|
|
||||||
def test_read_xml_config_mandatory_parameters_missing(self):
|
|
||||||
xml_file = 'hnas_nfs.xml'
|
|
||||||
|
|
||||||
self.mock_object(os, 'access', return_value=True)
|
|
||||||
self.mock_object(ETree, 'parse', return_value=ETree.ElementTree)
|
|
||||||
self.mock_object(ETree.ElementTree, 'getroot',
|
|
||||||
return_value=invalid_XML_etree_no_mandatory_params)
|
|
||||||
|
|
||||||
self.assertRaises(exception.ParameterNotFound,
|
|
||||||
hnas_utils.read_xml_config, xml_file,
|
|
||||||
service_parameters, optional_parameters)
|
|
||||||
|
|
||||||
def test_read_config_xml_without_authentication_parameter(self):
|
|
||||||
xml_file = 'hnas_nfs.xml'
|
|
||||||
|
|
||||||
self.mock_object(os, 'access', return_value=True)
|
|
||||||
self.mock_object(ETree, 'parse', return_value=ETree.ElementTree)
|
|
||||||
self.mock_object(ETree.ElementTree, 'getroot',
|
|
||||||
return_value=invalid_XML_etree_no_authentication)
|
|
||||||
|
|
||||||
self.assertRaises(exception.ConfigNotFound, hnas_utils.read_xml_config,
|
|
||||||
xml_file, service_parameters, optional_parameters)
|
|
||||||
|
|
||||||
def test_get_pool_with_vol_type(self):
|
|
||||||
self.mock_object(volume_types, 'get_volume_type_extra_specs',
|
|
||||||
return_value={'service_label': 'silver'})
|
|
||||||
|
|
||||||
self.volume.volume_type_id = fake_constants.VOLUME_TYPE_ID
|
|
||||||
self.volume.volume_type = self.volume_type
|
|
||||||
|
|
||||||
out = hnas_utils.get_pool(parsed_xml, self.volume)
|
|
||||||
|
|
||||||
self.assertEqual('silver', out)
|
|
||||||
|
|
||||||
def test_get_pool_with_vol_type_id_none(self):
|
|
||||||
self.volume.volume_type_id = None
|
|
||||||
self.volume.volume_type = self.volume_type
|
|
||||||
|
|
||||||
out = hnas_utils.get_pool(parsed_xml, self.volume)
|
|
||||||
|
|
||||||
self.assertEqual('default', out)
|
|
||||||
|
|
||||||
def test_get_pool_with_missing_service_label(self):
|
|
||||||
self.mock_object(volume_types, 'get_volume_type_extra_specs',
|
|
||||||
return_value={'service_label': 'gold'})
|
|
||||||
|
|
||||||
self.volume.volume_type_id = fake_constants.VOLUME_TYPE_ID
|
|
||||||
self.volume.volume_type = self.volume_type
|
|
||||||
|
|
||||||
out = hnas_utils.get_pool(parsed_xml, self.volume)
|
|
||||||
|
|
||||||
self.assertEqual('default', out)
|
|
||||||
|
|
||||||
def test_get_pool_without_vol_type(self):
|
|
||||||
out = hnas_utils.get_pool(parsed_xml, self.volume)
|
|
||||||
self.assertEqual('default', out)
|
|
||||||
|
|
||||||
def test_read_cinder_conf_nfs(self):
|
|
||||||
out = hnas_utils.read_cinder_conf(self.fake_conf)
|
|
||||||
|
|
||||||
self.assertEqual(config_from_cinder_conf, out)
|
|
||||||
|
|
||||||
def test_read_cinder_conf_break(self):
|
|
||||||
self.override_config('hnas_username', None, conf.SHARED_CONF_GROUP)
|
|
||||||
self.override_config('hnas_password', None, conf.SHARED_CONF_GROUP)
|
|
||||||
self.override_config('hnas_mgmt_ip0', None, conf.SHARED_CONF_GROUP)
|
|
||||||
out = hnas_utils.read_cinder_conf(self.fake_conf)
|
|
||||||
self.assertIsNone(out)
|
|
||||||
|
|
||||||
@ddt.data('hnas_username', 'hnas_password',
|
|
||||||
'hnas_mgmt_ip0', 'hnas_svc0_pool_name',
|
|
||||||
'hnas_svc0_hdp', )
|
|
||||||
def test_init_invalid_conf_parameters(self, attr_name):
|
|
||||||
self.override_config(attr_name, None, conf.SHARED_CONF_GROUP)
|
|
||||||
|
|
||||||
self.assertRaises(exception.InvalidParameterValue,
|
|
||||||
hnas_utils.read_cinder_conf, self.fake_conf)
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,283 +0,0 @@
|
|||||||
# Copyright (C) 2014, Hitachi, Ltd.
|
|
||||||
#
|
|
||||||
# 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 inspect
|
|
||||||
import os
|
|
||||||
import shlex
|
|
||||||
|
|
||||||
from oslo_concurrency import lockutils
|
|
||||||
from oslo_concurrency import processutils as putils
|
|
||||||
from oslo_log import log as logging
|
|
||||||
from oslo_utils import excutils
|
|
||||||
import six
|
|
||||||
|
|
||||||
from cinder import exception
|
|
||||||
from cinder.i18n import _
|
|
||||||
from cinder import utils
|
|
||||||
|
|
||||||
SMPL = 1
|
|
||||||
COPY = 2
|
|
||||||
PAIR = 3
|
|
||||||
PSUS = 4
|
|
||||||
PSUE = 5
|
|
||||||
UNKN = 0xff
|
|
||||||
|
|
||||||
FULL = 'Full copy'
|
|
||||||
THIN = 'Thin copy'
|
|
||||||
|
|
||||||
DEFAULT_TRY_RANGE = range(3)
|
|
||||||
MAX_PROCESS_WAITTIME = 86400
|
|
||||||
DEFAULT_PROCESS_WAITTIME = 900
|
|
||||||
|
|
||||||
GETSTORAGEARRAY_ONCE = 100
|
|
||||||
|
|
||||||
WARNING_ID = 300
|
|
||||||
|
|
||||||
DEFAULT_GROUP_RANGE = [0, 65535]
|
|
||||||
|
|
||||||
NAME_PREFIX = 'HBSD-'
|
|
||||||
|
|
||||||
NORMAL_VOLUME_TYPE = 'Normal'
|
|
||||||
|
|
||||||
LOCK_DIR = '/var/lock/hbsd/'
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
HBSD_INFO_MSG = {
|
|
||||||
1: _('The parameter of the storage backend. '
|
|
||||||
'(config_group: %(config_group)s)'),
|
|
||||||
3: _('The storage backend can be used. (config_group: %(config_group)s)'),
|
|
||||||
4: _('The volume %(volume_id)s is managed successfully. (LDEV: %(ldev)s)'),
|
|
||||||
5: _('The volume %(volume_id)s is unmanaged successfully. '
|
|
||||||
'(LDEV: %(ldev)s)'),
|
|
||||||
}
|
|
||||||
|
|
||||||
HBSD_WARN_MSG = {
|
|
||||||
301: _('A LUN (HLUN) was not found. (LDEV: %(ldev)s)'),
|
|
||||||
302: _('Failed to specify a logical device for the volume '
|
|
||||||
'%(volume_id)s to be unmapped.'),
|
|
||||||
303: _('An iSCSI CHAP user could not be deleted. (username: %(user)s)'),
|
|
||||||
304: _('Failed to specify a logical device to be deleted. '
|
|
||||||
'(method: %(method)s, id: %(id)s)'),
|
|
||||||
305: _('The logical device for specified %(type)s %(id)s '
|
|
||||||
'was already deleted.'),
|
|
||||||
306: _('A host group could not be deleted. (port: %(port)s, '
|
|
||||||
'gid: %(gid)s, name: %(name)s)'),
|
|
||||||
307: _('An iSCSI target could not be deleted. (port: %(port)s, '
|
|
||||||
'tno: %(tno)s, alias: %(alias)s)'),
|
|
||||||
308: _('A host group could not be added. (port: %(port)s, '
|
|
||||||
'name: %(name)s)'),
|
|
||||||
309: _('An iSCSI target could not be added. '
|
|
||||||
'(port: %(port)s, alias: %(alias)s, reason: %(reason)s)'),
|
|
||||||
310: _('Failed to unmap a logical device. (LDEV: %(ldev)s, '
|
|
||||||
'reason: %(reason)s)'),
|
|
||||||
311: _('A free LUN (HLUN) was not found. Add a different host'
|
|
||||||
' group. (LDEV: %(ldev)s)'),
|
|
||||||
312: _('Failed to get a storage resource. The system will attempt '
|
|
||||||
'to get the storage resource again. (resource: %(resource)s)'),
|
|
||||||
313: _('Failed to delete a logical device. (LDEV: %(ldev)s, '
|
|
||||||
'reason: %(reason)s)'),
|
|
||||||
314: _('Failed to map a logical device. (LDEV: %(ldev)s, LUN: %(lun)s, '
|
|
||||||
'port: %(port)s, id: %(id)s)'),
|
|
||||||
315: _('Failed to perform a zero-page reclamation. '
|
|
||||||
'(LDEV: %(ldev)s, reason: %(reason)s)'),
|
|
||||||
316: _('Failed to assign the iSCSI initiator IQN. (port: %(port)s, '
|
|
||||||
'reason: %(reason)s)'),
|
|
||||||
}
|
|
||||||
|
|
||||||
HBSD_ERR_MSG = {
|
|
||||||
600: _('The command %(cmd)s failed. (ret: %(ret)s, stdout: %(out)s, '
|
|
||||||
'stderr: %(err)s)'),
|
|
||||||
601: _('A parameter is invalid. (%(param)s)'),
|
|
||||||
602: _('A parameter value is invalid. (%(meta)s)'),
|
|
||||||
603: _('Failed to acquire a resource lock. (serial: %(serial)s, '
|
|
||||||
'inst: %(inst)s, ret: %(ret)s, stderr: %(err)s)'),
|
|
||||||
604: _('Cannot set both hitachi_serial_number and hitachi_unit_name.'),
|
|
||||||
605: _('Either hitachi_serial_number or hitachi_unit_name is required.'),
|
|
||||||
615: _('A pair could not be created. The maximum number of pair is '
|
|
||||||
'exceeded. (copy method: %(copy_method)s, P-VOL: %(pvol)s)'),
|
|
||||||
616: _('A pair cannot be deleted. (P-VOL: %(pvol)s, S-VOL: %(svol)s)'),
|
|
||||||
617: _('The specified operation is not supported. The volume size '
|
|
||||||
'must be the same as the source %(type)s. (volume: %(volume_id)s)'),
|
|
||||||
618: _('The volume %(volume_id)s could not be extended. '
|
|
||||||
'The volume type must be Normal.'),
|
|
||||||
619: _('The volume %(volume_id)s to be mapped was not found.'),
|
|
||||||
624: _('The %(type)s %(id)s source to be replicated was not found.'),
|
|
||||||
631: _('Failed to create a file. (file: %(file)s, ret: %(ret)s, '
|
|
||||||
'stderr: %(err)s)'),
|
|
||||||
632: _('Failed to open a file. (file: %(file)s, ret: %(ret)s, '
|
|
||||||
'stderr: %(err)s)'),
|
|
||||||
633: _('%(file)s: Permission denied.'),
|
|
||||||
636: _('Failed to add the logical device.'),
|
|
||||||
637: _('The method %(method)s is timed out. (timeout value: %(timeout)s)'),
|
|
||||||
640: _('A pool could not be found. (pool id: %(pool_id)s)'),
|
|
||||||
641: _('The host group or iSCSI target could not be added.'),
|
|
||||||
642: _('An iSCSI CHAP user could not be added. (username: %(user)s)'),
|
|
||||||
643: _('The iSCSI CHAP user %(user)s does not exist.'),
|
|
||||||
648: _('There are no resources available for use. '
|
|
||||||
'(resource: %(resource)s)'),
|
|
||||||
649: _('The host group or iSCSI target was not found.'),
|
|
||||||
650: _('The resource %(resource)s was not found.'),
|
|
||||||
651: _('The IP Address was not found.'),
|
|
||||||
653: _('The creation of a logical device could not be '
|
|
||||||
'completed. (LDEV: %(ldev)s)'),
|
|
||||||
654: _('A volume status is invalid. (status: %(status)s)'),
|
|
||||||
655: _('A snapshot status is invalid. (status: %(status)s)'),
|
|
||||||
659: _('A host group is invalid. (host group: %(gid)s)'),
|
|
||||||
660: _('The specified %(desc)s is busy.'),
|
|
||||||
700: _('There is no designation of the %(param)s. '
|
|
||||||
'The specified storage is essential to manage the volume.'),
|
|
||||||
701: _('There is no designation of the ldev. '
|
|
||||||
'The specified ldev is essential to manage the volume.'),
|
|
||||||
702: _('The specified ldev %(ldev)s could not be managed. '
|
|
||||||
'The volume type must be DP-VOL.'),
|
|
||||||
703: _('The specified ldev %(ldev)s could not be managed. '
|
|
||||||
'The ldev size must be in multiples of gigabyte.'),
|
|
||||||
704: _('The specified ldev %(ldev)s could not be managed. '
|
|
||||||
'The ldev must not be mapping.'),
|
|
||||||
705: _('The specified ldev %(ldev)s could not be managed. '
|
|
||||||
'The ldev must not be paired.'),
|
|
||||||
706: _('The volume %(volume_id)s could not be unmanaged. '
|
|
||||||
'The volume type must be %(volume_type)s.'),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def set_msg(msg_id, **kwargs):
|
|
||||||
if msg_id < WARNING_ID:
|
|
||||||
msg_header = 'MSGID%04d-I:' % msg_id
|
|
||||||
msg_body = HBSD_INFO_MSG.get(msg_id)
|
|
||||||
else:
|
|
||||||
msg_header = 'MSGID%04d-W:' % msg_id
|
|
||||||
msg_body = HBSD_WARN_MSG.get(msg_id)
|
|
||||||
|
|
||||||
return '%(header)s %(body)s' % {'header': msg_header,
|
|
||||||
'body': msg_body % kwargs}
|
|
||||||
|
|
||||||
|
|
||||||
def output_err(msg_id, **kwargs):
|
|
||||||
msg = HBSD_ERR_MSG.get(msg_id) % kwargs
|
|
||||||
|
|
||||||
LOG.error("MSGID%(id)04d-E: %(msg)s", {'id': msg_id, 'msg': msg})
|
|
||||||
|
|
||||||
return msg
|
|
||||||
|
|
||||||
|
|
||||||
def get_process_lock(file):
|
|
||||||
if not os.access(file, os.W_OK):
|
|
||||||
msg = output_err(633, file=file)
|
|
||||||
raise exception.HBSDError(message=msg)
|
|
||||||
return lockutils.InterProcessLock(file)
|
|
||||||
|
|
||||||
|
|
||||||
def create_empty_file(filename):
|
|
||||||
if not os.path.exists(filename):
|
|
||||||
try:
|
|
||||||
utils.execute('touch', filename)
|
|
||||||
except putils.ProcessExecutionError as ex:
|
|
||||||
msg = output_err(
|
|
||||||
631, file=filename, ret=ex.exit_code, err=ex.stderr)
|
|
||||||
raise exception.HBSDError(message=msg)
|
|
||||||
|
|
||||||
|
|
||||||
class FileLock(lockutils.InterProcessLock):
|
|
||||||
|
|
||||||
def __init__(self, name, lock_object):
|
|
||||||
self.lock_object = lock_object
|
|
||||||
|
|
||||||
super(FileLock, self).__init__(name)
|
|
||||||
|
|
||||||
def __enter__(self):
|
|
||||||
self.lock_object.acquire()
|
|
||||||
|
|
||||||
try:
|
|
||||||
ret = super(FileLock, self).__enter__()
|
|
||||||
except Exception:
|
|
||||||
with excutils.save_and_reraise_exception():
|
|
||||||
self.lock_object.release()
|
|
||||||
|
|
||||||
return ret
|
|
||||||
|
|
||||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
||||||
try:
|
|
||||||
super(FileLock, self).__exit__(exc_type, exc_val, exc_tb)
|
|
||||||
finally:
|
|
||||||
self.lock_object.release()
|
|
||||||
|
|
||||||
|
|
||||||
class NopLock(object):
|
|
||||||
|
|
||||||
def __enter__(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class HBSDBasicLib(object):
|
|
||||||
|
|
||||||
def __init__(self, conf=None):
|
|
||||||
self.conf = conf
|
|
||||||
|
|
||||||
def exec_command(self, cmd, args=None, printflag=True):
|
|
||||||
if printflag:
|
|
||||||
if args:
|
|
||||||
LOG.debug('cmd: %(cmd)s, args: %(args)s',
|
|
||||||
{'cmd': cmd, 'args': args})
|
|
||||||
else:
|
|
||||||
LOG.debug('cmd: %s', cmd)
|
|
||||||
|
|
||||||
cmd = [cmd]
|
|
||||||
|
|
||||||
if args:
|
|
||||||
if six.PY2 and isinstance(args, six.text_type):
|
|
||||||
cmd += shlex.split(args.encode())
|
|
||||||
else:
|
|
||||||
cmd += shlex.split(args)
|
|
||||||
|
|
||||||
try:
|
|
||||||
stdout, stderr = utils.execute(*cmd, run_as_root=True)
|
|
||||||
ret = 0
|
|
||||||
except putils.ProcessExecutionError as e:
|
|
||||||
ret = e.exit_code
|
|
||||||
stdout = e.stdout
|
|
||||||
stderr = e.stderr
|
|
||||||
|
|
||||||
LOG.debug('cmd: %s', cmd)
|
|
||||||
LOG.debug('from: %s', inspect.stack()[2])
|
|
||||||
LOG.debug('ret: %d', ret)
|
|
||||||
LOG.debug('stdout: %s', stdout.replace(os.linesep, ' '))
|
|
||||||
LOG.debug('stderr: %s', stderr.replace(os.linesep, ' '))
|
|
||||||
|
|
||||||
return ret, stdout, stderr
|
|
||||||
|
|
||||||
def set_pair_flock(self):
|
|
||||||
return NopLock()
|
|
||||||
|
|
||||||
def set_horcmgr_flock(self):
|
|
||||||
return NopLock()
|
|
||||||
|
|
||||||
def discard_zero_page(self, ldev):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def output_param_to_log(self, conf):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def connect_storage(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def get_max_hostgroups(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def restart_pair_horcm(self):
|
|
||||||
pass
|
|
@ -1,835 +0,0 @@
|
|||||||
# Copyright (C) 2014, Hitachi, Ltd.
|
|
||||||
#
|
|
||||||
# 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.
|
|
||||||
"""
|
|
||||||
Common class for Hitachi storage drivers.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
import re
|
|
||||||
import threading
|
|
||||||
|
|
||||||
from oslo_config import cfg
|
|
||||||
from oslo_log import log as logging
|
|
||||||
from oslo_utils import excutils
|
|
||||||
import six
|
|
||||||
|
|
||||||
from cinder import exception
|
|
||||||
from cinder import utils
|
|
||||||
from cinder.volume import configuration
|
|
||||||
from cinder.volume.drivers.hitachi import hbsd_basiclib as basic_lib
|
|
||||||
from cinder.volume.drivers.hitachi import hbsd_horcm as horcm
|
|
||||||
from cinder.volume.drivers.hitachi import hbsd_snm2 as snm2
|
|
||||||
from cinder.volume import utils as volume_utils
|
|
||||||
|
|
||||||
"""
|
|
||||||
Version history:
|
|
||||||
1.0.0 - Initial driver
|
|
||||||
1.1.0 - Add manage_existing/manage_existing_get_size/unmanage methods
|
|
||||||
"""
|
|
||||||
VERSION = '1.1.0'
|
|
||||||
|
|
||||||
PARAM_RANGE = {
|
|
||||||
'hitachi_copy_check_interval': {'min': 1, 'max': 600},
|
|
||||||
'hitachi_async_copy_check_interval': {'min': 1, 'max': 600},
|
|
||||||
'hitachi_copy_speed': {'min': 1, 'max': 15},
|
|
||||||
}
|
|
||||||
|
|
||||||
DEFAULT_LDEV_RANGE = [0, 65535]
|
|
||||||
|
|
||||||
COPY_METHOD = ('FULL', 'THIN')
|
|
||||||
VALID_DP_VOLUME_STATUS = ['available', 'in-use']
|
|
||||||
VALID_V_VOLUME_STATUS = ['available']
|
|
||||||
SYSTEM_LOCK_FILE = basic_lib.LOCK_DIR + 'system'
|
|
||||||
SERVICE_LOCK_PATH_BASE = basic_lib.LOCK_DIR + 'service_'
|
|
||||||
STORAGE_LOCK_PATH_BASE = basic_lib.LOCK_DIR + 'storage_'
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
volume_opts = [
|
|
||||||
cfg.StrOpt('hitachi_serial_number',
|
|
||||||
help='Serial number of storage system'),
|
|
||||||
cfg.StrOpt('hitachi_unit_name',
|
|
||||||
help='Name of an array unit'),
|
|
||||||
cfg.IntOpt('hitachi_pool_id',
|
|
||||||
help='Pool ID of storage system'),
|
|
||||||
cfg.IntOpt('hitachi_thin_pool_id',
|
|
||||||
help='Thin pool ID of storage system'),
|
|
||||||
cfg.StrOpt('hitachi_ldev_range',
|
|
||||||
help='Range of logical device of storage system'),
|
|
||||||
cfg.StrOpt('hitachi_default_copy_method',
|
|
||||||
default='FULL',
|
|
||||||
help='Default copy method of storage system'),
|
|
||||||
cfg.IntOpt('hitachi_copy_speed',
|
|
||||||
default=3,
|
|
||||||
help='Copy speed of storage system'),
|
|
||||||
cfg.IntOpt('hitachi_copy_check_interval',
|
|
||||||
default=3,
|
|
||||||
help='Interval to check copy'),
|
|
||||||
cfg.IntOpt('hitachi_async_copy_check_interval',
|
|
||||||
default=10,
|
|
||||||
help='Interval to check copy asynchronously'),
|
|
||||||
cfg.StrOpt('hitachi_target_ports',
|
|
||||||
help='Control port names for HostGroup or iSCSI Target'),
|
|
||||||
cfg.StrOpt('hitachi_group_range',
|
|
||||||
help='Range of group number'),
|
|
||||||
cfg.BoolOpt('hitachi_group_request',
|
|
||||||
default=False,
|
|
||||||
secret=True,
|
|
||||||
help='Request for creating HostGroup or iSCSI Target'),
|
|
||||||
]
|
|
||||||
|
|
||||||
CONF = cfg.CONF
|
|
||||||
CONF.register_opts(volume_opts, group=configuration.SHARED_CONF_GROUP)
|
|
||||||
|
|
||||||
|
|
||||||
class TryLock(object):
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.lock = threading.RLock()
|
|
||||||
self.desc = None
|
|
||||||
|
|
||||||
def set_desc(self, description):
|
|
||||||
self.desc = description
|
|
||||||
|
|
||||||
def __enter__(self):
|
|
||||||
if not self.lock.acquire(False):
|
|
||||||
msg = basic_lib.output_err(660, desc=self.desc)
|
|
||||||
raise exception.HBSDError(message=msg)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
||||||
self.lock.release()
|
|
||||||
|
|
||||||
|
|
||||||
class HBSDCommon(object):
|
|
||||||
|
|
||||||
def __init__(self, conf, parent, context, db):
|
|
||||||
self.configuration = conf
|
|
||||||
self.generated_from = parent
|
|
||||||
self.context = context
|
|
||||||
self.db = db
|
|
||||||
|
|
||||||
self.system_lock_file = SYSTEM_LOCK_FILE
|
|
||||||
self.service_lock_file = '%s%s' % (SERVICE_LOCK_PATH_BASE,
|
|
||||||
conf.config_group)
|
|
||||||
if conf.hitachi_serial_number:
|
|
||||||
self.storage_lock_file = '%s%s' % (STORAGE_LOCK_PATH_BASE,
|
|
||||||
six.text_type(
|
|
||||||
conf.hitachi_serial_number))
|
|
||||||
elif conf.hitachi_unit_name:
|
|
||||||
self.storage_lock_file = '%s%s' % (STORAGE_LOCK_PATH_BASE,
|
|
||||||
six.text_type(
|
|
||||||
conf.hitachi_unit_name))
|
|
||||||
|
|
||||||
self.storage_obj_lock = threading.Lock()
|
|
||||||
self.volinfo_lock = threading.Lock()
|
|
||||||
self.volume_info = {}
|
|
||||||
self.output_first = True
|
|
||||||
|
|
||||||
def get_volume(self, volume_id):
|
|
||||||
return self.db.volume_get(self.context, volume_id)
|
|
||||||
|
|
||||||
def get_volume_metadata(self, volume_id):
|
|
||||||
return self.db.volume_metadata_get(self.context, volume_id)
|
|
||||||
|
|
||||||
def get_snapshot_metadata(self, snapshot_id):
|
|
||||||
return self.db.snapshot_metadata_get(self.context, snapshot_id)
|
|
||||||
|
|
||||||
def _update_volume_metadata(self, volume_id, volume_metadata):
|
|
||||||
self.db.volume_metadata_update(self.context, volume_id,
|
|
||||||
volume_metadata, False)
|
|
||||||
|
|
||||||
def get_ldev(self, obj):
|
|
||||||
if not obj:
|
|
||||||
return None
|
|
||||||
|
|
||||||
ldev = obj.get('provider_location')
|
|
||||||
if not ldev or not ldev.isdigit():
|
|
||||||
return None
|
|
||||||
else:
|
|
||||||
return int(ldev)
|
|
||||||
|
|
||||||
def get_value(self, obj, name, key):
|
|
||||||
if not obj:
|
|
||||||
return None
|
|
||||||
|
|
||||||
if obj.get(name):
|
|
||||||
if isinstance(obj[name], dict):
|
|
||||||
return obj[name].get(key)
|
|
||||||
else:
|
|
||||||
for i in obj[name]:
|
|
||||||
if i['key'] == key:
|
|
||||||
return i['value']
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_is_vvol(self, obj, name):
|
|
||||||
return self.get_value(obj, name, 'type') == 'V-VOL'
|
|
||||||
|
|
||||||
def get_volume_is_vvol(self, volume):
|
|
||||||
return self.get_is_vvol(volume, 'volume_metadata')
|
|
||||||
|
|
||||||
def get_snapshot_is_vvol(self, snapshot):
|
|
||||||
return self.get_is_vvol(snapshot, 'metadata')
|
|
||||||
|
|
||||||
def get_copy_method(self, volume):
|
|
||||||
method = self.get_value(volume, 'volume_metadata', 'copy_method')
|
|
||||||
if method:
|
|
||||||
if method not in COPY_METHOD:
|
|
||||||
msg = basic_lib.output_err(602, meta='copy_method')
|
|
||||||
raise exception.HBSDError(message=msg)
|
|
||||||
elif (method == 'THIN'
|
|
||||||
and self.configuration.hitachi_thin_pool_id is None):
|
|
||||||
msg = basic_lib.output_err(601, param='hitachi_thin_pool_id')
|
|
||||||
raise exception.HBSDError(message=msg)
|
|
||||||
else:
|
|
||||||
method = self.configuration.hitachi_default_copy_method
|
|
||||||
return method
|
|
||||||
|
|
||||||
def _string2int(self, num):
|
|
||||||
if not num:
|
|
||||||
return None
|
|
||||||
if num.isdigit():
|
|
||||||
return int(num, 10)
|
|
||||||
if not re.match(r'\w\w:\w\w:\w\w', num):
|
|
||||||
return None
|
|
||||||
|
|
||||||
try:
|
|
||||||
num = int(num.replace(':', ''), 16)
|
|
||||||
except ValueError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
return num
|
|
||||||
|
|
||||||
def _range2list(self, conf, param):
|
|
||||||
str = getattr(conf, param)
|
|
||||||
lists = str.split('-')
|
|
||||||
if len(lists) != 2:
|
|
||||||
msg = basic_lib.output_err(601, param=param)
|
|
||||||
raise exception.HBSDError(message=msg)
|
|
||||||
|
|
||||||
first_type = None
|
|
||||||
for i in range(len(lists)):
|
|
||||||
if lists[i].isdigit():
|
|
||||||
lists[i] = int(lists[i], 10)
|
|
||||||
if first_type == 'hex':
|
|
||||||
msg = basic_lib.output_err(601, param=param)
|
|
||||||
raise exception.HBSDError(message=msg)
|
|
||||||
first_type = 'dig'
|
|
||||||
else:
|
|
||||||
if (first_type == 'dig'
|
|
||||||
or not re.match(r'\w\w:\w\w:\w\w', lists[i])):
|
|
||||||
msg = basic_lib.output_err(601, param=param)
|
|
||||||
raise exception.HBSDError(message=msg)
|
|
||||||
try:
|
|
||||||
lists[i] = int(lists[i].replace(':', ''), 16)
|
|
||||||
first_type = 'hex'
|
|
||||||
except Exception:
|
|
||||||
msg = basic_lib.output_err(601, param=param)
|
|
||||||
raise exception.HBSDError(message=msg)
|
|
||||||
if lists[0] > lists[1]:
|
|
||||||
msg = basic_lib.output_err(601, param=param)
|
|
||||||
raise exception.HBSDError(message=msg)
|
|
||||||
return lists
|
|
||||||
|
|
||||||
def output_param_to_log(self, storage_protocol):
|
|
||||||
essential_inherited_param = ['volume_backend_name', 'volume_driver']
|
|
||||||
conf = self.configuration
|
|
||||||
|
|
||||||
LOG.info(basic_lib.set_msg(1, config_group=conf.config_group))
|
|
||||||
version = self.command.get_comm_version()
|
|
||||||
if conf.hitachi_unit_name:
|
|
||||||
prefix = 'HSNM2 version'
|
|
||||||
else:
|
|
||||||
prefix = 'RAID Manager version'
|
|
||||||
LOG.info('\t%(prefix)-35s : %(version)s',
|
|
||||||
{'prefix': prefix, 'version': version})
|
|
||||||
for param in essential_inherited_param:
|
|
||||||
value = conf.safe_get(param)
|
|
||||||
LOG.info('\t%(param)-35s : %(value)s',
|
|
||||||
{'param': param, 'value': value})
|
|
||||||
for opt in volume_opts:
|
|
||||||
if not opt.secret:
|
|
||||||
value = getattr(conf, opt.name)
|
|
||||||
LOG.info('\t%(name)-35s : %(value)s',
|
|
||||||
{'name': opt.name, 'value': value})
|
|
||||||
|
|
||||||
if storage_protocol == 'iSCSI':
|
|
||||||
value = getattr(conf, 'hitachi_group_request')
|
|
||||||
LOG.info('\t%(request)-35s : %(value)s',
|
|
||||||
{'request': 'hitachi_group_request', 'value': value})
|
|
||||||
|
|
||||||
def check_param(self):
|
|
||||||
conf = self.configuration
|
|
||||||
|
|
||||||
if conf.hitachi_unit_name and conf.hitachi_serial_number:
|
|
||||||
msg = basic_lib.output_err(604)
|
|
||||||
raise exception.HBSDError(message=msg)
|
|
||||||
|
|
||||||
if not conf.hitachi_unit_name and not conf.hitachi_serial_number:
|
|
||||||
msg = basic_lib.output_err(605)
|
|
||||||
raise exception.HBSDError(message=msg)
|
|
||||||
|
|
||||||
if conf.hitachi_pool_id is None:
|
|
||||||
msg = basic_lib.output_err(601, param='hitachi_pool_id')
|
|
||||||
raise exception.HBSDError(message=msg)
|
|
||||||
|
|
||||||
for param in PARAM_RANGE.keys():
|
|
||||||
_value = getattr(conf, param)
|
|
||||||
if (_value and
|
|
||||||
(not PARAM_RANGE[param]['min'] <= _value <=
|
|
||||||
PARAM_RANGE[param]['max'])):
|
|
||||||
msg = basic_lib.output_err(601, param=param)
|
|
||||||
raise exception.HBSDError(message=msg)
|
|
||||||
|
|
||||||
if conf.hitachi_default_copy_method not in COPY_METHOD:
|
|
||||||
msg = basic_lib.output_err(601,
|
|
||||||
param='hitachi_default_copy_method')
|
|
||||||
raise exception.HBSDError(message=msg)
|
|
||||||
|
|
||||||
if (conf.hitachi_default_copy_method == 'THIN'
|
|
||||||
and conf.hitachi_thin_pool_id is None):
|
|
||||||
msg = basic_lib.output_err(601, param='hitachi_thin_pool_id')
|
|
||||||
raise exception.HBSDError(message=msg)
|
|
||||||
|
|
||||||
for param in ('hitachi_ldev_range', 'hitachi_group_range'):
|
|
||||||
if not getattr(conf, param):
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
_value = self._range2list(conf, param)
|
|
||||||
setattr(conf, param, _value)
|
|
||||||
|
|
||||||
if conf.hitachi_target_ports:
|
|
||||||
conf.hitachi_target_ports = conf.hitachi_target_ports.split(',')
|
|
||||||
|
|
||||||
for opt in volume_opts:
|
|
||||||
getattr(conf, opt.name)
|
|
||||||
|
|
||||||
if conf.hitachi_unit_name:
|
|
||||||
self.command = snm2.HBSDSNM2(conf)
|
|
||||||
else:
|
|
||||||
conf.append_config_values(horcm.volume_opts)
|
|
||||||
self.command = horcm.HBSDHORCM(conf)
|
|
||||||
self.command.check_param()
|
|
||||||
self.pair_flock = self.command.set_pair_flock()
|
|
||||||
self.horcmgr_flock = self.command.set_horcmgr_flock()
|
|
||||||
|
|
||||||
def create_lock_file(self):
|
|
||||||
basic_lib.create_empty_file(self.system_lock_file)
|
|
||||||
basic_lib.create_empty_file(self.service_lock_file)
|
|
||||||
basic_lib.create_empty_file(self.storage_lock_file)
|
|
||||||
self.command.create_lock_file()
|
|
||||||
|
|
||||||
def _add_ldev(self, volume_num, capacity, pool_id, is_vvol):
|
|
||||||
self.command.comm_add_ldev(pool_id, volume_num, capacity, is_vvol)
|
|
||||||
|
|
||||||
def _get_unused_volume_num(self, ldev_range):
|
|
||||||
return self.command.get_unused_ldev(ldev_range)
|
|
||||||
|
|
||||||
def add_volinfo(self, ldev, id=None, type='volume'):
|
|
||||||
with self.volinfo_lock:
|
|
||||||
if ldev not in self.volume_info:
|
|
||||||
self.init_volinfo(self.volume_info, ldev)
|
|
||||||
if id:
|
|
||||||
desc = '%s %s' % (type, id)
|
|
||||||
self.volume_info[ldev]['in_use'].set_desc(desc)
|
|
||||||
|
|
||||||
def delete_pair(self, ldev, all_split=True, is_vvol=None):
|
|
||||||
paired_info = self.command.get_paired_info(ldev)
|
|
||||||
LOG.debug('paired_info: %s', paired_info)
|
|
||||||
pvol = paired_info['pvol']
|
|
||||||
svols = paired_info['svol']
|
|
||||||
driver = self.generated_from
|
|
||||||
restart = False
|
|
||||||
svol_list = []
|
|
||||||
try:
|
|
||||||
if pvol is None:
|
|
||||||
return
|
|
||||||
elif pvol == ldev:
|
|
||||||
for svol in svols[:]:
|
|
||||||
if svol['is_vvol'] or svol['status'] != basic_lib.PSUS:
|
|
||||||
continue
|
|
||||||
|
|
||||||
self.command.delete_pair(pvol, svol['lun'], False)
|
|
||||||
restart = True
|
|
||||||
driver.pair_terminate_connection(svol['lun'])
|
|
||||||
svols.remove(svol)
|
|
||||||
|
|
||||||
if all_split and svols:
|
|
||||||
svol_list.append(six.text_type(svols[0]['lun']))
|
|
||||||
for svol in svols[1:]:
|
|
||||||
svol_list.append(', %d' % svol['lun'])
|
|
||||||
|
|
||||||
msg = basic_lib.output_err(616, pvol=pvol,
|
|
||||||
svol=''.join(svol_list))
|
|
||||||
raise exception.HBSDBusy(message=msg)
|
|
||||||
|
|
||||||
if not svols:
|
|
||||||
driver.pair_terminate_connection(pvol)
|
|
||||||
|
|
||||||
else:
|
|
||||||
self.add_volinfo(pvol)
|
|
||||||
if not self.volume_info[pvol]['in_use'].lock.acquire(False):
|
|
||||||
desc = self.volume_info[pvol]['in_use'].desc
|
|
||||||
msg = basic_lib.output_err(660, desc=desc)
|
|
||||||
raise exception.HBSDBusy(message=msg)
|
|
||||||
try:
|
|
||||||
paired_info = self.command.get_paired_info(ldev)
|
|
||||||
if paired_info['pvol'] is None:
|
|
||||||
return
|
|
||||||
svol = paired_info['svol'][0]
|
|
||||||
if svol['status'] != basic_lib.PSUS:
|
|
||||||
msg = basic_lib.output_err(616, pvol=pvol, svol=ldev)
|
|
||||||
raise exception.HBSDBusy(message=msg)
|
|
||||||
|
|
||||||
self.command.delete_pair(pvol, ldev, svol['is_vvol'])
|
|
||||||
if not svol['is_vvol']:
|
|
||||||
restart = True
|
|
||||||
driver.pair_terminate_connection(ldev)
|
|
||||||
paired_info = self.command.get_paired_info(pvol)
|
|
||||||
if paired_info['pvol'] is None:
|
|
||||||
driver.pair_terminate_connection(pvol)
|
|
||||||
finally:
|
|
||||||
self.volume_info[pvol]['in_use'].lock.release()
|
|
||||||
except Exception:
|
|
||||||
with excutils.save_and_reraise_exception():
|
|
||||||
if restart:
|
|
||||||
try:
|
|
||||||
self.command.restart_pair_horcm()
|
|
||||||
except Exception as e:
|
|
||||||
LOG.warning('Failed to restart horcm: %s', e)
|
|
||||||
else:
|
|
||||||
if (all_split or is_vvol) and restart:
|
|
||||||
try:
|
|
||||||
self.command.restart_pair_horcm()
|
|
||||||
except Exception as e:
|
|
||||||
LOG.warning('Failed to restart horcm: %s', e)
|
|
||||||
|
|
||||||
def copy_async_data(self, pvol, svol, is_vvol):
|
|
||||||
path_list = []
|
|
||||||
driver = self.generated_from
|
|
||||||
try:
|
|
||||||
with self.pair_flock:
|
|
||||||
self.delete_pair(pvol, all_split=False, is_vvol=is_vvol)
|
|
||||||
paired_info = self.command.get_paired_info(pvol)
|
|
||||||
if paired_info['pvol'] is None:
|
|
||||||
driver.pair_initialize_connection(pvol)
|
|
||||||
path_list.append(pvol)
|
|
||||||
driver.pair_initialize_connection(svol)
|
|
||||||
path_list.append(svol)
|
|
||||||
self.command.comm_create_pair(pvol, svol, is_vvol)
|
|
||||||
except Exception:
|
|
||||||
with excutils.save_and_reraise_exception():
|
|
||||||
for ldev in path_list:
|
|
||||||
try:
|
|
||||||
driver.pair_terminate_connection(ldev)
|
|
||||||
except Exception as ex:
|
|
||||||
LOG.warning(basic_lib.set_msg(310, ldev=ldev,
|
|
||||||
reason=ex))
|
|
||||||
|
|
||||||
def copy_sync_data(self, src_ldev, dest_ldev, size):
|
|
||||||
src_vol = {'provider_location': six.text_type(src_ldev),
|
|
||||||
'id': 'src_vol'}
|
|
||||||
dest_vol = {'provider_location': six.text_type(dest_ldev),
|
|
||||||
'id': 'dest_vol'}
|
|
||||||
properties = utils.brick_get_connector_properties()
|
|
||||||
driver = self.generated_from
|
|
||||||
src_info = None
|
|
||||||
dest_info = None
|
|
||||||
try:
|
|
||||||
dest_info = driver._attach_volume(self.context, dest_vol,
|
|
||||||
properties)
|
|
||||||
src_info = driver._attach_volume(self.context, src_vol,
|
|
||||||
properties)
|
|
||||||
volume_utils.copy_volume(src_info['device']['path'],
|
|
||||||
dest_info['device']['path'], size * 1024,
|
|
||||||
self.configuration.volume_dd_blocksize)
|
|
||||||
finally:
|
|
||||||
if dest_info:
|
|
||||||
driver._detach_volume(self.context, dest_info,
|
|
||||||
dest_vol, properties)
|
|
||||||
if src_info:
|
|
||||||
driver._detach_volume(self.context, src_info,
|
|
||||||
src_vol, properties)
|
|
||||||
self.command.discard_zero_page(dest_ldev)
|
|
||||||
|
|
||||||
def copy_data(self, pvol, size, p_is_vvol, method):
|
|
||||||
type = 'Normal'
|
|
||||||
is_vvol = method == 'THIN'
|
|
||||||
svol = self._create_volume(size, is_vvol=is_vvol)
|
|
||||||
try:
|
|
||||||
if p_is_vvol:
|
|
||||||
self.copy_sync_data(pvol, svol, size)
|
|
||||||
else:
|
|
||||||
if is_vvol:
|
|
||||||
type = 'V-VOL'
|
|
||||||
self.copy_async_data(pvol, svol, is_vvol)
|
|
||||||
except Exception:
|
|
||||||
with excutils.save_and_reraise_exception():
|
|
||||||
try:
|
|
||||||
self.delete_ldev(svol, is_vvol)
|
|
||||||
except Exception as ex:
|
|
||||||
LOG.warning(basic_lib.set_msg(313, ldev=svol,
|
|
||||||
reason=ex))
|
|
||||||
|
|
||||||
return six.text_type(svol), type
|
|
||||||
|
|
||||||
def add_lun(self, command, hostgroups, ldev, is_once=False):
|
|
||||||
lock = basic_lib.get_process_lock(self.storage_lock_file)
|
|
||||||
with lock:
|
|
||||||
self.command.comm_add_lun(command, hostgroups, ldev, is_once)
|
|
||||||
|
|
||||||
def create_ldev(self, size, ldev_range, pool_id, is_vvol):
|
|
||||||
LOG.debug('create start (normal)')
|
|
||||||
for i in basic_lib.DEFAULT_TRY_RANGE:
|
|
||||||
LOG.debug('Try number: %(tries)s / %(max_tries)s',
|
|
||||||
{'tries': i + 1,
|
|
||||||
'max_tries': len(basic_lib.DEFAULT_TRY_RANGE)})
|
|
||||||
new_ldev = self._get_unused_volume_num(ldev_range)
|
|
||||||
try:
|
|
||||||
self._add_ldev(new_ldev, size, pool_id, is_vvol)
|
|
||||||
except exception.HBSDNotFound:
|
|
||||||
LOG.warning(basic_lib.set_msg(312, resource='LDEV'))
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
msg = basic_lib.output_err(636)
|
|
||||||
raise exception.HBSDError(message=msg)
|
|
||||||
LOG.debug('create end (normal: %s)', new_ldev)
|
|
||||||
self.init_volinfo(self.volume_info, new_ldev)
|
|
||||||
return new_ldev
|
|
||||||
|
|
||||||
def _create_volume(self, size, is_vvol=False):
|
|
||||||
ldev_range = self.configuration.hitachi_ldev_range
|
|
||||||
if not ldev_range:
|
|
||||||
ldev_range = DEFAULT_LDEV_RANGE
|
|
||||||
pool_id = self.configuration.hitachi_pool_id
|
|
||||||
|
|
||||||
lock = basic_lib.get_process_lock(self.storage_lock_file)
|
|
||||||
with self.storage_obj_lock, lock:
|
|
||||||
ldev = self.create_ldev(size, ldev_range, pool_id, is_vvol)
|
|
||||||
return ldev
|
|
||||||
|
|
||||||
def create_volume(self, volume):
|
|
||||||
volume_metadata = self.get_volume_metadata(volume['id'])
|
|
||||||
volume_metadata['type'] = 'Normal'
|
|
||||||
|
|
||||||
size = volume['size']
|
|
||||||
ldev = self._create_volume(size)
|
|
||||||
volume_metadata['ldev'] = six.text_type(ldev)
|
|
||||||
|
|
||||||
return {'provider_location': six.text_type(ldev),
|
|
||||||
'metadata': volume_metadata}
|
|
||||||
|
|
||||||
def delete_ldev(self, ldev, is_vvol):
|
|
||||||
LOG.debug('Call delete_ldev (LDEV: %(ldev)d is_vvol: %(vvol)s)',
|
|
||||||
{'ldev': ldev, 'vvol': is_vvol})
|
|
||||||
with self.pair_flock:
|
|
||||||
self.delete_pair(ldev)
|
|
||||||
self.command.comm_delete_ldev(ldev, is_vvol)
|
|
||||||
with self.volinfo_lock:
|
|
||||||
if ldev in self.volume_info:
|
|
||||||
self.volume_info.pop(ldev)
|
|
||||||
LOG.debug('delete_ldev is finished '
|
|
||||||
'(LDEV: %(ldev)d, is_vvol: %(vvol)s)',
|
|
||||||
{'ldev': ldev, 'vvol': is_vvol})
|
|
||||||
|
|
||||||
def delete_volume(self, volume):
|
|
||||||
ldev = self.get_ldev(volume)
|
|
||||||
if ldev is None:
|
|
||||||
LOG.warning(basic_lib.set_msg(304, method='delete_volume',
|
|
||||||
id=volume['id']))
|
|
||||||
return
|
|
||||||
self.add_volinfo(ldev, volume['id'])
|
|
||||||
if not self.volume_info[ldev]['in_use'].lock.acquire(False):
|
|
||||||
desc = self.volume_info[ldev]['in_use'].desc
|
|
||||||
basic_lib.output_err(660, desc=desc)
|
|
||||||
raise exception.VolumeIsBusy(volume_name=volume['name'])
|
|
||||||
try:
|
|
||||||
is_vvol = self.get_volume_is_vvol(volume)
|
|
||||||
try:
|
|
||||||
self.delete_ldev(ldev, is_vvol)
|
|
||||||
except exception.HBSDNotFound:
|
|
||||||
with self.volinfo_lock:
|
|
||||||
if ldev in self.volume_info:
|
|
||||||
self.volume_info.pop(ldev)
|
|
||||||
LOG.warning(basic_lib.set_msg(
|
|
||||||
305, type='volume', id=volume['id']))
|
|
||||||
except exception.HBSDBusy:
|
|
||||||
raise exception.VolumeIsBusy(volume_name=volume['name'])
|
|
||||||
finally:
|
|
||||||
if ldev in self.volume_info:
|
|
||||||
self.volume_info[ldev]['in_use'].lock.release()
|
|
||||||
|
|
||||||
def check_volume_status(self, volume, is_vvol):
|
|
||||||
if not is_vvol:
|
|
||||||
status = VALID_DP_VOLUME_STATUS
|
|
||||||
else:
|
|
||||||
status = VALID_V_VOLUME_STATUS
|
|
||||||
if volume['status'] not in status:
|
|
||||||
msg = basic_lib.output_err(654, status=volume['status'])
|
|
||||||
raise exception.HBSDError(message=msg)
|
|
||||||
|
|
||||||
def create_snapshot(self, snapshot):
|
|
||||||
src_ref = self.get_volume(snapshot['volume_id'])
|
|
||||||
pvol = self.get_ldev(src_ref)
|
|
||||||
if pvol is None:
|
|
||||||
msg = basic_lib.output_err(624, type='volume', id=src_ref['id'])
|
|
||||||
raise exception.HBSDError(message=msg)
|
|
||||||
|
|
||||||
self.add_volinfo(pvol, src_ref['id'])
|
|
||||||
with self.volume_info[pvol]['in_use']:
|
|
||||||
is_vvol = self.get_volume_is_vvol(src_ref)
|
|
||||||
self.check_volume_status(src_ref, is_vvol)
|
|
||||||
size = snapshot['volume_size']
|
|
||||||
snap_metadata = snapshot.get('metadata')
|
|
||||||
method = None if is_vvol else self.get_copy_method(src_ref)
|
|
||||||
|
|
||||||
svol, type = self.copy_data(pvol, size, is_vvol, method)
|
|
||||||
|
|
||||||
if type == 'V-VOL':
|
|
||||||
snap_metadata['type'] = type
|
|
||||||
snap_metadata['ldev'] = svol
|
|
||||||
|
|
||||||
return {'provider_location': svol,
|
|
||||||
'metadata': snap_metadata}
|
|
||||||
|
|
||||||
def delete_snapshot(self, snapshot):
|
|
||||||
ldev = self.get_ldev(snapshot)
|
|
||||||
if ldev is None:
|
|
||||||
LOG.warning(basic_lib.set_msg(
|
|
||||||
304, method='delete_snapshot', id=snapshot['id']))
|
|
||||||
return
|
|
||||||
self.add_volinfo(ldev, id=snapshot['id'], type='snapshot')
|
|
||||||
if not self.volume_info[ldev]['in_use'].lock.acquire(False):
|
|
||||||
desc = self.volume_info[ldev]['in_use'].desc
|
|
||||||
basic_lib.output_err(660, desc=desc)
|
|
||||||
raise exception.SnapshotIsBusy(snapshot_name=snapshot['name'])
|
|
||||||
try:
|
|
||||||
is_vvol = self.get_snapshot_is_vvol(snapshot)
|
|
||||||
try:
|
|
||||||
self.delete_ldev(ldev, is_vvol)
|
|
||||||
except exception.HBSDNotFound:
|
|
||||||
with self.volinfo_lock:
|
|
||||||
if ldev in self.volume_info:
|
|
||||||
self.volume_info.pop(ldev)
|
|
||||||
LOG.warning(basic_lib.set_msg(
|
|
||||||
305, type='snapshot', id=snapshot['id']))
|
|
||||||
except exception.HBSDBusy:
|
|
||||||
raise exception.SnapshotIsBusy(snapshot_name=snapshot['name'])
|
|
||||||
finally:
|
|
||||||
if ldev in self.volume_info:
|
|
||||||
self.volume_info[ldev]['in_use'].lock.release()
|
|
||||||
|
|
||||||
def create_cloned_volume(self, volume, src_vref):
|
|
||||||
pvol = self.get_ldev(src_vref)
|
|
||||||
if pvol is None:
|
|
||||||
msg = basic_lib.output_err(624, type='volume', id=src_vref['id'])
|
|
||||||
raise exception.HBSDError(message=msg)
|
|
||||||
|
|
||||||
self.add_volinfo(pvol, src_vref['id'])
|
|
||||||
with self.volume_info[pvol]['in_use']:
|
|
||||||
is_vvol = self.get_volume_is_vvol(src_vref)
|
|
||||||
self.check_volume_status(self.get_volume(src_vref['id']), is_vvol)
|
|
||||||
size = volume['size']
|
|
||||||
src_size = src_vref['size']
|
|
||||||
if size < src_size:
|
|
||||||
msg = basic_lib.output_err(617, type='volume',
|
|
||||||
volume_id=volume['id'])
|
|
||||||
raise exception.HBSDError(message=msg)
|
|
||||||
|
|
||||||
metadata = self.get_volume_metadata(volume['id'])
|
|
||||||
method = None if is_vvol else self.get_copy_method(volume)
|
|
||||||
|
|
||||||
svol, type = self.copy_data(pvol, src_size, is_vvol, method)
|
|
||||||
|
|
||||||
if size > src_size:
|
|
||||||
self.extend_volume(volume, size)
|
|
||||||
|
|
||||||
metadata['type'] = type
|
|
||||||
metadata['volume'] = src_vref['id']
|
|
||||||
metadata['ldev'] = svol
|
|
||||||
|
|
||||||
return {'provider_location': svol, 'metadata': metadata}
|
|
||||||
|
|
||||||
def create_volume_from_snapshot(self, volume, snapshot):
|
|
||||||
pvol = self.get_ldev(snapshot)
|
|
||||||
if pvol is None:
|
|
||||||
msg = basic_lib.output_err(624, type='snapshot', id=snapshot['id'])
|
|
||||||
raise exception.HBSDError(message=msg)
|
|
||||||
|
|
||||||
self.add_volinfo(pvol, id=snapshot['id'], type='snapshot')
|
|
||||||
with self.volume_info[pvol]['in_use']:
|
|
||||||
is_vvol = self.get_snapshot_is_vvol(snapshot)
|
|
||||||
if snapshot['status'] != 'available':
|
|
||||||
msg = basic_lib.output_err(655, status=snapshot['status'])
|
|
||||||
raise exception.HBSDError(message=msg)
|
|
||||||
|
|
||||||
size = volume['size']
|
|
||||||
src_size = snapshot['volume_size']
|
|
||||||
if size != src_size:
|
|
||||||
msg = basic_lib.output_err(617, type='snapshot',
|
|
||||||
volume_id=volume['id'])
|
|
||||||
raise exception.HBSDError(message=msg)
|
|
||||||
|
|
||||||
metadata = self.get_volume_metadata(volume['id'])
|
|
||||||
method = None if is_vvol else self.get_copy_method(volume)
|
|
||||||
svol, type = self.copy_data(pvol, size, is_vvol, method)
|
|
||||||
|
|
||||||
metadata['type'] = type
|
|
||||||
metadata['snapshot'] = snapshot['id']
|
|
||||||
metadata['ldev'] = svol
|
|
||||||
|
|
||||||
return {'provider_location': svol, 'metadata': metadata}
|
|
||||||
|
|
||||||
def _extend_volume(self, ldev, old_size, new_size):
|
|
||||||
with self.pair_flock:
|
|
||||||
self.delete_pair(ldev)
|
|
||||||
self.command.comm_extend_ldev(ldev, old_size, new_size)
|
|
||||||
|
|
||||||
def extend_volume(self, volume, new_size):
|
|
||||||
pvol = self.get_ldev(volume)
|
|
||||||
self.add_volinfo(pvol, volume['id'])
|
|
||||||
with self.volume_info[pvol]['in_use']:
|
|
||||||
if self.get_volume_is_vvol(volume):
|
|
||||||
msg = basic_lib.output_err(618, volume_id=volume['id'])
|
|
||||||
raise exception.HBSDError(message=msg)
|
|
||||||
self._extend_volume(pvol, volume['size'], new_size)
|
|
||||||
|
|
||||||
def output_backend_available_once(self):
|
|
||||||
if self.output_first:
|
|
||||||
self.output_first = False
|
|
||||||
LOG.warning(basic_lib.set_msg(
|
|
||||||
3, config_group=self.configuration.config_group))
|
|
||||||
|
|
||||||
def update_volume_stats(self, storage_protocol):
|
|
||||||
data = {}
|
|
||||||
total_gb = None
|
|
||||||
free_gb = None
|
|
||||||
data['volume_backend_name'] = self.configuration.safe_get(
|
|
||||||
'volume_backend_name') or 'HBSD%s' % storage_protocol
|
|
||||||
data['vendor_name'] = 'Hitachi'
|
|
||||||
data['driver_version'] = VERSION
|
|
||||||
data['storage_protocol'] = storage_protocol
|
|
||||||
|
|
||||||
try:
|
|
||||||
total_gb, free_gb = self.command.comm_get_dp_pool(
|
|
||||||
self.configuration.hitachi_pool_id)
|
|
||||||
except Exception as ex:
|
|
||||||
LOG.error('Failed to update volume status: %s', ex)
|
|
||||||
return None
|
|
||||||
|
|
||||||
data['total_capacity_gb'] = total_gb
|
|
||||||
data['free_capacity_gb'] = free_gb
|
|
||||||
data['reserved_percentage'] = self.configuration.safe_get(
|
|
||||||
'reserved_percentage')
|
|
||||||
data['QoS_support'] = False
|
|
||||||
|
|
||||||
LOG.debug('Updating volume status (%s)', data)
|
|
||||||
|
|
||||||
return data
|
|
||||||
|
|
||||||
def init_volinfo(self, vol_info, ldev):
|
|
||||||
vol_info[ldev] = {'in_use': TryLock(), 'lock': threading.Lock()}
|
|
||||||
|
|
||||||
def manage_existing(self, volume, existing_ref):
|
|
||||||
"""Manage an existing Hitachi storage volume.
|
|
||||||
|
|
||||||
existing_ref is a dictionary of the form:
|
|
||||||
|
|
||||||
For HUS 100 Family:
|
|
||||||
|
|
||||||
.. code-block:: default
|
|
||||||
|
|
||||||
{
|
|
||||||
'ldev': <logical device number on storage>,
|
|
||||||
'unit_name': <storage device name>
|
|
||||||
}
|
|
||||||
|
|
||||||
For VSP G1000/VSP/HUS VM:
|
|
||||||
|
|
||||||
.. code-block:: default
|
|
||||||
|
|
||||||
{
|
|
||||||
'ldev': <logical device number on storage>,
|
|
||||||
'serial_number': <product number of storage system>
|
|
||||||
}
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
ldev = self._string2int(existing_ref.get('ldev'))
|
|
||||||
|
|
||||||
LOG.info(basic_lib.set_msg(4, volume_id=volume['id'], ldev=ldev))
|
|
||||||
|
|
||||||
return {'provider_location': ldev}
|
|
||||||
|
|
||||||
def _manage_existing_get_size(self, volume, existing_ref):
|
|
||||||
"""Return size of volume for manage_existing."""
|
|
||||||
|
|
||||||
ldev = self._string2int(existing_ref.get('ldev'))
|
|
||||||
if ldev is None:
|
|
||||||
msg = basic_lib.output_err(701)
|
|
||||||
raise exception.HBSDError(data=msg)
|
|
||||||
|
|
||||||
size = self.command.get_ldev_size_in_gigabyte(ldev, existing_ref)
|
|
||||||
|
|
||||||
metadata = {'type': basic_lib.NORMAL_VOLUME_TYPE, 'ldev': ldev}
|
|
||||||
self._update_volume_metadata(volume['id'], metadata)
|
|
||||||
|
|
||||||
return size
|
|
||||||
|
|
||||||
def manage_existing_get_size(self, volume, existing_ref):
|
|
||||||
try:
|
|
||||||
return self._manage_existing_get_size(volume, existing_ref)
|
|
||||||
except exception.HBSDError as ex:
|
|
||||||
raise exception.ManageExistingInvalidReference(
|
|
||||||
existing_ref=existing_ref,
|
|
||||||
reason=six.text_type(ex))
|
|
||||||
|
|
||||||
def _unmanage(self, volume, ldev):
|
|
||||||
with self.horcmgr_flock:
|
|
||||||
self.delete_pair(ldev)
|
|
||||||
|
|
||||||
with self.volinfo_lock:
|
|
||||||
if ldev in self.volume_info:
|
|
||||||
self.volume_info.pop(ldev)
|
|
||||||
|
|
||||||
def unmanage(self, volume):
|
|
||||||
"""Remove the specified volume from Cinder management."""
|
|
||||||
|
|
||||||
ldev = self.get_ldev(volume)
|
|
||||||
|
|
||||||
if ldev is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
self.add_volinfo(ldev, volume['id'])
|
|
||||||
if not self.volume_info[ldev]['in_use'].lock.acquire(False):
|
|
||||||
desc = self.volume_info[ldev]['in_use'].desc
|
|
||||||
basic_lib.output_err(660, desc=desc)
|
|
||||||
raise exception.HBSDVolumeIsBusy(volume_name=volume['name'])
|
|
||||||
|
|
||||||
is_vvol = self.get_volume_is_vvol(volume)
|
|
||||||
if is_vvol:
|
|
||||||
basic_lib.output_err(706, volume_id=volume['id'],
|
|
||||||
volume_type=basic_lib.NORMAL_VOLUME_TYPE)
|
|
||||||
raise exception.HBSDVolumeIsBusy(volume_name=volume['name'])
|
|
||||||
try:
|
|
||||||
self._unmanage(volume, ldev)
|
|
||||||
except exception.HBSDBusy:
|
|
||||||
raise exception.HBSDVolumeIsBusy(volume_name=volume['name'])
|
|
||||||
else:
|
|
||||||
LOG.info(basic_lib.set_msg(5, volume_id=volume['id'], ldev=ldev))
|
|
||||||
finally:
|
|
||||||
if ldev in self.volume_info:
|
|
||||||
self.volume_info[ldev]['in_use'].lock.release()
|
|
@ -1,539 +0,0 @@
|
|||||||
# Copyright (C) 2014, Hitachi, Ltd.
|
|
||||||
#
|
|
||||||
# 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.
|
|
||||||
"""
|
|
||||||
Fibre channel Cinder volume driver for Hitachi storage.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import threading
|
|
||||||
|
|
||||||
from oslo_config import cfg
|
|
||||||
from oslo_log import log as logging
|
|
||||||
from oslo_log import versionutils
|
|
||||||
from oslo_utils import excutils
|
|
||||||
import six
|
|
||||||
|
|
||||||
from cinder import exception
|
|
||||||
from cinder.i18n import _
|
|
||||||
from cinder import interface
|
|
||||||
from cinder import utils
|
|
||||||
from cinder.volume import configuration
|
|
||||||
import cinder.volume.driver
|
|
||||||
from cinder.volume.drivers.hitachi import hbsd_basiclib as basic_lib
|
|
||||||
from cinder.volume.drivers.hitachi import hbsd_common as common
|
|
||||||
from cinder.zonemanager import utils as fczm_utils
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
volume_opts = [
|
|
||||||
cfg.BoolOpt('hitachi_zoning_request',
|
|
||||||
default=False,
|
|
||||||
help='Request for FC Zone creating HostGroup'),
|
|
||||||
]
|
|
||||||
|
|
||||||
CONF = cfg.CONF
|
|
||||||
CONF.register_opts(volume_opts, group=configuration.SHARED_CONF_GROUP)
|
|
||||||
|
|
||||||
|
|
||||||
@interface.volumedriver
|
|
||||||
class HBSDFCDriver(cinder.volume.driver.FibreChannelDriver):
|
|
||||||
VERSION = common.VERSION
|
|
||||||
|
|
||||||
# ThirdPartySystems wiki page
|
|
||||||
CI_WIKI_NAME = ["Hitachi_HBSD_CI", "Hitachi_HBSD2_CI"]
|
|
||||||
|
|
||||||
SUPPORTED = False
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
os.environ['LANG'] = 'C'
|
|
||||||
super(HBSDFCDriver, self).__init__(*args, **kwargs)
|
|
||||||
self.db = kwargs.get('db')
|
|
||||||
self.common = None
|
|
||||||
self.configuration.append_config_values(common.volume_opts)
|
|
||||||
self._stats = {}
|
|
||||||
self.context = None
|
|
||||||
self.max_hostgroups = None
|
|
||||||
self.pair_hostgroups = []
|
|
||||||
self.pair_hostnum = 0
|
|
||||||
self.do_setup_status = threading.Event()
|
|
||||||
|
|
||||||
def _check_param(self):
|
|
||||||
self.configuration.append_config_values(volume_opts)
|
|
||||||
for opt in volume_opts:
|
|
||||||
getattr(self.configuration, opt.name)
|
|
||||||
|
|
||||||
def check_param(self):
|
|
||||||
try:
|
|
||||||
self.common.check_param()
|
|
||||||
self._check_param()
|
|
||||||
except exception.HBSDError:
|
|
||||||
raise
|
|
||||||
except Exception as ex:
|
|
||||||
msg = basic_lib.output_err(601, param=six.text_type(ex))
|
|
||||||
raise exception.HBSDError(message=msg)
|
|
||||||
|
|
||||||
def output_param_to_log(self):
|
|
||||||
lock = basic_lib.get_process_lock(self.common.system_lock_file)
|
|
||||||
|
|
||||||
with lock:
|
|
||||||
self.common.output_param_to_log('FC')
|
|
||||||
for opt in volume_opts:
|
|
||||||
if not opt.secret:
|
|
||||||
value = getattr(self.configuration, opt.name)
|
|
||||||
LOG.info('\t%(name)-35s : %(value)s',
|
|
||||||
{'name': opt.name, 'value': value})
|
|
||||||
self.common.command.output_param_to_log(self.configuration)
|
|
||||||
|
|
||||||
def _add_wwn(self, hgs, port, gid, wwns):
|
|
||||||
for wwn in wwns:
|
|
||||||
wwn = six.text_type(wwn)
|
|
||||||
self.common.command.comm_add_hbawwn(port, gid, wwn)
|
|
||||||
detected = self.common.command.is_detected(port, wwn)
|
|
||||||
hgs.append({'port': port, 'gid': gid, 'initiator_wwn': wwn,
|
|
||||||
'detected': detected})
|
|
||||||
LOG.debug('Create host group for %s', hgs)
|
|
||||||
|
|
||||||
def _add_lun(self, hostgroups, ldev):
|
|
||||||
if hostgroups is self.pair_hostgroups:
|
|
||||||
is_once = True
|
|
||||||
else:
|
|
||||||
is_once = False
|
|
||||||
self.common.add_lun('auhgmap', hostgroups, ldev, is_once)
|
|
||||||
|
|
||||||
def _delete_lun(self, hostgroups, ldev):
|
|
||||||
try:
|
|
||||||
self.common.command.comm_delete_lun(hostgroups, ldev)
|
|
||||||
except exception.HBSDNotFound:
|
|
||||||
LOG.warning(basic_lib.set_msg(301, ldev=ldev))
|
|
||||||
|
|
||||||
def _get_hgname_gid(self, port, host_grp_name):
|
|
||||||
return self.common.command.get_hgname_gid(port, host_grp_name)
|
|
||||||
|
|
||||||
def _get_unused_gid(self, port):
|
|
||||||
group_range = self.configuration.hitachi_group_range
|
|
||||||
if not group_range:
|
|
||||||
group_range = basic_lib.DEFAULT_GROUP_RANGE
|
|
||||||
return self.common.command.get_unused_gid(group_range, port)
|
|
||||||
|
|
||||||
def _get_hostgroup_info(self, hgs, wwns, login=True):
|
|
||||||
target_ports = self.configuration.hitachi_target_ports
|
|
||||||
return self.common.command.comm_get_hostgroup_info(
|
|
||||||
hgs, wwns, target_ports, login=login)
|
|
||||||
|
|
||||||
def _fill_group(self, hgs, port, host_grp_name, wwns):
|
|
||||||
added_hostgroup = False
|
|
||||||
LOG.debug('Create host group (hgs: %(hgs)s port: %(port)s '
|
|
||||||
'name: %(name)s wwns: %(wwns)s)',
|
|
||||||
{'hgs': hgs, 'port': port,
|
|
||||||
'name': host_grp_name, 'wwns': wwns})
|
|
||||||
gid = self._get_hgname_gid(port, host_grp_name)
|
|
||||||
if gid is None:
|
|
||||||
for retry_cnt in basic_lib.DEFAULT_TRY_RANGE:
|
|
||||||
try:
|
|
||||||
gid = self._get_unused_gid(port)
|
|
||||||
self._add_hostgroup(port, gid, host_grp_name)
|
|
||||||
added_hostgroup = True
|
|
||||||
except exception.HBSDNotFound:
|
|
||||||
gid = None
|
|
||||||
LOG.warning(basic_lib.set_msg(312, resource='GID'))
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
LOG.debug('Completed to add host target'
|
|
||||||
'(port: %(port)s gid: %(gid)d)',
|
|
||||||
{'port': port, 'gid': gid})
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
msg = basic_lib.output_err(641)
|
|
||||||
raise exception.HBSDError(message=msg)
|
|
||||||
|
|
||||||
try:
|
|
||||||
if wwns:
|
|
||||||
self._add_wwn(hgs, port, gid, wwns)
|
|
||||||
else:
|
|
||||||
hgs.append({'port': port, 'gid': gid, 'initiator_wwn': None,
|
|
||||||
'detected': True})
|
|
||||||
except Exception:
|
|
||||||
with excutils.save_and_reraise_exception():
|
|
||||||
if added_hostgroup:
|
|
||||||
self._delete_hostgroup(port, gid, host_grp_name)
|
|
||||||
|
|
||||||
def add_hostgroup_master(self, hgs, master_wwns, host_ip, security_ports):
|
|
||||||
target_ports = self.configuration.hitachi_target_ports
|
|
||||||
group_request = self.configuration.hitachi_group_request
|
|
||||||
wwns = []
|
|
||||||
for wwn in master_wwns:
|
|
||||||
wwns.append(wwn.lower())
|
|
||||||
if target_ports and group_request:
|
|
||||||
host_grp_name = '%s%s' % (basic_lib.NAME_PREFIX, host_ip)
|
|
||||||
for port in security_ports:
|
|
||||||
wwns_copy = wwns[:]
|
|
||||||
for hostgroup in hgs:
|
|
||||||
if (hostgroup['port'] == port and
|
|
||||||
hostgroup['initiator_wwn'].lower() in wwns_copy):
|
|
||||||
wwns_copy.remove(hostgroup['initiator_wwn'].lower())
|
|
||||||
if wwns_copy:
|
|
||||||
try:
|
|
||||||
self._fill_group(hgs, port, host_grp_name, wwns_copy)
|
|
||||||
except Exception as ex:
|
|
||||||
LOG.warning('Failed to add host group: %s', ex)
|
|
||||||
LOG.warning(basic_lib.set_msg(
|
|
||||||
308, port=port, name=host_grp_name))
|
|
||||||
|
|
||||||
if not hgs:
|
|
||||||
raise exception.HBSDError(message=basic_lib.output_err(649))
|
|
||||||
|
|
||||||
def add_hostgroup_pair(self, pair_hostgroups):
|
|
||||||
if self.configuration.hitachi_unit_name:
|
|
||||||
return
|
|
||||||
|
|
||||||
properties = utils.brick_get_connector_properties()
|
|
||||||
if 'wwpns' not in properties:
|
|
||||||
msg = basic_lib.output_err(650, resource='HBA')
|
|
||||||
raise exception.HBSDError(message=msg)
|
|
||||||
hostgroups = []
|
|
||||||
self._get_hostgroup_info(hostgroups, properties['wwpns'],
|
|
||||||
login=False)
|
|
||||||
host_grp_name = '%spair%02x' % (basic_lib.NAME_PREFIX,
|
|
||||||
self.pair_hostnum)
|
|
||||||
for hostgroup in hostgroups:
|
|
||||||
gid = self._get_hgname_gid(hostgroup['port'],
|
|
||||||
host_grp_name)
|
|
||||||
|
|
||||||
# When 'gid' is 0, it should be true.
|
|
||||||
# So, it cannot remove 'is not None'.
|
|
||||||
if gid is not None:
|
|
||||||
pair_hostgroups.append({'port': hostgroup['port'],
|
|
||||||
'gid': gid, 'initiator_wwn': None,
|
|
||||||
'detected': True})
|
|
||||||
break
|
|
||||||
|
|
||||||
if not pair_hostgroups:
|
|
||||||
for hostgroup in hostgroups:
|
|
||||||
pair_port = hostgroup['port']
|
|
||||||
try:
|
|
||||||
self._fill_group(pair_hostgroups, pair_port,
|
|
||||||
host_grp_name, None)
|
|
||||||
except Exception:
|
|
||||||
if hostgroup is hostgroups[-1]:
|
|
||||||
raise
|
|
||||||
else:
|
|
||||||
break
|
|
||||||
|
|
||||||
def add_hostgroup(self):
|
|
||||||
properties = utils.brick_get_connector_properties()
|
|
||||||
if 'wwpns' not in properties:
|
|
||||||
msg = basic_lib.output_err(650, resource='HBA')
|
|
||||||
raise exception.HBSDError(message=msg)
|
|
||||||
LOG.debug("wwpns: %s", properties['wwpns'])
|
|
||||||
|
|
||||||
hostgroups = []
|
|
||||||
security_ports = self._get_hostgroup_info(
|
|
||||||
hostgroups, properties['wwpns'], login=False)
|
|
||||||
self.add_hostgroup_master(hostgroups, properties['wwpns'],
|
|
||||||
properties['ip'], security_ports)
|
|
||||||
self.add_hostgroup_pair(self.pair_hostgroups)
|
|
||||||
|
|
||||||
def _get_target_wwn(self, port):
|
|
||||||
target_wwns = self.common.command.comm_set_target_wwns(
|
|
||||||
self.configuration.hitachi_target_ports)
|
|
||||||
return target_wwns[port]
|
|
||||||
|
|
||||||
def _add_hostgroup(self, port, gid, host_grp_name):
|
|
||||||
self.common.command.comm_add_hostgrp(port, gid, host_grp_name)
|
|
||||||
|
|
||||||
def _delete_hostgroup(self, port, gid, host_grp_name):
|
|
||||||
try:
|
|
||||||
self.common.command.comm_del_hostgrp(port, gid, host_grp_name)
|
|
||||||
except Exception:
|
|
||||||
with excutils.save_and_reraise_exception():
|
|
||||||
LOG.warning(basic_lib.set_msg(
|
|
||||||
306, port=port, gid=gid, name=host_grp_name))
|
|
||||||
|
|
||||||
def _check_volume_mapping(self, hostgroup):
|
|
||||||
port = hostgroup['port']
|
|
||||||
gid = hostgroup['gid']
|
|
||||||
if self.common.command.get_hostgroup_luns(port, gid):
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _build_initiator_target_map(self, hostgroups, terminate=False):
|
|
||||||
target_wwns = []
|
|
||||||
init_targ_map = {}
|
|
||||||
|
|
||||||
target_ports = self.configuration.hitachi_target_ports
|
|
||||||
zoning_request = self.configuration.hitachi_zoning_request
|
|
||||||
|
|
||||||
for hostgroup in hostgroups:
|
|
||||||
target_wwn = self._get_target_wwn(hostgroup['port'])
|
|
||||||
|
|
||||||
if target_wwn not in target_wwns:
|
|
||||||
target_wwns.append(target_wwn)
|
|
||||||
|
|
||||||
if target_ports and zoning_request:
|
|
||||||
if terminate and self._check_volume_mapping(hostgroup):
|
|
||||||
continue
|
|
||||||
|
|
||||||
initiator_wwn = hostgroup['initiator_wwn']
|
|
||||||
if initiator_wwn not in init_targ_map:
|
|
||||||
init_targ_map[initiator_wwn] = []
|
|
||||||
|
|
||||||
init_targ_map[initiator_wwn].append(target_wwn)
|
|
||||||
|
|
||||||
return target_wwns, init_targ_map
|
|
||||||
|
|
||||||
def _get_properties(self, volume, hostgroups, terminate=False):
|
|
||||||
properties = {}
|
|
||||||
|
|
||||||
target_wwns, init_targ_map = self._build_initiator_target_map(
|
|
||||||
hostgroups, terminate)
|
|
||||||
|
|
||||||
properties['target_wwn'] = target_wwns
|
|
||||||
|
|
||||||
if init_targ_map:
|
|
||||||
properties['initiator_target_map'] = init_targ_map
|
|
||||||
|
|
||||||
if not terminate:
|
|
||||||
properties['target_lun'] = hostgroups[0]['lun']
|
|
||||||
|
|
||||||
return properties
|
|
||||||
|
|
||||||
def do_setup(self, context):
|
|
||||||
self.context = context
|
|
||||||
self.common = common.HBSDCommon(self.configuration, self,
|
|
||||||
context, self.db)
|
|
||||||
msg = _("The HBSD FC driver is deprecated and "
|
|
||||||
"will be removed in P release.")
|
|
||||||
versionutils.report_deprecated_feature(LOG, msg)
|
|
||||||
|
|
||||||
self.check_param()
|
|
||||||
|
|
||||||
self.common.create_lock_file()
|
|
||||||
|
|
||||||
self.common.command.connect_storage()
|
|
||||||
self.max_hostgroups = self.common.command.get_max_hostgroups()
|
|
||||||
|
|
||||||
lock = basic_lib.get_process_lock(self.common.service_lock_file)
|
|
||||||
with lock:
|
|
||||||
self.add_hostgroup()
|
|
||||||
|
|
||||||
self.output_param_to_log()
|
|
||||||
self.do_setup_status.set()
|
|
||||||
|
|
||||||
def check_for_setup_error(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def extend_volume(self, volume, new_size):
|
|
||||||
self.do_setup_status.wait()
|
|
||||||
self.common.extend_volume(volume, new_size)
|
|
||||||
|
|
||||||
def get_volume_stats(self, refresh=False):
|
|
||||||
if refresh:
|
|
||||||
if self.do_setup_status.isSet():
|
|
||||||
self.common.output_backend_available_once()
|
|
||||||
_stats = self.common.update_volume_stats("FC")
|
|
||||||
if _stats:
|
|
||||||
self._stats = _stats
|
|
||||||
return self._stats
|
|
||||||
|
|
||||||
def create_volume(self, volume):
|
|
||||||
self.do_setup_status.wait()
|
|
||||||
metadata = self.common.create_volume(volume)
|
|
||||||
return metadata
|
|
||||||
|
|
||||||
def delete_volume(self, volume):
|
|
||||||
self.do_setup_status.wait()
|
|
||||||
self.common.delete_volume(volume)
|
|
||||||
|
|
||||||
def create_snapshot(self, snapshot):
|
|
||||||
self.do_setup_status.wait()
|
|
||||||
metadata = self.common.create_snapshot(snapshot)
|
|
||||||
return metadata
|
|
||||||
|
|
||||||
def delete_snapshot(self, snapshot):
|
|
||||||
self.do_setup_status.wait()
|
|
||||||
self.common.delete_snapshot(snapshot)
|
|
||||||
|
|
||||||
def create_cloned_volume(self, volume, src_vref):
|
|
||||||
self.do_setup_status.wait()
|
|
||||||
metadata = self.common.create_cloned_volume(volume, src_vref)
|
|
||||||
return metadata
|
|
||||||
|
|
||||||
def create_volume_from_snapshot(self, volume, snapshot):
|
|
||||||
self.do_setup_status.wait()
|
|
||||||
metadata = self.common.create_volume_from_snapshot(volume, snapshot)
|
|
||||||
return metadata
|
|
||||||
|
|
||||||
def _initialize_connection(self, ldev, connector, src_hgs=None):
|
|
||||||
LOG.debug("Call _initialize_connection "
|
|
||||||
"(config_group: %(group)s ldev: %(ldev)d)",
|
|
||||||
{'group': self.configuration.config_group, 'ldev': ldev})
|
|
||||||
if src_hgs is self.pair_hostgroups:
|
|
||||||
hostgroups = src_hgs
|
|
||||||
else:
|
|
||||||
hostgroups = []
|
|
||||||
security_ports = self._get_hostgroup_info(
|
|
||||||
hostgroups, connector['wwpns'], login=True)
|
|
||||||
self.add_hostgroup_master(hostgroups, connector['wwpns'],
|
|
||||||
connector['ip'], security_ports)
|
|
||||||
|
|
||||||
if src_hgs is self.pair_hostgroups:
|
|
||||||
try:
|
|
||||||
self._add_lun(hostgroups, ldev)
|
|
||||||
except exception.HBSDNotFound:
|
|
||||||
LOG.warning(basic_lib.set_msg(311, ldev=ldev))
|
|
||||||
for i in range(self.max_hostgroups + 1):
|
|
||||||
self.pair_hostnum += 1
|
|
||||||
pair_hostgroups = []
|
|
||||||
try:
|
|
||||||
self.add_hostgroup_pair(pair_hostgroups)
|
|
||||||
self.pair_hostgroups.extend(pair_hostgroups)
|
|
||||||
except exception.HBSDNotFound:
|
|
||||||
if i >= self.max_hostgroups:
|
|
||||||
msg = basic_lib.output_err(648, resource='GID')
|
|
||||||
raise exception.HBSDError(message=msg)
|
|
||||||
else:
|
|
||||||
break
|
|
||||||
self.pair_initialize_connection(ldev)
|
|
||||||
else:
|
|
||||||
self._add_lun(hostgroups, ldev)
|
|
||||||
|
|
||||||
return hostgroups
|
|
||||||
|
|
||||||
@fczm_utils.add_fc_zone
|
|
||||||
def initialize_connection(self, volume, connector):
|
|
||||||
self.do_setup_status.wait()
|
|
||||||
ldev = self.common.get_ldev(volume)
|
|
||||||
if ldev is None:
|
|
||||||
msg = basic_lib.output_err(619, volume_id=volume['id'])
|
|
||||||
raise exception.HBSDError(message=msg)
|
|
||||||
self.common.add_volinfo(ldev, volume['id'])
|
|
||||||
with self.common.volume_info[ldev]['lock'],\
|
|
||||||
self.common.volume_info[ldev]['in_use']:
|
|
||||||
hostgroups = self._initialize_connection(ldev, connector)
|
|
||||||
properties = self._get_properties(volume, hostgroups)
|
|
||||||
LOG.debug('Initialize volume_info: %s',
|
|
||||||
self.common.volume_info)
|
|
||||||
|
|
||||||
LOG.debug('HFCDrv: properties=%s', properties)
|
|
||||||
return {
|
|
||||||
'driver_volume_type': 'fibre_channel',
|
|
||||||
'data': properties
|
|
||||||
}
|
|
||||||
|
|
||||||
def _terminate_connection(self, ldev, connector, src_hgs):
|
|
||||||
LOG.debug("Call _terminate_connection(config_group: %s)",
|
|
||||||
self.configuration.config_group)
|
|
||||||
hostgroups = src_hgs[:]
|
|
||||||
self._delete_lun(hostgroups, ldev)
|
|
||||||
LOG.debug("*** _terminate_ ***")
|
|
||||||
|
|
||||||
@fczm_utils.remove_fc_zone
|
|
||||||
def terminate_connection(self, volume, connector, **kwargs):
|
|
||||||
self.do_setup_status.wait()
|
|
||||||
ldev = self.common.get_ldev(volume)
|
|
||||||
if ldev is None:
|
|
||||||
LOG.warning(basic_lib.set_msg(302, volume_id=volume['id']))
|
|
||||||
return
|
|
||||||
|
|
||||||
if 'wwpns' not in connector:
|
|
||||||
msg = basic_lib.output_err(650, resource='HBA')
|
|
||||||
raise exception.HBSDError(message=msg)
|
|
||||||
|
|
||||||
hostgroups = []
|
|
||||||
self._get_hostgroup_info(hostgroups,
|
|
||||||
connector['wwpns'], login=False)
|
|
||||||
if not hostgroups:
|
|
||||||
msg = basic_lib.output_err(649)
|
|
||||||
raise exception.HBSDError(message=msg)
|
|
||||||
|
|
||||||
self.common.add_volinfo(ldev, volume['id'])
|
|
||||||
with self.common.volume_info[ldev]['lock'],\
|
|
||||||
self.common.volume_info[ldev]['in_use']:
|
|
||||||
self._terminate_connection(ldev, connector, hostgroups)
|
|
||||||
properties = self._get_properties(volume, hostgroups,
|
|
||||||
terminate=True)
|
|
||||||
LOG.debug('Terminate volume_info: %s', self.common.volume_info)
|
|
||||||
|
|
||||||
return {
|
|
||||||
'driver_volume_type': 'fibre_channel',
|
|
||||||
'data': properties
|
|
||||||
}
|
|
||||||
|
|
||||||
def pair_initialize_connection(self, ldev):
|
|
||||||
if self.configuration.hitachi_unit_name:
|
|
||||||
return
|
|
||||||
self._initialize_connection(ldev, None, self.pair_hostgroups)
|
|
||||||
|
|
||||||
def pair_terminate_connection(self, ldev):
|
|
||||||
if self.configuration.hitachi_unit_name:
|
|
||||||
return
|
|
||||||
self._terminate_connection(ldev, None, self.pair_hostgroups)
|
|
||||||
|
|
||||||
def discard_zero_page(self, volume):
|
|
||||||
self.common.command.discard_zero_page(self.common.get_ldev(volume))
|
|
||||||
|
|
||||||
def create_export(self, context, volume, connector):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def ensure_export(self, context, volume):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def remove_export(self, context, volume):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def copy_image_to_volume(self, context, volume, image_service, image_id):
|
|
||||||
self.do_setup_status.wait()
|
|
||||||
super(HBSDFCDriver, self).copy_image_to_volume(context, volume,
|
|
||||||
image_service,
|
|
||||||
image_id)
|
|
||||||
self.discard_zero_page(volume)
|
|
||||||
|
|
||||||
def copy_volume_to_image(self, context, volume, image_service, image_meta):
|
|
||||||
self.do_setup_status.wait()
|
|
||||||
if volume['volume_attachment']:
|
|
||||||
desc = 'volume %s' % volume['id']
|
|
||||||
msg = basic_lib.output_err(660, desc=desc)
|
|
||||||
raise exception.HBSDError(message=msg)
|
|
||||||
super(HBSDFCDriver, self).copy_volume_to_image(context, volume,
|
|
||||||
image_service,
|
|
||||||
image_meta)
|
|
||||||
|
|
||||||
def before_volume_copy(self, context, src_vol, dest_vol, remote=None):
|
|
||||||
"""Driver-specific actions before copyvolume data.
|
|
||||||
|
|
||||||
This method will be called before _copy_volume_data during volume
|
|
||||||
migration
|
|
||||||
"""
|
|
||||||
self.do_setup_status.wait()
|
|
||||||
|
|
||||||
def after_volume_copy(self, context, src_vol, dest_vol, remote=None):
|
|
||||||
"""Driver-specific actions after copyvolume data.
|
|
||||||
|
|
||||||
This method will be called after _copy_volume_data during volume
|
|
||||||
migration
|
|
||||||
"""
|
|
||||||
self.discard_zero_page(dest_vol)
|
|
||||||
|
|
||||||
def manage_existing(self, volume, existing_ref):
|
|
||||||
return self.common.manage_existing(volume, existing_ref)
|
|
||||||
|
|
||||||
def manage_existing_get_size(self, volume, existing_ref):
|
|
||||||
self.do_setup_status.wait()
|
|
||||||
return self.common.manage_existing_get_size(volume, existing_ref)
|
|
||||||
|
|
||||||
def unmanage(self, volume):
|
|
||||||
self.do_setup_status.wait()
|
|
||||||
self.common.unmanage(volume)
|
|
File diff suppressed because it is too large
Load Diff
@ -1,432 +0,0 @@
|
|||||||
# Copyright (C) 2014, Hitachi, Ltd.
|
|
||||||
#
|
|
||||||
# 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.
|
|
||||||
"""
|
|
||||||
iSCSI Cinder volume driver for Hitachi storage.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import threading
|
|
||||||
|
|
||||||
from oslo_config import cfg
|
|
||||||
from oslo_log import log as logging
|
|
||||||
from oslo_log import versionutils
|
|
||||||
import six
|
|
||||||
|
|
||||||
from cinder import exception
|
|
||||||
from cinder.i18n import _
|
|
||||||
from cinder import interface
|
|
||||||
from cinder import utils
|
|
||||||
from cinder.volume import configuration
|
|
||||||
import cinder.volume.driver
|
|
||||||
from cinder.volume.drivers.hitachi import hbsd_basiclib as basic_lib
|
|
||||||
from cinder.volume.drivers.hitachi import hbsd_common as common
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
CHAP_METHOD = ('None', 'CHAP None', 'CHAP')
|
|
||||||
|
|
||||||
volume_opts = [
|
|
||||||
cfg.BoolOpt('hitachi_add_chap_user',
|
|
||||||
default=False,
|
|
||||||
help='Add CHAP user'),
|
|
||||||
cfg.StrOpt('hitachi_auth_method',
|
|
||||||
help='iSCSI authentication method'),
|
|
||||||
cfg.StrOpt('hitachi_auth_user',
|
|
||||||
default='%sCHAP-user' % basic_lib.NAME_PREFIX,
|
|
||||||
help='iSCSI authentication username'),
|
|
||||||
cfg.StrOpt('hitachi_auth_password',
|
|
||||||
default='%sCHAP-password' % basic_lib.NAME_PREFIX,
|
|
||||||
help='iSCSI authentication password', secret=True),
|
|
||||||
]
|
|
||||||
|
|
||||||
CONF = cfg.CONF
|
|
||||||
CONF.register_opts(volume_opts, group=configuration.SHARED_CONF_GROUP)
|
|
||||||
|
|
||||||
|
|
||||||
@interface.volumedriver
|
|
||||||
class HBSDISCSIDriver(cinder.volume.driver.ISCSIDriver):
|
|
||||||
VERSION = common.VERSION
|
|
||||||
|
|
||||||
# ThirdPartySystems wiki page
|
|
||||||
CI_WIKI_NAME = ["Hitachi_HBSD_CI", "Hitachi_HBSD2_CI"]
|
|
||||||
|
|
||||||
SUPPORTED = False
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
os.environ['LANG'] = 'C'
|
|
||||||
super(HBSDISCSIDriver, self).__init__(*args, **kwargs)
|
|
||||||
self.db = kwargs.get('db')
|
|
||||||
self.common = None
|
|
||||||
self.configuration.append_config_values(common.volume_opts)
|
|
||||||
self._stats = {}
|
|
||||||
self.context = None
|
|
||||||
self.do_setup_status = threading.Event()
|
|
||||||
|
|
||||||
def _check_param(self):
|
|
||||||
self.configuration.append_config_values(volume_opts)
|
|
||||||
if (self.configuration.hitachi_auth_method and
|
|
||||||
self.configuration.hitachi_auth_method not in CHAP_METHOD):
|
|
||||||
raise exception.HBSDError(
|
|
||||||
message=basic_lib.output_err(601, param='hitachi_auth_method'))
|
|
||||||
if self.configuration.hitachi_auth_method == 'None':
|
|
||||||
self.configuration.hitachi_auth_method = None
|
|
||||||
for opt in volume_opts:
|
|
||||||
getattr(self.configuration, opt.name)
|
|
||||||
|
|
||||||
def check_param(self):
|
|
||||||
try:
|
|
||||||
self.common.check_param()
|
|
||||||
self._check_param()
|
|
||||||
except exception.HBSDError:
|
|
||||||
raise
|
|
||||||
except Exception as ex:
|
|
||||||
raise exception.HBSDError(
|
|
||||||
message=basic_lib.output_err(601, param=six.text_type(ex)))
|
|
||||||
|
|
||||||
def output_param_to_log(self):
|
|
||||||
lock = basic_lib.get_process_lock(self.common.system_lock_file)
|
|
||||||
|
|
||||||
with lock:
|
|
||||||
self.common.output_param_to_log('iSCSI')
|
|
||||||
for opt in volume_opts:
|
|
||||||
if not opt.secret:
|
|
||||||
value = getattr(self.configuration, opt.name)
|
|
||||||
LOG.info('\t%(name)-35s : %(value)s',
|
|
||||||
{'name': opt.name, 'value': value})
|
|
||||||
|
|
||||||
def _delete_lun_iscsi(self, hostgroups, ldev):
|
|
||||||
try:
|
|
||||||
self.common.command.comm_delete_lun_iscsi(hostgroups, ldev)
|
|
||||||
except exception.HBSDNotFound:
|
|
||||||
LOG.warning(basic_lib.set_msg(301, ldev=ldev))
|
|
||||||
|
|
||||||
def _add_target(self, hostgroups, ldev):
|
|
||||||
self.common.add_lun('autargetmap', hostgroups, ldev)
|
|
||||||
|
|
||||||
def _add_initiator(self, hgs, port, gid, host_iqn):
|
|
||||||
self.common.command.comm_add_initiator(port, gid, host_iqn)
|
|
||||||
hgs.append({'port': port, 'gid': int(gid), 'detected': True})
|
|
||||||
LOG.debug("Create iSCSI target for %s", hgs)
|
|
||||||
|
|
||||||
def _get_unused_gid_iscsi(self, port):
|
|
||||||
group_range = self.configuration.hitachi_group_range
|
|
||||||
if not group_range:
|
|
||||||
group_range = basic_lib.DEFAULT_GROUP_RANGE
|
|
||||||
return self.common.command.get_unused_gid_iscsi(group_range, port)
|
|
||||||
|
|
||||||
def _delete_iscsi_target(self, port, target_no, target_alias):
|
|
||||||
ret, _stdout, _stderr = self.common.command.delete_iscsi_target(
|
|
||||||
port, target_no, target_alias)
|
|
||||||
if ret:
|
|
||||||
LOG.warning(basic_lib.set_msg(
|
|
||||||
307, port=port, tno=target_no, alias=target_alias))
|
|
||||||
|
|
||||||
def _delete_chap_user(self, port):
|
|
||||||
ret, _stdout, _stderr = self.common.command.delete_chap_user(port)
|
|
||||||
if ret:
|
|
||||||
LOG.warning(basic_lib.set_msg(
|
|
||||||
303, user=self.configuration.hitachi_auth_user))
|
|
||||||
|
|
||||||
def _get_hostgroup_info_iscsi(self, hgs, host_iqn):
|
|
||||||
return self.common.command.comm_get_hostgroup_info_iscsi(
|
|
||||||
hgs, host_iqn, self.configuration.hitachi_target_ports)
|
|
||||||
|
|
||||||
def _discovery_iscsi_target(self, hostgroups):
|
|
||||||
for hostgroup in hostgroups:
|
|
||||||
ip_addr, ip_port = self.common.command.comm_get_iscsi_ip(
|
|
||||||
hostgroup['port'])
|
|
||||||
target_iqn = self.common.command.comm_get_target_iqn(
|
|
||||||
hostgroup['port'], hostgroup['gid'])
|
|
||||||
hostgroup['ip_addr'] = ip_addr
|
|
||||||
hostgroup['ip_port'] = ip_port
|
|
||||||
hostgroup['target_iqn'] = target_iqn
|
|
||||||
LOG.debug("ip_addr=%(addr)s ip_port=%(port)s target_iqn=%(iqn)s",
|
|
||||||
{'addr': ip_addr, 'port': ip_port, 'iqn': target_iqn})
|
|
||||||
|
|
||||||
def _fill_groups(self, hgs, ports, target_iqn, target_alias, add_iqn):
|
|
||||||
for port in ports:
|
|
||||||
added_hostgroup = False
|
|
||||||
added_user = False
|
|
||||||
LOG.debug('Create target (hgs: %(hgs)s port: %(port)s '
|
|
||||||
'target_iqn: %(tiqn)s target_alias: %(alias)s '
|
|
||||||
'add_iqn: %(aiqn)s)',
|
|
||||||
{'hgs': hgs, 'port': port, 'tiqn': target_iqn,
|
|
||||||
'alias': target_alias, 'aiqn': add_iqn})
|
|
||||||
gid = self.common.command.get_gid_from_targetiqn(
|
|
||||||
target_iqn, target_alias, port)
|
|
||||||
if gid is None:
|
|
||||||
for retry_cnt in basic_lib.DEFAULT_TRY_RANGE:
|
|
||||||
gid = None
|
|
||||||
try:
|
|
||||||
gid = self._get_unused_gid_iscsi(port)
|
|
||||||
self.common.command.comm_add_hostgrp_iscsi(
|
|
||||||
port, gid, target_alias, target_iqn)
|
|
||||||
added_hostgroup = True
|
|
||||||
except exception.HBSDNotFound:
|
|
||||||
LOG.warning(basic_lib.set_msg(312, resource='GID'))
|
|
||||||
continue
|
|
||||||
except Exception as ex:
|
|
||||||
LOG.warning(basic_lib.set_msg(
|
|
||||||
309, port=port, alias=target_alias,
|
|
||||||
reason=ex))
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
LOG.debug('Completed to add target'
|
|
||||||
'(port: %(port)s gid: %(gid)d)',
|
|
||||||
{'port': port, 'gid': gid})
|
|
||||||
break
|
|
||||||
if gid is None:
|
|
||||||
LOG.error('Failed to add target(port: %s)', port)
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
if added_hostgroup:
|
|
||||||
if self.configuration.hitachi_auth_method:
|
|
||||||
added_user = self.common.command.set_chap_authention(
|
|
||||||
port, gid)
|
|
||||||
self.common.command.comm_set_hostgrp_reportportal(
|
|
||||||
port, target_alias)
|
|
||||||
self._add_initiator(hgs, port, gid, add_iqn)
|
|
||||||
except Exception as ex:
|
|
||||||
LOG.warning(basic_lib.set_msg(
|
|
||||||
316, port=port, reason=ex))
|
|
||||||
if added_hostgroup:
|
|
||||||
if added_user:
|
|
||||||
self._delete_chap_user(port)
|
|
||||||
self._delete_iscsi_target(port, gid, target_alias)
|
|
||||||
|
|
||||||
def add_hostgroup_core(self, hgs, ports, target_iqn,
|
|
||||||
target_alias, add_iqn):
|
|
||||||
if ports:
|
|
||||||
self._fill_groups(hgs, ports, target_iqn, target_alias, add_iqn)
|
|
||||||
|
|
||||||
def add_hostgroup_master(self, hgs, master_iqn, host_ip, security_ports):
|
|
||||||
target_ports = self.configuration.hitachi_target_ports
|
|
||||||
group_request = self.configuration.hitachi_group_request
|
|
||||||
target_alias = '%s%s' % (basic_lib.NAME_PREFIX, host_ip)
|
|
||||||
if target_ports and group_request:
|
|
||||||
target_iqn = '%s.target' % master_iqn
|
|
||||||
|
|
||||||
diff_ports = []
|
|
||||||
for port in security_ports:
|
|
||||||
for hostgroup in hgs:
|
|
||||||
if hostgroup['port'] == port:
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
diff_ports.append(port)
|
|
||||||
|
|
||||||
self.add_hostgroup_core(hgs, diff_ports, target_iqn,
|
|
||||||
target_alias, master_iqn)
|
|
||||||
if not hgs:
|
|
||||||
raise exception.HBSDError(message=basic_lib.output_err(649))
|
|
||||||
|
|
||||||
def add_hostgroup(self):
|
|
||||||
properties = utils.brick_get_connector_properties()
|
|
||||||
if 'initiator' not in properties:
|
|
||||||
raise exception.HBSDError(
|
|
||||||
message=basic_lib.output_err(650, resource='HBA'))
|
|
||||||
LOG.debug("initiator: %s", properties['initiator'])
|
|
||||||
hostgroups = []
|
|
||||||
security_ports = self._get_hostgroup_info_iscsi(
|
|
||||||
hostgroups, properties['initiator'])
|
|
||||||
self.add_hostgroup_master(hostgroups, properties['initiator'],
|
|
||||||
properties['ip'], security_ports)
|
|
||||||
|
|
||||||
def _get_properties(self, volume, hostgroups):
|
|
||||||
conf = self.configuration
|
|
||||||
properties = {}
|
|
||||||
self._discovery_iscsi_target(hostgroups)
|
|
||||||
hostgroup = hostgroups[0]
|
|
||||||
|
|
||||||
properties['target_discovered'] = True
|
|
||||||
properties['target_portal'] = "%s:%s" % (hostgroup['ip_addr'],
|
|
||||||
hostgroup['ip_port'])
|
|
||||||
properties['target_iqn'] = hostgroup['target_iqn']
|
|
||||||
properties['target_lun'] = hostgroup['lun']
|
|
||||||
|
|
||||||
if conf.hitachi_auth_method:
|
|
||||||
properties['auth_method'] = 'CHAP'
|
|
||||||
properties['auth_username'] = conf.hitachi_auth_user
|
|
||||||
properties['auth_password'] = conf.hitachi_auth_password
|
|
||||||
|
|
||||||
return properties
|
|
||||||
|
|
||||||
def do_setup(self, context):
|
|
||||||
self.context = context
|
|
||||||
self.common = common.HBSDCommon(self.configuration, self,
|
|
||||||
context, self.db)
|
|
||||||
msg = _("The HBSD iSCSI driver is deprecated and "
|
|
||||||
"will be removed in P release")
|
|
||||||
versionutils.report_deprecated_feature(LOG, msg)
|
|
||||||
|
|
||||||
self.check_param()
|
|
||||||
|
|
||||||
self.common.create_lock_file()
|
|
||||||
|
|
||||||
self.common.command.connect_storage()
|
|
||||||
|
|
||||||
lock = basic_lib.get_process_lock(self.common.service_lock_file)
|
|
||||||
with lock:
|
|
||||||
self.add_hostgroup()
|
|
||||||
|
|
||||||
self.output_param_to_log()
|
|
||||||
self.do_setup_status.set()
|
|
||||||
|
|
||||||
def check_for_setup_error(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def extend_volume(self, volume, new_size):
|
|
||||||
self.do_setup_status.wait()
|
|
||||||
self.common.extend_volume(volume, new_size)
|
|
||||||
|
|
||||||
def get_volume_stats(self, refresh=False):
|
|
||||||
if refresh:
|
|
||||||
if self.do_setup_status.isSet():
|
|
||||||
self.common.output_backend_available_once()
|
|
||||||
_stats = self.common.update_volume_stats("iSCSI")
|
|
||||||
if _stats:
|
|
||||||
self._stats = _stats
|
|
||||||
return self._stats
|
|
||||||
|
|
||||||
def create_volume(self, volume):
|
|
||||||
self.do_setup_status.wait()
|
|
||||||
metadata = self.common.create_volume(volume)
|
|
||||||
return metadata
|
|
||||||
|
|
||||||
def delete_volume(self, volume):
|
|
||||||
self.do_setup_status.wait()
|
|
||||||
self.common.delete_volume(volume)
|
|
||||||
|
|
||||||
def create_snapshot(self, snapshot):
|
|
||||||
self.do_setup_status.wait()
|
|
||||||
metadata = self.common.create_snapshot(snapshot)
|
|
||||||
return metadata
|
|
||||||
|
|
||||||
def delete_snapshot(self, snapshot):
|
|
||||||
self.do_setup_status.wait()
|
|
||||||
self.common.delete_snapshot(snapshot)
|
|
||||||
|
|
||||||
def create_cloned_volume(self, volume, src_vref):
|
|
||||||
self.do_setup_status.wait()
|
|
||||||
metadata = self.common.create_cloned_volume(volume, src_vref)
|
|
||||||
return metadata
|
|
||||||
|
|
||||||
def create_volume_from_snapshot(self, volume, snapshot):
|
|
||||||
self.do_setup_status.wait()
|
|
||||||
metadata = self.common.create_volume_from_snapshot(volume, snapshot)
|
|
||||||
return metadata
|
|
||||||
|
|
||||||
def _initialize_connection(self, ldev, connector, src_hgs=None):
|
|
||||||
LOG.debug("Call _initialize_connection "
|
|
||||||
"(config_group: %(group)s ldev: %(ldev)d)",
|
|
||||||
{'group': self.configuration.config_group, 'ldev': ldev})
|
|
||||||
if src_hgs:
|
|
||||||
hostgroups = src_hgs[:]
|
|
||||||
else:
|
|
||||||
hostgroups = []
|
|
||||||
security_ports = self._get_hostgroup_info_iscsi(
|
|
||||||
hostgroups, connector['initiator'])
|
|
||||||
self.add_hostgroup_master(hostgroups, connector['initiator'],
|
|
||||||
connector['ip'], security_ports)
|
|
||||||
|
|
||||||
self._add_target(hostgroups, ldev)
|
|
||||||
|
|
||||||
return hostgroups
|
|
||||||
|
|
||||||
def initialize_connection(self, volume, connector):
|
|
||||||
self.do_setup_status.wait()
|
|
||||||
ldev = self.common.get_ldev(volume)
|
|
||||||
if ldev is None:
|
|
||||||
raise exception.HBSDError(
|
|
||||||
message=basic_lib.output_err(619, volume_id=volume['id']))
|
|
||||||
self.common.add_volinfo(ldev, volume['id'])
|
|
||||||
with self.common.volume_info[ldev]['lock'],\
|
|
||||||
self.common.volume_info[ldev]['in_use']:
|
|
||||||
hostgroups = self._initialize_connection(ldev, connector)
|
|
||||||
protocol = 'iscsi'
|
|
||||||
properties = self._get_properties(volume, hostgroups)
|
|
||||||
LOG.debug('Initialize volume_info: %s',
|
|
||||||
self.common.volume_info)
|
|
||||||
|
|
||||||
LOG.debug('HFCDrv: properties=%s', properties)
|
|
||||||
return {
|
|
||||||
'driver_volume_type': protocol,
|
|
||||||
'data': properties
|
|
||||||
}
|
|
||||||
|
|
||||||
def _terminate_connection(self, ldev, connector, src_hgs):
|
|
||||||
LOG.debug("Call _terminate_connection(config_group: %s)",
|
|
||||||
self.configuration.config_group)
|
|
||||||
hostgroups = src_hgs[:]
|
|
||||||
self._delete_lun_iscsi(hostgroups, ldev)
|
|
||||||
|
|
||||||
LOG.debug("*** _terminate_ ***")
|
|
||||||
|
|
||||||
def terminate_connection(self, volume, connector, **kwargs):
|
|
||||||
self.do_setup_status.wait()
|
|
||||||
ldev = self.common.get_ldev(volume)
|
|
||||||
if ldev is None:
|
|
||||||
LOG.warning(basic_lib.set_msg(302, volume_id=volume['id']))
|
|
||||||
return
|
|
||||||
|
|
||||||
if 'initiator' not in connector:
|
|
||||||
raise exception.HBSDError(
|
|
||||||
message=basic_lib.output_err(650, resource='HBA'))
|
|
||||||
|
|
||||||
hostgroups = []
|
|
||||||
self._get_hostgroup_info_iscsi(hostgroups,
|
|
||||||
connector['initiator'])
|
|
||||||
if not hostgroups:
|
|
||||||
raise exception.HBSDError(message=basic_lib.output_err(649))
|
|
||||||
|
|
||||||
self.common.add_volinfo(ldev, volume['id'])
|
|
||||||
with self.common.volume_info[ldev]['lock'],\
|
|
||||||
self.common.volume_info[ldev]['in_use']:
|
|
||||||
self._terminate_connection(ldev, connector, hostgroups)
|
|
||||||
|
|
||||||
def create_export(self, context, volume, connector):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def ensure_export(self, context, volume):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def remove_export(self, context, volume):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def pair_initialize_connection(self, unused_ldev):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def pair_terminate_connection(self, unused_ldev):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def copy_volume_to_image(self, context, volume, image_service, image_meta):
|
|
||||||
self.do_setup_status.wait()
|
|
||||||
if volume['volume_attachment']:
|
|
||||||
desc = 'volume %s' % volume['id']
|
|
||||||
raise exception.HBSDError(
|
|
||||||
message=basic_lib.output_err(660, desc=desc))
|
|
||||||
super(HBSDISCSIDriver, self).copy_volume_to_image(context, volume,
|
|
||||||
image_service,
|
|
||||||
image_meta)
|
|
||||||
|
|
||||||
def manage_existing(self, volume, existing_ref):
|
|
||||||
return self.common.manage_existing(volume, existing_ref)
|
|
||||||
|
|
||||||
def manage_existing_get_size(self, volume, existing_ref):
|
|
||||||
self.do_setup_status.wait()
|
|
||||||
return self.common.manage_existing_get_size(volume, existing_ref)
|
|
||||||
|
|
||||||
def unmanage(self, volume):
|
|
||||||
self.do_setup_status.wait()
|
|
||||||
self.common.unmanage(volume)
|
|
File diff suppressed because it is too large
Load Diff
@ -1,483 +0,0 @@
|
|||||||
# 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.
|
|
||||||
#
|
|
||||||
|
|
||||||
"""
|
|
||||||
Hitachi Unified Storage (HUS-HNAS) platform. Backend operations.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from oslo_concurrency import processutils as putils
|
|
||||||
from oslo_log import log as logging
|
|
||||||
from oslo_utils import units
|
|
||||||
import six
|
|
||||||
|
|
||||||
from cinder import exception
|
|
||||||
from cinder.i18n import _
|
|
||||||
from cinder import ssh_utils
|
|
||||||
from cinder import utils
|
|
||||||
|
|
||||||
LOG = logging.getLogger("cinder.volume.driver")
|
|
||||||
HNAS_SSC_RETRIES = 5
|
|
||||||
|
|
||||||
|
|
||||||
class HNASSSHBackend(object):
|
|
||||||
def __init__(self, backend_opts):
|
|
||||||
|
|
||||||
self.mgmt_ip0 = backend_opts.get('mgmt_ip0')
|
|
||||||
self.hnas_cmd = backend_opts.get('ssc_cmd', 'ssc')
|
|
||||||
self.cluster_admin_ip0 = backend_opts.get('cluster_admin_ip0')
|
|
||||||
self.ssh_port = backend_opts.get('ssh_port', '22')
|
|
||||||
self.ssh_username = backend_opts.get('username')
|
|
||||||
self.ssh_pwd = backend_opts.get('password')
|
|
||||||
self.ssh_private_key = backend_opts.get('ssh_private_key')
|
|
||||||
self.storage_version = None
|
|
||||||
self.sshpool = None
|
|
||||||
self.fslist = {}
|
|
||||||
self.tgt_list = {}
|
|
||||||
|
|
||||||
@utils.retry(exceptions=exception.HNASConnError, retries=HNAS_SSC_RETRIES,
|
|
||||||
wait_random=True)
|
|
||||||
def _run_cmd(self, *args, **kwargs):
|
|
||||||
"""Runs a command on SMU using SSH.
|
|
||||||
|
|
||||||
:returns: stdout and stderr of the command
|
|
||||||
"""
|
|
||||||
if self.cluster_admin_ip0 is None:
|
|
||||||
# Connect to SMU through SSH and run ssc locally
|
|
||||||
args = (self.hnas_cmd, 'localhost') + args
|
|
||||||
else:
|
|
||||||
args = (self.hnas_cmd, '--smuauth', self.cluster_admin_ip0) + args
|
|
||||||
|
|
||||||
utils.check_ssh_injection(args)
|
|
||||||
command = ' '.join(args)
|
|
||||||
command = command.replace('"', '\\"')
|
|
||||||
|
|
||||||
if not self.sshpool:
|
|
||||||
self.sshpool = ssh_utils.SSHPool(ip=self.mgmt_ip0,
|
|
||||||
port=int(self.ssh_port),
|
|
||||||
conn_timeout=None,
|
|
||||||
login=self.ssh_username,
|
|
||||||
password=self.ssh_pwd,
|
|
||||||
privatekey=self.ssh_private_key)
|
|
||||||
|
|
||||||
with self.sshpool.item() as ssh:
|
|
||||||
try:
|
|
||||||
out, err = putils.ssh_execute(ssh, command,
|
|
||||||
check_exit_code=True)
|
|
||||||
LOG.debug("command %(cmd)s result: out = "
|
|
||||||
"%(out)s - err = %(err)s",
|
|
||||||
{'cmd': self.hnas_cmd, 'out': out, 'err': err})
|
|
||||||
return out, err
|
|
||||||
except putils.ProcessExecutionError as e:
|
|
||||||
if 'Failed to establish SSC connection' in e.stderr:
|
|
||||||
msg = _("Failed to establish SSC connection!")
|
|
||||||
LOG.exception(msg)
|
|
||||||
raise exception.HNASConnError(msg)
|
|
||||||
elif 'Connection reset' in e.stderr:
|
|
||||||
msg = _("HNAS connection reset!")
|
|
||||||
LOG.exception(msg)
|
|
||||||
raise exception.HNASConnError(msg)
|
|
||||||
else:
|
|
||||||
raise
|
|
||||||
|
|
||||||
def get_version(self):
|
|
||||||
"""Gets version information from the storage unit.
|
|
||||||
|
|
||||||
:returns: dictionary with HNAS information
|
|
||||||
|
|
||||||
.. code:: python
|
|
||||||
|
|
||||||
storage_version={
|
|
||||||
'mac': HNAS MAC ID,
|
|
||||||
'model': HNAS model,
|
|
||||||
'version': the software version,
|
|
||||||
'hardware': the hardware version,
|
|
||||||
'serial': HNAS serial number
|
|
||||||
}
|
|
||||||
|
|
||||||
"""
|
|
||||||
if not self.storage_version:
|
|
||||||
version_info = {}
|
|
||||||
out, err = self._run_cmd("cluster-getmac")
|
|
||||||
mac = out.split(':')[1].strip()
|
|
||||||
version_info['mac'] = mac
|
|
||||||
|
|
||||||
out, err = self._run_cmd("ver")
|
|
||||||
split_out = out.split('\n')
|
|
||||||
|
|
||||||
model = split_out[1].split(':')[1].strip()
|
|
||||||
version = split_out[3].split()[1]
|
|
||||||
hardware = split_out[5].split(':')[1].strip()
|
|
||||||
serial = split_out[12].split()[2]
|
|
||||||
|
|
||||||
version_info['model'] = model
|
|
||||||
version_info['version'] = version
|
|
||||||
version_info['hardware'] = hardware
|
|
||||||
version_info['serial'] = serial
|
|
||||||
|
|
||||||
self.storage_version = version_info
|
|
||||||
|
|
||||||
LOG.debug("version_info: %(info)s", {'info': self.storage_version})
|
|
||||||
return self.storage_version
|
|
||||||
|
|
||||||
def get_evs_info(self):
|
|
||||||
"""Gets the IP addresses of all EVSs in HNAS.
|
|
||||||
|
|
||||||
:returns: dictionary with EVS information
|
|
||||||
|
|
||||||
.. code:: python
|
|
||||||
|
|
||||||
evs_info={
|
|
||||||
<IP1>: {evs_number: number identifying the EVS1 on HNAS},
|
|
||||||
<IP2>: {evs_number: number identifying the EVS2 on HNAS},
|
|
||||||
...
|
|
||||||
}
|
|
||||||
|
|
||||||
"""
|
|
||||||
evs_info = {}
|
|
||||||
out, err = self._run_cmd("evsipaddr", "-l")
|
|
||||||
|
|
||||||
out = out.split('\n')
|
|
||||||
for line in out:
|
|
||||||
if 'evs' in line and 'admin' not in line:
|
|
||||||
ip = line.split()[3].strip()
|
|
||||||
evs_info[ip] = {}
|
|
||||||
evs_info[ip]['evs_number'] = line.split()[1].strip()
|
|
||||||
|
|
||||||
return evs_info
|
|
||||||
|
|
||||||
def get_fs_info(self, fs_label):
|
|
||||||
"""Gets the information of a given FS.
|
|
||||||
|
|
||||||
:param fs_label: Label of the filesystem
|
|
||||||
:returns: dictionary with FS information
|
|
||||||
|
|
||||||
.. code:: python
|
|
||||||
|
|
||||||
fs_info={
|
|
||||||
'id': a Logical Unit ID,
|
|
||||||
'label': a Logical Unit name,
|
|
||||||
'evs_id': the ID of the EVS in which the filesystem is created
|
|
||||||
(not present if there is a single EVS),
|
|
||||||
'total_size': the total size of the FS (in GB),
|
|
||||||
'used_size': the size that is already used (in GB),
|
|
||||||
'available_size': the free space (in GB)
|
|
||||||
}
|
|
||||||
|
|
||||||
"""
|
|
||||||
def _convert_size(param):
|
|
||||||
size = float(param) * units.Mi
|
|
||||||
return six.text_type(size)
|
|
||||||
|
|
||||||
fs_info = {}
|
|
||||||
single_evs = True
|
|
||||||
id, lbl, evs, t_sz, u_sz, a_sz = 0, 1, 2, 3, 5, 12
|
|
||||||
t_sz_unit, u_sz_unit, a_sz_unit = 4, 6, 13
|
|
||||||
|
|
||||||
out, err = self._run_cmd("df", "-af", fs_label)
|
|
||||||
|
|
||||||
invalid_outs = ['Not mounted', 'Not determined', 'not found']
|
|
||||||
|
|
||||||
for problem in invalid_outs:
|
|
||||||
if problem in out:
|
|
||||||
return {}
|
|
||||||
|
|
||||||
if 'EVS' in out:
|
|
||||||
single_evs = False
|
|
||||||
|
|
||||||
fs_data = out.split('\n')[3].split()
|
|
||||||
|
|
||||||
# Getting only the desired values from the output. If there is a single
|
|
||||||
# EVS, its ID is not shown in the output and we have to decrease the
|
|
||||||
# indexes to get the right values.
|
|
||||||
fs_info['id'] = fs_data[id]
|
|
||||||
fs_info['label'] = fs_data[lbl]
|
|
||||||
|
|
||||||
if not single_evs:
|
|
||||||
fs_info['evs_id'] = fs_data[evs]
|
|
||||||
|
|
||||||
fs_info['total_size'] = (
|
|
||||||
(fs_data[t_sz]) if not single_evs else fs_data[t_sz - 1])
|
|
||||||
fs_info['used_size'] = (
|
|
||||||
fs_data[u_sz] if not single_evs else fs_data[u_sz - 1])
|
|
||||||
fs_info['available_size'] = (
|
|
||||||
fs_data[a_sz] if not single_evs else fs_data[a_sz - 1])
|
|
||||||
|
|
||||||
# Converting the sizes if necessary.
|
|
||||||
if not single_evs:
|
|
||||||
if fs_data[t_sz_unit] == 'TB':
|
|
||||||
fs_info['total_size'] = _convert_size(fs_info['total_size'])
|
|
||||||
if fs_data[u_sz_unit] == 'TB':
|
|
||||||
fs_info['used_size'] = _convert_size(fs_info['used_size'])
|
|
||||||
if fs_data[a_sz_unit] == 'TB':
|
|
||||||
fs_info['available_size'] = _convert_size(
|
|
||||||
fs_info['available_size'])
|
|
||||||
else:
|
|
||||||
if fs_data[t_sz_unit - 1] == 'TB':
|
|
||||||
fs_info['total_size'] = _convert_size(fs_info['total_size'])
|
|
||||||
if fs_data[u_sz_unit - 1] == 'TB':
|
|
||||||
fs_info['used_size'] = _convert_size(fs_info['used_size'])
|
|
||||||
if fs_data[a_sz_unit - 1] == 'TB':
|
|
||||||
fs_info['available_size'] = _convert_size(
|
|
||||||
fs_info['available_size'])
|
|
||||||
|
|
||||||
fs_info['provisioned_capacity'] = 0
|
|
||||||
|
|
||||||
LOG.debug("File system info of %(fs)s (sizes in GB): %(info)s.",
|
|
||||||
{'fs': fs_label, 'info': fs_info})
|
|
||||||
|
|
||||||
return fs_info
|
|
||||||
|
|
||||||
def get_evs(self, fs_label):
|
|
||||||
"""Gets the EVS ID for the named filesystem.
|
|
||||||
|
|
||||||
:param fs_label: The filesystem label related to the EVS required
|
|
||||||
:returns: EVS ID of the filesystem
|
|
||||||
"""
|
|
||||||
if not self.fslist:
|
|
||||||
self._get_fs_list()
|
|
||||||
|
|
||||||
# When the FS is found in the list of known FS, returns the EVS ID
|
|
||||||
for key in self.fslist:
|
|
||||||
if fs_label == self.fslist[key]['label']:
|
|
||||||
LOG.debug("EVS ID for fs %(fs)s: %(id)s.",
|
|
||||||
{'fs': fs_label, 'id': self.fslist[key]['evsid']})
|
|
||||||
return self.fslist[key]['evsid']
|
|
||||||
LOG.debug("Can't find EVS ID for fs %(fs)s.", {'fs': fs_label})
|
|
||||||
|
|
||||||
def file_clone(self, fs_label, src, name):
|
|
||||||
"""Clones NFS files to a new one named 'name'.
|
|
||||||
|
|
||||||
Clone primitive used to support all NFS snapshot/cloning functions.
|
|
||||||
|
|
||||||
:param fs_label: file system label of the new file
|
|
||||||
:param src: source file
|
|
||||||
:param name: target path of the new created file
|
|
||||||
"""
|
|
||||||
fs_list = self._get_fs_list()
|
|
||||||
fs = fs_list.get(fs_label)
|
|
||||||
if not fs:
|
|
||||||
LOG.error("Can't find file %(file)s in FS %(label)s",
|
|
||||||
{'file': src, 'label': fs_label})
|
|
||||||
msg = _('FS label: %(fs_label)s') % {'fs_label': fs_label}
|
|
||||||
raise exception.InvalidParameterValue(err=msg)
|
|
||||||
|
|
||||||
self._run_cmd("console-context", "--evs", fs['evsid'],
|
|
||||||
'file-clone-create', '-f', fs_label, src, name)
|
|
||||||
LOG.debug('file_clone: fs:%(fs_label)s %(src)s/src: -> %(name)s/dst',
|
|
||||||
{'fs_label': fs_label, 'src': src, 'name': name})
|
|
||||||
|
|
||||||
def _get_fs_list(self):
|
|
||||||
"""Gets a list of file systems configured on the backend.
|
|
||||||
|
|
||||||
:returns: a list with the Filesystems configured on HNAS
|
|
||||||
"""
|
|
||||||
if not self.fslist:
|
|
||||||
fslist_out, err = self._run_cmd('evsfs', 'list')
|
|
||||||
list_raw = fslist_out.split('\n')[3:-2]
|
|
||||||
|
|
||||||
for fs_raw in list_raw:
|
|
||||||
fs = {}
|
|
||||||
|
|
||||||
fs_raw = fs_raw.split()
|
|
||||||
fs['id'] = fs_raw[0]
|
|
||||||
fs['label'] = fs_raw[1]
|
|
||||||
fs['permid'] = fs_raw[2]
|
|
||||||
fs['evsid'] = fs_raw[3]
|
|
||||||
fs['evslabel'] = fs_raw[4]
|
|
||||||
self.fslist[fs['label']] = fs
|
|
||||||
|
|
||||||
return self.fslist
|
|
||||||
|
|
||||||
def _get_evs_list(self):
|
|
||||||
"""Gets a list of EVS configured on the backend.
|
|
||||||
|
|
||||||
:returns: a list of the EVS configured on HNAS
|
|
||||||
"""
|
|
||||||
evslist_out, err = self._run_cmd('evs', 'list')
|
|
||||||
|
|
||||||
evslist = {}
|
|
||||||
idx = 0
|
|
||||||
for evs_raw in evslist_out.split('\n'):
|
|
||||||
idx += 1
|
|
||||||
if 'Service' in evs_raw and 'Online' in evs_raw:
|
|
||||||
evs = {}
|
|
||||||
evs_line = evs_raw.split()
|
|
||||||
evs['node'] = evs_line[0]
|
|
||||||
evs['id'] = evs_line[1]
|
|
||||||
evs['label'] = evs_line[3]
|
|
||||||
evs['ips'] = []
|
|
||||||
evs['ips'].append(evs_line[6])
|
|
||||||
# Each EVS can have a list of IPs that are displayed in the
|
|
||||||
# next lines of the evslist_out. We need to check if the next
|
|
||||||
# lines is a new EVS entry or and IP of this current EVS.
|
|
||||||
for evs_ip_raw in evslist_out.split('\n')[idx:]:
|
|
||||||
if 'Service' in evs_ip_raw or not evs_ip_raw.split():
|
|
||||||
break
|
|
||||||
ip = evs_ip_raw.split()[0]
|
|
||||||
evs['ips'].append(ip)
|
|
||||||
|
|
||||||
evslist[evs['label']] = evs
|
|
||||||
|
|
||||||
return evslist
|
|
||||||
|
|
||||||
def get_export_list(self):
|
|
||||||
"""Gets information on each NFS export.
|
|
||||||
|
|
||||||
:returns: a list of the exports configured on HNAS
|
|
||||||
"""
|
|
||||||
nfs_export_out, _ = self._run_cmd('for-each-evs', '-q', 'nfs-export',
|
|
||||||
'list')
|
|
||||||
fs_list = self._get_fs_list()
|
|
||||||
evs_list = self._get_evs_list()
|
|
||||||
|
|
||||||
export_list = []
|
|
||||||
|
|
||||||
for export_raw_data in nfs_export_out.split("Export name:")[1:]:
|
|
||||||
export_info = {}
|
|
||||||
export_data = export_raw_data.split('\n')
|
|
||||||
|
|
||||||
export_info['name'] = export_data[0].strip()
|
|
||||||
export_info['path'] = export_data[1].split(':')[1].strip()
|
|
||||||
export_info['fs'] = export_data[2].split(':')[1].strip()
|
|
||||||
|
|
||||||
if "*** not available ***" in export_raw_data:
|
|
||||||
export_info['size'] = -1
|
|
||||||
export_info['free'] = -1
|
|
||||||
else:
|
|
||||||
evslbl = fs_list[export_info['fs']]['evslabel']
|
|
||||||
export_info['evs'] = evs_list[evslbl]['ips']
|
|
||||||
|
|
||||||
size = export_data[3].split(':')[1].strip().split()[0]
|
|
||||||
multiplier = export_data[3].split(':')[1].strip().split()[1]
|
|
||||||
if multiplier == 'TB':
|
|
||||||
export_info['size'] = float(size) * units.Ki
|
|
||||||
else:
|
|
||||||
export_info['size'] = float(size)
|
|
||||||
|
|
||||||
free = export_data[4].split(':')[1].strip().split()[0]
|
|
||||||
fmultiplier = export_data[4].split(':')[1].strip().split()[1]
|
|
||||||
if fmultiplier == 'TB':
|
|
||||||
export_info['free'] = float(free) * units.Ki
|
|
||||||
else:
|
|
||||||
export_info['free'] = float(free)
|
|
||||||
|
|
||||||
export_list.append(export_info)
|
|
||||||
|
|
||||||
LOG.debug("get_export_list: %(exp_list)s", {'exp_list': export_list})
|
|
||||||
return export_list
|
|
||||||
|
|
||||||
def _get_file_handler(self, volume_path, _evs_id, fs_label,
|
|
||||||
raise_except):
|
|
||||||
|
|
||||||
try:
|
|
||||||
out, err = self._run_cmd("console-context", "--evs", _evs_id,
|
|
||||||
'file-clone-stat', '-f', fs_label,
|
|
||||||
volume_path)
|
|
||||||
except putils.ProcessExecutionError as e:
|
|
||||||
if 'File is not a clone' in e.stderr and raise_except:
|
|
||||||
msg = (_("%s is not a clone!") % volume_path)
|
|
||||||
raise exception.ManageExistingInvalidReference(
|
|
||||||
existing_ref=volume_path, reason=msg)
|
|
||||||
else:
|
|
||||||
return
|
|
||||||
|
|
||||||
lines = out.split('\n')
|
|
||||||
filehandle_list = []
|
|
||||||
|
|
||||||
for line in lines:
|
|
||||||
if "SnapshotFile:" in line and "FileHandle" in line:
|
|
||||||
item = line.split(':')
|
|
||||||
handler = item[1][:-1].replace(' FileHandle[', "")
|
|
||||||
filehandle_list.append(handler)
|
|
||||||
LOG.debug("Volume handler found: %(fh)s. Adding to list...",
|
|
||||||
{'fh': handler})
|
|
||||||
|
|
||||||
return filehandle_list
|
|
||||||
|
|
||||||
def get_cloned_file_relatives(self, file_path, fs_label,
|
|
||||||
raise_except=False):
|
|
||||||
"""Gets the files related to a clone
|
|
||||||
|
|
||||||
:param file_path: path of the cloned file
|
|
||||||
:param fs_label: filesystem of the cloned file
|
|
||||||
:param raise_except: If True exception will be raised for files that
|
|
||||||
aren't clones. If False, only an error message
|
|
||||||
is logged.
|
|
||||||
:returns: list with names of the related files
|
|
||||||
"""
|
|
||||||
relatives = []
|
|
||||||
|
|
||||||
_evs_id = self.get_evs(fs_label)
|
|
||||||
|
|
||||||
file_handler_list = self._get_file_handler(file_path, _evs_id,
|
|
||||||
fs_label, raise_except)
|
|
||||||
|
|
||||||
if file_handler_list:
|
|
||||||
for file_handler in file_handler_list:
|
|
||||||
out, err = self._run_cmd('console-context', '--evs', _evs_id,
|
|
||||||
'file-clone-stat-snapshot-file', '-f',
|
|
||||||
fs_label, file_handler)
|
|
||||||
|
|
||||||
results = out.split('\n')
|
|
||||||
|
|
||||||
for value in results:
|
|
||||||
if 'Clone:' in value and file_path not in value:
|
|
||||||
relative = value.split(':')[1]
|
|
||||||
relatives.append(relative)
|
|
||||||
else:
|
|
||||||
LOG.debug("File %(path)s is not a clone.", {
|
|
||||||
'path': file_path})
|
|
||||||
|
|
||||||
return relatives
|
|
||||||
|
|
||||||
def check_snapshot_parent(self, volume_path, snap_name, fs_label):
|
|
||||||
"""Check if a volume is the snapshot source
|
|
||||||
|
|
||||||
:param volume_path: path of the volume
|
|
||||||
:param snap_name: name of the snapshot
|
|
||||||
:param fs_label: filesystem label
|
|
||||||
:return: True if the volume is the snapshot's source or False otherwise
|
|
||||||
"""
|
|
||||||
lines = self.get_cloned_file_relatives(volume_path, fs_label, True)
|
|
||||||
|
|
||||||
for line in lines:
|
|
||||||
if snap_name in line:
|
|
||||||
LOG.debug("Snapshot %(snap)s found in children list from "
|
|
||||||
"%(vol)s!", {'snap': snap_name,
|
|
||||||
'vol': volume_path})
|
|
||||||
return True
|
|
||||||
|
|
||||||
LOG.debug("Snapshot %(snap)s was not found in children list from "
|
|
||||||
"%(vol)s, probably it is not the parent!",
|
|
||||||
{'snap': snap_name, 'vol': volume_path})
|
|
||||||
return False
|
|
||||||
|
|
||||||
def get_export_path(self, export, fs_label):
|
|
||||||
"""Gets the path of an export on HNAS
|
|
||||||
|
|
||||||
:param export: the export's name
|
|
||||||
:param fs_label: the filesystem name
|
|
||||||
:returns: string of the export's path
|
|
||||||
"""
|
|
||||||
evs_id = self.get_evs(fs_label)
|
|
||||||
out, err = self._run_cmd("console-context", "--evs", evs_id,
|
|
||||||
'nfs-export', 'list', export)
|
|
||||||
|
|
||||||
lines = out.split('\n')
|
|
||||||
|
|
||||||
for line in lines:
|
|
||||||
if 'Export path:' in line:
|
|
||||||
return line.split('Export path:')[1].strip()
|
|
File diff suppressed because it is too large
Load Diff
@ -1,342 +0,0 @@
|
|||||||
# Copyright (c) 2016 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.
|
|
||||||
|
|
||||||
"""
|
|
||||||
Shared code for HNAS drivers
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
|
|
||||||
from oslo_config import cfg
|
|
||||||
from oslo_log import log as logging
|
|
||||||
import six
|
|
||||||
from xml.etree import ElementTree as ETree
|
|
||||||
|
|
||||||
from cinder import exception
|
|
||||||
from cinder.i18n import _
|
|
||||||
from cinder.volume import configuration
|
|
||||||
from cinder.volume import volume_types
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
HNAS_DEFAULT_CONFIG = {'ssc_cmd': 'ssc',
|
|
||||||
'chap_enabled': True,
|
|
||||||
'ssh_port': 22}
|
|
||||||
|
|
||||||
MAX_HNAS_ISCSI_TARGETS = 32
|
|
||||||
|
|
||||||
drivers_common_opts = [
|
|
||||||
cfg.IPOpt('hnas_mgmt_ip0',
|
|
||||||
help='Management IP address of HNAS. This can '
|
|
||||||
'be any IP in the admin address on HNAS or '
|
|
||||||
'the SMU IP.'),
|
|
||||||
cfg.StrOpt('hnas_ssc_cmd',
|
|
||||||
default='ssc',
|
|
||||||
help='Command to communicate to HNAS.'),
|
|
||||||
cfg.StrOpt('hnas_username',
|
|
||||||
help='HNAS username.'),
|
|
||||||
cfg.StrOpt('hnas_password',
|
|
||||||
secret=True,
|
|
||||||
help='HNAS password.'),
|
|
||||||
cfg.PortOpt('hnas_ssh_port',
|
|
||||||
default=22,
|
|
||||||
help='Port to be used for SSH authentication.'),
|
|
||||||
cfg.StrOpt('hnas_ssh_private_key',
|
|
||||||
help='Path to the SSH private key used to '
|
|
||||||
'authenticate in HNAS SMU.'),
|
|
||||||
cfg.StrOpt('hnas_cluster_admin_ip0',
|
|
||||||
default=None,
|
|
||||||
help='The IP of the HNAS cluster admin. '
|
|
||||||
'Required only for HNAS multi-cluster setups.'),
|
|
||||||
cfg.StrOpt('hnas_svc0_pool_name',
|
|
||||||
help='Service 0 pool name',
|
|
||||||
deprecated_name='hnas_svc0_volume_type'),
|
|
||||||
cfg.StrOpt('hnas_svc0_hdp',
|
|
||||||
help='Service 0 HDP'),
|
|
||||||
cfg.StrOpt('hnas_svc1_pool_name',
|
|
||||||
help='Service 1 pool name',
|
|
||||||
deprecated_name='hnas_svc1_volume_type'),
|
|
||||||
cfg.StrOpt('hnas_svc1_hdp',
|
|
||||||
help='Service 1 HDP'),
|
|
||||||
cfg.StrOpt('hnas_svc2_pool_name',
|
|
||||||
help='Service 2 pool name',
|
|
||||||
deprecated_name='hnas_svc2_volume_type'),
|
|
||||||
cfg.StrOpt('hnas_svc2_hdp',
|
|
||||||
help='Service 2 HDP'),
|
|
||||||
cfg.StrOpt('hnas_svc3_pool_name',
|
|
||||||
help='Service 3 pool name:',
|
|
||||||
deprecated_name='hnas_svc3_volume_type'),
|
|
||||||
cfg.StrOpt('hnas_svc3_hdp',
|
|
||||||
help='Service 3 HDP')
|
|
||||||
]
|
|
||||||
|
|
||||||
CONF = cfg.CONF
|
|
||||||
CONF.register_opts(drivers_common_opts, group=configuration.SHARED_CONF_GROUP)
|
|
||||||
|
|
||||||
|
|
||||||
def _check_conf_params(config, pool_name, idx):
|
|
||||||
"""Validates if the configuration on cinder.conf is complete.
|
|
||||||
|
|
||||||
:param config: Dictionary with the driver configurations
|
|
||||||
:param pool_name: The name of the current pool
|
|
||||||
:param dv_type: The type of the driver (NFS or iSCSI)
|
|
||||||
:param idx: Index of the current pool
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Validating the inputs on cinder.conf
|
|
||||||
if config['username'] is None:
|
|
||||||
msg = (_("The config parameter hnas_username "
|
|
||||||
"is not set in the cinder.conf."))
|
|
||||||
LOG.error(msg)
|
|
||||||
raise exception.InvalidParameterValue(err=msg)
|
|
||||||
|
|
||||||
if (config['password'] is None and
|
|
||||||
config['ssh_private_key'] is None):
|
|
||||||
msg = (_("Credentials configuration parameters "
|
|
||||||
"missing: you need to set hnas_password "
|
|
||||||
"or hnas_ssh_private_key "
|
|
||||||
"in the cinder.conf."))
|
|
||||||
LOG.error(msg)
|
|
||||||
raise exception.InvalidParameterValue(err=msg)
|
|
||||||
|
|
||||||
if config['mgmt_ip0'] is None:
|
|
||||||
msg = (_("The config parameter hnas_mgmt_ip0 "
|
|
||||||
"is not set in the cinder.conf."))
|
|
||||||
LOG.error(msg)
|
|
||||||
raise exception.InvalidParameterValue(err=msg)
|
|
||||||
|
|
||||||
if config['services'][pool_name]['hdp'] is None:
|
|
||||||
msg = (_("The config parameter hnas_svc%(idx)s_hdp is "
|
|
||||||
"not set in the cinder.conf. Note that you need to "
|
|
||||||
"have at least one pool configured.") %
|
|
||||||
{'idx': idx})
|
|
||||||
LOG.error(msg)
|
|
||||||
raise exception.InvalidParameterValue(err=msg)
|
|
||||||
|
|
||||||
if config['services'][pool_name]['pool_name'] is None:
|
|
||||||
msg = (_("The config parameter "
|
|
||||||
"hnas_svc%(idx)s_pool_name is not set "
|
|
||||||
"in the cinder.conf. Note that you need to "
|
|
||||||
"have at least one pool configured.") %
|
|
||||||
{'idx': idx})
|
|
||||||
LOG.error(msg)
|
|
||||||
raise exception.InvalidParameterValue(err=msg)
|
|
||||||
|
|
||||||
|
|
||||||
def _xml_read(root, element, check=None):
|
|
||||||
"""Read an xml element.
|
|
||||||
|
|
||||||
:param root: XML object
|
|
||||||
:param element: string desired tag
|
|
||||||
:param check: string if present, throw exception if element missing
|
|
||||||
"""
|
|
||||||
|
|
||||||
val = root.findtext(element)
|
|
||||||
|
|
||||||
# mandatory parameter not found
|
|
||||||
if val is None and check:
|
|
||||||
LOG.error("Mandatory parameter not found: %(p)s", {'p': element})
|
|
||||||
raise exception.ParameterNotFound(param=element)
|
|
||||||
|
|
||||||
# tag not found
|
|
||||||
if val is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
svc_tag_pattern = re.compile("svc_[0-3]$")
|
|
||||||
# tag found but empty parameter.
|
|
||||||
if not val.strip():
|
|
||||||
if svc_tag_pattern.search(element):
|
|
||||||
return ""
|
|
||||||
LOG.error("Parameter not found: %(param)s", {'param': element})
|
|
||||||
raise exception.ParameterNotFound(param=element)
|
|
||||||
|
|
||||||
LOG.debug("%(element)s: %(val)s",
|
|
||||||
{'element': element,
|
|
||||||
'val': val if element != 'password' else '***'})
|
|
||||||
|
|
||||||
return val.strip()
|
|
||||||
|
|
||||||
|
|
||||||
def read_xml_config(xml_config_file, svc_params, optional_params):
|
|
||||||
"""Read Hitachi driver specific xml config file.
|
|
||||||
|
|
||||||
:param xml_config_file: string filename containing XML configuration
|
|
||||||
:param svc_params: parameters to configure the services
|
|
||||||
|
|
||||||
.. code:: python
|
|
||||||
|
|
||||||
['volume_type', 'hdp']
|
|
||||||
|
|
||||||
:param optional_params: parameters to configure that are not mandatory
|
|
||||||
|
|
||||||
.. code:: python
|
|
||||||
|
|
||||||
['ssc_cmd', 'cluster_admin_ip0', 'chap_enabled']
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
if not os.access(xml_config_file, os.R_OK):
|
|
||||||
msg = (_("Can't find HNAS configurations on cinder.conf neither "
|
|
||||||
"on the path %(xml)s.") % {'xml': xml_config_file})
|
|
||||||
LOG.error(msg)
|
|
||||||
raise exception.ConfigNotFound(message=msg)
|
|
||||||
else:
|
|
||||||
LOG.warning("This XML configuration file %(xml)s is deprecated. "
|
|
||||||
"Please, move all the configurations to the "
|
|
||||||
"cinder.conf file. If you keep both configuration "
|
|
||||||
"files, the options set on cinder.conf will be "
|
|
||||||
"used.", {'xml': xml_config_file})
|
|
||||||
|
|
||||||
try:
|
|
||||||
root = ETree.parse(xml_config_file).getroot()
|
|
||||||
except ETree.ParseError:
|
|
||||||
msg = (_("Error parsing config file: %(xml_config_file)s") %
|
|
||||||
{'xml_config_file': xml_config_file})
|
|
||||||
LOG.error(msg)
|
|
||||||
raise exception.ConfigNotFound(message=msg)
|
|
||||||
|
|
||||||
# mandatory parameters for NFS
|
|
||||||
config = {}
|
|
||||||
arg_prereqs = ['mgmt_ip0', 'username']
|
|
||||||
for req in arg_prereqs:
|
|
||||||
config[req] = _xml_read(root, req, 'check')
|
|
||||||
|
|
||||||
# optional parameters for NFS
|
|
||||||
for req in optional_params:
|
|
||||||
config[req] = _xml_read(root, req)
|
|
||||||
if config[req] is None and HNAS_DEFAULT_CONFIG.get(req) is not None:
|
|
||||||
config[req] = HNAS_DEFAULT_CONFIG.get(req)
|
|
||||||
|
|
||||||
config['ssh_private_key'] = _xml_read(root, 'ssh_private_key')
|
|
||||||
config['password'] = _xml_read(root, 'password')
|
|
||||||
|
|
||||||
if config['ssh_private_key'] is None and config['password'] is None:
|
|
||||||
msg = _("Missing authentication option (passw or private key file).")
|
|
||||||
LOG.error(msg)
|
|
||||||
raise exception.ConfigNotFound(message=msg)
|
|
||||||
|
|
||||||
if _xml_read(root, 'ssh_port') is not None:
|
|
||||||
config['ssh_port'] = int(_xml_read(root, 'ssh_port'))
|
|
||||||
else:
|
|
||||||
config['ssh_port'] = HNAS_DEFAULT_CONFIG['ssh_port']
|
|
||||||
|
|
||||||
config['fs'] = {}
|
|
||||||
config['services'] = {}
|
|
||||||
|
|
||||||
# min one needed
|
|
||||||
for svc in ['svc_0', 'svc_1', 'svc_2', 'svc_3']:
|
|
||||||
if _xml_read(root, svc) is None:
|
|
||||||
continue
|
|
||||||
service = {'label': svc}
|
|
||||||
|
|
||||||
# none optional
|
|
||||||
for arg in svc_params:
|
|
||||||
service[arg] = _xml_read(root, svc + '/' + arg, 'check')
|
|
||||||
|
|
||||||
# Backward compatibility with volume_type
|
|
||||||
service.setdefault('pool_name', service.pop('volume_type', None))
|
|
||||||
|
|
||||||
config['services'][service['pool_name']] = service
|
|
||||||
config['fs'][service['hdp']] = service['hdp']
|
|
||||||
|
|
||||||
# at least one service required!
|
|
||||||
if not config['services'].keys():
|
|
||||||
LOG.error("No service found in xml config file")
|
|
||||||
raise exception.ParameterNotFound(param="svc_0")
|
|
||||||
|
|
||||||
return config
|
|
||||||
|
|
||||||
|
|
||||||
def get_pool(config, volume):
|
|
||||||
"""Get the pool of a volume.
|
|
||||||
|
|
||||||
:param config: dictionary containing the configuration parameters
|
|
||||||
:param volume: dictionary volume reference
|
|
||||||
:returns: the pool related to the volume
|
|
||||||
"""
|
|
||||||
|
|
||||||
if volume.volume_type:
|
|
||||||
metadata = {}
|
|
||||||
type_id = volume.volume_type_id
|
|
||||||
if type_id is not None:
|
|
||||||
metadata = volume_types.get_volume_type_extra_specs(type_id)
|
|
||||||
if metadata.get('service_label'):
|
|
||||||
if metadata['service_label'] in config['services'].keys():
|
|
||||||
return metadata['service_label']
|
|
||||||
return 'default'
|
|
||||||
|
|
||||||
|
|
||||||
def read_cinder_conf(config_opts):
|
|
||||||
"""Reads cinder.conf
|
|
||||||
|
|
||||||
Gets the driver specific information set on cinder.conf configuration
|
|
||||||
file.
|
|
||||||
|
|
||||||
:param config_opts: Configuration object that contains the information
|
|
||||||
needed by HNAS driver
|
|
||||||
:param dv_type: The type of the driver (NFS or iSCSI)
|
|
||||||
:returns: Dictionary with the driver configuration
|
|
||||||
"""
|
|
||||||
|
|
||||||
config = {}
|
|
||||||
config['services'] = {}
|
|
||||||
config['fs'] = {}
|
|
||||||
mandatory_parameters = ['username', 'password', 'mgmt_ip0']
|
|
||||||
optional_parameters = ['ssc_cmd',
|
|
||||||
'ssh_port', 'cluster_admin_ip0',
|
|
||||||
'ssh_private_key']
|
|
||||||
|
|
||||||
# Trying to get the mandatory parameters from cinder.conf
|
|
||||||
for opt in mandatory_parameters:
|
|
||||||
config[opt] = config_opts.safe_get('hnas_%(opt)s' % {'opt': opt})
|
|
||||||
|
|
||||||
# If there is at least one of the mandatory parameters in
|
|
||||||
# cinder.conf, we assume that we should use the configuration
|
|
||||||
# from this file.
|
|
||||||
# Otherwise, we use the configuration from the deprecated XML file.
|
|
||||||
for param in mandatory_parameters:
|
|
||||||
if config[param] is not None:
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Getting the optional parameters from cinder.conf
|
|
||||||
for opt in optional_parameters:
|
|
||||||
config[opt] = config_opts.safe_get('hnas_%(opt)s' % {'opt': opt})
|
|
||||||
|
|
||||||
# It's possible to have up to 4 pools configured.
|
|
||||||
for i in range(0, 4):
|
|
||||||
idx = six.text_type(i)
|
|
||||||
svc_pool_name = (config_opts.safe_get(
|
|
||||||
'hnas_svc%(idx)s_pool_name' % {'idx': idx}))
|
|
||||||
|
|
||||||
svc_hdp = (config_opts.safe_get(
|
|
||||||
'hnas_svc%(idx)s_hdp' % {'idx': idx}))
|
|
||||||
|
|
||||||
# It's mandatory to have at least 1 pool configured (svc_0)
|
|
||||||
if (idx == '0' or svc_pool_name is not None or
|
|
||||||
svc_hdp is not None):
|
|
||||||
config['services'][svc_pool_name] = {}
|
|
||||||
config['fs'][svc_hdp] = svc_hdp
|
|
||||||
config['services'][svc_pool_name]['hdp'] = svc_hdp
|
|
||||||
config['services'][svc_pool_name]['pool_name'] = svc_pool_name
|
|
||||||
|
|
||||||
config['services'][svc_pool_name]['label'] = (
|
|
||||||
'svc_%(idx)s' % {'idx': idx})
|
|
||||||
# Checking to ensure that the pools configurations are complete
|
|
||||||
_check_conf_params(config, svc_pool_name, idx)
|
|
||||||
|
|
||||||
return config
|
|
@ -1,955 +0,0 @@
|
|||||||
# Copyright (C) 2016, Hitachi, Ltd.
|
|
||||||
#
|
|
||||||
# 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.
|
|
||||||
#
|
|
||||||
"""Common module for Hitachi VSP Driver."""
|
|
||||||
|
|
||||||
import abc
|
|
||||||
import re
|
|
||||||
|
|
||||||
from oslo_config import cfg
|
|
||||||
from oslo_log import log as logging
|
|
||||||
from oslo_utils import excutils
|
|
||||||
from oslo_utils import units
|
|
||||||
import six
|
|
||||||
|
|
||||||
from cinder import coordination
|
|
||||||
from cinder import exception
|
|
||||||
from cinder import utils as cinder_utils
|
|
||||||
from cinder.volume import configuration
|
|
||||||
from cinder.volume.drivers.hitachi import vsp_utils as utils
|
|
||||||
from cinder.volume import utils as volume_utils
|
|
||||||
|
|
||||||
|
|
||||||
VERSION = '1.0.0'
|
|
||||||
|
|
||||||
_COPY_METHOD = set(['FULL', 'THIN'])
|
|
||||||
|
|
||||||
_INHERITED_VOLUME_OPTS = [
|
|
||||||
'volume_backend_name',
|
|
||||||
'volume_driver',
|
|
||||||
'reserved_percentage',
|
|
||||||
'use_multipath_for_image_xfer',
|
|
||||||
'enforce_multipath_for_image_xfer',
|
|
||||||
'num_volume_device_scan_tries',
|
|
||||||
]
|
|
||||||
|
|
||||||
common_opts = [
|
|
||||||
cfg.StrOpt(
|
|
||||||
'vsp_storage_id',
|
|
||||||
help='Product number of the storage system.'),
|
|
||||||
cfg.StrOpt(
|
|
||||||
'vsp_pool',
|
|
||||||
help='Pool number or pool name of the DP pool.'),
|
|
||||||
cfg.StrOpt(
|
|
||||||
'vsp_thin_pool',
|
|
||||||
help='Pool number or pool name of the Thin Image pool.'),
|
|
||||||
cfg.StrOpt(
|
|
||||||
'vsp_ldev_range',
|
|
||||||
help='Range of the LDEV numbers in the format of \'xxxx-yyyy\' that '
|
|
||||||
'can be used by the driver. Values can be in decimal format '
|
|
||||||
'(e.g. 1000) or in colon-separated hexadecimal format '
|
|
||||||
'(e.g. 00:03:E8).'),
|
|
||||||
cfg.StrOpt(
|
|
||||||
'vsp_default_copy_method',
|
|
||||||
default='FULL',
|
|
||||||
choices=['FULL', 'THIN'],
|
|
||||||
help='Method of volume copy. FULL indicates full data copy by '
|
|
||||||
'Shadow Image and THIN indicates differential data copy by Thin '
|
|
||||||
'Image.'),
|
|
||||||
cfg.IntOpt(
|
|
||||||
'vsp_copy_speed',
|
|
||||||
min=1,
|
|
||||||
max=15,
|
|
||||||
default=3,
|
|
||||||
help='Speed at which data is copied by Shadow Image. 1 or 2 indicates '
|
|
||||||
'low speed, 3 indicates middle speed, and a value between 4 and '
|
|
||||||
'15 indicates high speed.'),
|
|
||||||
cfg.IntOpt(
|
|
||||||
'vsp_copy_check_interval',
|
|
||||||
min=1,
|
|
||||||
max=600,
|
|
||||||
default=3,
|
|
||||||
help='Interval in seconds at which volume pair synchronization status '
|
|
||||||
'is checked when volume pairs are created.'),
|
|
||||||
cfg.IntOpt(
|
|
||||||
'vsp_async_copy_check_interval',
|
|
||||||
min=1,
|
|
||||||
max=600,
|
|
||||||
default=10,
|
|
||||||
help='Interval in seconds at which volume pair synchronization status '
|
|
||||||
'is checked when volume pairs are deleted.'),
|
|
||||||
cfg.ListOpt(
|
|
||||||
'vsp_target_ports',
|
|
||||||
help='IDs of the storage ports used to attach volumes to the '
|
|
||||||
'controller node. To specify multiple ports, connect them by '
|
|
||||||
'commas (e.g. CL1-A,CL2-A).'),
|
|
||||||
cfg.ListOpt(
|
|
||||||
'vsp_compute_target_ports',
|
|
||||||
help='IDs of the storage ports used to attach volumes to compute '
|
|
||||||
'nodes. To specify multiple ports, connect them by commas '
|
|
||||||
'(e.g. CL1-A,CL2-A).'),
|
|
||||||
cfg.BoolOpt(
|
|
||||||
'vsp_group_request',
|
|
||||||
default=False,
|
|
||||||
help='If True, the driver will create host groups or iSCSI targets on '
|
|
||||||
'storage ports as needed.'),
|
|
||||||
]
|
|
||||||
|
|
||||||
_REQUIRED_COMMON_OPTS = [
|
|
||||||
'vsp_storage_id',
|
|
||||||
'vsp_pool',
|
|
||||||
]
|
|
||||||
|
|
||||||
CONF = cfg.CONF
|
|
||||||
CONF.register_opts(common_opts, group=configuration.SHARED_CONF_GROUP)
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
|
||||||
MSG = utils.VSPMsg
|
|
||||||
|
|
||||||
|
|
||||||
def _str2int(num):
|
|
||||||
"""Convert a string into an integer."""
|
|
||||||
if not num:
|
|
||||||
return None
|
|
||||||
if num.isdigit():
|
|
||||||
return int(num)
|
|
||||||
if not re.match(r'\w\w:\w\w:\w\w', num):
|
|
||||||
return None
|
|
||||||
try:
|
|
||||||
return int(num.replace(':', ''), 16)
|
|
||||||
except ValueError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
@six.add_metaclass(abc.ABCMeta)
|
|
||||||
class VSPCommon(object):
|
|
||||||
"""Common class for Hitachi VSP Driver."""
|
|
||||||
|
|
||||||
def __init__(self, conf, driverinfo, db):
|
|
||||||
"""Initialize instance variables."""
|
|
||||||
self.conf = conf
|
|
||||||
self.db = db
|
|
||||||
self.ctxt = None
|
|
||||||
self.lock = {}
|
|
||||||
self.driver_info = driverinfo
|
|
||||||
self.storage_info = {
|
|
||||||
'protocol': driverinfo['proto'],
|
|
||||||
'pool_id': None,
|
|
||||||
'ldev_range': [],
|
|
||||||
'controller_ports': [],
|
|
||||||
'compute_ports': [],
|
|
||||||
'pair_ports': [],
|
|
||||||
'wwns': {},
|
|
||||||
'portals': {},
|
|
||||||
'output_first': True,
|
|
||||||
}
|
|
||||||
|
|
||||||
self._stats = {}
|
|
||||||
|
|
||||||
def run_and_verify_storage_cli(self, *cmd, **kwargs):
|
|
||||||
"""Run storage CLI and return the result or raise an exception."""
|
|
||||||
do_raise = kwargs.pop('do_raise', True)
|
|
||||||
ignore_error = kwargs.get('ignore_error')
|
|
||||||
success_code = kwargs.get('success_code', set([0]))
|
|
||||||
(ret, stdout, stderr) = self.run_storage_cli(*cmd, **kwargs)
|
|
||||||
if (ret not in success_code and
|
|
||||||
not utils.check_ignore_error(ignore_error, stderr)):
|
|
||||||
msg = utils.output_log(
|
|
||||||
MSG.STORAGE_COMMAND_FAILED, cmd=utils.mask_password(cmd),
|
|
||||||
ret=ret, out=' '.join(stdout.splitlines()),
|
|
||||||
err=' '.join(stderr.splitlines()))
|
|
||||||
if do_raise:
|
|
||||||
raise exception.VSPError(msg)
|
|
||||||
return ret, stdout, stderr
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def run_storage_cli(self, *cmd, **kwargs):
|
|
||||||
"""Run storage CLI."""
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
def get_copy_method(self, metadata):
|
|
||||||
"""Return copy method(FULL or THIN)."""
|
|
||||||
method = metadata.get(
|
|
||||||
'copy_method', self.conf.vsp_default_copy_method)
|
|
||||||
if method not in _COPY_METHOD:
|
|
||||||
msg = utils.output_log(MSG.INVALID_PARAMETER_VALUE,
|
|
||||||
meta='copy_method')
|
|
||||||
raise exception.VSPError(msg)
|
|
||||||
if method == 'THIN' and not self.conf.vsp_thin_pool:
|
|
||||||
msg = utils.output_log(MSG.INVALID_PARAMETER,
|
|
||||||
param='vsp_thin_pool')
|
|
||||||
raise exception.VSPError(msg)
|
|
||||||
return method
|
|
||||||
|
|
||||||
def create_volume(self, volume):
|
|
||||||
"""Create a volume and return its properties."""
|
|
||||||
try:
|
|
||||||
ldev = self.create_ldev(volume['size'])
|
|
||||||
except exception.VSPError:
|
|
||||||
with excutils.save_and_reraise_exception():
|
|
||||||
utils.output_log(MSG.CREATE_LDEV_FAILED)
|
|
||||||
return {
|
|
||||||
'provider_location': six.text_type(ldev),
|
|
||||||
}
|
|
||||||
|
|
||||||
def create_ldev(self, size, is_vvol=False):
|
|
||||||
"""Create an LDEV and return its LDEV number."""
|
|
||||||
ldev = self.get_unused_ldev()
|
|
||||||
self.create_ldev_on_storage(ldev, size, is_vvol)
|
|
||||||
LOG.debug('Created logical device. (LDEV: %s)', ldev)
|
|
||||||
return ldev
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def create_ldev_on_storage(self, ldev, size, is_vvol):
|
|
||||||
"""Create an LDEV on the storage system."""
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def get_unused_ldev(self):
|
|
||||||
"""Find an unused LDEV and return its LDEV number."""
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
def create_volume_from_snapshot(self, volume, snapshot):
|
|
||||||
"""Create a volume from a snapshot and return its properties."""
|
|
||||||
ldev = utils.get_ldev(snapshot)
|
|
||||||
# When 'ldev' is 0, it should be true.
|
|
||||||
# Therefore, it cannot remove 'is None'.
|
|
||||||
if ldev is None:
|
|
||||||
msg = utils.output_log(
|
|
||||||
MSG.INVALID_LDEV_FOR_VOLUME_COPY, type='snapshot',
|
|
||||||
id=snapshot['id'])
|
|
||||||
raise exception.VSPError(msg)
|
|
||||||
size = volume['size']
|
|
||||||
metadata = utils.get_volume_metadata(volume)
|
|
||||||
if size < snapshot['volume_size']:
|
|
||||||
msg = utils.output_log(
|
|
||||||
MSG.INVALID_VOLUME_SIZE_FOR_COPY, type='snapshot',
|
|
||||||
volume_id=volume['id'])
|
|
||||||
raise exception.VSPError(msg)
|
|
||||||
elif (size > snapshot['volume_size'] and not self.check_vvol(ldev) and
|
|
||||||
self.get_copy_method(metadata) == "THIN"):
|
|
||||||
msg = utils.output_log(MSG.INVALID_VOLUME_SIZE_FOR_TI,
|
|
||||||
copy_method=utils.THIN,
|
|
||||||
type='snapshot', volume_id=volume['id'])
|
|
||||||
raise exception.VSPError(msg)
|
|
||||||
sync = size > snapshot['volume_size']
|
|
||||||
new_ldev = self._copy_ldev(
|
|
||||||
ldev, snapshot['volume_size'], metadata, sync)
|
|
||||||
if sync:
|
|
||||||
self.delete_pair(new_ldev)
|
|
||||||
self.extend_ldev(new_ldev, snapshot['volume_size'], size)
|
|
||||||
return {
|
|
||||||
'provider_location': six.text_type(new_ldev),
|
|
||||||
}
|
|
||||||
|
|
||||||
def _copy_ldev(self, ldev, size, metadata, sync=False):
|
|
||||||
"""Create a copy of the specified volume and return its properties."""
|
|
||||||
try:
|
|
||||||
return self.copy_on_storage(ldev, size, metadata, sync)
|
|
||||||
except exception.VSPNotSupported:
|
|
||||||
return self._copy_on_host(ldev, size)
|
|
||||||
|
|
||||||
def _copy_on_host(self, src_ldev, size):
|
|
||||||
"""Create a copy of the specified LDEV via host."""
|
|
||||||
dest_ldev = self.create_ldev(size)
|
|
||||||
try:
|
|
||||||
self._copy_with_dd(src_ldev, dest_ldev, size)
|
|
||||||
except Exception:
|
|
||||||
with excutils.save_and_reraise_exception():
|
|
||||||
try:
|
|
||||||
self._delete_ldev(dest_ldev)
|
|
||||||
except exception.VSPError:
|
|
||||||
utils.output_log(MSG.DELETE_LDEV_FAILED, ldev=dest_ldev)
|
|
||||||
return dest_ldev
|
|
||||||
|
|
||||||
def _copy_with_dd(self, src_ldev, dest_ldev, size):
|
|
||||||
"""Copy the content of a volume by dd command."""
|
|
||||||
src_info = None
|
|
||||||
dest_info = None
|
|
||||||
properties = cinder_utils.brick_get_connector_properties(
|
|
||||||
multipath=self.conf.use_multipath_for_image_xfer,
|
|
||||||
enforce_multipath=self.conf.enforce_multipath_for_image_xfer)
|
|
||||||
try:
|
|
||||||
dest_info = self._attach_ldev(dest_ldev, properties)
|
|
||||||
src_info = self._attach_ldev(src_ldev, properties)
|
|
||||||
volume_utils.copy_volume(
|
|
||||||
src_info['device']['path'], dest_info['device']['path'],
|
|
||||||
size * units.Ki, self.conf.volume_dd_blocksize)
|
|
||||||
finally:
|
|
||||||
if src_info:
|
|
||||||
self._detach_ldev(src_info, src_ldev, properties)
|
|
||||||
if dest_info:
|
|
||||||
self._detach_ldev(dest_info, dest_ldev, properties)
|
|
||||||
self.discard_zero_page({'provider_location': six.text_type(dest_ldev)})
|
|
||||||
|
|
||||||
def _attach_ldev(self, ldev, properties):
|
|
||||||
"""Attach the specified LDEV to the server."""
|
|
||||||
volume = {
|
|
||||||
'provider_location': six.text_type(ldev),
|
|
||||||
}
|
|
||||||
conn = self.initialize_connection(volume, properties)
|
|
||||||
try:
|
|
||||||
connector = cinder_utils.brick_get_connector(
|
|
||||||
conn['driver_volume_type'],
|
|
||||||
use_multipath=self.conf.use_multipath_for_image_xfer,
|
|
||||||
device_scan_attempts=self.conf.num_volume_device_scan_tries,
|
|
||||||
conn=conn)
|
|
||||||
device = connector.connect_volume(conn['data'])
|
|
||||||
except Exception as ex:
|
|
||||||
with excutils.save_and_reraise_exception():
|
|
||||||
utils.output_log(MSG.CONNECT_VOLUME_FAILED, ldev=ldev,
|
|
||||||
reason=six.text_type(ex))
|
|
||||||
self._terminate_connection(volume, properties)
|
|
||||||
return {
|
|
||||||
'conn': conn,
|
|
||||||
'device': device,
|
|
||||||
'connector': connector,
|
|
||||||
}
|
|
||||||
|
|
||||||
def _detach_ldev(self, attach_info, ldev, properties):
|
|
||||||
"""Detach the specified LDEV from the server."""
|
|
||||||
volume = {
|
|
||||||
'provider_location': six.text_type(ldev),
|
|
||||||
}
|
|
||||||
connector = attach_info['connector']
|
|
||||||
try:
|
|
||||||
connector.disconnect_volume(
|
|
||||||
attach_info['conn']['data'], attach_info['device'])
|
|
||||||
except Exception as ex:
|
|
||||||
utils.output_log(MSG.DISCONNECT_VOLUME_FAILED, ldev=ldev,
|
|
||||||
reason=six.text_type(ex))
|
|
||||||
self._terminate_connection(volume, properties)
|
|
||||||
|
|
||||||
def _terminate_connection(self, volume, connector):
|
|
||||||
"""Disconnect the specified volume from the server."""
|
|
||||||
try:
|
|
||||||
self.terminate_connection(volume, connector)
|
|
||||||
except exception.VSPError:
|
|
||||||
utils.output_log(MSG.UNMAP_LDEV_FAILED,
|
|
||||||
ldev=utils.get_ldev(volume))
|
|
||||||
|
|
||||||
def copy_on_storage(self, pvol, size, metadata, sync):
|
|
||||||
"""Create a copy of the specified LDEV on the storage."""
|
|
||||||
is_thin = self.get_copy_method(metadata) == "THIN"
|
|
||||||
svol = self.create_ldev(size, is_vvol=is_thin)
|
|
||||||
try:
|
|
||||||
self.create_pair_on_storage(pvol, svol, is_thin)
|
|
||||||
if sync:
|
|
||||||
self.wait_full_copy_completion(pvol, svol)
|
|
||||||
except exception.VSPError:
|
|
||||||
with excutils.save_and_reraise_exception():
|
|
||||||
try:
|
|
||||||
self._delete_ldev(svol)
|
|
||||||
except exception.VSPError:
|
|
||||||
utils.output_log(MSG.DELETE_LDEV_FAILED, ldev=svol)
|
|
||||||
return svol
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def create_pair_on_storage(self, pvol, svol, is_thin):
|
|
||||||
"""Create a copy pair on the storage."""
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
def _delete_ldev(self, ldev):
|
|
||||||
"""Delete the specified LDEV."""
|
|
||||||
self.delete_pair(ldev)
|
|
||||||
self.unmap_ldev_from_storage(ldev)
|
|
||||||
self.delete_ldev_from_storage(ldev)
|
|
||||||
|
|
||||||
def unmap_ldev_from_storage(self, ldev):
|
|
||||||
"""Delete the connection between the specified LDEV and servers."""
|
|
||||||
targets = {
|
|
||||||
'list': [],
|
|
||||||
}
|
|
||||||
self.find_all_mapped_targets_from_storage(targets, ldev)
|
|
||||||
self.unmap_ldev(targets, ldev)
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def find_all_mapped_targets_from_storage(self, targets, ldev):
|
|
||||||
"""Add all port-gids connected with the LDEV to the list."""
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
def delete_pair(self, ldev, all_split=True):
|
|
||||||
"""Disconnect all volume pairs to which the specified LDEV belongs."""
|
|
||||||
pair_info = self.get_pair_info(ldev)
|
|
||||||
if not pair_info:
|
|
||||||
return
|
|
||||||
if pair_info['pvol'] == ldev:
|
|
||||||
self.delete_pair_based_on_pvol(pair_info, all_split)
|
|
||||||
else:
|
|
||||||
self.delete_pair_based_on_svol(
|
|
||||||
pair_info['pvol'], pair_info['svol_info'][0])
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def get_pair_info(self, ldev):
|
|
||||||
"""Return volume pair info(LDEV number, pair status and pair type)."""
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def delete_pair_based_on_pvol(self, pair_info, all_split):
|
|
||||||
"""Disconnect all volume pairs to which the specified P-VOL belongs."""
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def delete_pair_based_on_svol(self, pvol, svol_info):
|
|
||||||
"""Disconnect all volume pairs to which the specified S-VOL belongs."""
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def delete_pair_from_storage(self, pvol, svol, is_thin):
|
|
||||||
"""Disconnect the volume pair that consists of the specified LDEVs."""
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def delete_ldev_from_storage(self, ldev):
|
|
||||||
"""Delete the specified LDEV from the storage."""
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
def create_cloned_volume(self, volume, src_vref):
|
|
||||||
"""Create a clone of the specified volume and return its properties."""
|
|
||||||
ldev = utils.get_ldev(src_vref)
|
|
||||||
# When 'ldev' is 0, it should be true.
|
|
||||||
# Therefore, it cannot remove 'is not None'.
|
|
||||||
if ldev is None:
|
|
||||||
msg = utils.output_log(MSG.INVALID_LDEV_FOR_VOLUME_COPY,
|
|
||||||
type='volume', id=src_vref['id'])
|
|
||||||
raise exception.VSPError(msg)
|
|
||||||
size = volume['size']
|
|
||||||
metadata = utils.get_volume_metadata(volume)
|
|
||||||
if size < src_vref['size']:
|
|
||||||
msg = utils.output_log(MSG.INVALID_VOLUME_SIZE_FOR_COPY,
|
|
||||||
type='volume', volume_id=volume['id'])
|
|
||||||
raise exception.VSPError(msg)
|
|
||||||
elif (size > src_vref['size'] and not self.check_vvol(ldev) and
|
|
||||||
self.get_copy_method(metadata) == "THIN"):
|
|
||||||
msg = utils.output_log(MSG.INVALID_VOLUME_SIZE_FOR_TI,
|
|
||||||
copy_method=utils.THIN, type='volume',
|
|
||||||
volume_id=volume['id'])
|
|
||||||
raise exception.VSPError(msg)
|
|
||||||
sync = size > src_vref['size']
|
|
||||||
new_ldev = self._copy_ldev(ldev, src_vref['size'], metadata, sync)
|
|
||||||
if sync:
|
|
||||||
self.delete_pair(new_ldev)
|
|
||||||
self.extend_ldev(new_ldev, src_vref['size'], size)
|
|
||||||
return {
|
|
||||||
'provider_location': six.text_type(new_ldev),
|
|
||||||
}
|
|
||||||
|
|
||||||
def delete_volume(self, volume):
|
|
||||||
"""Delete the specified volume."""
|
|
||||||
ldev = utils.get_ldev(volume)
|
|
||||||
# When 'ldev' is 0, it should be true.
|
|
||||||
# Therefore, it cannot remove 'is not None'.
|
|
||||||
if ldev is None:
|
|
||||||
utils.output_log(MSG.INVALID_LDEV_FOR_DELETION,
|
|
||||||
method='delete_volume', id=volume['id'])
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
self._delete_ldev(ldev)
|
|
||||||
except exception.VSPBusy:
|
|
||||||
raise exception.VolumeIsBusy(volume_name=volume['name'])
|
|
||||||
|
|
||||||
def create_snapshot(self, snapshot):
|
|
||||||
"""Create a snapshot from a volume and return its properties."""
|
|
||||||
src_vref = snapshot.volume
|
|
||||||
ldev = utils.get_ldev(src_vref)
|
|
||||||
# When 'ldev' is 0, it should be true.
|
|
||||||
# Therefore, it cannot remove 'is None'.
|
|
||||||
if ldev is None:
|
|
||||||
msg = utils.output_log(MSG.INVALID_LDEV_FOR_VOLUME_COPY,
|
|
||||||
type='volume', id=src_vref['id'])
|
|
||||||
raise exception.VSPError(msg)
|
|
||||||
size = snapshot['volume_size']
|
|
||||||
metadata = utils.get_volume_metadata(src_vref)
|
|
||||||
new_ldev = self._copy_ldev(ldev, size, metadata)
|
|
||||||
return {
|
|
||||||
'provider_location': six.text_type(new_ldev),
|
|
||||||
}
|
|
||||||
|
|
||||||
def delete_snapshot(self, snapshot):
|
|
||||||
"""Delete the specified snapshot."""
|
|
||||||
ldev = utils.get_ldev(snapshot)
|
|
||||||
# When 'ldev' is 0, it should be true.
|
|
||||||
# Therefore, it cannot remove 'is None'.
|
|
||||||
if ldev is None:
|
|
||||||
utils.output_log(
|
|
||||||
MSG.INVALID_LDEV_FOR_DELETION, method='delete_snapshot',
|
|
||||||
id=snapshot['id'])
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
self._delete_ldev(ldev)
|
|
||||||
except exception.VSPBusy:
|
|
||||||
raise exception.SnapshotIsBusy(snapshot_name=snapshot['name'])
|
|
||||||
|
|
||||||
def get_volume_stats(self, refresh=False):
|
|
||||||
"""Return properties, capabilities and current states of the driver."""
|
|
||||||
if refresh:
|
|
||||||
if self.storage_info['output_first']:
|
|
||||||
self.storage_info['output_first'] = False
|
|
||||||
utils.output_log(MSG.DRIVER_READY_FOR_USE,
|
|
||||||
config_group=self.conf.config_group)
|
|
||||||
self._update_volume_stats()
|
|
||||||
return self._stats
|
|
||||||
|
|
||||||
def _update_volume_stats(self):
|
|
||||||
"""Update properties, capabilities and current states of the driver."""
|
|
||||||
data = {}
|
|
||||||
backend_name = self.conf.safe_get('volume_backend_name')
|
|
||||||
data['volume_backend_name'] = (
|
|
||||||
backend_name or self.driver_info['volume_backend_name'])
|
|
||||||
data['vendor_name'] = 'Hitachi'
|
|
||||||
data['driver_version'] = VERSION
|
|
||||||
data['storage_protocol'] = self.storage_info['protocol']
|
|
||||||
try:
|
|
||||||
total_gb, free_gb = self.get_pool_info()
|
|
||||||
except exception.VSPError:
|
|
||||||
utils.output_log(MSG.POOL_INFO_RETRIEVAL_FAILED,
|
|
||||||
pool=self.conf.vsp_pool)
|
|
||||||
return
|
|
||||||
data['total_capacity_gb'] = total_gb
|
|
||||||
data['free_capacity_gb'] = free_gb
|
|
||||||
data['reserved_percentage'] = self.conf.safe_get('reserved_percentage')
|
|
||||||
data['QoS_support'] = False
|
|
||||||
data['multiattach'] = False
|
|
||||||
LOG.debug("Updating volume status. (%s)", data)
|
|
||||||
self._stats = data
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def get_pool_info(self):
|
|
||||||
"""Return the total and free capacity of the storage pool."""
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def discard_zero_page(self, volume):
|
|
||||||
"""Return the volume's no-data pages to the storage pool."""
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
def extend_volume(self, volume, new_size):
|
|
||||||
"""Extend the specified volume to the specified size."""
|
|
||||||
ldev = utils.get_ldev(volume)
|
|
||||||
# When 'ldev' is 0, it should be true.
|
|
||||||
# Therefore, it cannot remove 'is None'.
|
|
||||||
if ldev is None:
|
|
||||||
msg = utils.output_log(MSG.INVALID_LDEV_FOR_EXTENSION,
|
|
||||||
volume_id=volume['id'])
|
|
||||||
raise exception.VSPError(msg)
|
|
||||||
if self.check_vvol(ldev):
|
|
||||||
msg = utils.output_log(MSG.INVALID_VOLUME_TYPE_FOR_EXTEND,
|
|
||||||
volume_id=volume['id'])
|
|
||||||
raise exception.VSPError(msg)
|
|
||||||
self.delete_pair(ldev)
|
|
||||||
self.extend_ldev(ldev, volume['size'], new_size)
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def check_vvol(self, ldev):
|
|
||||||
"""Return True if the specified LDEV is V-VOL, False otherwise."""
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def extend_ldev(self, ldev, old_size, new_size):
|
|
||||||
"""Extend the specified LDEV to the specified new size."""
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
def manage_existing(self, existing_ref):
|
|
||||||
"""Return volume properties which Cinder needs to manage the volume."""
|
|
||||||
ldev = _str2int(existing_ref.get('source-id'))
|
|
||||||
return {
|
|
||||||
'provider_location': six.text_type(ldev),
|
|
||||||
}
|
|
||||||
|
|
||||||
def manage_existing_get_size(self, existing_ref):
|
|
||||||
"""Return the size[GB] of the specified volume."""
|
|
||||||
ldev = _str2int(existing_ref.get('source-id'))
|
|
||||||
# When 'ldev' is 0, it should be true.
|
|
||||||
# Therefore, it cannot remove 'is None'.
|
|
||||||
if ldev is None:
|
|
||||||
msg = utils.output_log(MSG.INVALID_LDEV_FOR_MANAGE)
|
|
||||||
raise exception.ManageExistingInvalidReference(
|
|
||||||
existing_ref=existing_ref, reason=msg)
|
|
||||||
return self.get_ldev_size_in_gigabyte(ldev, existing_ref)
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def get_ldev_size_in_gigabyte(self, ldev, existing_ref):
|
|
||||||
"""Return the size[GB] of the specified LDEV."""
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
def unmanage(self, volume):
|
|
||||||
"""Prepare the volume for removing it from Cinder management."""
|
|
||||||
ldev = utils.get_ldev(volume)
|
|
||||||
# When 'ldev' is 0, it should be true.
|
|
||||||
# Therefore, it cannot remove 'is None'.
|
|
||||||
if ldev is None:
|
|
||||||
utils.output_log(MSG.INVALID_LDEV_FOR_DELETION, method='unmanage',
|
|
||||||
id=volume['id'])
|
|
||||||
return
|
|
||||||
if self.check_vvol(ldev):
|
|
||||||
utils.output_log(
|
|
||||||
MSG.INVALID_LDEV_TYPE_FOR_UNMANAGE, volume_id=volume['id'],
|
|
||||||
volume_type=utils.NORMAL_LDEV_TYPE)
|
|
||||||
raise exception.VolumeIsBusy(volume_name=volume['name'])
|
|
||||||
try:
|
|
||||||
self.delete_pair(ldev)
|
|
||||||
except exception.VSPBusy:
|
|
||||||
raise exception.VolumeIsBusy(volume_name=volume['name'])
|
|
||||||
|
|
||||||
def do_setup(self, context):
|
|
||||||
"""Prepare for the startup of the driver."""
|
|
||||||
self.ctxt = context
|
|
||||||
|
|
||||||
self.check_param()
|
|
||||||
self.config_lock()
|
|
||||||
self.connect_storage()
|
|
||||||
self.init_cinder_hosts()
|
|
||||||
self.output_param_to_log()
|
|
||||||
|
|
||||||
def check_param(self):
|
|
||||||
"""Check parameter values and consistency among them."""
|
|
||||||
utils.check_opt_value(self.conf, _INHERITED_VOLUME_OPTS)
|
|
||||||
utils.check_opts(self.conf, common_opts)
|
|
||||||
utils.check_opts(self.conf, self.driver_info['volume_opts'])
|
|
||||||
if (self.conf.vsp_default_copy_method == 'THIN' and
|
|
||||||
not self.conf.vsp_thin_pool):
|
|
||||||
msg = utils.output_log(MSG.INVALID_PARAMETER,
|
|
||||||
param='vsp_thin_pool')
|
|
||||||
raise exception.VSPError(msg)
|
|
||||||
if self.conf.vsp_ldev_range:
|
|
||||||
self.storage_info['ldev_range'] = self._range2list(
|
|
||||||
'vsp_ldev_range')
|
|
||||||
if (not self.conf.vsp_target_ports and
|
|
||||||
not self.conf.vsp_compute_target_ports):
|
|
||||||
msg = utils.output_log(MSG.INVALID_PARAMETER,
|
|
||||||
param='vsp_target_ports or '
|
|
||||||
'vsp_compute_target_ports')
|
|
||||||
raise exception.VSPError(msg)
|
|
||||||
for opt in _REQUIRED_COMMON_OPTS:
|
|
||||||
if not self.conf.safe_get(opt):
|
|
||||||
msg = utils.output_log(MSG.INVALID_PARAMETER, param=opt)
|
|
||||||
raise exception.VSPError(msg)
|
|
||||||
if self.storage_info['protocol'] == 'iSCSI':
|
|
||||||
self.check_param_iscsi()
|
|
||||||
|
|
||||||
def check_param_iscsi(self):
|
|
||||||
"""Check iSCSI-related parameter values and consistency among them."""
|
|
||||||
if self.conf.vsp_use_chap_auth:
|
|
||||||
if not self.conf.vsp_auth_user:
|
|
||||||
msg = utils.output_log(MSG.INVALID_PARAMETER,
|
|
||||||
param='vsp_auth_user')
|
|
||||||
raise exception.VSPError(msg)
|
|
||||||
if not self.conf.vsp_auth_password:
|
|
||||||
msg = utils.output_log(MSG.INVALID_PARAMETER,
|
|
||||||
param='vsp_auth_password')
|
|
||||||
raise exception.VSPError(msg)
|
|
||||||
|
|
||||||
def _range2list(self, param):
|
|
||||||
"""Analyze a 'xxx-xxx' string and return a list of two integers."""
|
|
||||||
values = [_str2int(value) for value in
|
|
||||||
self.conf.safe_get(param).split('-')]
|
|
||||||
if (len(values) != 2 or
|
|
||||||
values[0] is None or values[1] is None or
|
|
||||||
values[0] > values[1]):
|
|
||||||
msg = utils.output_log(MSG.INVALID_PARAMETER, param=param)
|
|
||||||
raise exception.VSPError(msg)
|
|
||||||
return values
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def config_lock(self):
|
|
||||||
"""Initialize lock resource names."""
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
def connect_storage(self):
|
|
||||||
"""Prepare for using the storage."""
|
|
||||||
self.storage_info['pool_id'] = self.get_pool_id()
|
|
||||||
# When 'pool_id' is 0, it should be true.
|
|
||||||
# Therefore, it cannot remove 'is None'.
|
|
||||||
if self.storage_info['pool_id'] is None:
|
|
||||||
msg = utils.output_log(MSG.POOL_NOT_FOUND, pool=self.conf.vsp_pool)
|
|
||||||
raise exception.VSPError(msg)
|
|
||||||
utils.output_log(MSG.SET_CONFIG_VALUE, object='DP Pool ID',
|
|
||||||
value=self.storage_info['pool_id'])
|
|
||||||
|
|
||||||
def check_ports_info(self):
|
|
||||||
"""Check if available storage ports exist."""
|
|
||||||
if (self.conf.vsp_target_ports and
|
|
||||||
not self.storage_info['controller_ports']):
|
|
||||||
msg = utils.output_log(MSG.RESOURCE_NOT_FOUND,
|
|
||||||
resource="Target ports")
|
|
||||||
raise exception.VSPError(msg)
|
|
||||||
if (self.conf.vsp_compute_target_ports and
|
|
||||||
not self.storage_info['compute_ports']):
|
|
||||||
msg = utils.output_log(MSG.RESOURCE_NOT_FOUND,
|
|
||||||
resource="Compute target ports")
|
|
||||||
raise exception.VSPError(msg)
|
|
||||||
utils.output_log(MSG.SET_CONFIG_VALUE, object='target port list',
|
|
||||||
value=self.storage_info['controller_ports'])
|
|
||||||
utils.output_log(MSG.SET_CONFIG_VALUE,
|
|
||||||
object='compute target port list',
|
|
||||||
value=self.storage_info['compute_ports'])
|
|
||||||
|
|
||||||
def get_pool_id(self):
|
|
||||||
"""Return the storage pool ID as integer."""
|
|
||||||
pool = self.conf.vsp_pool
|
|
||||||
if pool.isdigit():
|
|
||||||
return int(pool)
|
|
||||||
return None
|
|
||||||
|
|
||||||
def init_cinder_hosts(self, **kwargs):
|
|
||||||
"""Initialize server-storage connection."""
|
|
||||||
targets = kwargs.pop('targets', {'info': {}, 'list': [], 'iqns': {}})
|
|
||||||
connector = cinder_utils.brick_get_connector_properties(
|
|
||||||
multipath=self.conf.use_multipath_for_image_xfer,
|
|
||||||
enforce_multipath=self.conf.enforce_multipath_for_image_xfer)
|
|
||||||
target_ports = self.storage_info['controller_ports']
|
|
||||||
|
|
||||||
if target_ports:
|
|
||||||
if (self.find_targets_from_storage(
|
|
||||||
targets, connector, target_ports) and
|
|
||||||
self.conf.vsp_group_request):
|
|
||||||
self.create_mapping_targets(targets, connector)
|
|
||||||
|
|
||||||
utils.require_target_existed(targets)
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def find_targets_from_storage(self, targets, connector, target_ports):
|
|
||||||
"""Find mapped ports, memorize them and return unmapped port count."""
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
def create_mapping_targets(self, targets, connector):
|
|
||||||
"""Create server-storage connection for all specified storage ports."""
|
|
||||||
hba_ids = self.get_hba_ids_from_connector(connector)
|
|
||||||
for port in targets['info'].keys():
|
|
||||||
if targets['info'][port]:
|
|
||||||
continue
|
|
||||||
|
|
||||||
try:
|
|
||||||
self._create_target(targets, port, connector, hba_ids)
|
|
||||||
except exception.VSPError:
|
|
||||||
utils.output_log(
|
|
||||||
self.driver_info['msg_id']['target'], port=port)
|
|
||||||
|
|
||||||
if not targets['list']:
|
|
||||||
self.find_targets_from_storage(
|
|
||||||
targets, connector, targets['info'].keys())
|
|
||||||
|
|
||||||
def get_hba_ids_from_connector(self, connector):
|
|
||||||
"""Return the HBA ID stored in the connector."""
|
|
||||||
if self.driver_info['hba_id'] in connector:
|
|
||||||
return connector[self.driver_info['hba_id']]
|
|
||||||
msg = utils.output_log(MSG.RESOURCE_NOT_FOUND,
|
|
||||||
resource=self.driver_info['hba_id_type'])
|
|
||||||
raise exception.VSPError(msg)
|
|
||||||
|
|
||||||
def _create_target(self, targets, port, connector, hba_ids):
|
|
||||||
"""Create a host group or an iSCSI target on the storage port."""
|
|
||||||
target_name, gid = self.create_target_to_storage(port, connector,
|
|
||||||
hba_ids)
|
|
||||||
utils.output_log(MSG.OBJECT_CREATED, object='a target',
|
|
||||||
details='port: %(port)s, gid: %(gid)s, target_name: '
|
|
||||||
'%(target)s' %
|
|
||||||
{'port': port, 'gid': gid, 'target': target_name})
|
|
||||||
try:
|
|
||||||
self.set_target_mode(port, gid)
|
|
||||||
self.set_hba_ids(port, gid, hba_ids)
|
|
||||||
except exception.VSPError:
|
|
||||||
with excutils.save_and_reraise_exception():
|
|
||||||
self.delete_target_from_storage(port, gid)
|
|
||||||
targets['info'][port] = True
|
|
||||||
targets['list'].append((port, gid))
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def create_target_to_storage(self, port, connector, hba_ids):
|
|
||||||
"""Create a host group or an iSCSI target on the specified port."""
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def set_target_mode(self, port, gid):
|
|
||||||
"""Configure the target to meet the environment."""
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def set_hba_ids(self, port, gid, hba_ids):
|
|
||||||
"""Connect all specified HBAs with the specified port."""
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def delete_target_from_storage(self, port, gid):
|
|
||||||
"""Delete the host group or the iSCSI target from the port."""
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
def output_param_to_log(self):
|
|
||||||
"""Output configuration parameter values to the log file."""
|
|
||||||
utils.output_log(MSG.OUTPUT_PARAMETER_VALUES,
|
|
||||||
config_group=self.conf.config_group)
|
|
||||||
name, version = self.get_storage_cli_info()
|
|
||||||
utils.output_storage_cli_info(name, version)
|
|
||||||
utils.output_opt_info(self.conf, _INHERITED_VOLUME_OPTS)
|
|
||||||
utils.output_opts(self.conf, common_opts)
|
|
||||||
utils.output_opts(self.conf, self.driver_info['volume_opts'])
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def get_storage_cli_info(self):
|
|
||||||
"""Return a tuple of the storage CLI name and its version."""
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
@coordination.synchronized('vsp-host-{self.conf.vsp_storage_id}-'
|
|
||||||
'{connector[host]}')
|
|
||||||
def initialize_connection(self, volume, connector):
|
|
||||||
"""Initialize connection between the server and the volume."""
|
|
||||||
targets = {
|
|
||||||
'info': {},
|
|
||||||
'list': [],
|
|
||||||
'lun': {},
|
|
||||||
'iqns': {},
|
|
||||||
}
|
|
||||||
ldev = utils.get_ldev(volume)
|
|
||||||
# When 'ldev' is 0, it should be true.
|
|
||||||
# Therefore, it cannot remove 'is None'.
|
|
||||||
if ldev is None:
|
|
||||||
msg = utils.output_log(MSG.INVALID_LDEV_FOR_CONNECTION,
|
|
||||||
volume_id=volume['id'])
|
|
||||||
raise exception.VSPError(msg)
|
|
||||||
|
|
||||||
target_ports = self.get_target_ports(connector)
|
|
||||||
if (self.find_targets_from_storage(
|
|
||||||
targets, connector, target_ports) and
|
|
||||||
self.conf.vsp_group_request):
|
|
||||||
self.create_mapping_targets(targets, connector)
|
|
||||||
|
|
||||||
utils.require_target_existed(targets)
|
|
||||||
|
|
||||||
targets['list'].sort()
|
|
||||||
for port in target_ports:
|
|
||||||
targets['lun'][port] = False
|
|
||||||
target_lun = int(self.map_ldev(targets, ldev))
|
|
||||||
|
|
||||||
return {
|
|
||||||
'driver_volume_type': self.driver_info['volume_type'],
|
|
||||||
'data': self.get_properties(targets, connector, target_lun),
|
|
||||||
}
|
|
||||||
|
|
||||||
def get_target_ports(self, connector):
|
|
||||||
"""Return a list of ports corresponding to the specified connector."""
|
|
||||||
if 'ip' in connector and connector['ip'] == CONF.my_ip:
|
|
||||||
return self.storage_info['controller_ports']
|
|
||||||
return (self.storage_info['compute_ports'] or
|
|
||||||
self.storage_info['controller_ports'])
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def map_ldev(self, targets, ldev):
|
|
||||||
"""Create the path between the server and the LDEV and return LUN."""
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
def get_properties(self, targets, connector, target_lun=None):
|
|
||||||
"""Return server-LDEV connection info."""
|
|
||||||
multipath = connector.get('multipath', False)
|
|
||||||
if self.storage_info['protocol'] == 'FC':
|
|
||||||
data = self.get_properties_fc(targets)
|
|
||||||
elif self.storage_info['protocol'] == 'iSCSI':
|
|
||||||
data = self.get_properties_iscsi(targets, multipath)
|
|
||||||
if target_lun is not None:
|
|
||||||
data['target_discovered'] = False
|
|
||||||
if not multipath or self.storage_info['protocol'] == 'FC':
|
|
||||||
data['target_lun'] = target_lun
|
|
||||||
else:
|
|
||||||
target_luns = []
|
|
||||||
for target in targets['list']:
|
|
||||||
if targets['lun'][target[0]]:
|
|
||||||
target_luns.append(target_lun)
|
|
||||||
data['target_luns'] = target_luns
|
|
||||||
return data
|
|
||||||
|
|
||||||
def get_properties_fc(self, targets):
|
|
||||||
"""Return FC-specific server-LDEV connection info."""
|
|
||||||
data = {}
|
|
||||||
data['target_wwn'] = [
|
|
||||||
self.storage_info['wwns'][target[0]] for target in targets['list']
|
|
||||||
if targets['lun'][target[0]]]
|
|
||||||
return data
|
|
||||||
|
|
||||||
def get_properties_iscsi(self, targets, multipath):
|
|
||||||
"""Return iSCSI-specific server-LDEV connection info."""
|
|
||||||
data = {}
|
|
||||||
primary_target = targets['list'][0]
|
|
||||||
if not multipath:
|
|
||||||
data['target_portal'] = self.storage_info[
|
|
||||||
'portals'][primary_target[0]]
|
|
||||||
data['target_iqn'] = targets['iqns'][primary_target]
|
|
||||||
else:
|
|
||||||
data['target_portals'] = [
|
|
||||||
self.storage_info['portals'][target[0]] for target in
|
|
||||||
targets['list'] if targets['lun'][target[0]]]
|
|
||||||
data['target_iqns'] = [
|
|
||||||
targets['iqns'][target] for target in targets['list']
|
|
||||||
if targets['lun'][target[0]]]
|
|
||||||
if self.conf.vsp_use_chap_auth:
|
|
||||||
data['auth_method'] = 'CHAP'
|
|
||||||
data['auth_username'] = self.conf.vsp_auth_user
|
|
||||||
data['auth_password'] = self.conf.vsp_auth_password
|
|
||||||
return data
|
|
||||||
|
|
||||||
@coordination.synchronized('vsp-host-{self.conf.vsp_storage_id}-'
|
|
||||||
'{connector[host]}')
|
|
||||||
def terminate_connection(self, volume, connector):
|
|
||||||
"""Terminate connection between the server and the volume."""
|
|
||||||
targets = {
|
|
||||||
'info': {},
|
|
||||||
'list': [],
|
|
||||||
'iqns': {},
|
|
||||||
}
|
|
||||||
mapped_targets = {
|
|
||||||
'list': [],
|
|
||||||
}
|
|
||||||
unmap_targets = {}
|
|
||||||
|
|
||||||
ldev = utils.get_ldev(volume)
|
|
||||||
if ldev is None:
|
|
||||||
utils.output_log(MSG.INVALID_LDEV_FOR_UNMAPPING,
|
|
||||||
volume_id=volume['id'])
|
|
||||||
return
|
|
||||||
target_ports = self.get_target_ports(connector)
|
|
||||||
self.find_targets_from_storage(targets, connector, target_ports)
|
|
||||||
if not targets['list']:
|
|
||||||
utils.output_log(MSG.NO_CONNECTED_TARGET)
|
|
||||||
self.find_mapped_targets_from_storage(
|
|
||||||
mapped_targets, ldev, target_ports)
|
|
||||||
|
|
||||||
unmap_targets['list'] = self.get_unmap_targets_list(
|
|
||||||
targets['list'], mapped_targets['list'])
|
|
||||||
unmap_targets['list'].sort(reverse=True)
|
|
||||||
self.unmap_ldev(unmap_targets, ldev)
|
|
||||||
|
|
||||||
if self.storage_info['protocol'] == 'FC':
|
|
||||||
target_wwn = [
|
|
||||||
self.storage_info['wwns'][port_gid[:utils.PORT_ID_LENGTH]]
|
|
||||||
for port_gid in unmap_targets['list']]
|
|
||||||
return {'driver_volume_type': self.driver_info['volume_type'],
|
|
||||||
'data': {'target_wwn': target_wwn}}
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def find_mapped_targets_from_storage(self, targets, ldev, target_ports):
|
|
||||||
"""Find and store IDs of ports used for server-LDEV connection."""
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def get_unmap_targets_list(self, target_list, mapped_list):
|
|
||||||
"""Return a list of IDs of ports that need to be disconnected."""
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def unmap_ldev(self, targets, ldev):
|
|
||||||
"""Delete the LUN between the specified LDEV and port-gid."""
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def wait_full_copy_completion(self, pvol, svol):
|
|
||||||
"""Wait until FULL copy is completed."""
|
|
||||||
raise NotImplementedError()
|
|
@ -1,181 +0,0 @@
|
|||||||
# Copyright (C) 2016, Hitachi, Ltd.
|
|
||||||
#
|
|
||||||
# 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.
|
|
||||||
#
|
|
||||||
"""Fibre channel module for Hitachi VSP Driver."""
|
|
||||||
|
|
||||||
from oslo_config import cfg
|
|
||||||
|
|
||||||
from cinder import interface
|
|
||||||
from cinder.volume import configuration
|
|
||||||
from cinder.volume import driver
|
|
||||||
from cinder.volume.drivers.hitachi import vsp_common as common
|
|
||||||
from cinder.volume.drivers.hitachi import vsp_utils as utils
|
|
||||||
|
|
||||||
fc_opts = [
|
|
||||||
cfg.BoolOpt(
|
|
||||||
'vsp_zoning_request',
|
|
||||||
default=False,
|
|
||||||
help='If True, the driver will configure FC zoning between the server '
|
|
||||||
'and the storage system provided that FC zoning manager is '
|
|
||||||
'enabled.'),
|
|
||||||
]
|
|
||||||
|
|
||||||
MSG = utils.VSPMsg
|
|
||||||
|
|
||||||
_DRIVER_INFO = {
|
|
||||||
'proto': 'FC',
|
|
||||||
'hba_id': 'wwpns',
|
|
||||||
'hba_id_type': 'World Wide Name',
|
|
||||||
'msg_id': {
|
|
||||||
'target': MSG.CREATE_HOST_GROUP_FAILED,
|
|
||||||
},
|
|
||||||
'volume_backend_name': utils.DRIVER_PREFIX + 'FC',
|
|
||||||
'volume_opts': fc_opts,
|
|
||||||
'volume_type': 'fibre_channel',
|
|
||||||
}
|
|
||||||
|
|
||||||
CONF = cfg.CONF
|
|
||||||
CONF.register_opts(fc_opts, group=configuration.SHARED_CONF_GROUP)
|
|
||||||
|
|
||||||
|
|
||||||
@interface.volumedriver
|
|
||||||
class VSPFCDriver(driver.FibreChannelDriver):
|
|
||||||
"""Fibre channel class for Hitachi VSP Driver.
|
|
||||||
|
|
||||||
Version history:
|
|
||||||
|
|
||||||
.. code-block:: none
|
|
||||||
|
|
||||||
1.0.0 - Initial driver.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
VERSION = common.VERSION
|
|
||||||
|
|
||||||
# ThirdPartySystems wiki page
|
|
||||||
CI_WIKI_NAME = "Hitachi_VSP_CI"
|
|
||||||
|
|
||||||
SUPPORTED = False
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
"""Initialize instance variables."""
|
|
||||||
utils.output_log(MSG.DRIVER_INITIALIZATION_START,
|
|
||||||
driver=self.__class__.__name__,
|
|
||||||
version=self.get_version())
|
|
||||||
super(VSPFCDriver, self).__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
self.configuration.append_config_values(common.common_opts)
|
|
||||||
self.configuration.append_config_values(fc_opts)
|
|
||||||
self.common = utils.import_object(
|
|
||||||
self.configuration, _DRIVER_INFO, kwargs.get('db'))
|
|
||||||
|
|
||||||
def check_for_setup_error(self):
|
|
||||||
"""Error are checked in do_setup() instead of this method."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@utils.output_start_end_log
|
|
||||||
def create_volume(self, volume):
|
|
||||||
"""Create a volume and return its properties."""
|
|
||||||
return self.common.create_volume(volume)
|
|
||||||
|
|
||||||
@utils.output_start_end_log
|
|
||||||
def create_volume_from_snapshot(self, volume, snapshot):
|
|
||||||
"""Create a volume from a snapshot and return its properties."""
|
|
||||||
return self.common.create_volume_from_snapshot(volume, snapshot)
|
|
||||||
|
|
||||||
@utils.output_start_end_log
|
|
||||||
def create_cloned_volume(self, volume, src_vref):
|
|
||||||
"""Create a clone of the specified volume and return its properties."""
|
|
||||||
return self.common.create_cloned_volume(volume, src_vref)
|
|
||||||
|
|
||||||
@utils.output_start_end_log
|
|
||||||
def delete_volume(self, volume):
|
|
||||||
"""Delete the specified volume."""
|
|
||||||
self.common.delete_volume(volume)
|
|
||||||
|
|
||||||
@utils.output_start_end_log
|
|
||||||
def create_snapshot(self, snapshot):
|
|
||||||
"""Create a snapshot from a volume and return its properties."""
|
|
||||||
return self.common.create_snapshot(snapshot)
|
|
||||||
|
|
||||||
@utils.output_start_end_log
|
|
||||||
def delete_snapshot(self, snapshot):
|
|
||||||
"""Delete the specified snapshot."""
|
|
||||||
self.common.delete_snapshot(snapshot)
|
|
||||||
|
|
||||||
def get_volume_stats(self, refresh=False):
|
|
||||||
"""Return properties, capabilities and current states of the driver."""
|
|
||||||
return self.common.get_volume_stats(refresh)
|
|
||||||
|
|
||||||
@utils.output_start_end_log
|
|
||||||
def update_migrated_volume(
|
|
||||||
self, ctxt, volume, new_volume, original_volume_status):
|
|
||||||
"""Do any remaining jobs after migration."""
|
|
||||||
self.common.discard_zero_page(new_volume)
|
|
||||||
super(VSPFCDriver, self).update_migrated_volume(
|
|
||||||
ctxt, volume, new_volume, original_volume_status)
|
|
||||||
|
|
||||||
@utils.output_start_end_log
|
|
||||||
def copy_image_to_volume(self, context, volume, image_service, image_id):
|
|
||||||
"""Fetch the image from image_service and write it to the volume."""
|
|
||||||
super(VSPFCDriver, self).copy_image_to_volume(
|
|
||||||
context, volume, image_service, image_id)
|
|
||||||
self.common.discard_zero_page(volume)
|
|
||||||
|
|
||||||
@utils.output_start_end_log
|
|
||||||
def extend_volume(self, volume, new_size):
|
|
||||||
"""Extend the specified volume to the specified size."""
|
|
||||||
self.common.extend_volume(volume, new_size)
|
|
||||||
|
|
||||||
@utils.output_start_end_log
|
|
||||||
def manage_existing(self, volume, existing_ref):
|
|
||||||
"""Return volume properties which Cinder needs to manage the volume."""
|
|
||||||
return self.common.manage_existing(existing_ref)
|
|
||||||
|
|
||||||
@utils.output_start_end_log
|
|
||||||
def manage_existing_get_size(self, volume, existing_ref):
|
|
||||||
"""Return the size[GB] of the specified volume."""
|
|
||||||
return self.common.manage_existing_get_size(existing_ref)
|
|
||||||
|
|
||||||
@utils.output_start_end_log
|
|
||||||
def unmanage(self, volume):
|
|
||||||
"""Prepare the volume for removing it from Cinder management."""
|
|
||||||
self.common.unmanage(volume)
|
|
||||||
|
|
||||||
@utils.output_start_end_log
|
|
||||||
def do_setup(self, context):
|
|
||||||
"""Prepare for the startup of the driver."""
|
|
||||||
self.common.do_setup(context)
|
|
||||||
|
|
||||||
def ensure_export(self, context, volume):
|
|
||||||
"""Synchronously recreate an export for a volume."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
def create_export(self, context, volume, connector):
|
|
||||||
"""Export the volume."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
def remove_export(self, context, volume):
|
|
||||||
"""Remove an export for a volume."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@utils.output_start_end_log
|
|
||||||
def initialize_connection(self, volume, connector):
|
|
||||||
"""Initialize connection between the server and the volume."""
|
|
||||||
return self.common.initialize_connection(volume, connector)
|
|
||||||
|
|
||||||
@utils.output_start_end_log
|
|
||||||
def terminate_connection(self, volume, connector, **kwargs):
|
|
||||||
"""Terminate connection between the server and the volume."""
|
|
||||||
self.common.terminate_connection(volume, connector)
|
|
File diff suppressed because it is too large
Load Diff
@ -1,189 +0,0 @@
|
|||||||
# Copyright (C) 2016, Hitachi, Ltd.
|
|
||||||
#
|
|
||||||
# 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.
|
|
||||||
#
|
|
||||||
"""HORCM interface fibre channel module for Hitachi VSP Driver."""
|
|
||||||
|
|
||||||
import re
|
|
||||||
|
|
||||||
from oslo_log import log as logging
|
|
||||||
|
|
||||||
from cinder import exception
|
|
||||||
from cinder.volume.drivers.hitachi import vsp_horcm as horcm
|
|
||||||
from cinder.volume.drivers.hitachi import vsp_utils as utils
|
|
||||||
from cinder.zonemanager import utils as fczm_utils
|
|
||||||
|
|
||||||
_FC_LINUX_MODE_OPTS = ['-host_mode', 'LINUX']
|
|
||||||
_HOST_GROUPS_PATTERN = re.compile(
|
|
||||||
r"^CL\w-\w+ +(?P<gid>\d+) +%s(?!pair00 )\S* +\d+ " % utils.TARGET_PREFIX,
|
|
||||||
re.M)
|
|
||||||
_FC_PORT_PATTERN = re.compile(
|
|
||||||
(r"^(CL\w-\w)\w* +(?:FIBRE|FCoE) +TAR +\w+ +\w+ +\w +\w+ +Y +"
|
|
||||||
r"\d+ +\d+ +(\w{16})"), re.M)
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
|
||||||
MSG = utils.VSPMsg
|
|
||||||
|
|
||||||
|
|
||||||
class VSPHORCMFC(horcm.VSPHORCM):
|
|
||||||
"""HORCM interface fibre channel class for Hitachi VSP Driver."""
|
|
||||||
|
|
||||||
def __init__(self, conf, storage_protocol, db):
|
|
||||||
"""Initialize instance variables."""
|
|
||||||
super(VSPHORCMFC, self).__init__(conf, storage_protocol, db)
|
|
||||||
self._lookup_service = fczm_utils.create_lookup_service()
|
|
||||||
|
|
||||||
def connect_storage(self):
|
|
||||||
"""Prepare for using the storage."""
|
|
||||||
target_ports = self.conf.vsp_target_ports
|
|
||||||
compute_target_ports = self.conf.vsp_compute_target_ports
|
|
||||||
pair_target_ports = self.conf.vsp_horcm_pair_target_ports
|
|
||||||
|
|
||||||
super(VSPHORCMFC, self).connect_storage()
|
|
||||||
result = self.run_raidcom('get', 'port')
|
|
||||||
for port, wwn in _FC_PORT_PATTERN.findall(result[1]):
|
|
||||||
if target_ports and port in target_ports:
|
|
||||||
self.storage_info['controller_ports'].append(port)
|
|
||||||
self.storage_info['wwns'][port] = wwn
|
|
||||||
if compute_target_ports and port in compute_target_ports:
|
|
||||||
self.storage_info['compute_ports'].append(port)
|
|
||||||
self.storage_info['wwns'][port] = wwn
|
|
||||||
if pair_target_ports and port in pair_target_ports:
|
|
||||||
self.storage_info['pair_ports'].append(port)
|
|
||||||
|
|
||||||
self.check_ports_info()
|
|
||||||
if pair_target_ports and not self.storage_info['pair_ports']:
|
|
||||||
msg = utils.output_log(MSG.RESOURCE_NOT_FOUND,
|
|
||||||
resource="Pair target ports")
|
|
||||||
raise exception.VSPError(msg)
|
|
||||||
utils.output_log(MSG.SET_CONFIG_VALUE,
|
|
||||||
object='pair target port list',
|
|
||||||
value=self.storage_info['pair_ports'])
|
|
||||||
utils.output_log(MSG.SET_CONFIG_VALUE, object='port-wwn list',
|
|
||||||
value=self.storage_info['wwns'])
|
|
||||||
|
|
||||||
def create_target_to_storage(self, port, connector, hba_ids):
|
|
||||||
"""Create a host group on the specified port."""
|
|
||||||
wwpns = self.get_hba_ids_from_connector(connector)
|
|
||||||
target_name = utils.TARGET_PREFIX + min(wwpns)
|
|
||||||
try:
|
|
||||||
result = self.run_raidcom(
|
|
||||||
'add', 'host_grp', '-port', port, '-host_grp_name',
|
|
||||||
target_name)
|
|
||||||
except exception.VSPError:
|
|
||||||
result = self.run_raidcom('get', 'host_grp', '-port', port)
|
|
||||||
hostgroup_pt = re.compile(
|
|
||||||
r"^CL\w-\w+ +(?P<gid>\d+) +%s +\d+ " %
|
|
||||||
target_name, re.M)
|
|
||||||
gid = hostgroup_pt.findall(result[1])
|
|
||||||
if gid:
|
|
||||||
return target_name, gid[0]
|
|
||||||
else:
|
|
||||||
raise
|
|
||||||
return target_name, horcm.find_value(result[1], 'gid')
|
|
||||||
|
|
||||||
def set_hba_ids(self, port, gid, hba_ids):
|
|
||||||
"""Connect all specified HBAs with the specified port."""
|
|
||||||
registered_wwns = []
|
|
||||||
for wwn in hba_ids:
|
|
||||||
try:
|
|
||||||
self.run_raidcom(
|
|
||||||
'add', 'hba_wwn', '-port',
|
|
||||||
'-'.join([port, gid]), '-hba_wwn', wwn)
|
|
||||||
registered_wwns.append(wwn)
|
|
||||||
except exception.VSPError:
|
|
||||||
utils.output_log(MSG.ADD_HBA_WWN_FAILED, port=port, gid=gid,
|
|
||||||
wwn=wwn)
|
|
||||||
if not registered_wwns:
|
|
||||||
msg = utils.output_log(MSG.NO_HBA_WWN_ADDED_TO_HOST_GRP, port=port,
|
|
||||||
gid=gid)
|
|
||||||
raise exception.VSPError(msg)
|
|
||||||
|
|
||||||
def set_target_mode(self, port, gid):
|
|
||||||
"""Configure the host group to meet the environment."""
|
|
||||||
self.run_raidcom(
|
|
||||||
'modify', 'host_grp', '-port',
|
|
||||||
'-'.join([port, gid]), *_FC_LINUX_MODE_OPTS,
|
|
||||||
success_code=horcm.ALL_EXIT_CODE)
|
|
||||||
|
|
||||||
def find_targets_from_storage(self, targets, connector, target_ports):
|
|
||||||
"""Find mapped ports, memorize them and return unmapped port count."""
|
|
||||||
nr_not_found = 0
|
|
||||||
old_target_name = None
|
|
||||||
if 'ip' in connector:
|
|
||||||
old_target_name = utils.TARGET_PREFIX + connector['ip']
|
|
||||||
success_code = horcm.HORCM_EXIT_CODE.union([horcm.EX_ENOOBJ])
|
|
||||||
wwpns = self.get_hba_ids_from_connector(connector)
|
|
||||||
wwpns_pattern = re.compile(
|
|
||||||
r'^CL\w-\w+ +\d+ +\S+ +(%s) ' % '|'.join(wwpns), re.M | re.I)
|
|
||||||
target_name = utils.TARGET_PREFIX + min(wwpns)
|
|
||||||
|
|
||||||
for port in target_ports:
|
|
||||||
targets['info'][port] = False
|
|
||||||
|
|
||||||
result = self.run_raidcom(
|
|
||||||
'get', 'hba_wwn', '-port', port, target_name,
|
|
||||||
success_code=success_code)
|
|
||||||
wwpns = wwpns_pattern.findall(result[1])
|
|
||||||
if not wwpns and old_target_name:
|
|
||||||
result = self.run_raidcom(
|
|
||||||
'get', 'hba_wwn', '-port', port, old_target_name,
|
|
||||||
success_code=success_code)
|
|
||||||
wwpns = wwpns_pattern.findall(result[1])
|
|
||||||
if wwpns:
|
|
||||||
gid = result[1].splitlines()[1].split()[1]
|
|
||||||
targets['info'][port] = True
|
|
||||||
targets['list'].append((port, gid))
|
|
||||||
LOG.debug(
|
|
||||||
'Found wwpns in host group immediately. '
|
|
||||||
'(port: %(port)s, gid: %(gid)s, wwpns: %(wwpns)s)',
|
|
||||||
{'port': port, 'gid': gid, 'wwpns': wwpns})
|
|
||||||
continue
|
|
||||||
|
|
||||||
result = self.run_raidcom(
|
|
||||||
'get', 'host_grp', '-port', port)
|
|
||||||
for gid in _HOST_GROUPS_PATTERN.findall(result[1]):
|
|
||||||
result = self.run_raidcom(
|
|
||||||
'get', 'hba_wwn', '-port', '-'.join([port, gid]))
|
|
||||||
wwpns = wwpns_pattern.findall(result[1])
|
|
||||||
if wwpns:
|
|
||||||
targets['info'][port] = True
|
|
||||||
targets['list'].append((port, gid))
|
|
||||||
LOG.debug(
|
|
||||||
'Found wwpns in host group. (port: %(port)s, '
|
|
||||||
'gid: %(gid)s, wwpns: %(wwpns)s)',
|
|
||||||
{'port': port, 'gid': gid, 'wwpns': wwpns})
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
nr_not_found += 1
|
|
||||||
|
|
||||||
return nr_not_found
|
|
||||||
|
|
||||||
@fczm_utils.add_fc_zone
|
|
||||||
def initialize_connection(self, volume, connector):
|
|
||||||
"""Initialize connection between the server and the volume."""
|
|
||||||
conn_info = super(VSPHORCMFC, self).initialize_connection(
|
|
||||||
volume, connector)
|
|
||||||
if self.conf.vsp_zoning_request:
|
|
||||||
utils.update_conn_info(conn_info, connector, self._lookup_service)
|
|
||||||
return conn_info
|
|
||||||
|
|
||||||
@fczm_utils.remove_fc_zone
|
|
||||||
def terminate_connection(self, volume, connector):
|
|
||||||
"""Terminate connection between the server and the volume."""
|
|
||||||
conn_info = super(VSPHORCMFC, self).terminate_connection(
|
|
||||||
volume, connector)
|
|
||||||
if self.conf.vsp_zoning_request and (
|
|
||||||
conn_info and conn_info['data']['target_wwn']):
|
|
||||||
utils.update_conn_info(conn_info, connector, self._lookup_service)
|
|
||||||
return conn_info
|
|
@ -1,191 +0,0 @@
|
|||||||
# Copyright (C) 2016, Hitachi, Ltd.
|
|
||||||
#
|
|
||||||
# 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.
|
|
||||||
#
|
|
||||||
"""HORCM interface iSCSI module for Hitachi VSP Driver."""
|
|
||||||
|
|
||||||
import re
|
|
||||||
|
|
||||||
from oslo_log import log as logging
|
|
||||||
|
|
||||||
from cinder import exception
|
|
||||||
from cinder.volume.drivers.hitachi import vsp_horcm as horcm
|
|
||||||
from cinder.volume.drivers.hitachi import vsp_utils as utils
|
|
||||||
|
|
||||||
_ISCSI_LINUX_MODE_OPTS = ['-host_mode', 'LINUX']
|
|
||||||
_ISCSI_HOST_MODE_OPT = '-host_mode_opt'
|
|
||||||
_ISCSI_HMO_REPORT_FULL_PORTAL = 83
|
|
||||||
_ISCSI_TARGETS_PATTERN = re.compile(
|
|
||||||
(r"^CL\w-\w+ +(?P<gid>\d+) +%s(?!pair00 )\S* +(?P<iqn>\S+) +"
|
|
||||||
r"\w+ +\w +\d+ ") % utils.TARGET_PREFIX, re.M)
|
|
||||||
_ISCSI_PORT_PATTERN = re.compile(
|
|
||||||
r"^(CL\w-\w)\w* +ISCSI +TAR +\w+ +\w+ +\w +\w+ +Y ", re.M)
|
|
||||||
_ISCSI_IPV4_ADDR_PATTERN = re.compile(
|
|
||||||
r"^IPV4_ADDR +: +(?P<ipv4_addr>\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$", re.M)
|
|
||||||
_ISCSI_TCP_PORT_PATTERN = re.compile(
|
|
||||||
r'^TCP_PORT\ +:\ +(?P<tcp_port>\d+)$', re.M)
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
|
||||||
MSG = utils.VSPMsg
|
|
||||||
|
|
||||||
|
|
||||||
class VSPHORCMISCSI(horcm.VSPHORCM):
|
|
||||||
"""HORCM interface iscsi class for Hitachi VSP Driver."""
|
|
||||||
|
|
||||||
def connect_storage(self):
|
|
||||||
"""Prepare for using the storage."""
|
|
||||||
target_ports = self.conf.vsp_target_ports
|
|
||||||
compute_target_ports = self.conf.vsp_compute_target_ports
|
|
||||||
pair_target_ports = self.conf.vsp_horcm_pair_target_ports
|
|
||||||
|
|
||||||
super(VSPHORCMISCSI, self).connect_storage()
|
|
||||||
result = self.run_raidcom('get', 'port')
|
|
||||||
for port in _ISCSI_PORT_PATTERN.findall(result[1]):
|
|
||||||
if (target_ports and port in target_ports and
|
|
||||||
self._set_target_portal(port)):
|
|
||||||
self.storage_info['controller_ports'].append(port)
|
|
||||||
if (compute_target_ports and port in compute_target_ports and
|
|
||||||
(port in self.storage_info['portals'] or
|
|
||||||
self._set_target_portal(port))):
|
|
||||||
self.storage_info['compute_ports'].append(port)
|
|
||||||
if pair_target_ports and port in pair_target_ports:
|
|
||||||
self.storage_info['pair_ports'].append(port)
|
|
||||||
|
|
||||||
self.check_ports_info()
|
|
||||||
if pair_target_ports and not self.storage_info['pair_ports']:
|
|
||||||
msg = utils.output_log(MSG.RESOURCE_NOT_FOUND,
|
|
||||||
resource="Pair target ports")
|
|
||||||
raise exception.VSPError(msg)
|
|
||||||
utils.output_log(MSG.SET_CONFIG_VALUE,
|
|
||||||
object='pair target port list',
|
|
||||||
value=self.storage_info['pair_ports'])
|
|
||||||
utils.output_log(MSG.SET_CONFIG_VALUE,
|
|
||||||
object='port-<IP address:port> list',
|
|
||||||
value=self.storage_info['portals'])
|
|
||||||
|
|
||||||
def _set_target_portal(self, port):
|
|
||||||
"""Get port info and store it in an instance variable."""
|
|
||||||
ipv4_addr = None
|
|
||||||
tcp_port = None
|
|
||||||
result = self.run_raidcom(
|
|
||||||
'get', 'port', '-port', port, '-key', 'opt')
|
|
||||||
match = _ISCSI_IPV4_ADDR_PATTERN.search(result[1])
|
|
||||||
if match:
|
|
||||||
ipv4_addr = match.group('ipv4_addr')
|
|
||||||
match = _ISCSI_TCP_PORT_PATTERN.search(result[1])
|
|
||||||
if match:
|
|
||||||
tcp_port = match.group('tcp_port')
|
|
||||||
if not ipv4_addr or not tcp_port:
|
|
||||||
return False
|
|
||||||
self.storage_info['portals'][port] = ':'.join(
|
|
||||||
[ipv4_addr, tcp_port])
|
|
||||||
return True
|
|
||||||
|
|
||||||
def create_target_to_storage(self, port, connector, hba_ids):
|
|
||||||
"""Create an iSCSI target on the specified port."""
|
|
||||||
target_name = utils.TARGET_PREFIX + connector['ip']
|
|
||||||
args = [
|
|
||||||
'add', 'host_grp', '-port', port, '-host_grp_name', target_name]
|
|
||||||
if hba_ids:
|
|
||||||
args.extend(['-iscsi_name', hba_ids + utils.TARGET_IQN_SUFFIX])
|
|
||||||
try:
|
|
||||||
result = self.run_raidcom(*args)
|
|
||||||
except exception.VSPError:
|
|
||||||
result = self.run_raidcom('get', 'host_grp', '-port', port)
|
|
||||||
hostgroup_pt = re.compile(
|
|
||||||
r"^CL\w-\w+ +(?P<gid>\d+) +%s +\S+ " %
|
|
||||||
target_name.replace('.', r'\.'), re.M)
|
|
||||||
gid = hostgroup_pt.findall(result[1])
|
|
||||||
if gid:
|
|
||||||
return target_name, gid[0]
|
|
||||||
else:
|
|
||||||
raise
|
|
||||||
return target_name, horcm.find_value(result[1], 'gid')
|
|
||||||
|
|
||||||
def set_hba_ids(self, port, gid, hba_ids):
|
|
||||||
"""Connect the specified HBA with the specified port."""
|
|
||||||
self.run_raidcom(
|
|
||||||
'add', 'hba_iscsi', '-port', '-'.join([port, gid]),
|
|
||||||
'-hba_iscsi_name', hba_ids)
|
|
||||||
|
|
||||||
def set_target_mode(self, port, gid):
|
|
||||||
"""Configure the iSCSI target to meet the environment."""
|
|
||||||
hostmode_setting = []
|
|
||||||
hostmode_setting[:] = _ISCSI_LINUX_MODE_OPTS
|
|
||||||
hostmode_setting.append(_ISCSI_HOST_MODE_OPT)
|
|
||||||
hostmode_setting.append(_ISCSI_HMO_REPORT_FULL_PORTAL)
|
|
||||||
self.run_raidcom(
|
|
||||||
'modify', 'host_grp', '-port',
|
|
||||||
'-'.join([port, gid]), *hostmode_setting)
|
|
||||||
|
|
||||||
def find_targets_from_storage(self, targets, connector, target_ports):
|
|
||||||
"""Find mapped ports, memorize them and return unmapped port count."""
|
|
||||||
nr_not_found = 0
|
|
||||||
target_name = utils.TARGET_PREFIX + connector['ip']
|
|
||||||
success_code = horcm.HORCM_EXIT_CODE.union([horcm.EX_ENOOBJ])
|
|
||||||
iqn = self.get_hba_ids_from_connector(connector)
|
|
||||||
iqn_pattern = re.compile(
|
|
||||||
r'^CL\w-\w+ +\d+ +\S+ +%s ' % iqn, re.M)
|
|
||||||
|
|
||||||
for port in target_ports:
|
|
||||||
targets['info'][port] = False
|
|
||||||
|
|
||||||
result = self.run_raidcom(
|
|
||||||
'get', 'hba_iscsi', '-port', port, target_name,
|
|
||||||
success_code=success_code)
|
|
||||||
if iqn_pattern.search(result[1]):
|
|
||||||
gid = result[1].splitlines()[1].split()[1]
|
|
||||||
targets['info'][port] = True
|
|
||||||
targets['list'].append((port, gid))
|
|
||||||
continue
|
|
||||||
|
|
||||||
result = self.run_raidcom(
|
|
||||||
'get', 'host_grp', '-port', port)
|
|
||||||
for gid, iqn in _ISCSI_TARGETS_PATTERN.findall(result[1]):
|
|
||||||
result = self.run_raidcom(
|
|
||||||
'get', 'hba_iscsi', '-port', '-'.join([port, gid]))
|
|
||||||
if iqn_pattern.search(result[1]):
|
|
||||||
targets['info'][port] = True
|
|
||||||
targets['list'].append((port, gid))
|
|
||||||
targets['iqns'][(port, gid)] = iqn
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
nr_not_found += 1
|
|
||||||
|
|
||||||
return nr_not_found
|
|
||||||
|
|
||||||
def get_properties_iscsi(self, targets, multipath):
|
|
||||||
"""Check if specified iSCSI targets exist and store their IQNs."""
|
|
||||||
if not multipath:
|
|
||||||
target_list = targets['list'][:1]
|
|
||||||
else:
|
|
||||||
target_list = targets['list'][:]
|
|
||||||
|
|
||||||
for target in target_list:
|
|
||||||
if target not in targets['iqns']:
|
|
||||||
port, gid = target
|
|
||||||
result = self.run_raidcom('get', 'host_grp', '-port', port)
|
|
||||||
match = re.search(
|
|
||||||
r"^CL\w-\w+ +%s +\S+ +(?P<iqn>\S+) +\w+ +\w +\d+ " % gid,
|
|
||||||
result[1], re.M)
|
|
||||||
if not match:
|
|
||||||
msg = utils.output_log(MSG.RESOURCE_NOT_FOUND,
|
|
||||||
resource='Target IQN')
|
|
||||||
raise exception.VSPError(msg)
|
|
||||||
targets['iqns'][target] = match.group('iqn')
|
|
||||||
LOG.debug('Found iqn of the iSCSI target. (port: %(port)s, '
|
|
||||||
'gid: %(gid)s, target iqn: %(iqn)s)',
|
|
||||||
{'port': port, 'gid': gid,
|
|
||||||
'iqn': match.group('iqn')})
|
|
||||||
return super(VSPHORCMISCSI, self).get_properties_iscsi(
|
|
||||||
targets, multipath)
|
|
@ -1,188 +0,0 @@
|
|||||||
# Copyright (C) 2016, Hitachi, Ltd.
|
|
||||||
#
|
|
||||||
# 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.
|
|
||||||
#
|
|
||||||
"""iSCSI module for Hitachi VSP Driver."""
|
|
||||||
|
|
||||||
from oslo_config import cfg
|
|
||||||
|
|
||||||
from cinder import interface
|
|
||||||
from cinder.volume import configuration
|
|
||||||
from cinder.volume import driver
|
|
||||||
from cinder.volume.drivers.hitachi import vsp_common as common
|
|
||||||
from cinder.volume.drivers.hitachi import vsp_utils as utils
|
|
||||||
|
|
||||||
iscsi_opts = [
|
|
||||||
cfg.BoolOpt(
|
|
||||||
'vsp_use_chap_auth',
|
|
||||||
default=False,
|
|
||||||
help='If True, CHAP authentication will be applied to communication '
|
|
||||||
'between hosts and any of the iSCSI targets on the storage ports.'),
|
|
||||||
cfg.StrOpt(
|
|
||||||
'vsp_auth_user',
|
|
||||||
help='Name of the user used for CHAP authentication performed in '
|
|
||||||
'communication between hosts and iSCSI targets on the storage ports.'),
|
|
||||||
cfg.StrOpt(
|
|
||||||
'vsp_auth_password',
|
|
||||||
secret=True,
|
|
||||||
help='Password corresponding to vsp_auth_user.'),
|
|
||||||
]
|
|
||||||
|
|
||||||
MSG = utils.VSPMsg
|
|
||||||
|
|
||||||
_DRIVER_INFO = {
|
|
||||||
'proto': 'iSCSI',
|
|
||||||
'hba_id': 'initiator',
|
|
||||||
'hba_id_type': 'iSCSI initiator IQN',
|
|
||||||
'msg_id': {
|
|
||||||
'target': MSG.CREATE_ISCSI_TARGET_FAILED,
|
|
||||||
},
|
|
||||||
'volume_backend_name': utils.DRIVER_PREFIX + 'iSCSI',
|
|
||||||
'volume_opts': iscsi_opts,
|
|
||||||
'volume_type': 'iscsi',
|
|
||||||
}
|
|
||||||
|
|
||||||
CONF = cfg.CONF
|
|
||||||
CONF.register_opts(iscsi_opts, group=configuration.SHARED_CONF_GROUP)
|
|
||||||
|
|
||||||
|
|
||||||
@interface.volumedriver
|
|
||||||
class VSPISCSIDriver(driver.ISCSIDriver):
|
|
||||||
"""iSCSI class for Hitachi VSP Driver.
|
|
||||||
|
|
||||||
Version history:
|
|
||||||
|
|
||||||
.. code-block:: none
|
|
||||||
|
|
||||||
1.0.0 - Initial driver.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
VERSION = common.VERSION
|
|
||||||
|
|
||||||
# ThirdPartySystems wiki page
|
|
||||||
CI_WIKI_NAME = "Hitachi_VSP_CI"
|
|
||||||
|
|
||||||
SUPPORTED = False
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
"""Initialize instance variables."""
|
|
||||||
utils.output_log(MSG.DRIVER_INITIALIZATION_START,
|
|
||||||
driver=self.__class__.__name__,
|
|
||||||
version=self.get_version())
|
|
||||||
super(VSPISCSIDriver, self).__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
self.configuration.append_config_values(common.common_opts)
|
|
||||||
self.configuration.append_config_values(iscsi_opts)
|
|
||||||
self.common = utils.import_object(
|
|
||||||
self.configuration, _DRIVER_INFO, kwargs.get('db'))
|
|
||||||
|
|
||||||
def check_for_setup_error(self):
|
|
||||||
"""Error are checked in do_setup() instead of this method."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@utils.output_start_end_log
|
|
||||||
def create_volume(self, volume):
|
|
||||||
"""Create a volume and return its properties."""
|
|
||||||
return self.common.create_volume(volume)
|
|
||||||
|
|
||||||
@utils.output_start_end_log
|
|
||||||
def create_volume_from_snapshot(self, volume, snapshot):
|
|
||||||
"""Create a volume from a snapshot and return its properties."""
|
|
||||||
return self.common.create_volume_from_snapshot(volume, snapshot)
|
|
||||||
|
|
||||||
@utils.output_start_end_log
|
|
||||||
def create_cloned_volume(self, volume, src_vref):
|
|
||||||
"""Create a clone of the specified volume and return its properties."""
|
|
||||||
return self.common.create_cloned_volume(volume, src_vref)
|
|
||||||
|
|
||||||
@utils.output_start_end_log
|
|
||||||
def delete_volume(self, volume):
|
|
||||||
"""Delete the specified volume."""
|
|
||||||
self.common.delete_volume(volume)
|
|
||||||
|
|
||||||
@utils.output_start_end_log
|
|
||||||
def create_snapshot(self, snapshot):
|
|
||||||
"""Create a snapshot from a volume and return its properties."""
|
|
||||||
return self.common.create_snapshot(snapshot)
|
|
||||||
|
|
||||||
@utils.output_start_end_log
|
|
||||||
def delete_snapshot(self, snapshot):
|
|
||||||
"""Delete the specified snapshot."""
|
|
||||||
self.common.delete_snapshot(snapshot)
|
|
||||||
|
|
||||||
def get_volume_stats(self, refresh=False):
|
|
||||||
"""Return properties, capabilities and current states of the driver."""
|
|
||||||
return self.common.get_volume_stats(refresh)
|
|
||||||
|
|
||||||
@utils.output_start_end_log
|
|
||||||
def update_migrated_volume(
|
|
||||||
self, ctxt, volume, new_volume, original_volume_status):
|
|
||||||
"""Do any remaining jobs after migration."""
|
|
||||||
self.common.discard_zero_page(new_volume)
|
|
||||||
super(VSPISCSIDriver, self).update_migrated_volume(
|
|
||||||
ctxt, volume, new_volume, original_volume_status)
|
|
||||||
|
|
||||||
@utils.output_start_end_log
|
|
||||||
def copy_image_to_volume(self, context, volume, image_service, image_id):
|
|
||||||
"""Fetch the image from image_service and write it to the volume."""
|
|
||||||
super(VSPISCSIDriver, self).copy_image_to_volume(
|
|
||||||
context, volume, image_service, image_id)
|
|
||||||
self.common.discard_zero_page(volume)
|
|
||||||
|
|
||||||
@utils.output_start_end_log
|
|
||||||
def extend_volume(self, volume, new_size):
|
|
||||||
"""Extend the specified volume to the specified size."""
|
|
||||||
self.common.extend_volume(volume, new_size)
|
|
||||||
|
|
||||||
@utils.output_start_end_log
|
|
||||||
def manage_existing(self, volume, existing_ref):
|
|
||||||
"""Return volume properties which Cinder needs to manage the volume."""
|
|
||||||
return self.common.manage_existing(existing_ref)
|
|
||||||
|
|
||||||
@utils.output_start_end_log
|
|
||||||
def manage_existing_get_size(self, volume, existing_ref):
|
|
||||||
"""Return the size[GB] of the specified volume."""
|
|
||||||
return self.common.manage_existing_get_size(existing_ref)
|
|
||||||
|
|
||||||
@utils.output_start_end_log
|
|
||||||
def unmanage(self, volume):
|
|
||||||
"""Prepare the volume for removing it from Cinder management."""
|
|
||||||
self.common.unmanage(volume)
|
|
||||||
|
|
||||||
@utils.output_start_end_log
|
|
||||||
def do_setup(self, context):
|
|
||||||
"""Prepare for the startup of the driver."""
|
|
||||||
self.common.do_setup(context)
|
|
||||||
|
|
||||||
def ensure_export(self, context, volume):
|
|
||||||
"""Synchronously recreate an export for a volume."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
def create_export(self, context, volume, connector):
|
|
||||||
"""Export the volume."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
def remove_export(self, context, volume):
|
|
||||||
"""Remove an export for a volume."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@utils.output_start_end_log
|
|
||||||
def initialize_connection(self, volume, connector):
|
|
||||||
"""Initialize connection between the server and the volume."""
|
|
||||||
return self.common.initialize_connection(volume, connector)
|
|
||||||
|
|
||||||
@utils.output_start_end_log
|
|
||||||
def terminate_connection(self, volume, connector, **kwargs):
|
|
||||||
"""Terminate connection between the server and the volume."""
|
|
||||||
self.common.terminate_connection(volume, connector)
|
|
@ -1,667 +0,0 @@
|
|||||||
# Copyright (C) 2016, Hitachi, Ltd.
|
|
||||||
#
|
|
||||||
# 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.
|
|
||||||
#
|
|
||||||
"""Utility module for Hitachi VSP Driver."""
|
|
||||||
|
|
||||||
import functools
|
|
||||||
import inspect
|
|
||||||
import logging as base_logging
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
|
|
||||||
import enum
|
|
||||||
from oslo_concurrency import processutils as putils
|
|
||||||
from oslo_config import cfg
|
|
||||||
from oslo_log import log as logging
|
|
||||||
from oslo_utils import excutils
|
|
||||||
from oslo_utils import importutils
|
|
||||||
from oslo_utils import strutils
|
|
||||||
from oslo_utils import timeutils
|
|
||||||
from oslo_utils import units
|
|
||||||
import six
|
|
||||||
|
|
||||||
from cinder import exception
|
|
||||||
from cinder import utils as cinder_utils
|
|
||||||
|
|
||||||
|
|
||||||
_DRIVER_DIR = 'cinder.volume.drivers.hitachi'
|
|
||||||
|
|
||||||
_DRIVERS = {
|
|
||||||
'HORCM': {
|
|
||||||
'FC': 'vsp_horcm_fc.VSPHORCMFC',
|
|
||||||
'iSCSI': 'vsp_horcm_iscsi.VSPHORCMISCSI',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
DRIVER_PREFIX = 'VSP'
|
|
||||||
TARGET_PREFIX = 'HBSD-'
|
|
||||||
TARGET_IQN_SUFFIX = '.hbsd-target'
|
|
||||||
GIGABYTE_PER_BLOCK_SIZE = units.Gi / 512
|
|
||||||
|
|
||||||
MAX_PROCESS_WAITTIME = 24 * 60 * 60
|
|
||||||
DEFAULT_PROCESS_WAITTIME = 15 * 60
|
|
||||||
|
|
||||||
NORMAL_LDEV_TYPE = 'Normal'
|
|
||||||
NVOL_LDEV_TYPE = 'DP-VOL'
|
|
||||||
|
|
||||||
FULL = 'Full copy'
|
|
||||||
THIN = 'Thin copy'
|
|
||||||
|
|
||||||
INFO_SUFFIX = 'I'
|
|
||||||
WARNING_SUFFIX = 'W'
|
|
||||||
ERROR_SUFFIX = 'E'
|
|
||||||
|
|
||||||
PORT_ID_LENGTH = 5
|
|
||||||
|
|
||||||
|
|
||||||
@enum.unique
|
|
||||||
class VSPMsg(enum.Enum):
|
|
||||||
"""messages for Hitachi VSP Driver."""
|
|
||||||
|
|
||||||
METHOD_START = {
|
|
||||||
'msg_id': 0,
|
|
||||||
'loglevel': base_logging.INFO,
|
|
||||||
'msg': '%(method)s starts. (config_group: %(config_group)s)',
|
|
||||||
'suffix': INFO_SUFFIX
|
|
||||||
}
|
|
||||||
OUTPUT_PARAMETER_VALUES = {
|
|
||||||
'msg_id': 1,
|
|
||||||
'loglevel': base_logging.INFO,
|
|
||||||
'msg': 'The parameter of the storage backend. (config_group: '
|
|
||||||
'%(config_group)s)',
|
|
||||||
'suffix': INFO_SUFFIX
|
|
||||||
}
|
|
||||||
METHOD_END = {
|
|
||||||
'msg_id': 2,
|
|
||||||
'loglevel': base_logging.INFO,
|
|
||||||
'msg': '%(method)s ended. (config_group: %(config_group)s)',
|
|
||||||
'suffix': INFO_SUFFIX
|
|
||||||
}
|
|
||||||
DRIVER_READY_FOR_USE = {
|
|
||||||
'msg_id': 3,
|
|
||||||
'loglevel': base_logging.INFO,
|
|
||||||
'msg': 'The storage backend can be used. (config_group: '
|
|
||||||
'%(config_group)s)',
|
|
||||||
'suffix': INFO_SUFFIX
|
|
||||||
}
|
|
||||||
DRIVER_INITIALIZATION_START = {
|
|
||||||
'msg_id': 4,
|
|
||||||
'loglevel': base_logging.INFO,
|
|
||||||
'msg': 'Initialization of %(driver)s %(version)s started.',
|
|
||||||
'suffix': INFO_SUFFIX
|
|
||||||
}
|
|
||||||
SET_CONFIG_VALUE = {
|
|
||||||
'msg_id': 5,
|
|
||||||
'loglevel': base_logging.INFO,
|
|
||||||
'msg': 'Set %(object)s to %(value)s.',
|
|
||||||
'suffix': INFO_SUFFIX
|
|
||||||
}
|
|
||||||
OBJECT_CREATED = {
|
|
||||||
'msg_id': 6,
|
|
||||||
'loglevel': base_logging.INFO,
|
|
||||||
'msg': 'Created %(object)s. (%(details)s)',
|
|
||||||
'suffix': INFO_SUFFIX
|
|
||||||
}
|
|
||||||
INVALID_LDEV_FOR_UNMAPPING = {
|
|
||||||
'msg_id': 302,
|
|
||||||
'loglevel': base_logging.WARNING,
|
|
||||||
'msg': 'Failed to specify a logical device for the volume '
|
|
||||||
'%(volume_id)s to be unmapped.',
|
|
||||||
'suffix': WARNING_SUFFIX
|
|
||||||
}
|
|
||||||
INVALID_LDEV_FOR_DELETION = {
|
|
||||||
'msg_id': 304,
|
|
||||||
'loglevel': base_logging.WARNING,
|
|
||||||
'msg': 'Failed to specify a logical device to be deleted. '
|
|
||||||
'(method: %(method)s, id: %(id)s)',
|
|
||||||
'suffix': WARNING_SUFFIX
|
|
||||||
}
|
|
||||||
DELETE_TARGET_FAILED = {
|
|
||||||
'msg_id': 306,
|
|
||||||
'loglevel': base_logging.WARNING,
|
|
||||||
'msg': 'A host group or an iSCSI target could not be deleted. '
|
|
||||||
'(port: %(port)s, gid: %(id)s)',
|
|
||||||
'suffix': WARNING_SUFFIX
|
|
||||||
}
|
|
||||||
CREATE_HOST_GROUP_FAILED = {
|
|
||||||
'msg_id': 308,
|
|
||||||
'loglevel': base_logging.WARNING,
|
|
||||||
'msg': 'A host group could not be added. (port: %(port)s)',
|
|
||||||
'suffix': WARNING_SUFFIX
|
|
||||||
}
|
|
||||||
CREATE_ISCSI_TARGET_FAILED = {
|
|
||||||
'msg_id': 309,
|
|
||||||
'loglevel': base_logging.WARNING,
|
|
||||||
'msg': 'An iSCSI target could not be added. (port: %(port)s)',
|
|
||||||
'suffix': WARNING_SUFFIX
|
|
||||||
}
|
|
||||||
UNMAP_LDEV_FAILED = {
|
|
||||||
'msg_id': 310,
|
|
||||||
'loglevel': base_logging.WARNING,
|
|
||||||
'msg': 'Failed to unmap a logical device. (LDEV: %(ldev)s)',
|
|
||||||
'suffix': WARNING_SUFFIX
|
|
||||||
}
|
|
||||||
DELETE_LDEV_FAILED = {
|
|
||||||
'msg_id': 313,
|
|
||||||
'loglevel': base_logging.WARNING,
|
|
||||||
'msg': 'Failed to delete a logical device. (LDEV: %(ldev)s)',
|
|
||||||
'suffix': WARNING_SUFFIX
|
|
||||||
}
|
|
||||||
MAP_LDEV_FAILED = {
|
|
||||||
'msg_id': 314,
|
|
||||||
'loglevel': base_logging.WARNING,
|
|
||||||
'msg': 'Failed to map a logical device. (LDEV: %(ldev)s, port: '
|
|
||||||
'%(port)s, id: %(id)s, lun: %(lun)s)',
|
|
||||||
'suffix': WARNING_SUFFIX
|
|
||||||
}
|
|
||||||
DISCARD_ZERO_PAGE_FAILED = {
|
|
||||||
'msg_id': 315,
|
|
||||||
'loglevel': base_logging.WARNING,
|
|
||||||
'msg': 'Failed to perform a zero-page reclamation. (LDEV: '
|
|
||||||
'%(ldev)s)',
|
|
||||||
'suffix': WARNING_SUFFIX
|
|
||||||
}
|
|
||||||
ADD_HBA_WWN_FAILED = {
|
|
||||||
'msg_id': 317,
|
|
||||||
'loglevel': base_logging.WARNING,
|
|
||||||
'msg': 'Failed to assign the WWN. (port: %(port)s, gid: %(gid)s, '
|
|
||||||
'wwn: %(wwn)s)',
|
|
||||||
'suffix': WARNING_SUFFIX
|
|
||||||
}
|
|
||||||
LDEV_NOT_EXIST = {
|
|
||||||
'msg_id': 319,
|
|
||||||
'loglevel': base_logging.WARNING,
|
|
||||||
'msg': 'The logical device does not exist in the storage system. '
|
|
||||||
'(LDEV: %(ldev)s)',
|
|
||||||
'suffix': WARNING_SUFFIX
|
|
||||||
}
|
|
||||||
HORCM_START_FAILED = {
|
|
||||||
'msg_id': 320,
|
|
||||||
'loglevel': base_logging.WARNING,
|
|
||||||
'msg': 'Failed to start HORCM. (inst: %(inst)s)',
|
|
||||||
'suffix': WARNING_SUFFIX
|
|
||||||
}
|
|
||||||
HORCM_RESTART_FOR_SI_FAILED = {
|
|
||||||
'msg_id': 322,
|
|
||||||
'loglevel': base_logging.WARNING,
|
|
||||||
'msg': 'Failed to reload the configuration of full copy pair. '
|
|
||||||
'(inst: %(inst)s)',
|
|
||||||
'suffix': WARNING_SUFFIX
|
|
||||||
}
|
|
||||||
HORCM_LOGIN_FAILED = {
|
|
||||||
'msg_id': 323,
|
|
||||||
'loglevel': base_logging.WARNING,
|
|
||||||
'msg': 'Failed to perform user authentication of HORCM. '
|
|
||||||
'(user: %(user)s)',
|
|
||||||
'suffix': WARNING_SUFFIX
|
|
||||||
}
|
|
||||||
DELETE_SI_PAIR_FAILED = {
|
|
||||||
'msg_id': 324,
|
|
||||||
'loglevel': base_logging.WARNING,
|
|
||||||
'msg': 'Failed to delete full copy pair. (P-VOL: %(pvol)s, S-VOL: '
|
|
||||||
'%(svol)s)',
|
|
||||||
'suffix': WARNING_SUFFIX
|
|
||||||
}
|
|
||||||
DELETE_TI_PAIR_FAILED = {
|
|
||||||
'msg_id': 325,
|
|
||||||
'loglevel': base_logging.WARNING,
|
|
||||||
'msg': 'Failed to delete thin copy pair. (P-VOL: %(pvol)s, S-VOL: '
|
|
||||||
'%(svol)s)',
|
|
||||||
'suffix': WARNING_SUFFIX
|
|
||||||
}
|
|
||||||
WAIT_SI_PAIR_STATUS_FAILED = {
|
|
||||||
'msg_id': 326,
|
|
||||||
'loglevel': base_logging.WARNING,
|
|
||||||
'msg': 'Failed to change the status of full copy pair. (P-VOL: '
|
|
||||||
'%(pvol)s, S-VOL: %(svol)s)',
|
|
||||||
'suffix': WARNING_SUFFIX
|
|
||||||
}
|
|
||||||
DELETE_DEVICE_GRP_FAILED = {
|
|
||||||
'msg_id': 327,
|
|
||||||
'loglevel': base_logging.WARNING,
|
|
||||||
'msg': 'Failed to delete the configuration of full copy pair. '
|
|
||||||
'(P-VOL: %(pvol)s, S-VOL: %(svol)s)',
|
|
||||||
'suffix': WARNING_SUFFIX
|
|
||||||
}
|
|
||||||
DISCONNECT_VOLUME_FAILED = {
|
|
||||||
'msg_id': 329,
|
|
||||||
'loglevel': base_logging.WARNING,
|
|
||||||
'msg': 'Failed to detach the logical device. (LDEV: %(ldev)s, '
|
|
||||||
'reason: %(reason)s)',
|
|
||||||
'suffix': WARNING_SUFFIX
|
|
||||||
}
|
|
||||||
STORAGE_COMMAND_FAILED = {
|
|
||||||
'msg_id': 600,
|
|
||||||
'loglevel': base_logging.ERROR,
|
|
||||||
'msg': 'The command %(cmd)s failed. (ret: %(ret)s, stdout: '
|
|
||||||
'%(out)s, stderr: %(err)s)',
|
|
||||||
'suffix': ERROR_SUFFIX
|
|
||||||
}
|
|
||||||
INVALID_PARAMETER = {
|
|
||||||
'msg_id': 601,
|
|
||||||
'loglevel': base_logging.ERROR,
|
|
||||||
'msg': 'A parameter is invalid. (%(param)s)',
|
|
||||||
'suffix': ERROR_SUFFIX
|
|
||||||
}
|
|
||||||
INVALID_PARAMETER_VALUE = {
|
|
||||||
'msg_id': 602,
|
|
||||||
'loglevel': base_logging.ERROR,
|
|
||||||
'msg': 'A parameter value is invalid. (%(meta)s)',
|
|
||||||
'suffix': ERROR_SUFFIX
|
|
||||||
}
|
|
||||||
HORCM_SHUTDOWN_FAILED = {
|
|
||||||
'msg_id': 608,
|
|
||||||
'loglevel': base_logging.ERROR,
|
|
||||||
'msg': 'Failed to shutdown HORCM. (inst: %(inst)s)',
|
|
||||||
'suffix': ERROR_SUFFIX
|
|
||||||
}
|
|
||||||
HORCM_RESTART_FAILED = {
|
|
||||||
'msg_id': 609,
|
|
||||||
'loglevel': base_logging.ERROR,
|
|
||||||
'msg': 'Failed to restart HORCM. (inst: %(inst)s)',
|
|
||||||
'suffix': ERROR_SUFFIX
|
|
||||||
}
|
|
||||||
SI_PAIR_STATUS_WAIT_TIMEOUT = {
|
|
||||||
'msg_id': 610,
|
|
||||||
'loglevel': base_logging.ERROR,
|
|
||||||
'msg': 'The status change of full copy pair could not be '
|
|
||||||
'completed. (S-VOL: %(svol)s)',
|
|
||||||
'suffix': ERROR_SUFFIX
|
|
||||||
}
|
|
||||||
TI_PAIR_STATUS_WAIT_TIMEOUT = {
|
|
||||||
'msg_id': 611,
|
|
||||||
'loglevel': base_logging.ERROR,
|
|
||||||
'msg': 'The status change of thin copy pair could not be '
|
|
||||||
'completed. (S-VOL: %(svol)s)',
|
|
||||||
'suffix': ERROR_SUFFIX
|
|
||||||
}
|
|
||||||
INVALID_LDEV_STATUS_FOR_COPY = {
|
|
||||||
'msg_id': 612,
|
|
||||||
'loglevel': base_logging.ERROR,
|
|
||||||
'msg': 'The source logical device to be replicated does not exist '
|
|
||||||
'in the storage system. (LDEV: %(ldev)s)',
|
|
||||||
'suffix': ERROR_SUFFIX
|
|
||||||
}
|
|
||||||
INVALID_LDEV_FOR_EXTENSION = {
|
|
||||||
'msg_id': 613,
|
|
||||||
'loglevel': base_logging.ERROR,
|
|
||||||
'msg': 'The volume %(volume_id)s to be extended was not found.',
|
|
||||||
'suffix': ERROR_SUFFIX
|
|
||||||
}
|
|
||||||
NO_HBA_WWN_ADDED_TO_HOST_GRP = {
|
|
||||||
'msg_id': 614,
|
|
||||||
'loglevel': base_logging.ERROR,
|
|
||||||
'msg': 'No WWN is assigned. (port: %(port)s, gid: %(gid)s)',
|
|
||||||
'suffix': ERROR_SUFFIX
|
|
||||||
}
|
|
||||||
NO_AVAILABLE_MIRROR_UNIT = {
|
|
||||||
'msg_id': 615,
|
|
||||||
'loglevel': base_logging.ERROR,
|
|
||||||
'msg': 'A pair could not be created. The maximum number of pair '
|
|
||||||
'is exceeded. (copy method: %(copy_method)s, P-VOL: '
|
|
||||||
'%(pvol)s)',
|
|
||||||
'suffix': ERROR_SUFFIX
|
|
||||||
}
|
|
||||||
UNABLE_TO_DELETE_PAIR = {
|
|
||||||
'msg_id': 616,
|
|
||||||
'loglevel': base_logging.ERROR,
|
|
||||||
'msg': 'A pair cannot be deleted. (P-VOL: %(pvol)s, S-VOL: '
|
|
||||||
'%(svol)s)',
|
|
||||||
'suffix': ERROR_SUFFIX
|
|
||||||
}
|
|
||||||
INVALID_VOLUME_SIZE_FOR_COPY = {
|
|
||||||
'msg_id': 617,
|
|
||||||
'loglevel': base_logging.ERROR,
|
|
||||||
'msg': 'Failed to create a volume from a %(type)s. The size of '
|
|
||||||
'the new volume must be equal to or greater than the size '
|
|
||||||
'of the original %(type)s. (new volume: %(volume_id)s)',
|
|
||||||
'suffix': ERROR_SUFFIX
|
|
||||||
}
|
|
||||||
INVALID_VOLUME_TYPE_FOR_EXTEND = {
|
|
||||||
'msg_id': 618,
|
|
||||||
'loglevel': base_logging.ERROR,
|
|
||||||
'msg': 'The volume %(volume_id)s could not be extended. The '
|
|
||||||
'volume type must be Normal.',
|
|
||||||
'suffix': ERROR_SUFFIX
|
|
||||||
}
|
|
||||||
INVALID_LDEV_FOR_CONNECTION = {
|
|
||||||
'msg_id': 619,
|
|
||||||
'loglevel': base_logging.ERROR,
|
|
||||||
'msg': 'The volume %(volume_id)s to be mapped was not found.',
|
|
||||||
'suffix': ERROR_SUFFIX
|
|
||||||
}
|
|
||||||
POOL_INFO_RETRIEVAL_FAILED = {
|
|
||||||
'msg_id': 620,
|
|
||||||
'loglevel': base_logging.ERROR,
|
|
||||||
'msg': 'Failed to provide information about a pool. (pool: '
|
|
||||||
'%(pool)s)',
|
|
||||||
'suffix': ERROR_SUFFIX
|
|
||||||
}
|
|
||||||
INVALID_VOLUME_SIZE_FOR_TI = {
|
|
||||||
'msg_id': 621,
|
|
||||||
'loglevel': base_logging.ERROR,
|
|
||||||
'msg': 'Failed to create a volume from a %(type)s. The size of '
|
|
||||||
'the new volume must be equal to the size of the original '
|
|
||||||
'%(type)s when the new volume is created by '
|
|
||||||
'%(copy_method)s. (new volume: %(volume_id)s)',
|
|
||||||
'suffix': ERROR_SUFFIX
|
|
||||||
}
|
|
||||||
INVALID_LDEV_FOR_VOLUME_COPY = {
|
|
||||||
'msg_id': 624,
|
|
||||||
'loglevel': base_logging.ERROR,
|
|
||||||
'msg': 'The %(type)s %(id)s source to be replicated was not '
|
|
||||||
'found.',
|
|
||||||
'suffix': ERROR_SUFFIX
|
|
||||||
}
|
|
||||||
CREATE_HORCM_CONF_FILE_FAILED = {
|
|
||||||
'msg_id': 632,
|
|
||||||
'loglevel': base_logging.ERROR,
|
|
||||||
'msg': 'Failed to open a file. (file: %(file)s, ret: %(ret)s, '
|
|
||||||
'stderr: %(err)s)',
|
|
||||||
'suffix': ERROR_SUFFIX
|
|
||||||
}
|
|
||||||
CONNECT_VOLUME_FAILED = {
|
|
||||||
'msg_id': 634,
|
|
||||||
'loglevel': base_logging.ERROR,
|
|
||||||
'msg': 'Failed to attach the logical device. (LDEV: %(ldev)s, '
|
|
||||||
'reason: %(reason)s)',
|
|
||||||
'suffix': ERROR_SUFFIX
|
|
||||||
}
|
|
||||||
CREATE_LDEV_FAILED = {
|
|
||||||
'msg_id': 636,
|
|
||||||
'loglevel': base_logging.ERROR,
|
|
||||||
'msg': 'Failed to add the logical device.',
|
|
||||||
'suffix': ERROR_SUFFIX
|
|
||||||
}
|
|
||||||
ADD_PAIR_TARGET_FAILED = {
|
|
||||||
'msg_id': 638,
|
|
||||||
'loglevel': base_logging.ERROR,
|
|
||||||
'msg': 'Failed to add the pair target.',
|
|
||||||
'suffix': ERROR_SUFFIX
|
|
||||||
}
|
|
||||||
NO_MAPPING_FOR_LDEV = {
|
|
||||||
'msg_id': 639,
|
|
||||||
'loglevel': base_logging.ERROR,
|
|
||||||
'msg': 'Failed to map a logical device to any pair targets. '
|
|
||||||
'(LDEV: %(ldev)s)',
|
|
||||||
'suffix': ERROR_SUFFIX
|
|
||||||
}
|
|
||||||
POOL_NOT_FOUND = {
|
|
||||||
'msg_id': 640,
|
|
||||||
'loglevel': base_logging.ERROR,
|
|
||||||
'msg': 'A pool could not be found. (pool: %(pool)s)',
|
|
||||||
'suffix': ERROR_SUFFIX
|
|
||||||
}
|
|
||||||
NO_AVAILABLE_RESOURCE = {
|
|
||||||
'msg_id': 648,
|
|
||||||
'loglevel': base_logging.ERROR,
|
|
||||||
'msg': 'There are no resources available for use. (resource: '
|
|
||||||
'%(resource)s)',
|
|
||||||
'suffix': ERROR_SUFFIX
|
|
||||||
}
|
|
||||||
NO_CONNECTED_TARGET = {
|
|
||||||
'msg_id': 649,
|
|
||||||
'loglevel': base_logging.ERROR,
|
|
||||||
'msg': 'The host group or iSCSI target was not found.',
|
|
||||||
'suffix': ERROR_SUFFIX
|
|
||||||
}
|
|
||||||
RESOURCE_NOT_FOUND = {
|
|
||||||
'msg_id': 650,
|
|
||||||
'loglevel': base_logging.ERROR,
|
|
||||||
'msg': 'The resource %(resource)s was not found.',
|
|
||||||
'suffix': ERROR_SUFFIX
|
|
||||||
}
|
|
||||||
LDEV_DELETION_WAIT_TIMEOUT = {
|
|
||||||
'msg_id': 652,
|
|
||||||
'loglevel': base_logging.ERROR,
|
|
||||||
'msg': 'Failed to delete a logical device. (LDEV: %(ldev)s)',
|
|
||||||
'suffix': ERROR_SUFFIX
|
|
||||||
}
|
|
||||||
LDEV_CREATION_WAIT_TIMEOUT = {
|
|
||||||
'msg_id': 653,
|
|
||||||
'loglevel': base_logging.ERROR,
|
|
||||||
'msg': 'The creation of a logical device could not be completed. '
|
|
||||||
'(LDEV: %(ldev)s)',
|
|
||||||
'suffix': ERROR_SUFFIX
|
|
||||||
}
|
|
||||||
INVALID_LDEV_ATTR_FOR_MANAGE = {
|
|
||||||
'msg_id': 702,
|
|
||||||
'loglevel': base_logging.ERROR,
|
|
||||||
'msg': 'Failed to manage the specified LDEV (%(ldev)s). The LDEV '
|
|
||||||
'must be an unpaired %(ldevtype)s.',
|
|
||||||
'suffix': ERROR_SUFFIX
|
|
||||||
}
|
|
||||||
INVALID_LDEV_SIZE_FOR_MANAGE = {
|
|
||||||
'msg_id': 703,
|
|
||||||
'loglevel': base_logging.ERROR,
|
|
||||||
'msg': 'Failed to manage the specified LDEV (%(ldev)s). The LDEV '
|
|
||||||
'size must be expressed in gigabytes.',
|
|
||||||
'suffix': ERROR_SUFFIX
|
|
||||||
}
|
|
||||||
INVALID_LDEV_PORT_FOR_MANAGE = {
|
|
||||||
'msg_id': 704,
|
|
||||||
'loglevel': base_logging.ERROR,
|
|
||||||
'msg': 'Failed to manage the specified LDEV (%(ldev)s). The LDEV '
|
|
||||||
'must not be mapped.',
|
|
||||||
'suffix': ERROR_SUFFIX
|
|
||||||
}
|
|
||||||
INVALID_LDEV_TYPE_FOR_UNMANAGE = {
|
|
||||||
'msg_id': 706,
|
|
||||||
'loglevel': base_logging.ERROR,
|
|
||||||
'msg': 'Failed to unmanage the volume %(volume_id)s. The volume '
|
|
||||||
'type must be %(volume_type)s.',
|
|
||||||
'suffix': ERROR_SUFFIX
|
|
||||||
}
|
|
||||||
INVALID_LDEV_FOR_MANAGE = {
|
|
||||||
'msg_id': 707,
|
|
||||||
'loglevel': base_logging.ERROR,
|
|
||||||
'msg': 'No valid value is specified for "source-id". A valid LDEV '
|
|
||||||
'number must be specified in "source-id" to manage the '
|
|
||||||
'volume.',
|
|
||||||
'suffix': ERROR_SUFFIX
|
|
||||||
}
|
|
||||||
VOLUME_COPY_FAILED = {
|
|
||||||
'msg_id': 722,
|
|
||||||
'loglevel': base_logging.ERROR,
|
|
||||||
'msg': 'Failed to copy a volume. (copy method: %(copy_method)s, '
|
|
||||||
'P-VOL: %(pvol)s, S-VOL: %(svol)s)',
|
|
||||||
'suffix': ERROR_SUFFIX
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self, error_info):
|
|
||||||
"""Initialize Enum attributes."""
|
|
||||||
self.msg_id = error_info['msg_id']
|
|
||||||
self.level = error_info['loglevel']
|
|
||||||
self.msg = error_info['msg']
|
|
||||||
self.suffix = error_info['suffix']
|
|
||||||
|
|
||||||
def output_log(self, **kwargs):
|
|
||||||
"""Output the message to the log file and return the message."""
|
|
||||||
msg = self.msg % kwargs
|
|
||||||
LOG.log(self.level, "MSGID%(msg_id)04d-%(msg_suffix)s: %(msg)s",
|
|
||||||
{'msg_id': self.msg_id, 'msg_suffix': self.suffix, 'msg': msg})
|
|
||||||
return msg
|
|
||||||
|
|
||||||
|
|
||||||
def output_log(msg_enum, **kwargs):
|
|
||||||
"""Output the specified message to the log file and return the message."""
|
|
||||||
return msg_enum.output_log(**kwargs)
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
|
||||||
MSG = VSPMsg
|
|
||||||
|
|
||||||
|
|
||||||
def output_start_end_log(func):
|
|
||||||
"""Output the log of the start and the end of the method."""
|
|
||||||
@functools.wraps(func)
|
|
||||||
def wrap(self, *args, **kwargs):
|
|
||||||
"""Wrap the method to add logging function."""
|
|
||||||
def _output_start_end_log(*_args, **_kwargs):
|
|
||||||
"""Output the log of the start and the end of the method."""
|
|
||||||
output_log(MSG.METHOD_START,
|
|
||||||
method=func.__name__,
|
|
||||||
config_group=self.configuration.config_group)
|
|
||||||
ret = func(*_args, **_kwargs)
|
|
||||||
output_log(MSG.METHOD_END,
|
|
||||||
method=func.__name__,
|
|
||||||
config_group=self.configuration.config_group)
|
|
||||||
return ret
|
|
||||||
return _output_start_end_log(self, *args, **kwargs)
|
|
||||||
return wrap
|
|
||||||
|
|
||||||
|
|
||||||
def get_ldev(obj):
|
|
||||||
"""Get the LDEV number from the given object and return it as integer."""
|
|
||||||
if not obj:
|
|
||||||
return None
|
|
||||||
ldev = obj.get('provider_location')
|
|
||||||
if not ldev or not ldev.isdigit():
|
|
||||||
return None
|
|
||||||
return int(ldev)
|
|
||||||
|
|
||||||
|
|
||||||
def check_timeout(start_time, timeout):
|
|
||||||
"""Return True if the specified time has passed, False otherwise."""
|
|
||||||
return timeutils.is_older_than(start_time, timeout)
|
|
||||||
|
|
||||||
|
|
||||||
def mask_password(cmd):
|
|
||||||
"""Return a string in which the password is masked."""
|
|
||||||
if len(cmd) > 3 and cmd[0] == 'raidcom' and cmd[1] == '-login':
|
|
||||||
tmp = list(cmd)
|
|
||||||
tmp[3] = strutils.mask_dict_password({'password': ''}).get('password')
|
|
||||||
else:
|
|
||||||
tmp = cmd
|
|
||||||
return ' '.join([six.text_type(c) for c in tmp])
|
|
||||||
|
|
||||||
|
|
||||||
def execute(*cmd, **kwargs):
|
|
||||||
"""Run the specified command and return its results."""
|
|
||||||
process_input = kwargs.pop('process_input', None)
|
|
||||||
run_as_root = kwargs.pop('run_as_root', True)
|
|
||||||
ret = 0
|
|
||||||
try:
|
|
||||||
if len(cmd) > 3 and cmd[0] == 'raidcom' and cmd[1] == '-login':
|
|
||||||
stdout, stderr = cinder_utils.execute(
|
|
||||||
*cmd, process_input=process_input, run_as_root=run_as_root,
|
|
||||||
loglevel=base_logging.NOTSET)[:2]
|
|
||||||
else:
|
|
||||||
stdout, stderr = cinder_utils.execute(
|
|
||||||
*cmd, process_input=process_input, run_as_root=run_as_root)[:2]
|
|
||||||
except putils.ProcessExecutionError as ex:
|
|
||||||
ret = ex.exit_code
|
|
||||||
stdout = ex.stdout
|
|
||||||
stderr = ex.stderr
|
|
||||||
LOG.debug('cmd: %s', mask_password(cmd))
|
|
||||||
LOG.debug('from: %s', inspect.stack()[2])
|
|
||||||
LOG.debug('ret: %s', ret)
|
|
||||||
LOG.debug('stdout: %s', ' '.join(stdout.splitlines()))
|
|
||||||
LOG.debug('stderr: %s', ' '.join(stderr.splitlines()))
|
|
||||||
return ret, stdout, stderr
|
|
||||||
|
|
||||||
|
|
||||||
def import_object(conf, driver_info, db):
|
|
||||||
"""Import a class and return an instance of it."""
|
|
||||||
os.environ['LANG'] = 'C'
|
|
||||||
cli = _DRIVERS.get('HORCM')
|
|
||||||
return importutils.import_object(
|
|
||||||
'.'.join([_DRIVER_DIR, cli[driver_info['proto']]]),
|
|
||||||
conf, driver_info, db)
|
|
||||||
|
|
||||||
|
|
||||||
def check_ignore_error(ignore_error, stderr):
|
|
||||||
"""Return True if ignore_error is in stderr, False otherwise."""
|
|
||||||
if not ignore_error or not stderr:
|
|
||||||
return False
|
|
||||||
if not isinstance(ignore_error, six.string_types):
|
|
||||||
ignore_error = '|'.join(ignore_error)
|
|
||||||
|
|
||||||
if re.search(ignore_error, stderr):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def check_opts(conf, opts):
|
|
||||||
"""Check if the specified configuration is valid."""
|
|
||||||
names = []
|
|
||||||
for opt in opts:
|
|
||||||
names.append(opt.name)
|
|
||||||
check_opt_value(conf, names)
|
|
||||||
|
|
||||||
|
|
||||||
def check_opt_value(conf, names):
|
|
||||||
"""Check if the parameter names and values are valid."""
|
|
||||||
for name in names:
|
|
||||||
try:
|
|
||||||
getattr(conf, name)
|
|
||||||
except (cfg.NoSuchOptError, cfg.ConfigFileValueError):
|
|
||||||
with excutils.save_and_reraise_exception():
|
|
||||||
output_log(MSG.INVALID_PARAMETER, param=name)
|
|
||||||
|
|
||||||
|
|
||||||
def output_storage_cli_info(name, version):
|
|
||||||
"""Output storage CLI info to the log file."""
|
|
||||||
LOG.info('\t%(name)-35s%(version)s',
|
|
||||||
{'name': name + ' version: ', 'version': version})
|
|
||||||
|
|
||||||
|
|
||||||
def output_opt_info(conf, names):
|
|
||||||
"""Output parameter names and values to the log file."""
|
|
||||||
for name in names:
|
|
||||||
LOG.info('\t%(name)-35s%(attr)s',
|
|
||||||
{'name': name + ': ', 'attr': getattr(conf, name)})
|
|
||||||
|
|
||||||
|
|
||||||
def output_opts(conf, opts):
|
|
||||||
"""Output parameter names and values to the log file."""
|
|
||||||
names = [opt.name for opt in opts if not opt.secret]
|
|
||||||
output_opt_info(conf, names)
|
|
||||||
|
|
||||||
|
|
||||||
def require_target_existed(targets):
|
|
||||||
"""Check if the target list includes one or more members."""
|
|
||||||
if not targets['list']:
|
|
||||||
msg = output_log(MSG.NO_CONNECTED_TARGET)
|
|
||||||
raise exception.VSPError(msg)
|
|
||||||
|
|
||||||
|
|
||||||
def get_volume_metadata(volume):
|
|
||||||
"""Return a dictionary of the metadata of the specified volume."""
|
|
||||||
volume_metadata = volume.get('volume_metadata', {})
|
|
||||||
return {item['key']: item['value'] for item in volume_metadata}
|
|
||||||
|
|
||||||
|
|
||||||
def update_conn_info(conn_info, connector, lookup_service):
|
|
||||||
"""Set wwn mapping list to the connection info."""
|
|
||||||
init_targ_map = build_initiator_target_map(
|
|
||||||
connector, conn_info['data']['target_wwn'], lookup_service)
|
|
||||||
if init_targ_map:
|
|
||||||
conn_info['data']['initiator_target_map'] = init_targ_map
|
|
||||||
|
|
||||||
|
|
||||||
def build_initiator_target_map(connector, target_wwns, lookup_service):
|
|
||||||
"""Return a dictionary mapping server-wwns and lists of storage-wwns."""
|
|
||||||
init_targ_map = {}
|
|
||||||
initiator_wwns = connector['wwpns']
|
|
||||||
if lookup_service:
|
|
||||||
dev_map = lookup_service.get_device_mapping_from_network(
|
|
||||||
initiator_wwns, target_wwns)
|
|
||||||
for fabric_name in dev_map:
|
|
||||||
fabric = dev_map[fabric_name]
|
|
||||||
for initiator in fabric['initiator_port_wwn_list']:
|
|
||||||
init_targ_map[initiator] = fabric['target_port_wwn_list']
|
|
||||||
else:
|
|
||||||
for initiator in initiator_wwns:
|
|
||||||
init_targ_map[initiator] = target_wwns
|
|
||||||
return init_targ_map
|
|
@ -1,548 +0,0 @@
|
|||||||
==========================================
|
|
||||||
Hitachi NAS Platform NFS driver
|
|
||||||
==========================================
|
|
||||||
|
|
||||||
This OpenStack Block Storage volume drivers provides NFS support
|
|
||||||
for `Hitachi NAS Platform (HNAS) <http://www.hds.com/products/file-and-content/
|
|
||||||
network-attached-storage/>`_ Models 3080, 3090, 4040, 4060, 4080, and 4100
|
|
||||||
with NAS OS 12.2 or higher.
|
|
||||||
|
|
||||||
Supported operations
|
|
||||||
~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
The NFS driver support these operations:
|
|
||||||
|
|
||||||
* Create, delete, attach, and detach volumes.
|
|
||||||
* Create, list, and delete volume snapshots.
|
|
||||||
* Create a volume from a snapshot.
|
|
||||||
* Copy an image to a volume.
|
|
||||||
* Copy a volume to an image.
|
|
||||||
* Clone a volume.
|
|
||||||
* Extend a volume.
|
|
||||||
* Get volume statistics.
|
|
||||||
* Manage and unmanage a volume.
|
|
||||||
* Manage and unmanage snapshots (`HNAS NFS only`).
|
|
||||||
* List manageable volumes and snapshots (`HNAS NFS only`).
|
|
||||||
|
|
||||||
HNAS storage requirements
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
Before using NFS services, use the HNAS configuration and management
|
|
||||||
GUI (SMU) or SSC CLI to configure HNAS to work with the drivers. Additionally:
|
|
||||||
|
|
||||||
1. General:
|
|
||||||
|
|
||||||
* It is mandatory to have at least ``1 storage pool, 1 EVS and 1 file
|
|
||||||
system`` to be able to run any of the HNAS drivers.
|
|
||||||
* HNAS drivers consider the space allocated to the file systems to
|
|
||||||
provide the reports to cinder. So, when creating a file system, make sure
|
|
||||||
it has enough space to fit your needs.
|
|
||||||
* The file system used should not be created as a ``replication target`` and
|
|
||||||
should be mounted.
|
|
||||||
* It is possible to configure HNAS drivers to use distinct EVSs and file
|
|
||||||
systems, but ``all compute nodes and controllers`` in the cloud must have
|
|
||||||
access to the EVSs.
|
|
||||||
|
|
||||||
2. For NFS:
|
|
||||||
|
|
||||||
* Create NFS exports, choose a path for them (it must be different from
|
|
||||||
``/``) and set the :guilabel: `Show snapshots` option to ``hide and
|
|
||||||
disable access``.
|
|
||||||
* For each export used, set the option ``norootsquash`` in the share
|
|
||||||
``Access configuration`` so Block Storage services can change the
|
|
||||||
permissions of its volumes. For example, ``"* (rw, norootsquash)"``.
|
|
||||||
* Make sure that all computes and controllers have R/W access to the
|
|
||||||
shares used by cinder HNAS driver.
|
|
||||||
* In order to use the hardware accelerated features of HNAS NFS, we
|
|
||||||
recommend setting ``max-nfs-version`` to 3. Refer to Hitachi NAS Platform
|
|
||||||
command line reference to see how to configure this option.
|
|
||||||
|
|
||||||
Block Storage host requirements
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
The HNAS drivers are supported for Red Hat Enterprise Linux OpenStack
|
|
||||||
Platform, SUSE OpenStack Cloud, and Ubuntu OpenStack.
|
|
||||||
The following packages must be installed in all compute, controller and
|
|
||||||
storage (if any) nodes:
|
|
||||||
|
|
||||||
* ``nfs-utils`` for Red Hat Enterprise Linux OpenStack Platform
|
|
||||||
* ``nfs-client`` for SUSE OpenStack Cloud
|
|
||||||
* ``nfs-common``, ``libc6-i386`` for Ubuntu OpenStack
|
|
||||||
|
|
||||||
Package installation
|
|
||||||
--------------------
|
|
||||||
|
|
||||||
If you are installing the driver from an RPM or DEB package,
|
|
||||||
follow the steps below:
|
|
||||||
|
|
||||||
#. Install the dependencies:
|
|
||||||
|
|
||||||
In Red Hat:
|
|
||||||
|
|
||||||
.. code-block:: console
|
|
||||||
|
|
||||||
# yum install nfs-utils nfs-utils-lib
|
|
||||||
|
|
||||||
Or in Ubuntu:
|
|
||||||
|
|
||||||
.. code-block:: console
|
|
||||||
|
|
||||||
# apt-get install nfs-common
|
|
||||||
|
|
||||||
Or in SUSE:
|
|
||||||
|
|
||||||
.. code-block:: console
|
|
||||||
|
|
||||||
# zypper install nfs-client
|
|
||||||
|
|
||||||
If you are using Ubuntu 12.04, you also need to install ``libc6-i386``
|
|
||||||
|
|
||||||
.. code-block:: console
|
|
||||||
|
|
||||||
# apt-get install libc6-i386
|
|
||||||
|
|
||||||
#. Configure the driver as described in the :ref:`hnas-driver-configuration`
|
|
||||||
section.
|
|
||||||
|
|
||||||
#. Restart all Block Storage services (volume, scheduler, and backup).
|
|
||||||
|
|
||||||
.. _hnas-driver-configuration:
|
|
||||||
|
|
||||||
Driver configuration
|
|
||||||
~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
HNAS supports a variety of storage options and file system capabilities,
|
|
||||||
which are selected through the definition of volume types combined with the
|
|
||||||
use of multiple back ends and multiple services. Each back end can configure
|
|
||||||
up to ``4 service pools``, which can be mapped to cinder volume types.
|
|
||||||
|
|
||||||
The configuration for the driver is read from the back-end sections of the
|
|
||||||
``cinder.conf``. Each back-end section must have the appropriate configurations
|
|
||||||
to communicate with your HNAS back end, such as the IP address of the HNAS EVS
|
|
||||||
that is hosting your data, HNAS SSH access credentials, the configuration of
|
|
||||||
each of the services in that back end, and so on. You can find examples of such
|
|
||||||
configurations in the :ref:`configuration_example` section.
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
HNAS cinder drivers still support the XML configuration the
|
|
||||||
same way it was in the older versions, but we recommend configuring the
|
|
||||||
HNAS cinder drivers only through the ``cinder.conf`` file,
|
|
||||||
since the XML configuration file from previous versions is being
|
|
||||||
deprecated as of Newton Release.
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
We do not recommend the use of the same NFS export for different back ends.
|
|
||||||
If possible, configure each back end to
|
|
||||||
use a different NFS export/file system.
|
|
||||||
|
|
||||||
The following is the definition of each configuration option that can be used
|
|
||||||
in a HNAS back-end section in the ``cinder.conf`` file:
|
|
||||||
|
|
||||||
.. list-table:: **Configuration options in cinder.conf**
|
|
||||||
:header-rows: 1
|
|
||||||
:widths: 25, 10, 15, 50
|
|
||||||
|
|
||||||
* - Option
|
|
||||||
- Type
|
|
||||||
- Default
|
|
||||||
- Description
|
|
||||||
* - ``volume_backend_name``
|
|
||||||
- Optional
|
|
||||||
- N/A
|
|
||||||
- A name that identifies the back end and can be used as an extra-spec to
|
|
||||||
redirect the volumes to the referenced back end.
|
|
||||||
* - ``volume_driver``
|
|
||||||
- Required
|
|
||||||
- N/A
|
|
||||||
- The python module path to the HNAS volume driver python class. When
|
|
||||||
installing through the rpm or deb packages, you should configure this
|
|
||||||
to `cinder.volume.drivers.hitachi.hnas_nfs.HNASNFSDriver`.
|
|
||||||
* - ``nfs_shares_config``
|
|
||||||
- Required (only for NFS)
|
|
||||||
- /etc/cinder/nfs_shares
|
|
||||||
- Path to the ``nfs_shares`` file. This is required by the base cinder
|
|
||||||
generic NFS driver and therefore also required by the HNAS NFS driver.
|
|
||||||
This file should list, one per line, every NFS share being used by the
|
|
||||||
back end. For example, all the values found in the configuration keys
|
|
||||||
hnas_svcX_hdp in the HNAS NFS back-end sections.
|
|
||||||
* - ``hnas_mgmt_ip0``
|
|
||||||
- Required
|
|
||||||
- N/A
|
|
||||||
- HNAS management IP address. Should be the IP address of the `Admin`
|
|
||||||
EVS. It is also the IP through which you access the web SMU
|
|
||||||
administration frontend of HNAS.
|
|
||||||
* - ``hnas_username``
|
|
||||||
- Required
|
|
||||||
- N/A
|
|
||||||
- HNAS SSH username
|
|
||||||
* - ``hds_hnas_nfs_config_file``
|
|
||||||
- Optional (deprecated)
|
|
||||||
- /opt/hds/hnas/cinder_nfs_conf.xml
|
|
||||||
- Path to the deprecated XML configuration file (only required if using
|
|
||||||
the XML file)
|
|
||||||
* - ``hnas_cluster_admin_ip0``
|
|
||||||
- Optional (required only for HNAS multi-farm setups)
|
|
||||||
- N/A
|
|
||||||
- The IP of the HNAS farm admin. If your SMU controls more than one
|
|
||||||
system or cluster, this option must be set with the IP of the desired
|
|
||||||
node. This is different for HNAS multi-cluster setups, which
|
|
||||||
does not require this option to be set.
|
|
||||||
* - ``hnas_ssh_private_key``
|
|
||||||
- Optional
|
|
||||||
- N/A
|
|
||||||
- Path to the SSH private key used to authenticate to the HNAS SMU. Only
|
|
||||||
required if you do not want to set `hnas_password`.
|
|
||||||
* - ``hnas_ssh_port``
|
|
||||||
- Optional
|
|
||||||
- 22
|
|
||||||
- Port on which HNAS is listening for SSH connections
|
|
||||||
* - ``hnas_password``
|
|
||||||
- Required (unless hnas_ssh_private_key is provided)
|
|
||||||
- N/A
|
|
||||||
- HNAS password
|
|
||||||
* - ``hnas_svcX_hdp`` [1]_
|
|
||||||
- Required (at least 1)
|
|
||||||
- N/A
|
|
||||||
- HDP (export) where the volumes will be created. Use
|
|
||||||
exports paths to configure this.
|
|
||||||
* - ``hnas_svcX_pool_name``
|
|
||||||
- Required
|
|
||||||
- N/A
|
|
||||||
- A `unique string` that is used to refer to this pool within the
|
|
||||||
context of cinder. You can tell cinder to put volumes of a specific
|
|
||||||
volume type into this back end, within this pool. See,
|
|
||||||
``Service Labels`` and :ref:`configuration_example` sections
|
|
||||||
for more details.
|
|
||||||
|
|
||||||
.. [1]
|
|
||||||
Replace X with a number from 0 to 3 (keep the sequence when configuring
|
|
||||||
the driver)
|
|
||||||
|
|
||||||
Service labels
|
|
||||||
~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
HNAS driver supports differentiated types of service using the service labels.
|
|
||||||
It is possible to create up to 4 types of them for each back end. (For example
|
|
||||||
gold, platinum, silver, ssd, and so on).
|
|
||||||
|
|
||||||
After creating the services in the ``cinder.conf`` configuration file, you
|
|
||||||
need to configure one cinder ``volume_type`` per service. Each ``volume_type``
|
|
||||||
must have the metadata service_label with the same name configured in the
|
|
||||||
``hnas_svcX_pool_name option`` of that service. See the
|
|
||||||
:ref:`configuration_example` section for more details. If the ``volume_type``
|
|
||||||
is not set, the cinder service pool with largest available free space or
|
|
||||||
other criteria configured in scheduler filters.
|
|
||||||
|
|
||||||
.. code-block:: console
|
|
||||||
|
|
||||||
$ openstack volume type create default
|
|
||||||
$ openstack volume type set --property service_label=default default
|
|
||||||
$ openstack volume type create platinum-tier
|
|
||||||
$ openstack volume type set --property service_label=platinum platinum
|
|
||||||
|
|
||||||
Multi-backend configuration
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
You can deploy multiple OpenStack HNAS Driver instances (back ends) that each
|
|
||||||
controls a separate HNAS or a single HNAS. If you use multiple cinder
|
|
||||||
back ends, remember that each cinder back end can host up to 4 services. Each
|
|
||||||
back-end section must have the appropriate configurations to communicate with
|
|
||||||
your HNAS back end, such as the IP address of the HNAS EVS that is hosting
|
|
||||||
your data, HNAS SSH access credentials, the configuration of each of the
|
|
||||||
services in that back end, and so on. You can find examples of such
|
|
||||||
configurations in the :ref:`configuration_example` section.
|
|
||||||
|
|
||||||
If you want the volumes from a volume_type to be casted into a specific
|
|
||||||
back end, you must configure an extra_spec in the ``volume_type`` with the
|
|
||||||
value of the ``volume_backend_name`` option from that back end.
|
|
||||||
|
|
||||||
For multiple NFS back ends configuration, each back end should have a
|
|
||||||
separated ``nfs_shares_config`` and also a separated ``nfs_shares file``
|
|
||||||
defined (For example, ``nfs_shares1``, ``nfs_shares2``) with the desired
|
|
||||||
shares listed in separated lines.
|
|
||||||
|
|
||||||
SSH configuration
|
|
||||||
~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
As of the Newton OpenStack release, the user can no longer run the
|
|
||||||
driver using a locally installed instance of the :command:`SSC` utility
|
|
||||||
package. Instead, all communications with the HNAS back end are handled
|
|
||||||
through :command:`SSH`.
|
|
||||||
|
|
||||||
You can use your username and password to authenticate the Block Storage node
|
|
||||||
to the HNAS back end. In order to do that, simply configure ``hnas_username``
|
|
||||||
and ``hnas_password`` in your back end section within the ``cinder.conf``
|
|
||||||
file.
|
|
||||||
|
|
||||||
For example:
|
|
||||||
|
|
||||||
.. code-block:: ini
|
|
||||||
|
|
||||||
[hnas-backend]
|
|
||||||
# ...
|
|
||||||
hnas_username = supervisor
|
|
||||||
hnas_password = supervisor
|
|
||||||
|
|
||||||
Alternatively, the HNAS cinder driver also supports SSH authentication
|
|
||||||
through public key. To configure that:
|
|
||||||
|
|
||||||
#. If you do not have a pair of public keys already generated, create it in
|
|
||||||
the Block Storage node (leave the pass-phrase empty):
|
|
||||||
|
|
||||||
.. code-block:: console
|
|
||||||
|
|
||||||
$ mkdir -p /opt/hitachi/ssh
|
|
||||||
$ ssh-keygen -f /opt/hds/ssh/hnaskey
|
|
||||||
|
|
||||||
#. Change the owner of the key to cinder (or the user the volume service will
|
|
||||||
be run as):
|
|
||||||
|
|
||||||
.. code-block:: console
|
|
||||||
|
|
||||||
# chown -R cinder.cinder /opt/hitachi/ssh
|
|
||||||
|
|
||||||
#. Create the directory ``ssh_keys`` in the SMU server:
|
|
||||||
|
|
||||||
.. code-block:: console
|
|
||||||
|
|
||||||
$ ssh [manager|supervisor]@<smu-ip> 'mkdir -p /var/opt/mercury-main/home/[manager|supervisor]/ssh_keys/'
|
|
||||||
|
|
||||||
#. Copy the public key to the ``ssh_keys`` directory:
|
|
||||||
|
|
||||||
.. code-block:: console
|
|
||||||
|
|
||||||
$ scp /opt/hitachi/ssh/hnaskey.pub [manager|supervisor]@<smu-ip>:/var/opt/mercury-main/home/[manager|supervisor]/ssh_keys/
|
|
||||||
|
|
||||||
#. Access the SMU server:
|
|
||||||
|
|
||||||
.. code-block:: console
|
|
||||||
|
|
||||||
$ ssh [manager|supervisor]@<smu-ip>
|
|
||||||
|
|
||||||
#. Run the command to register the SSH keys:
|
|
||||||
|
|
||||||
.. code-block:: console
|
|
||||||
|
|
||||||
$ ssh-register-public-key -u [manager|supervisor] -f ssh_keys/hnaskey.pub
|
|
||||||
|
|
||||||
#. Check the communication with HNAS in the Block Storage node:
|
|
||||||
|
|
||||||
For multi-farm HNAS:
|
|
||||||
|
|
||||||
.. code-block:: console
|
|
||||||
|
|
||||||
$ ssh -i /opt/hitachi/ssh/hnaskey [manager|supervisor]@<smu-ip> 'ssc <cluster_admin_ip0> df -a'
|
|
||||||
|
|
||||||
Or, for Single-node/Multi-Cluster:
|
|
||||||
|
|
||||||
.. code-block:: console
|
|
||||||
|
|
||||||
$ ssh -i /opt/hitachi/ssh/hnaskey [manager|supervisor]@<smu-ip> 'ssc localhost df -a'
|
|
||||||
|
|
||||||
#. Configure your backend section in ``cinder.conf`` to use your public key:
|
|
||||||
|
|
||||||
.. code-block:: ini
|
|
||||||
|
|
||||||
[hnas-backend]
|
|
||||||
# ...
|
|
||||||
hnas_ssh_private_key = /opt/hitachi/ssh/hnaskey
|
|
||||||
|
|
||||||
Managing volumes
|
|
||||||
~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
If there are some existing volumes on HNAS that you want to import to cinder,
|
|
||||||
it is possible to use the manage volume feature to do this. The manage action
|
|
||||||
on an existing volume is very similar to a volume creation. It creates a
|
|
||||||
volume entry on cinder database, but instead of creating a new volume in the
|
|
||||||
back end, it only adds a link to an existing volume.
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
It is an admin only feature and you have to be logged as an user
|
|
||||||
with admin rights to be able to use this.
|
|
||||||
|
|
||||||
#. Under the :menuselection:`System > Volumes` tab,
|
|
||||||
choose the option :guilabel:`Manage Volume`.
|
|
||||||
|
|
||||||
#. Fill the fields :guilabel:`Identifier`, :guilabel:`Host`,
|
|
||||||
:guilabel:`Volume Name`, and :guilabel:`Volume Type` with volume
|
|
||||||
information to be managed:
|
|
||||||
|
|
||||||
* :guilabel:`Identifier`: ip:/type/volume_name (*For example:*
|
|
||||||
172.24.44.34:/silver/volume-test)
|
|
||||||
* :guilabel:`Host`: `host@backend-name#pool_name` (*For example:*
|
|
||||||
`ubuntu@hnas-nfs#test_silver`)
|
|
||||||
* :guilabel:`Volume Name`: volume_name (*For example:* volume-test)
|
|
||||||
* :guilabel:`Volume Type`: choose a type of volume (*For example:* silver)
|
|
||||||
|
|
||||||
By CLI:
|
|
||||||
|
|
||||||
.. code-block:: console
|
|
||||||
|
|
||||||
$ cinder manage [--id-type <id-type>][--name <name>][--description <description>]
|
|
||||||
[--volume-type <volume-type>][--availability-zone <availability-zone>]
|
|
||||||
[--metadata [<key=value> [<key=value> ...]]][--bootable] <host> <identifier>
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
.. code-block:: console
|
|
||||||
|
|
||||||
$ cinder manage --name volume-test --volume-type silver
|
|
||||||
ubuntu@hnas-nfs#test_silver 172.24.44.34:/silver/volume-test
|
|
||||||
|
|
||||||
Managing snapshots
|
|
||||||
~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
The manage snapshots feature works very similarly to the manage volumes
|
|
||||||
feature, currently supported on HNAS cinder drivers. So, if you have a volume
|
|
||||||
already managed by cinder which has snapshots that are not managed by cinder,
|
|
||||||
it is possible to use manage snapshots to import these snapshots and link them
|
|
||||||
with their original volume.
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
For HNAS NFS cinder driver, the snapshots of volumes are clones of volumes
|
|
||||||
that were created using :command:`file-clone-create`, not the HNAS
|
|
||||||
:command:`snapshot-\*` feature. Check the HNAS users
|
|
||||||
documentation to have details about those 2 features.
|
|
||||||
|
|
||||||
Currently, the manage snapshots function does not support importing snapshots
|
|
||||||
(generally created by storage's :command:`file-clone` operation)
|
|
||||||
``without parent volumes`` or when the parent volume is ``in-use``. In this
|
|
||||||
case, the ``manage volumes`` should be used to import the snapshot as a normal
|
|
||||||
cinder volume.
|
|
||||||
|
|
||||||
Also, it is an admin only feature and you have to be logged as a user with
|
|
||||||
admin rights to be able to use this.
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
Although there is a verification to prevent importing snapshots using
|
|
||||||
non-related volumes as parents, it is possible to manage a snapshot using
|
|
||||||
any related cloned volume. So, when managing a snapshot, it is extremely
|
|
||||||
important to make sure that you are using the correct parent volume.
|
|
||||||
|
|
||||||
.. code-block:: console
|
|
||||||
|
|
||||||
$ cinder snapshot-manage <volume> <identifier>
|
|
||||||
|
|
||||||
* :guilabel:`Identifier`: evs_ip:/export_name/snapshot_name
|
|
||||||
(*For example:* 172.24.44.34:/export1/snapshot-test)
|
|
||||||
|
|
||||||
* :guilabel:`Volume`: Parent volume ID (*For example:*
|
|
||||||
061028c0-60cf-499f-99e2-2cd6afea081f)
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
.. code-block:: console
|
|
||||||
|
|
||||||
$ cinder snapshot-manage 061028c0-60cf-499f-99e2-2cd6afea081f 172.24.44.34:/export1/snapshot-test
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
This feature is currently available only for HNAS NFS Driver.
|
|
||||||
|
|
||||||
.. _configuration_example:
|
|
||||||
|
|
||||||
Configuration example
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
Below are configuration examples for NFS backend:
|
|
||||||
|
|
||||||
#. HNAS NFS Driver
|
|
||||||
|
|
||||||
#. For HNAS NFS driver, create this section in your ``cinder.conf`` file:
|
|
||||||
|
|
||||||
.. code-block:: ini
|
|
||||||
|
|
||||||
[hnas-nfs]
|
|
||||||
volume_driver = cinder.volume.drivers.hitachi.hnas_nfs.HNASNFSDriver
|
|
||||||
nfs_shares_config = /home/cinder/nfs_shares
|
|
||||||
volume_backend_name = hnas_nfs_backend
|
|
||||||
hnas_username = supervisor
|
|
||||||
hnas_password = supervisor
|
|
||||||
hnas_mgmt_ip0 = 172.24.44.15
|
|
||||||
|
|
||||||
hnas_svc0_pool_name = nfs_gold
|
|
||||||
hnas_svc0_hdp = 172.24.49.21:/gold_export
|
|
||||||
|
|
||||||
hnas_svc1_pool_name = nfs_platinum
|
|
||||||
hnas_svc1_hdp = 172.24.49.21:/silver_platinum
|
|
||||||
|
|
||||||
hnas_svc2_pool_name = nfs_silver
|
|
||||||
hnas_svc2_hdp = 172.24.49.22:/silver_export
|
|
||||||
|
|
||||||
hnas_svc3_pool_name = nfs_bronze
|
|
||||||
hnas_svc3_hdp = 172.24.49.23:/bronze_export
|
|
||||||
|
|
||||||
#. Add it to the ``enabled_backends`` list, under the ``DEFAULT`` section
|
|
||||||
of your ``cinder.conf`` file:
|
|
||||||
|
|
||||||
.. code-block:: ini
|
|
||||||
|
|
||||||
[DEFAULT]
|
|
||||||
enabled_backends = hnas-nfs
|
|
||||||
|
|
||||||
#. Add the configured exports to the ``nfs_shares`` file:
|
|
||||||
|
|
||||||
.. code-block:: vim
|
|
||||||
|
|
||||||
172.24.49.21:/gold_export
|
|
||||||
172.24.49.21:/silver_platinum
|
|
||||||
172.24.49.22:/silver_export
|
|
||||||
172.24.49.23:/bronze_export
|
|
||||||
|
|
||||||
#. Register a volume type with cinder and associate it with
|
|
||||||
this backend:
|
|
||||||
|
|
||||||
.. code-block:: console
|
|
||||||
|
|
||||||
$ openstack volume type create hnas_nfs_gold
|
|
||||||
$ openstack volume type set --property volume_backend_name=hnas_nfs_backend \
|
|
||||||
service_label=nfs_gold hnas_nfs_gold
|
|
||||||
$ openstack volume type create hnas_nfs_platinum
|
|
||||||
$ openstack volume type set --property volume_backend_name=hnas_nfs_backend \
|
|
||||||
service_label=nfs_platinum hnas_nfs_platinum
|
|
||||||
$ openstack volume type create hnas_nfs_silver
|
|
||||||
$ openstack volume type set --property volume_backend_name=hnas_nfs_backend \
|
|
||||||
service_label=nfs_silver hnas_nfs_silver
|
|
||||||
$ openstack volume type create hnas_nfs_bronze
|
|
||||||
$ openstack volume type set --property volume_backend_name=hnas_nfs_backend \
|
|
||||||
service_label=nfs_bronze hnas_nfs_bronze
|
|
||||||
|
|
||||||
Additional notes and limitations
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
* The ``get_volume_stats()`` function always provides the available
|
|
||||||
capacity based on the combined sum of all the HDPs that are used in
|
|
||||||
these services labels.
|
|
||||||
|
|
||||||
* After changing the configuration on the storage node, the Block Storage
|
|
||||||
driver must be restarted.
|
|
||||||
|
|
||||||
* On Red Hat, if the system is configured to use SELinux, you need to
|
|
||||||
set ``virt_use_nfs = on`` for NFS driver work properly.
|
|
||||||
|
|
||||||
.. code-block:: console
|
|
||||||
|
|
||||||
# setsebool -P virt_use_nfs on
|
|
||||||
|
|
||||||
* It is not possible to manage a volume if there is a slash (``/``) or
|
|
||||||
a colon (``:``) in the volume name.
|
|
||||||
|
|
||||||
* File system ``auto-expansion``: Although supported, we do not recommend using
|
|
||||||
file systems with auto-expansion setting enabled because the scheduler uses
|
|
||||||
the file system capacity reported by the driver to determine if new volumes
|
|
||||||
can be created. For instance, in a setup with a file system that can expand
|
|
||||||
to 200GB but is at 100GB capacity, with 10GB free, the scheduler will not
|
|
||||||
allow a 15GB volume to be created. In this case, manual expansion would
|
|
||||||
have to be triggered by an administrator. We recommend always creating the
|
|
||||||
file system at the ``maximum capacity`` or periodically expanding the file
|
|
||||||
system manually.
|
|
||||||
|
|
||||||
* The ``hnas_svcX_pool_name`` option must be unique for a given back end. It
|
|
||||||
is still possible to use the deprecated form ``hnas_svcX_volume_type``, but
|
|
||||||
this support will be removed in a future release.
|
|
||||||
|
|
||||||
* SSC simultaneous connections limit: In very busy environments, if 2 or
|
|
||||||
more volume hosts are configured to use the same storage, some requests
|
|
||||||
(create, delete and so on) can have some attempts failed and re-tried (
|
|
||||||
``5 attempts`` by default) due to an HNAS connection limitation (
|
|
||||||
``max of 5`` simultaneous connections).
|
|
@ -1,169 +0,0 @@
|
|||||||
=============================
|
|
||||||
Hitachi storage volume driver
|
|
||||||
=============================
|
|
||||||
|
|
||||||
Hitachi storage volume driver provides iSCSI and Fibre Channel
|
|
||||||
support for Hitachi storages.
|
|
||||||
|
|
||||||
System requirements
|
|
||||||
~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
Supported storages:
|
|
||||||
|
|
||||||
* Hitachi Virtual Storage Platform G1000 (VSP G1000)
|
|
||||||
* Hitachi Virtual Storage Platform (VSP)
|
|
||||||
* Hitachi Unified Storage VM (HUS VM)
|
|
||||||
* Hitachi Unified Storage 100 Family (HUS 100 Family)
|
|
||||||
|
|
||||||
Required software:
|
|
||||||
|
|
||||||
* RAID Manager Ver 01-32-03/01 or later for VSP G1000/VSP/HUS VM
|
|
||||||
* Hitachi Storage Navigator Modular 2 (HSNM2) Ver 27.50 or later
|
|
||||||
for HUS 100 Family
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
HSNM2 needs to be installed under ``/usr/stonavm``.
|
|
||||||
|
|
||||||
Required licenses:
|
|
||||||
|
|
||||||
* Hitachi In-System Replication Software for VSP G1000/VSP/HUS VM
|
|
||||||
* (Mandatory) ShadowImage in-system replication for HUS 100 Family
|
|
||||||
* (Optional) Copy-on-Write Snapshot for HUS 100 Family
|
|
||||||
|
|
||||||
Additionally, the ``pexpect`` package is required.
|
|
||||||
|
|
||||||
Supported operations
|
|
||||||
~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
* Create, delete, attach, and detach volumes.
|
|
||||||
* Create, list, and delete volume snapshots.
|
|
||||||
* Manage and unmanage volume snapshots.
|
|
||||||
* Create a volume from a snapshot.
|
|
||||||
* Copy a volume to an image.
|
|
||||||
* Copy an image to a volume.
|
|
||||||
* Clone a volume.
|
|
||||||
* Extend a volume.
|
|
||||||
* Get volume statistics.
|
|
||||||
|
|
||||||
Configuration
|
|
||||||
~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
Set up Hitachi storage
|
|
||||||
----------------------
|
|
||||||
|
|
||||||
You need to specify settings as described below. For details about each step,
|
|
||||||
see the user's guide of the storage device. Use a storage administrative
|
|
||||||
software such as ``Storage Navigator`` to set up the storage device so that
|
|
||||||
LDEVs and host groups can be created and deleted, and LDEVs can be connected
|
|
||||||
to the server and can be asynchronously copied.
|
|
||||||
|
|
||||||
#. Create a Dynamic Provisioning pool.
|
|
||||||
|
|
||||||
#. Connect the ports at the storage to the controller node and compute nodes.
|
|
||||||
|
|
||||||
#. For VSP G1000/VSP/HUS VM, set ``port security`` to ``enable`` for the
|
|
||||||
ports at the storage.
|
|
||||||
|
|
||||||
#. For HUS 100 Family, set ``Host Group security`` or
|
|
||||||
``iSCSI target security`` to ``ON`` for the ports at the storage.
|
|
||||||
|
|
||||||
#. For the ports at the storage, create host groups (iSCSI targets) whose
|
|
||||||
names begin with HBSD- for the controller node and each compute node.
|
|
||||||
Then register a WWN (initiator IQN) for each of the controller node and
|
|
||||||
compute nodes.
|
|
||||||
|
|
||||||
#. For VSP G1000/VSP/HUS VM, perform the following:
|
|
||||||
|
|
||||||
* Create a storage device account belonging to the Administrator User
|
|
||||||
Group. (To use multiple storage devices, create the same account name
|
|
||||||
for all the target storage devices, and specify the same resource
|
|
||||||
group and permissions.)
|
|
||||||
* Create a command device (In-Band), and set user authentication to ``ON``.
|
|
||||||
* Register the created command device to the host group for the controller
|
|
||||||
node.
|
|
||||||
* To use the Thin Image function, create a pool for Thin Image.
|
|
||||||
|
|
||||||
#. For HUS 100 Family, perform the following:
|
|
||||||
|
|
||||||
* Use the :command:`auunitaddauto` command to register the
|
|
||||||
unit name and controller of the storage device to HSNM2.
|
|
||||||
* When connecting via iSCSI, if you are using CHAP certification, specify
|
|
||||||
the same user and password as that used for the storage port.
|
|
||||||
|
|
||||||
Set up Hitachi Gigabit Fibre Channel adaptor
|
|
||||||
--------------------------------------------
|
|
||||||
|
|
||||||
Change a parameter of the hfcldd driver and update the ``initram`` file
|
|
||||||
if Hitachi Gigabit Fibre Channel adaptor is used:
|
|
||||||
|
|
||||||
.. code-block:: console
|
|
||||||
|
|
||||||
# /opt/hitachi/drivers/hba/hfcmgr -E hfc_rport_lu_scan 1
|
|
||||||
# dracut -f initramfs-KERNEL_VERSION.img KERNEL_VERSION
|
|
||||||
# reboot
|
|
||||||
|
|
||||||
Set up Hitachi storage volume driver
|
|
||||||
------------------------------------
|
|
||||||
|
|
||||||
#. Create a directory:
|
|
||||||
|
|
||||||
.. code-block:: console
|
|
||||||
|
|
||||||
# mkdir /var/lock/hbsd
|
|
||||||
# chown cinder:cinder /var/lock/hbsd
|
|
||||||
|
|
||||||
#. Create ``volume type`` and ``volume key``.
|
|
||||||
|
|
||||||
This example shows that HUS100_SAMPLE is created as ``volume type``
|
|
||||||
and hus100_backend is registered as ``volume key``:
|
|
||||||
|
|
||||||
.. code-block:: console
|
|
||||||
|
|
||||||
$ openstack volume type create HUS100_SAMPLE
|
|
||||||
$ openstack volume type set --property volume_backend_name=hus100_backend HUS100_SAMPLE
|
|
||||||
|
|
||||||
#. Specify any identical ``volume type`` name and ``volume key``.
|
|
||||||
|
|
||||||
To confirm the created ``volume type``, please execute the following
|
|
||||||
command:
|
|
||||||
|
|
||||||
.. code-block:: console
|
|
||||||
|
|
||||||
$ openstack volume type list --long
|
|
||||||
|
|
||||||
#. Edit the ``/etc/cinder/cinder.conf`` file as follows.
|
|
||||||
|
|
||||||
If you use Fibre Channel:
|
|
||||||
|
|
||||||
.. code-block:: ini
|
|
||||||
|
|
||||||
volume_driver = cinder.volume.drivers.hitachi.hbsd_fc.HBSDFCDriver
|
|
||||||
|
|
||||||
If you use iSCSI:
|
|
||||||
|
|
||||||
.. code-block:: ini
|
|
||||||
|
|
||||||
volume_driver = cinder.volume.drivers.hitachi.hbsd_iscsi.HBSDISCSIDriver
|
|
||||||
|
|
||||||
Also, set ``volume_backend_name`` created by :command:`openstack volume type set`
|
|
||||||
command:
|
|
||||||
|
|
||||||
.. code-block:: ini
|
|
||||||
|
|
||||||
volume_backend_name = hus100_backend
|
|
||||||
|
|
||||||
This table shows configuration options for Hitachi storage volume driver.
|
|
||||||
|
|
||||||
.. include:: ../../tables/cinder-hitachi-hbsd.inc
|
|
||||||
|
|
||||||
#. Restart the Block Storage service.
|
|
||||||
|
|
||||||
When the startup is done, "MSGID0003-I: The storage backend can be used."
|
|
||||||
is output into ``/var/log/cinder/volume.log`` as follows:
|
|
||||||
|
|
||||||
.. code-block:: console
|
|
||||||
|
|
||||||
2014-09-01 10:34:14.169 28734 WARNING cinder.volume.drivers.hitachi.
|
|
||||||
hbsd_common [req-a0bb70b5-7c3f-422a-a29e-6a55d6508135 None None]
|
|
||||||
MSGID0003-I: The storage backend can be used. (config_group: hus100_backend)
|
|
@ -25,8 +25,6 @@ Volume drivers
|
|||||||
drivers/emc-vnx-driver.rst
|
drivers/emc-vnx-driver.rst
|
||||||
drivers/emc-xtremio-driver.rst
|
drivers/emc-xtremio-driver.rst
|
||||||
drivers/fujitsu-eternus-dx-driver.rst
|
drivers/fujitsu-eternus-dx-driver.rst
|
||||||
drivers/hds-hnas-driver.rst
|
|
||||||
drivers/hitachi-storage-volume-driver.rst
|
|
||||||
drivers/hpe-3par-driver.rst
|
drivers/hpe-3par-driver.rst
|
||||||
drivers/hpe-lefthand-driver.rst
|
drivers/hpe-lefthand-driver.rst
|
||||||
drivers/hp-msa-driver.rst
|
drivers/hp-msa-driver.rst
|
||||||
|
@ -1,64 +0,0 @@
|
|||||||
..
|
|
||||||
Warning: Do not edit this file. It is automatically generated from the
|
|
||||||
software project's code and your changes will be overwritten.
|
|
||||||
|
|
||||||
The tool to generate this file lives in openstack-doc-tools repository.
|
|
||||||
|
|
||||||
Please make any changes needed in the code, then run the
|
|
||||||
autogenerate-config-doc tool from the openstack-doc-tools repository, or
|
|
||||||
ask for help on the documentation mailing list, IRC channel or meeting.
|
|
||||||
|
|
||||||
.. _cinder-hitachi-hbsd:
|
|
||||||
|
|
||||||
.. list-table:: Description of Hitachi storage volume driver configuration options
|
|
||||||
:header-rows: 1
|
|
||||||
:class: config-ref-table
|
|
||||||
|
|
||||||
* - Configuration option = Default value
|
|
||||||
- Description
|
|
||||||
* - **[DEFAULT]**
|
|
||||||
-
|
|
||||||
* - ``hitachi_add_chap_user`` = ``False``
|
|
||||||
- (Boolean) Add CHAP user
|
|
||||||
* - ``hitachi_async_copy_check_interval`` = ``10``
|
|
||||||
- (Integer) Interval to check copy asynchronously
|
|
||||||
* - ``hitachi_auth_method`` = ``None``
|
|
||||||
- (String) iSCSI authentication method
|
|
||||||
* - ``hitachi_auth_password`` = ``HBSD-CHAP-password``
|
|
||||||
- (String) iSCSI authentication password
|
|
||||||
* - ``hitachi_auth_user`` = ``HBSD-CHAP-user``
|
|
||||||
- (String) iSCSI authentication username
|
|
||||||
* - ``hitachi_copy_check_interval`` = ``3``
|
|
||||||
- (Integer) Interval to check copy
|
|
||||||
* - ``hitachi_copy_speed`` = ``3``
|
|
||||||
- (Integer) Copy speed of storage system
|
|
||||||
* - ``hitachi_default_copy_method`` = ``FULL``
|
|
||||||
- (String) Default copy method of storage system
|
|
||||||
* - ``hitachi_group_range`` = ``None``
|
|
||||||
- (String) Range of group number
|
|
||||||
* - ``hitachi_group_request`` = ``False``
|
|
||||||
- (Boolean) Request for creating HostGroup or iSCSI Target
|
|
||||||
* - ``hitachi_horcm_add_conf`` = ``True``
|
|
||||||
- (Boolean) Add to HORCM configuration
|
|
||||||
* - ``hitachi_horcm_numbers`` = ``200,201``
|
|
||||||
- (String) Instance numbers for HORCM
|
|
||||||
* - ``hitachi_horcm_password`` = ``None``
|
|
||||||
- (String) Password of storage system for HORCM
|
|
||||||
* - ``hitachi_horcm_resource_lock_timeout`` = ``600``
|
|
||||||
- (Integer) Timeout until a resource lock is released, in seconds. The value must be between 0 and 7200.
|
|
||||||
* - ``hitachi_horcm_user`` = ``None``
|
|
||||||
- (String) Username of storage system for HORCM
|
|
||||||
* - ``hitachi_ldev_range`` = ``None``
|
|
||||||
- (String) Range of logical device of storage system
|
|
||||||
* - ``hitachi_pool_id`` = ``None``
|
|
||||||
- (Integer) Pool ID of storage system
|
|
||||||
* - ``hitachi_serial_number`` = ``None``
|
|
||||||
- (String) Serial number of storage system
|
|
||||||
* - ``hitachi_target_ports`` = ``None``
|
|
||||||
- (String) Control port names for HostGroup or iSCSI Target
|
|
||||||
* - ``hitachi_thin_pool_id`` = ``None``
|
|
||||||
- (Integer) Thin pool ID of storage system
|
|
||||||
* - ``hitachi_unit_name`` = ``None``
|
|
||||||
- (String) Name of an array unit
|
|
||||||
* - ``hitachi_zoning_request`` = ``False``
|
|
||||||
- (Boolean) Request for FC Zone creating HostGroup
|
|
@ -1,64 +0,0 @@
|
|||||||
..
|
|
||||||
Warning: Do not edit this file. It is automatically generated from the
|
|
||||||
software project's code and your changes will be overwritten.
|
|
||||||
|
|
||||||
The tool to generate this file lives in openstack-doc-tools repository.
|
|
||||||
|
|
||||||
Please make any changes needed in the code, then run the
|
|
||||||
autogenerate-config-doc tool from the openstack-doc-tools repository, or
|
|
||||||
ask for help on the documentation mailing list, IRC channel or meeting.
|
|
||||||
|
|
||||||
.. _cinder-hitachi-hnas:
|
|
||||||
|
|
||||||
.. list-table:: Description of Hitachi HNAS iSCSI and NFS driver configuration options
|
|
||||||
:header-rows: 1
|
|
||||||
:class: config-ref-table
|
|
||||||
|
|
||||||
* - Configuration option = Default value
|
|
||||||
- Description
|
|
||||||
* - **[DEFAULT]**
|
|
||||||
-
|
|
||||||
* - ``hds_hnas_iscsi_config_file`` = ``/opt/hds/hnas/cinder_iscsi_conf.xml``
|
|
||||||
- (String) DEPRECATED: Legacy configuration file for HNAS iSCSI Cinder plugin. This is not needed if you fill all configuration on cinder.conf
|
|
||||||
* - ``hds_hnas_nfs_config_file`` = ``/opt/hds/hnas/cinder_nfs_conf.xml``
|
|
||||||
- (String) DEPRECATED: Legacy configuration file for HNAS NFS Cinder plugin. This is not needed if you fill all configuration on cinder.conf
|
|
||||||
* - ``hnas_chap_enabled`` = ``True``
|
|
||||||
- (Boolean) Whether the chap authentication is enabled in the iSCSI target or not.
|
|
||||||
* - ``hnas_cluster_admin_ip0`` = ``None``
|
|
||||||
- (String) The IP of the HNAS cluster admin. Required only for HNAS multi-cluster setups.
|
|
||||||
* - ``hnas_mgmt_ip0`` = ``None``
|
|
||||||
- (IP) Management IP address of HNAS. This can be any IP in the admin address on HNAS or the SMU IP.
|
|
||||||
* - ``hnas_password`` = ``None``
|
|
||||||
- (String) HNAS password.
|
|
||||||
* - ``hnas_ssc_cmd`` = ``ssc``
|
|
||||||
- (String) Command to communicate to HNAS.
|
|
||||||
* - ``hnas_ssh_port`` = ``22``
|
|
||||||
- (Port number) Port to be used for SSH authentication.
|
|
||||||
* - ``hnas_ssh_private_key`` = ``None``
|
|
||||||
- (String) Path to the SSH private key used to authenticate in HNAS SMU.
|
|
||||||
* - ``hnas_svc0_hdp`` = ``None``
|
|
||||||
- (String) Service 0 HDP
|
|
||||||
* - ``hnas_svc0_iscsi_ip`` = ``None``
|
|
||||||
- (IP) Service 0 iSCSI IP
|
|
||||||
* - ``hnas_svc0_pool_name`` = ``None``
|
|
||||||
- (String) Service 0 pool name
|
|
||||||
* - ``hnas_svc1_hdp`` = ``None``
|
|
||||||
- (String) Service 1 HDP
|
|
||||||
* - ``hnas_svc1_iscsi_ip`` = ``None``
|
|
||||||
- (IP) Service 1 iSCSI IP
|
|
||||||
* - ``hnas_svc1_pool_name`` = ``None``
|
|
||||||
- (String) Service 1 pool name
|
|
||||||
* - ``hnas_svc2_hdp`` = ``None``
|
|
||||||
- (String) Service 2 HDP
|
|
||||||
* - ``hnas_svc2_iscsi_ip`` = ``None``
|
|
||||||
- (IP) Service 2 iSCSI IP
|
|
||||||
* - ``hnas_svc2_pool_name`` = ``None``
|
|
||||||
- (String) Service 2 pool name
|
|
||||||
* - ``hnas_svc3_hdp`` = ``None``
|
|
||||||
- (String) Service 3 HDP
|
|
||||||
* - ``hnas_svc3_iscsi_ip`` = ``None``
|
|
||||||
- (IP) Service 3 iSCSI IP
|
|
||||||
* - ``hnas_svc3_pool_name`` = ``None``
|
|
||||||
- (String) Service 3 pool name:
|
|
||||||
* - ``hnas_username`` = ``None``
|
|
||||||
- (String) HNAS username.
|
|
@ -1,60 +0,0 @@
|
|||||||
..
|
|
||||||
Warning: Do not edit this file. It is automatically generated from the
|
|
||||||
software project's code and your changes will be overwritten.
|
|
||||||
|
|
||||||
The tool to generate this file lives in openstack-doc-tools repository.
|
|
||||||
|
|
||||||
Please make any changes needed in the code, then run the
|
|
||||||
autogenerate-config-doc tool from the openstack-doc-tools repository, or
|
|
||||||
ask for help on the documentation mailing list, IRC channel or meeting.
|
|
||||||
|
|
||||||
.. _cinder-hitachi-vsp:
|
|
||||||
|
|
||||||
.. list-table:: Description of HORCM interface module for Hitachi VSP driver configuration options
|
|
||||||
:header-rows: 1
|
|
||||||
:class: config-ref-table
|
|
||||||
|
|
||||||
* - Configuration option = Default value
|
|
||||||
- Description
|
|
||||||
* - **[DEFAULT]**
|
|
||||||
-
|
|
||||||
* - ``vsp_async_copy_check_interval`` = ``10``
|
|
||||||
- (Integer) Interval in seconds at which volume pair synchronization status is checked when volume pairs are deleted.
|
|
||||||
* - ``vsp_auth_password`` = ``None``
|
|
||||||
- (String) Password corresponding to vsp_auth_user.
|
|
||||||
* - ``vsp_auth_user`` = ``None``
|
|
||||||
- (String) Name of the user used for CHAP authentication performed in communication between hosts and iSCSI targets on the storage ports.
|
|
||||||
* - ``vsp_compute_target_ports`` = ``None``
|
|
||||||
- (List) IDs of the storage ports used to attach volumes to compute nodes. To specify multiple ports, connect them by commas (e.g. CL1-A,CL2-A).
|
|
||||||
* - ``vsp_copy_check_interval`` = ``3``
|
|
||||||
- (Integer) Interval in seconds at which volume pair synchronization status is checked when volume pairs are created.
|
|
||||||
* - ``vsp_copy_speed`` = ``3``
|
|
||||||
- (Integer) Speed at which data is copied by Shadow Image. 1 or 2 indicates low speed, 3 indicates middle speed, and a value between 4 and 15 indicates high speed.
|
|
||||||
* - ``vsp_default_copy_method`` = ``FULL``
|
|
||||||
- (String) Method of volume copy. FULL indicates full data copy by Shadow Image and THIN indicates differential data copy by Thin Image.
|
|
||||||
* - ``vsp_group_request`` = ``False``
|
|
||||||
- (Boolean) If True, the driver will create host groups or iSCSI targets on storage ports as needed.
|
|
||||||
* - ``vsp_horcm_add_conf`` = ``True``
|
|
||||||
- (Boolean) If True, the driver will create or update the Command Control Interface configuration file as needed.
|
|
||||||
* - ``vsp_horcm_numbers`` = ``200, 201``
|
|
||||||
- (List) Command Control Interface instance numbers in the format of 'xxx,yyy'. The second one is for Shadow Image operation and the first one is for other purposes.
|
|
||||||
* - ``vsp_horcm_pair_target_ports`` = ``None``
|
|
||||||
- (List) IDs of the storage ports used to copy volumes by Shadow Image or Thin Image. To specify multiple ports, connect them by commas (e.g. CL1-A,CL2-A).
|
|
||||||
* - ``vsp_horcm_password`` = ``None``
|
|
||||||
- (String) Password corresponding to vsp_horcm_user.
|
|
||||||
* - ``vsp_horcm_user`` = ``None``
|
|
||||||
- (String) Name of the user on the storage system.
|
|
||||||
* - ``vsp_ldev_range`` = ``None``
|
|
||||||
- (String) Range of the LDEV numbers in the format of 'xxxx-yyyy' that can be used by the driver. Values can be in decimal format (e.g. 1000) or in colon-separated hexadecimal format (e.g. 00:03:E8).
|
|
||||||
* - ``vsp_pool`` = ``None``
|
|
||||||
- (String) Pool number or pool name of the DP pool.
|
|
||||||
* - ``vsp_storage_id`` = ``None``
|
|
||||||
- (String) Product number of the storage system.
|
|
||||||
* - ``vsp_target_ports`` = ``None``
|
|
||||||
- (List) IDs of the storage ports used to attach volumes to the controller node. To specify multiple ports, connect them by commas (e.g. CL1-A,CL2-A).
|
|
||||||
* - ``vsp_thin_pool`` = ``None``
|
|
||||||
- (String) Pool number or pool name of the Thin Image pool.
|
|
||||||
* - ``vsp_use_chap_auth`` = ``False``
|
|
||||||
- (Boolean) If True, CHAP authentication will be applied to communication between hosts and any of the iSCSI targets on the storage ports.
|
|
||||||
* - ``vsp_zoning_request`` = ``False``
|
|
||||||
- (Boolean) If True, the driver will configure FC zoning between the server and the storage system provided that FC zoning manager is enabled.
|
|
6
releasenotes/notes/remove-hitachi-57d0b37cb9cc7e13.yaml
Normal file
6
releasenotes/notes/remove-hitachi-57d0b37cb9cc7e13.yaml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
upgrade:
|
||||||
|
- |
|
||||||
|
The Hitachi HNAS, HBSD, and VSP volume drivers were marked as deprecated
|
||||||
|
in the Pike release and have now been removed. Hitachi storage drivers are
|
||||||
|
now only available directly from Hitachi.
|
Loading…
Reference in New Issue
Block a user