manila/manila/share/drivers/hp/hp_3par_mediator.py

640 lines
25 KiB
Python

# Copyright 2015 Hewlett Packard Development Company, L.P.
#
# 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.
"""HP 3PAR Mediator for OpenStack Manila.
This 'mediator' de-couples the 3PAR focused client from the OpenStack focused
driver.
"""
from oslo_log import log
from oslo_utils import importutils
from oslo_utils import units
import six
from manila import exception
from manila.i18n import _
hp3parclient = importutils.try_import("hp3parclient")
if hp3parclient:
from hp3parclient import file_client
LOG = log.getLogger(__name__)
DENY = '-'
ALLOW = '+'
OPEN_STACK_MANILA_FSHARE = 'OpenStack Manila fshare'
class HP3ParMediator(object):
def __init__(self, **kwargs):
self.hp3par_username = kwargs.get('hp3par_username')
self.hp3par_password = kwargs.get('hp3par_password')
self.hp3par_api_url = kwargs.get('hp3par_api_url')
self.hp3par_debug = kwargs.get('hp3par_debug')
self.hp3par_san_ip = kwargs.get('hp3par_san_ip')
self.hp3par_san_login = kwargs.get('hp3par_san_login')
self.hp3par_san_password = kwargs.get('hp3par_san_password')
self.hp3par_san_ssh_port = kwargs.get('hp3par_san_ssh_port')
self.hp3par_san_private_key = kwargs.get('hp3par_san_private_key')
self.hp3par_fstore_per_share = kwargs.get('hp3par_fstore_per_share')
self.ssh_conn_timeout = kwargs.get('ssh_conn_timeout')
self._client = None
def do_setup(self):
if hp3parclient is None:
msg = _('You must install hp3parclient before using the 3PAR '
'driver.')
LOG.exception(msg)
raise exception.HP3ParInvalidClient(message=msg)
try:
self._client = file_client.HP3ParFilePersonaClient(
self.hp3par_api_url)
except Exception as e:
msg = (_('Failed to connect to HP 3PAR File Persona Client: %s') %
six.text_type(e))
LOG.exception(msg)
raise exception.ShareBackendException(message=msg)
try:
ssh_kwargs = {}
if self.hp3par_san_ssh_port:
ssh_kwargs['port'] = self.hp3par_san_ssh_port
if self.ssh_conn_timeout:
ssh_kwargs['conn_timeout'] = self.ssh_conn_timeout
if self.hp3par_san_private_key:
ssh_kwargs['privatekey'] = self.hp3par_san_private_key
self._client.setSSHOptions(
self.hp3par_san_ip,
self.hp3par_san_login,
self.hp3par_san_password,
**ssh_kwargs
)
except Exception as e:
msg = (_('Failed to set SSH options for HP 3PAR File Persona '
'Client: %s') % six.text_type(e))
LOG.exception(msg)
raise exception.ShareBackendException(message=msg)
if self.hp3par_debug:
self._client.ssh.set_debug_flag(True)
def get_capacity(self, fpg):
try:
result = self._client.getfpg(fpg)
except Exception as e:
msg = (_('Failed to get capacity for fpg %(fpg)s: %(e)s') %
{'fpg': fpg, 'e': six.text_type(e)})
LOG.exception(msg)
raise exception.ShareBackendException(message=msg)
if result['total'] != 1:
msg = (_('Failed to get capacity for fpg %s.') % fpg)
LOG.exception(msg)
raise exception.ShareBackendException(message=msg)
else:
member = result['members'][0]
total_capacity_gb = int(member['capacityKiB']) / units.Mi
free_capacity_gb = int(member['availCapacityKiB']) / units.Mi
return {
'total_capacity_gb': total_capacity_gb,
'free_capacity_gb': free_capacity_gb
}
@staticmethod
def ensure_supported_protocol(share_proto):
protocol = share_proto.lower()
if protocol == 'cifs':
protocol = 'smb'
if protocol not in ['smb', 'nfs']:
message = (_('Invalid protocol. Expected nfs or smb. Got %s.') %
protocol)
LOG.exception(message)
raise exception.InvalidInput(message)
return protocol
@staticmethod
def other_protocol(share_proto):
"""Given 'nfs' or 'smb' (or equivalent) return the other one."""
protocol = HP3ParMediator.ensure_supported_protocol(share_proto)
return 'nfs' if protocol == 'smb' else 'smb'
@staticmethod
def ensure_prefix(uid, protocol=None):
if uid.startswith('osf-'):
return uid
elif protocol:
return 'osf-%s-%s' % (
HP3ParMediator.ensure_supported_protocol(protocol), uid)
else:
return 'osf-%s' % uid
def create_share(self, project_id, share_id, share_proto, fpg, vfs,
fstore=None, sharedir=None, readonly=False, size=None):
"""Create the share and return its path.
This method can create a share when called by the driver or when
called locally from create_share_from_snapshot(). The optional
parameters allow re-use.
:param project_id: The tenant ID.
:param share_id: The share-id with or without osf- prefix.
:param share_proto: The protocol (to map to smb or nfs)
:param fpg: The file provisioning group
:param vfs: The virtual file system
:param fstore: (optional) The file store. When provided, an existing
file store is used. Otherwise one is created.
:param sharedir: (optional) Share directory.
:param readonly: (optional) Create share as read-only.
:param size: (optional) Size limit for file store if creating one.
:return: share path string
"""
protocol = self.ensure_supported_protocol(share_proto)
share_name = self.ensure_prefix(share_id)
if not fstore:
if self.hp3par_fstore_per_share:
fstore = share_name
else:
fstore = self.ensure_prefix(project_id, protocol)
try:
result = self._client.createfstore(
vfs, fstore, fpg=fpg,
comment='OpenStack Manila fstore')
LOG.debug("createfstore result=%s", result)
except Exception as e:
msg = (_('Failed to create fstore %(fstore)s: %(e)s') %
{'fstore': fstore, 'e': six.text_type(e)})
LOG.exception(msg)
raise exception.ShareBackendException(msg)
if size:
if self.hp3par_fstore_per_share:
hcapacity = six.text_type(size * units.Ki)
scapacity = hcapacity
else:
hard_size_mb = size * units.Ki
soft_size_mb = hard_size_mb
result = self._client.getfsquota(
fpg=fpg, vfs=vfs, fstore=fstore)
LOG.debug("getfsquota result=%s", result)
quotas = result['members']
if len(quotas) == 1:
hard_size_mb += int(quotas[0].get('hardBlock', '0'))
soft_size_mb += int(quotas[0].get('softBlock', '0'))
hcapacity = six.text_type(hard_size_mb)
scapacity = six.text_type(soft_size_mb)
try:
result = self._client.setfsquota(
vfs, fpg=fpg, fstore=fstore,
scapacity=scapacity, hcapacity=hcapacity)
LOG.debug("setfsquota result=%s", result)
except Exception as e:
msg = (_('Failed to setfsquota on %(fstore)s: %(e)s') %
{'fstore': fstore, 'e': six.text_type(e)})
LOG.exception(msg)
raise exception.ShareBackendException(msg)
if not (sharedir or self.hp3par_fstore_per_share):
sharedir = share_name
try:
if protocol == 'nfs':
if readonly:
options = 'ro,no_root_squash,insecure'
else:
options = 'rw,no_root_squash,insecure'
result = self._client.createfshare(
protocol, vfs, share_name,
fpg=fpg, fstore=fstore, sharedir=sharedir,
clientip='127.0.0.1',
options=options,
comment=OPEN_STACK_MANILA_FSHARE)
else:
result = self._client.createfshare(
protocol, vfs, share_name,
fpg=fpg, fstore=fstore, sharedir=sharedir,
allowip='127.0.0.1',
comment=OPEN_STACK_MANILA_FSHARE)
LOG.debug("createfshare result=%s", result)
except Exception as e:
msg = (_('Failed to create share %(share_name)s: %(e)s') %
{'share_name': share_name, 'e': six.text_type(e)})
LOG.exception(msg)
raise exception.ShareBackendException(msg)
try:
result = self._client.getfshare(
protocol, share_name,
fpg=fpg, vfs=vfs, fstore=fstore)
LOG.debug("getfshare result=%s", result)
except Exception as e:
msg = (_('Failed to get fshare %(share_name)s after creating it: '
'%(e)s') % {'share_name': share_name,
'e': six.text_type(e)})
LOG.exception(msg)
raise exception.ShareBackendException(msg)
if result['total'] != 1:
msg = (_('Failed to get fshare %(share_name)s after creating it. '
'Expected to get 1 fshare. Got %(total)s.') %
{'share_name': share_name, 'total': result['total']})
LOG.exception(msg)
raise exception.ShareBackendException(msg)
if protocol == 'nfs':
return result['members'][0]['sharePath']
else:
return result['members'][0]['shareName']
def create_share_from_snapshot(self, share_id, share_proto,
orig_project_id, orig_share_id, orig_proto,
snapshot_id, fpg, vfs):
protocol = self.ensure_supported_protocol(share_proto)
snapshot_tag = self.ensure_prefix(snapshot_id)
orig_share_name = self.ensure_prefix(orig_share_id)
snapshot = self._find_fsnap(orig_project_id,
orig_share_name,
orig_proto,
snapshot_tag,
fpg,
vfs)
if not snapshot:
msg = (_('Failed to create share from snapshot for '
'FPG/VFS/tag %(fpg)s/%(vfs)s/%(tag)s. '
'Snapshot not found.') %
{
'fpg': fpg,
'vfs': vfs,
'tag': snapshot_tag})
LOG.exception(msg)
raise exception.ShareBackendException(msg)
fstore = snapshot['fstoreName']
share_name = self.ensure_prefix(share_id)
if fstore == orig_share_name:
# No subdir for original share created with fstore_per_share
sharedir = '.snapshot/%s' % snapshot['snapName']
else:
sharedir = '.snapshot/%s/%s' % (snapshot['snapName'],
orig_share_name)
return self.create_share(
orig_project_id,
share_name,
protocol,
fpg,
vfs,
fstore=fstore,
sharedir=sharedir,
readonly=True,
)
def delete_share(self, project_id, share_id, share_proto, fpg, vfs):
protocol = self.ensure_supported_protocol(share_proto)
share_name = self.ensure_prefix(share_id)
fstore = self._find_fstore(project_id, share_name, protocol, fpg, vfs,
allow_cross_protocol=True)
if not fstore:
# Share does not exist.
return
try:
self._client.removefshare(protocol, vfs, share_name,
fpg=fpg, fstore=fstore)
except Exception as e:
msg = (_('Failed to remove share %(share_name)s: %(e)s') %
{'share_name': share_name, 'e': six.text_type(e)})
LOG.exception(msg)
raise exception.ShareBackendException(message=msg)
if fstore == share_name:
try:
self._client.removefstore(vfs, fstore, fpg=fpg)
except Exception as e:
msg = (_('Failed to remove fstore %(fstore)s: %(e)s') %
{'fstore': fstore, 'e': six.text_type(e)})
LOG.exception(msg)
raise exception.ShareBackendException(message=msg)
def get_vfs_name(self, fpg):
return self.get_vfs(fpg)['vfsname']
def get_vfs(self, fpg, vfs=None):
"""Get the VFS or raise an exception."""
try:
result = self._client.getvfs(fpg=fpg, vfs=vfs)
except Exception as e:
msg = (_('Exception during getvfs %(vfs)s: %(e)s') %
{'vfs': vfs, 'e': six.text_type(e)})
LOG.exception(msg)
raise exception.ShareBackendException(msg)
if result['total'] != 1:
error_msg = result.get('message')
if error_msg:
message = (_('Error while validating FPG/VFS '
'(%(fpg)s/%(vfs)s): %(msg)s') %
{'fpg': fpg, 'vfs': vfs, 'msg': error_msg})
LOG.error(message)
raise exception.ShareBackendException(message)
else:
message = (_('Error while validating FPG/VFS '
'(%(fpg)s/%(vfs)s): Expected 1, '
'got %(total)s.') %
{'fpg': fpg, 'vfs': vfs,
'total': result['total']})
LOG.error(message)
raise exception.ShareBackendException(message)
return result['members'][0]
def create_snapshot(self, orig_project_id, orig_share_id, orig_share_proto,
snapshot_id, fpg, vfs):
"""Creates a snapshot of a share."""
fshare = self._find_fshare(orig_project_id,
orig_share_id,
orig_share_proto,
fpg,
vfs)
if not fshare:
msg = (_('Failed to create snapshot for FPG/VFS/fshare '
'%(fpg)s/%(vfs)s/%(fshare)s: Failed to find fshare.') %
{'fpg': fpg, 'vfs': vfs, 'fshare': orig_share_id})
LOG.exception(msg)
raise exception.ShareBackendException(msg)
sharedir = fshare.get('shareDir')
if sharedir and sharedir.startswith('.snapshot'):
msg = (_('Failed to create snapshot for FPG/VFS/fshare '
'%(fpg)s/%(vfs)s/%(fshare)s: Share is a read-only '
'share of an existing snapshot.') %
{'fpg': fpg, 'vfs': vfs, 'fshare': orig_share_id})
LOG.exception(msg)
raise exception.ShareBackendException(msg)
fstore = fshare.get('fstoreName')
snapshot_tag = self.ensure_prefix(snapshot_id)
try:
result = self._client.createfsnap(
vfs, fstore, snapshot_tag, fpg=fpg)
LOG.debug("createfsnap result=%s", result)
except Exception as e:
msg = (_('Failed to create snapshot for FPG/VFS/fstore '
'%(fpg)s/%(vfs)s/%(fstore)s: %(e)s') %
{'fpg': fpg, 'vfs': vfs, 'fstore': fstore,
'e': six.text_type(e)})
LOG.exception(msg)
raise exception.ShareBackendException(msg)
def delete_snapshot(self, orig_project_id, orig_share_id, orig_proto,
snapshot_id, fpg, vfs):
"""Deletes a snapshot of a share."""
snapshot_tag = self.ensure_prefix(snapshot_id)
snapshot = self._find_fsnap(orig_project_id, orig_share_id, orig_proto,
snapshot_tag, fpg, vfs)
if not snapshot:
return
fstore = snapshot.get('fstoreName')
for protocol in ('nfs', 'smb'):
try:
shares = self._client.getfshare(protocol,
fpg=fpg,
vfs=vfs,
fstore=fstore)
except Exception as e:
msg = (_('Unexpected exception while getting share list. '
'Cannot delete snapshot without checking for '
'dependent shares first: %s') % six.text_type(e))
LOG.exception(msg)
raise exception.ShareBackendException(msg)
for share in shares['members']:
if protocol == 'nfs':
path = share['sharePath'][1:].split('/')
dot_snapshot_index = 3
else:
if share['shareDir']:
path = share['shareDir'].split('/')
else:
path = None
dot_snapshot_index = 0
snapshot_index = dot_snapshot_index + 1
if path and len(path) > snapshot_index:
if (path[dot_snapshot_index] == '.snapshot' and
path[snapshot_index].endswith(snapshot_tag)):
msg = (_('Cannot delete snapshot because it has a '
'dependent share.'))
raise exception.Invalid(msg)
snapname = snapshot['snapName']
try:
result = self._client.removefsnap(
vfs, fstore, snapname=snapname, fpg=fpg)
LOG.debug("removefsnap result=%s", result)
except Exception as e:
msg = (_('Failed to delete snapshot for FPG/VFS/fstore/snapshot '
'%(fpg)s/%(vfs)s/%(fstore)s/%(snapname)s: %(e)s') %
{
'fpg': fpg,
'vfs': vfs,
'fstore': fstore,
'snapname': snapname,
'e': six.text_type(e)})
LOG.exception(msg)
raise exception.ShareBackendException(msg)
# Try to reclaim the space
try:
self._client.startfsnapclean(fpg, reclaimStrategy='maxspeed')
except Exception as e:
# Remove already happened so only log this.
msg = (_('Unexpected exception calling startfsnapclean for FPG '
'%(fpg)s: %(e)s') % {'fpg': fpg, 'e': six.text_type(e)})
LOG.exception(msg)
@staticmethod
def validate_access_type(protocol, access_type):
if access_type not in ('ip', 'user'):
msg = (_("Invalid access type. Expected 'ip' or 'user'. "
"Actual '%s'.") % access_type)
LOG.exception(msg)
raise exception.InvalidInput(msg)
if protocol == 'nfs' and access_type != 'ip':
msg = (_("Invalid NFS access type. HP 3PAR NFS supports 'ip'. "
"Actual '%s'.") % access_type)
LOG.exception(msg)
raise exception.HP3ParInvalid(msg)
return protocol
def _change_access(self, plus_or_minus, project_id, share_id, share_proto,
access_type, access_to, fpg, vfs):
"""Allow or deny access to a share.
Plus_or_minus character indicates add to allow list (+) or remove from
allow list (-).
"""
protocol = self.ensure_supported_protocol(share_proto)
self.validate_access_type(protocol, access_type)
share_name = self.ensure_prefix(share_id)
fstore = self._find_fstore(project_id, share_id, protocol, fpg, vfs,
allow_cross_protocol=True)
try:
if protocol == 'nfs':
result = self._client.setfshare(
protocol, vfs, share_name, fpg=fpg, fstore=fstore,
clientip='%s%s' % (plus_or_minus, access_to))
elif protocol == 'smb':
if access_type == 'ip':
result = self._client.setfshare(
protocol, vfs, share_name, fpg=fpg, fstore=fstore,
allowip='%s%s' % (plus_or_minus, access_to))
else:
access_str = 'fullcontrol'
perm = '%s%s:%s' % (plus_or_minus, access_to, access_str)
result = self._client.setfshare(protocol, vfs, share_name,
fpg=fpg, fstore=fstore,
allowperm=perm)
else:
msg = (_("Unexpected error: After ensure_supported_protocol "
"only 'nfs' or 'smb' strings are allowed, but found: "
"%s.") % protocol)
raise exception.HP3ParUnexpectedError(msg)
LOG.debug("setfshare result=%s", result)
except Exception as e:
msg = (_('Failed to change (%(change)s) access to FPG/share '
'%(fpg)s/%(share)s to %(type)s %(to)s): %(e)s') %
{'change': plus_or_minus, 'fpg': fpg, 'share': share_name,
'type': access_type, 'to': access_to,
'e': six.text_type(e)})
LOG.exception(msg)
raise exception.ShareBackendException(msg)
def _find_fstore(self, project_id, share_id, share_proto, fpg, vfs,
allow_cross_protocol=False):
share = self._find_fshare(project_id, share_id, share_proto, fpg, vfs)
if not share and allow_cross_protocol:
share = self._find_fshare(project_id,
share_id,
self.other_protocol(share_proto),
fpg,
vfs)
return share.get('fstoreName') if share else None
def _find_fshare(self, project_id, share_id, share_proto, fpg, vfs):
protocol = self.ensure_supported_protocol(share_proto)
share_name = self.ensure_prefix(share_id)
project_fstore = self.ensure_prefix(project_id, share_proto)
search_order = [
{'fpg': fpg, 'vfs': vfs, 'fstore': project_fstore},
{'fpg': fpg, 'vfs': vfs, 'fstore': share_name},
{'fpg': fpg},
{}
]
try:
for search_params in search_order:
result = self._client.getfshare(protocol, share_name,
**search_params)
shares = result.get('members', [])
if len(shares) == 1:
return shares[0]
except Exception as e:
msg = (_('Unexpected exception while getting share list: %s') %
six.text_type(e))
raise exception.ShareBackendException(msg)
def _find_fsnap(self, project_id, share_id, orig_proto, snapshot_tag,
fpg, vfs):
share_name = self.ensure_prefix(share_id)
osf_project_id = self.ensure_prefix(project_id, orig_proto)
pattern = '*_%s' % self.ensure_prefix(snapshot_tag)
search_order = [
{'pat': True, 'fpg': fpg, 'vfs': vfs, 'fstore': osf_project_id},
{'pat': True, 'fpg': fpg, 'vfs': vfs, 'fstore': share_name},
{'pat': True, 'fpg': fpg},
{'pat': True},
]
try:
for search_params in search_order:
result = self._client.getfsnap(pattern, **search_params)
snapshots = result.get('members', [])
if len(snapshots) == 1:
return snapshots[0]
except Exception as e:
msg = (_('Unexpected exception while getting snapshots: %s') %
six.text_type(e))
raise exception.ShareBackendException(msg)
def allow_access(self, project_id, share_id, share_proto, access_type,
access_to, fpg, vfs):
"""Grant access to a share."""
self._change_access(ALLOW, project_id, share_id, share_proto,
access_type, access_to, fpg, vfs)
def deny_access(self, project_id, share_id, share_proto, access_type,
access_to, fpg, vfs):
"""Deny access to a share."""
self._change_access(DENY, project_id, share_id, share_proto,
access_type, access_to, fpg, vfs)