HP 3PAR driver handles shares servers

HP 3PAR driver can use driver_handle_share_servers=True
mode to automatically setup the FSIP using the IP
address, subnet and VLAN tag obtained from the share
network.

The driver_handles_share_servers=False is still supported
with a pre-configured IP address.

Implements Blueprint: hp3par-share-server-support

Change-Id: Iae63a22923a5d7622a8810629071f5ea9823b0ed
This commit is contained in:
Mark Sturdevant 2015-06-30 17:16:55 -07:00
parent 18fb649e4c
commit 7537951be6
5 changed files with 527 additions and 19 deletions

View File

@ -31,6 +31,7 @@ from manila.i18n import _LI
from manila.share import driver
from manila.share.drivers.hp import hp_3par_mediator
from manila.share import share_types
from manila import utils
HP3PAR_OPTS = [
cfg.StrOpt('hp3par_api_url',
@ -80,13 +81,19 @@ LOG = log.getLogger(__name__)
class HP3ParShareDriver(driver.ShareDriver):
"""HP 3PAR driver for Manila.
Supports NFS and CIFS protocols on arrays with File Persona.
"""
Supports NFS and CIFS protocols on arrays with File Persona.
VERSION = "1.0.01"
Version history:
1.0.00 - Begin Liberty development (post-Kilo)
1.0.01 - Report thin/dedup/hp_flash_cache capabilities
1.0.02 - Add share server/share network support
"""
VERSION = "1.0.02"
def __init__(self, *args, **kwargs):
super(HP3ParShareDriver, self).__init__(False, *args, **kwargs)
super(HP3ParShareDriver, self).__init__((True, False), *args, **kwargs)
self.configuration = kwargs.get('configuration', None)
self.configuration.append_config_values(HP3PAR_OPTS)
@ -103,11 +110,13 @@ class HP3ParShareDriver(driver.ShareDriver):
{'driver_name': self.__class__.__name__,
'version': self.VERSION})
self.share_ip_address = self.configuration.hp3par_share_ip_address
if not self.share_ip_address:
raise exception.HP3ParInvalid(
_("Unsupported configuration. "
"hp3par_share_ip_address is not set."))
if not self.driver_handles_share_servers:
self.share_ip_address = self.configuration.hp3par_share_ip_address
if not self.share_ip_address:
raise exception.HP3ParInvalid(
_("Unsupported configuration. "
"hp3par_share_ip_address must be set when "
"driver_handles_share_servers is False."))
mediator = hp_3par_mediator.HP3ParMediator(
hp3par_username=self.configuration.hp3par_username,
@ -163,8 +172,60 @@ class HP3ParShareDriver(driver.ShareDriver):
return sha1.hexdigest()
def get_network_allocations_number(self):
return 1
@staticmethod
def _validate_network_type(network_type):
if network_type not in ('flat', 'vlan', None):
reason = _('Invalid network type. %s is not supported by the '
'3PAR driver.')
raise exception.NetworkBadConfigurationException(
reason=reason % network_type)
def _setup_server(self, network_info, metadata=None):
LOG.debug("begin _setup_server with %s", network_info)
self._validate_network_type(network_info['network_type'])
ip = network_info['network_allocations'][0]['ip_address']
subnet = utils.cidr_to_netmask(network_info['cidr'])
vlantag = network_info['segmentation_id']
self._hp3par.create_fsip(ip, subnet, vlantag, self.fpg, self.vfs)
return {
'share_server_name': network_info['server_id'],
'share_server_id': network_info['server_id'],
'ip': ip,
'subnet': subnet,
'vlantag': vlantag if vlantag else 0,
'fpg': self.fpg,
'vfs': self.vfs,
}
def _teardown_server(self, server_details, security_services=None):
LOG.debug("begin _teardown_server with %s", server_details)
self._hp3par.remove_fsip(server_details.get('ip'),
server_details.get('fpg'),
server_details.get('vfs'))
def _get_share_ip(self, share_server):
return share_server['backend_details'].get('ip') if share_server else (
self.share_ip_address)
@staticmethod
def _build_export_location(protocol, ip, path):
if not ip:
message = _('Failed to build export location due to missing IP.')
raise exception.InvalidInput(message)
if not path:
message = _('Failed to build export location due to missing path.')
raise exception.InvalidInput(message)
if protocol == 'NFS':
location = ':'.join((ip, path))
elif protocol == 'CIFS':
@ -173,6 +234,7 @@ class HP3ParShareDriver(driver.ShareDriver):
message = _('Invalid protocol. Expected NFS or CIFS. '
'Got %s.') % protocol
raise exception.InvalidInput(message)
return location
@staticmethod
@ -194,7 +256,7 @@ class HP3ParShareDriver(driver.ShareDriver):
def create_share(self, context, share, share_server=None):
"""Is called to create share."""
ip = self.share_ip_address
ip = self._get_share_ip(share_server)
protocol = share['share_proto']
extra_specs = share_types.get_extra_specs_from_share(share)
@ -215,7 +277,7 @@ class HP3ParShareDriver(driver.ShareDriver):
share_server=None):
"""Is called to create share from snapshot."""
ip = self.share_ip_address
ip = self._get_share_ip(share_server)
protocol = share['share_proto']
extra_specs = share_types.get_extra_specs_from_share(share)

View File

@ -53,8 +53,16 @@ SMB_EXTRA_SPECS_MAP = {
class HP3ParMediator(object):
"""3PAR client-facing code for the 3PAR driver.
VERSION = "1.0.01"
Version history:
1.0.00 - Begin Liberty development (post-Kilo)
1.0.01 - Report thin/dedup/hp_flash_cache capabilities
1.0.02 - Add share server/share network support
"""
VERSION = "1.0.02"
def __init__(self, **kwargs):
@ -828,3 +836,88 @@ class HP3ParMediator(object):
self._change_access(DENY, project_id, share_id, share_proto,
access_type, access_to, fpg, vfs)
def fsip_exists(self, fsip):
"""Try to get FSIP. Return True if it exists."""
vfs = fsip['vfs']
fpg = fsip['fspool']
try:
result = self._client.getfsip(vfs, fpg=fpg)
LOG.debug("getfsip result: %s", result)
except Exception as e:
LOG.exception(e)
msg = (_('Failed to get FSIPs for FPG/VFS %(fspool)s/%(vfs)s.') %
fsip)
LOG.exception(msg)
raise exception.ShareBackendException(msg=msg)
for member in result['members']:
if all(item in member.items() for item in fsip.items()):
return True
return False
def create_fsip(self, ip, subnet, vlantag, fpg, vfs):
vlantag_str = six.text_type(vlantag) if vlantag else '0'
# Try to create it. It's OK if it already exists.
try:
result = self._client.createfsip(ip,
subnet,
vfs,
fpg=fpg,
vlantag=vlantag_str)
LOG.debug("createfsip result: %s", result)
except Exception as e:
LOG.exception(e)
msg = (_('Failed to create FSIP for %s') % ip)
LOG.exception(msg)
raise exception.ShareBackendException(msg=msg)
# Verify that it really exists.
fsip = {
'fspool': fpg,
'vfs': vfs,
'address': ip,
'prefixLen': subnet,
'vlanTag': vlantag_str,
}
if not self.fsip_exists(fsip):
msg = (_('Failed to get FSIP after creating it for '
'FPG/VFS/IP/subnet/VLAN '
'%(fspool)s/%(vfs)s/'
'%(address)s/%(prefixLen)s/%(vlanTag)s.') % fsip)
LOG.exception(msg)
raise exception.ShareBackendException(msg=msg)
def remove_fsip(self, ip, fpg, vfs):
if not (vfs and ip):
# If there is no VFS and/or IP, then there is no FSIP to remove.
return
try:
result = self._client.removefsip(vfs, ip, fpg=fpg)
LOG.debug("removefsip result: %s", result)
except Exception as e:
LOG.exception(e)
msg = (_('Failed to remove FSIP %s') % ip)
LOG.exception(msg)
raise exception.ShareBackendException(msg=msg)
# Verify that it really no longer exists.
fsip = {
'fspool': fpg,
'vfs': vfs,
'address': ip,
}
if self.fsip_exists(fsip):
msg = (_('Failed to remove FSIP for FPG/VFS/IP '
'%(fspool)s/%(vfs)s/%(address)s.') % fsip)
LOG.exception(msg)
raise exception.ShareBackendException(msg=msg)

View File

@ -25,11 +25,16 @@ API_URL = 'https://1.2.3.4:8080/api/v1'
TIMEOUT = 60
PORT = 22
SHARE_TYPE_ID = 123456789
CIDR_PREFIX = '24'
# Constants to use with Mock and expect in results
EXPECTED_IP_10203040 = '10.20.30.40'
EXPECTED_IP_1234 = '1.2.3.4'
EXPECTED_IP_127 = '127.0.0.1'
EXPECTED_SUBNET = '255.255.255.0' # based on CIDR_PREFIX above
EXPECTED_VLAN_TYPE = 'vlan'
EXPECTED_VLAN_TAG = '101'
EXPECTED_SERVER_ID = '1a1a1a1a-2b2b-3c3c-4d4d-5e5e5e5e5e5e'
EXPECTED_PROJECT_ID = 'osf-nfs-project-id'
EXPECTED_SHARE_ID = 'osf-share-id'
EXPECTED_SHARE_NAME = 'share-name'
@ -50,6 +55,22 @@ GET_FSQUOTA = {'message': None,
'total': 1,
'members': [{'hardBlock': '1024', 'softBlock': '1024'}]}
EXPECTED_FSIP = {
'fspool': EXPECTED_FPG,
'vfs': EXPECTED_VFS,
'address': EXPECTED_IP_1234,
'prefixLen': EXPECTED_SUBNET,
'vlanTag': EXPECTED_VLAN_TAG,
}
OTHER_FSIP = {
'fspool': EXPECTED_FPG,
'vfs': EXPECTED_VFS,
'address': '9.9.9.9',
'prefixLen': EXPECTED_SUBNET,
'vlanTag': EXPECTED_VLAN_TAG,
}
NFS_SHARE_INFO = {
'project_id': EXPECTED_PROJECT_ID,
'id': EXPECTED_SHARE_ID,

View File

@ -34,7 +34,7 @@ class HP3ParDriverTestCase(test.TestCase):
# Create a mock configuration with attributes and a safe_get()
self.conf = mock.Mock()
self.conf.driver_handles_share_servers = False
self.conf.driver_handles_share_servers = True
self.conf.hp3par_debug = constants.EXPECTED_HP_DEBUG
self.conf.hp3par_username = constants.USERNAME
self.conf.hp3par_password = constants.PASSWORD
@ -45,7 +45,7 @@ class HP3ParDriverTestCase(test.TestCase):
self.conf.hp3par_fpg = constants.EXPECTED_FPG
self.conf.hp3par_san_ssh_port = constants.PORT
self.conf.ssh_conn_timeout = constants.TIMEOUT
self.conf.hp3par_share_ip_address = constants.EXPECTED_IP_10203040
self.conf.hp3par_share_ip_address = None
self.conf.hp3par_fstore_per_share = False
self.conf.network_config_group = 'test_network_config_group'
@ -89,6 +89,21 @@ class HP3ParDriverTestCase(test.TestCase):
self.assertEqual(constants.EXPECTED_VFS, self.driver.vfs)
def test_driver_setup_no_dhss_success(self):
"""Driver do_setup without any errors with dhss=False."""
self.conf.driver_handles_share_servers = False
self.conf.hp3par_share_ip_address = constants.EXPECTED_IP_10203040
self.test_driver_setup_success()
def test_driver_setup_no_ss_no_ip(self):
"""Configured IP address is required for dhss=False."""
self.conf.driver_handles_share_servers = False
self.assertRaises(exception.HP3ParInvalid,
self.driver.do_setup, None)
def test_driver_with_setup_error(self):
"""Driver do_setup when the mediator setup fails."""
@ -145,7 +160,6 @@ class HP3ParDriverTestCase(test.TestCase):
self.driver._hp3par = self.mock_mediator
self.driver.vfs = constants.EXPECTED_VFS
self.driver.fpg = constants.EXPECTED_FPG
self.driver.share_ip_address = self.conf.hp3par_share_ip_address
self.mock_object(hp3pardriver, 'share_types')
get_extra_specs = hp3pardriver.share_types.get_extra_specs_from_share
get_extra_specs.return_value = constants.EXPECTED_EXTRA_SPECS
@ -154,7 +168,8 @@ class HP3ParDriverTestCase(test.TestCase):
expected_share_id, expected_size):
"""Re-usable code for create share."""
context = None
share_server = None
share_server = {
'backend_details': {'ip': constants.EXPECTED_IP_10203040}}
share = {
'display_name': constants.EXPECTED_SHARE_NAME,
'host': constants.EXPECTED_HOST,
@ -175,7 +190,11 @@ class HP3ParDriverTestCase(test.TestCase):
expected_size):
"""Re-usable code for create share from snapshot."""
context = None
share_server = None
share_server = {
'backend_details': {
'ip': constants.EXPECTED_IP_10203040,
},
}
share = {
'display_name': constants.EXPECTED_SHARE_NAME,
'host': constants.EXPECTED_HOST,
@ -436,6 +455,37 @@ class HP3ParDriverTestCase(test.TestCase):
]
self.mock_mediator.assert_has_calls(expected_calls)
def test_driver_get_share_stats_not_ready(self):
"""Protect against stats update before driver is ready."""
self.mock_object(hp3pardriver, 'LOG')
expected_result = {
'driver_handles_share_servers': True,
'QoS_support': False,
'driver_version': self.driver.VERSION,
'free_capacity_gb': 0,
'max_over_subscription_ratio': None,
'reserved_percentage': 0,
'provisioned_capacity_gb': 0,
'share_backend_name': 'HP_3PAR',
'snapshot_support': True,
'storage_protocol': 'NFS_CIFS',
'thin_provisioning': True,
'total_capacity_gb': 0,
'vendor_name': 'HP',
'pools': None,
}
result = self.driver.get_share_stats(refresh=True)
self.assertEqual(expected_result, result)
expected_calls = [
mock.call.info('Skipping capacity and capabilities update. '
'Setup has not completed.')
]
hp3pardriver.LOG.assert_has_calls(expected_calls)
def test_driver_get_share_stats_no_refresh(self):
"""Driver does not call mediator when refresh=False."""
@ -464,8 +514,8 @@ class HP3ParDriverTestCase(test.TestCase):
}
expected_result = {
'driver_handles_share_servers': True,
'QoS_support': False,
'driver_handles_share_servers': False,
'driver_version': expected_version,
'free_capacity_gb': expected_free,
'max_over_subscription_ratio': None,
@ -500,7 +550,7 @@ class HP3ParDriverTestCase(test.TestCase):
expected_result = {
'QoS_support': False,
'driver_handles_share_servers': False,
'driver_handles_share_servers': True,
'driver_version': expected_version,
'free_capacity_gb': 0,
'max_over_subscription_ratio': None,
@ -549,3 +599,84 @@ class HP3ParDriverTestCase(test.TestCase):
# Don't test with same regex as the code uses.
for c in "'\".,;":
self.assertNotIn(c, comment)
def test_get_network_allocations_number(self):
self.assertEqual(1, self.driver.get_network_allocations_number())
def test_build_export_location_bad_protocol(self):
self.assertRaises(exception.InvalidInput,
self.driver._build_export_location,
"BOGUS",
constants.EXPECTED_IP_1234,
constants.EXPECTED_SHARE_PATH)
def test_build_export_location_bad_ip(self):
self.assertRaises(exception.InvalidInput,
self.driver._build_export_location,
constants.NFS,
None,
None)
def test_build_export_location_bad_path(self):
self.assertRaises(exception.InvalidInput,
self.driver._build_export_location,
constants.NFS,
constants.EXPECTED_IP_1234,
None)
def test_setup_server(self):
"""Setup server by creating a new FSIP."""
self.init_driver()
network_info = {
'network_allocations': [
{'ip_address': constants.EXPECTED_IP_1234}],
'cidr': '/'.join((constants.EXPECTED_IP_1234,
constants.CIDR_PREFIX)),
'network_type': constants.EXPECTED_VLAN_TYPE,
'segmentation_id': constants.EXPECTED_VLAN_TAG,
'server_id': constants.EXPECTED_SERVER_ID,
}
expected_result = {
'share_server_name': constants.EXPECTED_SERVER_ID,
'share_server_id': constants.EXPECTED_SERVER_ID,
'ip': constants.EXPECTED_IP_1234,
'subnet': constants.EXPECTED_SUBNET,
'vlantag': constants.EXPECTED_VLAN_TAG,
'fpg': constants.EXPECTED_FPG,
'vfs': constants.EXPECTED_VFS,
}
result = self.driver._setup_server(network_info)
expected_calls = [
mock.call.create_fsip(constants.EXPECTED_IP_1234,
constants.EXPECTED_SUBNET,
constants.EXPECTED_VLAN_TAG,
constants.EXPECTED_FPG,
constants.EXPECTED_VFS)
]
self.mock_mediator.assert_has_calls(expected_calls)
self.assertEqual(expected_result, result)
def test_teardown_server(self):
self.init_driver()
server_details = {
'ip': constants.EXPECTED_IP_1234,
'fpg': constants.EXPECTED_FPG,
'vfs': constants.EXPECTED_VFS,
}
self.driver._teardown_server(server_details)
expected_calls = [
mock.call.remove_fsip(constants.EXPECTED_IP_1234,
constants.EXPECTED_FPG,
constants.EXPECTED_VFS)
]
self.mock_mediator.assert_has_calls(expected_calls)

View File

@ -1432,6 +1432,207 @@ class HP3ParMediatorTestCase(test.TestCase):
self.assertEqual(expected_result, result)
def test_fsip_exists(self):
self.init_mediator()
# Make the result member a superset of the fsip items.
fsip_plus = constants.EXPECTED_FSIP.copy()
fsip_plus.update({'k': 'v', 'k2': 'v2'})
self.mock_client.getfsip.return_value = {
'total': 3,
'members': [{'bogus1': 1}, fsip_plus, {'bogus2': '2'}]
}
self.assertTrue(self.mediator.fsip_exists(constants.EXPECTED_FSIP))
self.mock_client.getfsip.assert_called_once_with(
constants.EXPECTED_VFS,
fpg=constants.EXPECTED_FPG)
def test_fsip_does_not_exist(self):
self.init_mediator()
self.mock_client.getfsip.return_value = {
'total': 3,
'members': [{'bogus1': 1}, constants.OTHER_FSIP, {'bogus2': '2'}]
}
self.assertFalse(self.mediator.fsip_exists(constants.EXPECTED_FSIP))
self.mock_client.getfsip.assert_called_once_with(
constants.EXPECTED_VFS,
fpg=constants.EXPECTED_FPG)
def test_fsip_exists_exception(self):
self.init_mediator()
class FakeException(Exception):
pass
self.mock_client.getfsip.side_effect = FakeException()
self.assertRaises(exception.ShareBackendException,
self.mediator.fsip_exists,
constants.EXPECTED_FSIP)
self.mock_client.getfsip.assert_called_once_with(
constants.EXPECTED_VFS,
fpg=constants.EXPECTED_FPG)
def test_create_fsip_success(self):
self.init_mediator()
# Make the result member a superset of the fsip items.
fsip_plus = constants.EXPECTED_FSIP.copy()
fsip_plus.update({'k': 'v', 'k2': 'v2'})
self.mock_client.getfsip.return_value = {
'total': 3,
'members': [{'bogus1': 1}, fsip_plus, {'bogus2': '2'}]
}
self.mediator.create_fsip(constants.EXPECTED_IP_1234,
constants.EXPECTED_SUBNET,
constants.EXPECTED_VLAN_TAG,
constants.EXPECTED_FPG,
constants.EXPECTED_VFS)
self.mock_client.getfsip.assert_called_once_with(
constants.EXPECTED_VFS,
fpg=constants.EXPECTED_FPG)
expected_calls = [
mock.call.createfsip(constants.EXPECTED_IP_1234,
constants.EXPECTED_SUBNET,
constants.EXPECTED_VFS,
fpg=constants.EXPECTED_FPG,
vlantag=constants.EXPECTED_VLAN_TAG),
mock.call.getfsip(constants.EXPECTED_VFS,
fpg=constants.EXPECTED_FPG),
]
self.mock_client.assert_has_calls(expected_calls)
def test_create_fsip_exception(self):
self.init_mediator()
class FakeException(Exception):
pass
self.mock_client.createfsip.side_effect = FakeException()
self.assertRaises(exception.ShareBackendException,
self.mediator.create_fsip,
constants.EXPECTED_IP_1234,
constants.EXPECTED_SUBNET,
constants.EXPECTED_VLAN_TAG,
constants.EXPECTED_FPG,
constants.EXPECTED_VFS)
self.mock_client.createfsip.assert_called_once_with(
constants.EXPECTED_IP_1234,
constants.EXPECTED_SUBNET,
constants.EXPECTED_VFS,
fpg=constants.EXPECTED_FPG,
vlantag=constants.EXPECTED_VLAN_TAG)
def test_create_fsip_get_none(self):
self.init_mediator()
self.mock_client.getfsip.return_value = {'members': []}
self.assertRaises(exception.ShareBackendException,
self.mediator.create_fsip,
constants.EXPECTED_IP_1234,
constants.EXPECTED_SUBNET,
constants.EXPECTED_VLAN_TAG,
constants.EXPECTED_FPG,
constants.EXPECTED_VFS)
expected_calls = [
mock.call.createfsip(constants.EXPECTED_IP_1234,
constants.EXPECTED_SUBNET,
constants.EXPECTED_VFS,
fpg=constants.EXPECTED_FPG,
vlantag=constants.EXPECTED_VLAN_TAG),
mock.call.getfsip(constants.EXPECTED_VFS,
fpg=constants.EXPECTED_FPG),
]
self.mock_client.assert_has_calls(expected_calls)
def test_remove_fsip_success(self):
self.init_mediator()
self.mock_client.getfsip.return_value = {
'members': [constants.OTHER_FSIP]
}
self.mediator.remove_fsip(constants.EXPECTED_IP_1234,
constants.EXPECTED_FPG,
constants.EXPECTED_VFS)
expected_calls = [
mock.call.removefsip(constants.EXPECTED_VFS,
constants.EXPECTED_IP_1234,
fpg=constants.EXPECTED_FPG),
mock.call.getfsip(constants.EXPECTED_VFS,
fpg=constants.EXPECTED_FPG),
]
self.mock_client.assert_has_calls(expected_calls)
@ddt.data(('ip', None),
('ip', ''),
(None, 'vfs'),
('', 'vfs'),
(None, None),
('', ''))
@ddt.unpack
def test_remove_fsip_without_ip_or_vfs(self, ip, vfs):
self.init_mediator()
self.mediator.remove_fsip(ip, constants.EXPECTED_FPG, vfs)
self.assertFalse(self.mock_client.removefsip.called)
def test_remove_fsip_not_gone(self):
self.init_mediator()
self.mock_client.getfsip.return_value = {
'members': [constants.EXPECTED_FSIP]
}
self.assertRaises(exception.ShareBackendException,
self.mediator.remove_fsip,
constants.EXPECTED_IP_1234,
constants.EXPECTED_FPG,
constants.EXPECTED_VFS)
expected_calls = [
mock.call.removefsip(constants.EXPECTED_VFS,
constants.EXPECTED_IP_1234,
fpg=constants.EXPECTED_FPG),
mock.call.getfsip(constants.EXPECTED_VFS,
fpg=constants.EXPECTED_FPG),
]
self.mock_client.assert_has_calls(expected_calls)
def test_remove_fsip_exception(self):
self.init_mediator()
class FakeException(Exception):
pass
self.mock_client.removefsip.side_effect = FakeException()
self.assertRaises(exception.ShareBackendException,
self.mediator.remove_fsip,
constants.EXPECTED_IP_1234,
constants.EXPECTED_FPG,
constants.EXPECTED_VFS)
self.mock_client.removefsip.assert_called_once_with(
constants.EXPECTED_VFS,
constants.EXPECTED_IP_1234,
fpg=constants.EXPECTED_FPG)
class OptionMatcher(object):
"""Options string order can vary. Compare as lists."""