manila/manila/share/drivers/hds/sop.py

408 lines
17 KiB
Python

# Copyright (c) 2015 Hitachi Data Systems.
# 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 Data Systems Scale-out-Platform Manila Driver.
"""
import base64
import socket
import time
import httplib2
from oslo_config import cfg
from oslo_serialization import jsonutils as json
from oslo_utils import units
import six
from manila import exception
from manila.i18n import _LW
from manila.openstack.common import log as logging
from manila.share import driver
LOG = logging.getLogger(__name__)
hdssop_share_opts = [
cfg.StrOpt('hdssop_target',
help='Specifies the SOPAPI cluster VIP. '
'It is of the form https://<SOPAPI cluster VIP>.'),
cfg.StrOpt('hdssop_adminuser',
help='Specifies the sop admin user'),
cfg.StrOpt('hdssop_adminpassword',
help='Specifies the sop admin user password',
secret=True)
]
CONF = cfg.CONF
CONF.register_opts(hdssop_share_opts)
class SopShareDriver(driver.ShareDriver):
"""Execute commands relating to Shares."""
def __init__(self, db, *args, **kwargs):
super(SopShareDriver, self).__init__(False, *args, **kwargs)
self.db = db
self.configuration.append_config_values(hdssop_share_opts)
self.backend_name = self.configuration.safe_get(
'share_backend_name') or 'HDS_SOP'
self.sop_target = self.configuration.safe_get('hdssop_target')
self.sopuser = self.configuration.safe_get('hdssop_adminuser')
self.soppassword = self.configuration.safe_get('hdssop_adminpassword')
def get_sop_auth_header(self):
return 'Basic ' + base64.b64encode(
self.sopuser + ':' +
self.soppassword).encode('utf-8').decode('ascii')
def _wait_for_job_completion(self, httpclient, job_uri):
"""Wait for job identified by job_uri to complete."""
count = 0
headers = dict(Authorization=self.get_sop_auth_header())
# NOTE(jasonsb): timeout logic here needs be revisited after
# load testing results are in.
while True:
if count > 300:
raise exception.SopAPIError(err=_('job timed out'))
resp_headers, resp_content = httpclient.request(job_uri, 'GET',
body='',
headers=headers)
if int(resp_headers['status']) != 200:
raise exception.SopAPIError(err=_('error getting job status'))
job = json.loads(resp_content)
if job['properties']['completion-status'] == 'ERROR':
raise exception.SopAPIError(err=_('job errored out'))
if job['properties']['completion-status'] == 'COMPLETE':
return job
time.sleep(1)
count += 1
def _add_file_system_sopapi(self, httpclient, payload):
"""Add a new filesystem via SOPAPI."""
sopuri = '/file-systems/'
headers = dict(Authorization=self.get_sop_auth_header())
uri = self.sop_target + '/sopapi' + sopuri
payload_json = json.dumps(payload)
resp_headers, resp_content = httpclient.request(uri, 'POST',
body=payload_json,
headers=headers)
resp_code = int(resp_headers['status'])
if resp_code == 202:
job_loc = resp_headers['location']
self._wait_for_job_completion(httpclient, job_loc)
else:
raise exception.SopAPIError(
err=(_('received error: %s') %
resp_content['messages'][0]['message']))
def _add_share_sopapi(self, httpclient, payload):
"""Add a new filesystem via SOPAPI."""
sopuri = '/shares/'
headers = dict(Authorization=self.get_sop_auth_header())
payload_json = json.dumps(payload)
uri = self.sop_target + '/sopapi' + sopuri
resp_headers, resp_content = httpclient.request(uri, 'POST',
body=payload_json,
headers=headers)
resp_code = int(resp_headers['status'])
if resp_code == 202:
job_loc = resp_headers['location']
job = self._wait_for_job_completion(httpclient, job_loc)
if job['properties']['completion-status'] == 'COMPLETE':
return job['properties']['resource-name']
else:
raise exception.SopAPIError(err=_('received error: %s') %
resp_headers['status'])
def _get_file_system_id_by_name(self, httpclient, fsname):
sopuri = '/file-systems/list?name=' + fsname
headers = dict(Authorization=self.get_sop_auth_header())
uri = self.sop_target + '/sopapi' + sopuri
resp_headers, resp_content = httpclient.request(uri, 'GET',
body='',
headers=headers)
response = json.loads(resp_content)
num_of_resources = 0
if int(resp_headers['status']) != 200 and 'messages' in response:
raise exception.SopAPIError(
err=(_('received error: %s') %
response['messages'][0]['message']))
resource_list = []
resource_list = response['list']
num_of_resources = len(resource_list)
if num_of_resources <= 0:
return ''
return resource_list[0]['id']
def _get_share_id_by_name(self, httpclient, share_name):
"""Look up share given the share name."""
sopuri = '/shares/list?name=' + share_name
headers = dict(Authorization=self.get_sop_auth_header())
uri = self.sop_target + '/sopapi' + sopuri
resp_headers, resp_content = httpclient.request(uri, 'GET',
body='',
headers=headers)
response = json.loads(resp_content)
num_of_resources = 0
if int(resp_headers['status']) != 200 and 'messages' in response:
raise exception.SopAPIError(
err=(_('received error: %s') %
response['messages'][0]['message']))
resource_list = response['list']
num_of_resources = len(resource_list)
if num_of_resources == 0:
return ''
return resource_list[0]['id']
def create_share(self, ctx, share, share_server=None):
"""Create new share on HDS Scale-out Platform."""
sharesize = int(six.text_type(share['size']))
httpclient = httplib2.Http(disable_ssl_certificate_validation=True,
timeout=None)
if share['share_proto'] != 'NFS':
raise exception.InvalidShare(
reason=(_('Invalid NAS protocol supplied: %s.') %
share['share_proto']))
payload = {
'quota': sharesize * units.Gi,
'enabled': True,
'description': '',
'record-access-time': True,
'tags': '',
'space-hwm': 90,
'space-lwm': 70,
'name': share['id'],
}
self._add_file_system_sopapi(httpclient, payload)
payload = {
'description': '',
'type': 'NFS',
'enabled': True,
'tags': '',
'name': share['id'],
'file-system-id': self._get_file_system_id_by_name(
httpclient, share['id']),
}
return self.sop_target + ':/' + self._add_share_sopapi(
httpclient, payload)
def _delete_file_system_sopapi(self, httpclient, fs_id):
"""Delete filesystem on SOP."""
sopuri = '/file-systems/' + fs_id
headers = dict(Authorization=self.get_sop_auth_header())
uri = self.sop_target + '/sopapi' + sopuri
resp_headers, resp_content = httpclient.request(uri, 'DELETE',
body='',
headers=headers)
resp_code = int(resp_headers['status'])
if resp_code == 202:
job_loc = resp_headers['location']
self._wait_for_job_completion(httpclient, job_loc)
else:
raise exception.SopAPIError(err=_('received error: %s') %
resp_headers['status'])
def _delete_share_sopapi(self, httpclient, share_id):
"""Delete share on SOP."""
sopuri = '/shares/' + share_id
headers = dict(Authorization=self.get_sop_auth_header())
uri = self.sop_target + '/sopapi' + sopuri
resp_headers, resp_content = httpclient.request(uri, 'DELETE',
body='',
headers=headers)
resp_code = int(resp_headers['status'])
if resp_code == 202:
job_loc = resp_headers['location']
self._wait_for_job_completion(httpclient, job_loc)
else:
raise exception.SopAPIError(err=_('received error: %s') %
resp_headers['status'])
def delete_share(self, context, share, share_server=None):
"""Remove a share from Sop volume."""
httpclient = httplib2.Http(disable_ssl_certificate_validation=True,
timeout=None)
self._delete_share_sopapi(
httpclient,
self._get_share_id_by_name(httpclient, share['id']))
self._delete_file_system_sopapi(
httpclient,
self._get_file_system_id_by_name(httpclient, share['id']))
def create_snapshot(self, context, snapshot, share_server=None):
"""Not currently supported on HDS Scale-out Platform."""
raise NotImplementedError()
def create_share_from_snapshot(self, context, share, snapshot,
share_server=None):
"""Not currently supported on HDS Scale-out Platform."""
raise NotImplementedError()
def delete_snapshot(self, context, snapshot, share_server=None):
"""Not currently supported on HDS Scale-out Platform."""
raise NotImplementedError()
def allow_access(self, context, share, access, share_server=None):
"""Allow access to a share.
Currently only IP based access control is supported.
"""
if access['access_type'] != 'ip':
raise exception.InvalidShareAccess(
reason=_('only IP access type allowed'))
httpclient = httplib2.Http(disable_ssl_certificate_validation=True,
timeout=None)
sop_share_id = self._get_share_id_by_name(httpclient, share['id'])
if access['access_level'] == 'rw':
access_level = True
elif access['access_level'] == 'ro':
access_level = False
else:
raise exception.InvalidShareAccess(
reason=(_('Unsupported level of access was provided - %s') %
access['access_level']))
payload = {
'action': 'add-access-rule',
'all-squash': True,
'anongid': 65534,
'anonuid': 65534,
'host-specification': access['access_to'],
'description': '',
'read-write': access_level,
'root-squash': False,
'tags': 'nfs',
'name': '%s-%s' % (share['id'], access['access_to']),
}
sopuri = '/shares/'
headers = dict(Authorization=self.get_sop_auth_header())
uri = self.sop_target + '/sopapi' + sopuri + sop_share_id
resp_headers, resp_content = httpclient.request(
uri, 'POST',
body=json.dumps(payload),
headers=headers)
resp_code = int(resp_headers['status'])
if resp_code == 202:
job_loc = resp_headers['location']
self._wait_for_job_completion(httpclient, job_loc)
else:
raise exception.SopAPIError(err=_('received error: %s') %
resp_headers['status'])
def deny_access(self, context, share, access, share_server=None):
"""Deny access to a share.
Currently only IP based access control is supported.
"""
if access['access_type'] != 'ip':
LOG.warn(_LW('Only ip access type allowed.'))
return
httpclient = httplib2.Http(disable_ssl_certificate_validation=True,
timeout=None)
sop_share_id = self._get_share_id_by_name(httpclient, share['id'])
payload = {
'action': 'delete-access-rule',
'name': '%s-%s' % (share['id'], access['access_to']),
}
sopuri = '/shares/' + sop_share_id
headers = dict(Authorization=self.get_sop_auth_header())
uri = self.sop_target + '/sopapi' + sopuri
resp_headers, resp_content = httpclient.request(
uri, 'POST',
body=json.dumps(payload),
headers=headers)
resp_code = int(resp_headers['status'])
if resp_code == 202:
job_loc = resp_headers['location']
self._wait_for_job_completion(httpclient, job_loc)
else:
raise exception.SopAPIError(err=_('received error: %s') %
resp_headers['status'])
def check_for_setup_error(self):
"""Check for setup error.
Socket timeout set for 5 seconds to verify SOPAPI rest
interface is reachable and the credentials will allow us
to login.
"""
headers = dict(Authorization=self.get_sop_auth_header())
uri = self.sop_target + '/sopapi/clusters'
try:
httpclient = httplib2.Http(disable_ssl_certificate_validation=True,
timeout=5)
resp_headers, resp_content = httpclient.request(uri, 'GET',
body='',
headers=headers)
response = json.loads(resp_content)
if 'messages' in response:
soperror = _('received error: %(code)s: %(msg)s') % {
'code': response['messages'][0]['code'],
'msg': response['messages'][0]['message'],
}
raise exception.SopAPIError(err=soperror)
except socket.timeout:
raise exception.SopAPIError(
err=_('connection to SOPAPI timed out'))
def _get_sop_filesystem_stats(self):
"""Calculate cluster storage capacity and return in GiB."""
headers = dict(Authorization=self.get_sop_auth_header())
uri = self.sop_target + '/sopapi/clusters'
httpclient = httplib2.Http(disable_ssl_certificate_validation=True,
timeout=None)
resp_headers, resp_content = httpclient.request(uri, 'GET',
body='',
headers=headers)
response = json.loads(resp_content)
if resp_content is not None:
for cluster in response['element-links']:
(resp_headers, resp_content) = httpclient.request(
cluster,
'GET',
body='',
headers=headers)
response = json.loads(resp_content)
totalspace = int(response['properties']
['total-storage-capacity']) / units.Gi
spaceavail = int(response['properties']
['total-storage-available']) / units.Gi
return (totalspace, spaceavail)
def _update_share_stats(self):
"""Retrieve stats info from SOPAPI."""
totalspace, spaceavail = self._get_sop_filesystem_stats()
data = dict(
share_backend_name=self.backend_name,
vendor_name='Hitach Data Systems',
storage_protocol='NFS',
total_capacity_gb=totalspace,
free_capacity_gb=spaceavail)
super(SopShareDriver, self)._update_share_stats(data)