Merge "Add share driver for HDS NAS Scale-out Platform"

This commit is contained in:
Jenkins 2015-02-04 00:27:44 +00:00 committed by Gerrit Code Review
commit 8412517852
7 changed files with 1031 additions and 0 deletions

View File

@ -481,3 +481,7 @@ class InvalidSqliteDB(Invalid):
class SSHException(ManilaException):
message = _("Exception in SSH protocol negotiation or logic.")
class SopAPIError(Invalid):
message = _("%(err)s")

View File

@ -52,6 +52,7 @@ import manila.share.drivers.emc.driver
import manila.share.drivers.generic
import manila.share.drivers.glusterfs
import manila.share.drivers.glusterfs_native
import manila.share.drivers.hds.sop
import manila.share.drivers.huawei.huawei_nas
import manila.share.drivers.ibm.gpfs
import manila.share.drivers.netapp.cluster_mode
@ -106,6 +107,7 @@ _global_opt_lists = [
manila.share.drivers.generic.share_opts,
manila.share.drivers.glusterfs.GlusterfsManilaShare_opts,
manila.share.drivers.glusterfs_native.glusterfs_native_manila_share_opts,
manila.share.drivers.hds.sop.hdssop_share_opts,
manila.share.drivers.huawei.huawei_nas.huawei_opts,
manila.share.drivers.ibm.gpfs.gpfs_share_opts,
manila.share.drivers.netapp.cluster_mode.NETAPP_NAS_OPTS,

View File

View File

@ -0,0 +1,407 @@
# 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)

View File

@ -0,0 +1,617 @@
# 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.
"""Unit tests for the Hitachi Data Systems Scale-out Platform manila driver."""
import time
import httplib2
import mock
from oslo_config import cfg
from oslo_serialization import jsonutils as json
from oslo_utils import units
from manila import context
from manila import exception
from manila.share import configuration as config
from manila.share.drivers.hds import sop
from manila import test
from manila.tests import fake_share
CONF = cfg.CONF
fake_authorization = {'Authorization': u'Basic ZmFrZXVzZXI6ZmFrZXBhc3N3b3Jk'}
class SopShareDriverTestCase(test.TestCase):
"""Tests SopShareDriver."""
def setUp(self):
super(SopShareDriverTestCase, self).setUp()
self._context = context.get_admin_context()
self.server = {
'instance_id': 'fake_instance_id',
'ip': 'fake_ip',
'username': 'fake_username',
'password': 'fake_password',
'pk_path': 'fake_pk_path',
'backend_details': {
'ip': '1.2.3.4',
'instance_id': 'fake',
},
}
CONF.set_default('hdssop_target', 'https://1.2.3.4')
CONF.set_default('hdssop_adminuser', 'fakeuser')
CONF.set_default('hdssop_adminpassword', 'fakepassword')
CONF.set_default('driver_handles_share_servers', False)
self.fake_conf = config.Configuration(None)
self._db = mock.Mock()
self._driver = sop.SopShareDriver(
self._db, configuration=self.fake_conf)
self.share = fake_share.fake_share(share_proto='NFS')
self._driver.share_backend_name = 'HDS_SOP'
def test_add_file_system_sopapi(self):
httpclient = httplib2.Http(disable_ssl_certificate_validation=True,
timeout=None)
httpretval = ({'status': '202',
'content-length': '0',
'x-sopapi-version': '1.0.0',
'set-cookie': 'JSESSIONID=abcdef;Path=/sopapi;Secure',
'expires': 'Thu, 01 Jan 1970 00:00:00 GMT',
'server': 'Jetty(8.1.3.v20120416)',
'location': 'https://1.2.3.4/sopapi/jobs/fakeuuid',
'date': 'Tue, 20 Jan 2015 22:41:29 GMT'}, '')
self.mock_object(httpclient, 'request',
mock.Mock(return_value=httpretval))
self.mock_object(self._driver, '_wait_for_job_completion', mock.Mock())
fakepayload1 = {
'quota': 145 * units.Gi,
'enabled': True,
'description': '',
'record-access-time': True,
'tags': '',
'space-hwm': 90,
'space-lwm': 70,
'name': 'fakeid',
}
fsadd = self._driver._add_file_system_sopapi(httpclient, fakepayload1)
self.assertEqual(None, fsadd)
httpclient.request.assert_called_once_with(
'https://' +
self.server['backend_details']['ip'] +
'/sopapi/file-systems/',
'POST',
body=json.dumps(fakepayload1),
headers=fake_authorization)
self._driver._wait_for_job_completion.assert_called_once_with(
httpclient,
'https://1.2.3.4/sopapi/jobs/fakeuuid')
def test_add_file_system_sopapi_belowminsize(self):
httpclient = httplib2.Http(disable_ssl_certificate_validation=True,
timeout=None)
httpretval = ({'status': '400',
'content-type': 'application/jsson',
'transfer-encoding': 'chunked',
'x-sopapi-version': '1.0.0',
'set-cookie': 'JSESSIONID=abcdef;Path=/sopapi;Secure',
'expires': 'Thu, 01 Jan 1970 00:00:00 GMT',
'server': 'Jetty(8.1.3.v20120416)',
'location': 'https://1.2.3.4/sopapi/jobs/fakeuuid',
'date': 'Tue, 20 Jan 2015 22:41:29 GMT'},
{'messages': [{'category': 1,
'message': '''"Property 'quota' is inv'''
'alid. Specify a value from 137438953472 '
'to 6755399441055744."',
'code': 'schema_number_min_constraint',
'type': 'error'},
]})
self.mock_object(httpclient, 'request',
mock.Mock(return_value=httpretval))
self.mock_object(self._driver, '_wait_for_job_completion', mock.Mock())
fakepayload = {
'quota': 3 * units.Gi,
'enabled': True,
'description': '',
'record-access-time': True,
'tags': '',
'space-hwm': 90,
'space-lwm': 70,
'name': 'fakeid',
}
self.assertRaises(exception.SopAPIError,
self._driver._add_file_system_sopapi,
httpclient, fakepayload)
httpclient.request.assert_called_once_with(
'https://' +
self.server['backend_details']['ip'] +
'/sopapi/file-systems/',
'POST',
body=json.dumps(fakepayload),
headers=fake_authorization)
self.assertEqual(False, self._driver._wait_for_job_completion.called)
def test_wait_for_job_completion_simple(self):
httpclient = httplib2.Http(disable_ssl_certificate_validation=True,
timeout=None)
httpreturn = [
({'status': '200',
'content-location': 'https://1.2.3.4/sopapi/jobs/fakeuuid',
'transfer-encoding': 'chunked',
'set-cookie': 'JSESSIONID=abcdef;Path=/sopapi;Secure',
'expires': 'Thu, 01 Jan 1970 00:00:00 GMT',
'server': 'Jetty(8.1.3.v20120416)',
'x-sopapi-version': '1.0.0',
'date': 'Wed, 21 Jan 2015 04:49:51 GMT',
'content-type': 'application/json'},
'{"id":"fakeuuid","properties":{"'
'resource-name":"","resource-type":"share","creation-timestam'
'p":1421815791,"completion-status":"PROCESSING","completion-d'
'etails":"Saving changes","completion-substatus":"RUNNING","r'
'esource-action":"ADD","percent-complete":75,"resource-id":"b'
'fakeuuid","target-node-name":"Node005","target-node-id":"fak'
'euuid","spawned-jobs":false,"spawned-jobs-list-uri":""}}'),
({'status': '200',
'content-location': 'https://1.2.3.4/sopapi/jobs/fakeuuid',
'transfer-encoding': 'chunked',
'set-cookie': 'JSESSIONID=abcdef;Path=/sopapi;Secure',
'expires': 'Thu, 01 Jan 1970 00:00:00 GMT',
'server': 'Jetty(8.1.3.v20120416)',
'x-sopapi-version': '1.0.0',
'date': 'Wed, 21 Jan 2015 04:49:51 GMT',
'content-type': 'application/json'},
'{"id":"fakeuuid","properties":{"'
'resource-name":"fakeuuid","resou'
'rce-type":"share","creation-timestamp":1421815791,"completio'
'n-status":"COMPLETE","completion-details":"Adding share comp'
'leted","completion-substatus":"OK","resource-action":"ADD","'
'percent-complete":100,"resource-id":"fakeuuid'
'","target-node-name":"Node005","target-node-id"'
':"fakeuuid","spawned-jobs":false'
',"spawned-jobs-list-uri":""}}'),
]
self.mock_object(httpclient, 'request',
mock.Mock(side_effect=httpreturn))
fsadd = self._driver._wait_for_job_completion(httpclient, 'fakeuri')
expectedresult = {
u'id': u'fakeuuid',
u'properties': {
u'completion-details':
u'Adding share completed',
u'completion-status': u'COMPLETE',
u'completion-substatus': u'OK',
u'creation-timestamp': 1421815791,
u'percent-complete': 100,
u'resource-action': u'ADD',
u'resource-id': u'fakeuuid',
u'resource-name': u'fakeuuid',
u'resource-type': u'share',
u'spawned-jobs': False,
u'spawned-jobs-list-uri': u'',
u'target-node-id': u'fakeuuid',
u'target-node-name': u'Node005',
},
}
self.assertEqual(expectedresult, fsadd)
httpcalls = [mock.call('fakeuri',
'GET',
body='',
headers=fake_authorization) for x in xrange(2)]
self.assertEqual(httpcalls, httpclient.request.call_args_list)
def test_wait_for_job_completion_notimeout(self):
httpclient = httplib2.Http(disable_ssl_certificate_validation=True,
timeout=None)
httpreturn = [({'status': '200',
'content-location':
'https://1.2.3.4/sopapi/jobs/fakeuuid',
'transfer-encoding': 'chunked',
'set-cookie': 'JSESSIONID=abcdef;Path=/sopapi;Secure',
'expires': 'Thu, 01 Jan 1970 00:00:00 GMT',
'server': 'Jetty(8.1.3.v20120416)',
'x-sopapi-version': '1.0.0',
'date': 'Wed, 21 Jan 2015 04:49:51 GMT',
'content-type': 'application/json'},
'{"id":"fakeuuid","properties":{"resource-name":"","re'
'source-type":"share","creation-timestamp":1421815791,'
'"completion-status":"PROCESSING","completion-details"'
':"Saving changes","completion-substatus":"RUNNING","r'
'esource-action":"ADD","percent-complete":75,"resource'
'-id":"fakeuuid","target-node-name":"Node005","target-'
'node-id":"fakeuuid","spawned-jobs":false,"spawned-job'
's-list-uri":""}}') for x in xrange(200)
]
httpreturn.append(({'status': '200',
'content-location':
'https://1.2.3.4/sopapi/jobs/fakeuuid',
'transfer-encoding': 'chunked',
'set-cookie':
'JSESSIONID=abcdef;Path=/sopapi;Secure',
'expires': 'Thu, 01 Jan 1970 00:00:00 GMT',
'server': 'Jetty(8.1.3.v20120416)',
'x-sopapi-version': '1.0.0',
'date': 'Wed, 21 Jan 2015 04:49:51 GMT',
'content-type': 'application/json'},
'{"id":"fakeuuid","properties":{"resource-name":"fa'
'keuuid","resource-type":"share","creation-timestam'
'p":1421816291,"completion-status":"COMPLETE","comp'
'letion-details":"Adding share completed","completi'
'on-substatus":"OK","resource-action":"ADD","percen'
't-complete":100,"resource-id":"fakeuuid","target-n'
'ode-name":"Node005","target-node-id":"fakeuuid","s'
'pawned-jobs":false,"spawned-jobs-list-uri":""}}'))
self.mock_object(httpclient, 'request',
mock.Mock(side_effect=httpreturn))
self.mock_object(time, 'sleep', mock.Mock())
fsadd = self._driver._wait_for_job_completion(httpclient, 'fakeuri')
expectedresult = {
u'id': u'fakeuuid',
u'properties': {
u'completion-details':
u'Adding share completed',
u'completion-status': u'COMPLETE',
u'completion-substatus': u'OK',
u'creation-timestamp': 1421816291,
u'percent-complete': 100,
u'resource-action': u'ADD',
u'resource-id': u'fakeuuid',
u'resource-name': u'fakeuuid',
u'resource-type': u'share',
u'spawned-jobs': False,
u'spawned-jobs-list-uri': u'',
u'target-node-id': u'fakeuuid',
u'target-node-name': u'Node005',
},
}
self.assertEqual(expectedresult, fsadd)
httpcalls = [mock.call('fakeuri',
'GET',
body='',
headers=fake_authorization)
for x in xrange(201)]
self.assertEqual(httpcalls, httpclient.request.call_args_list)
timecalls = [mock.call(1) for x in xrange(200)]
self.assertEqual(timecalls, time.sleep.call_args_list)
def test_wait_for_job_completion_timeout(self):
httpclient = httplib2.Http(disable_ssl_certificate_validation=True,
timeout=None)
httpret = [({'status': '200',
'content-location': 'https://1.2.3.4/sopapi/jobs/'
'fakeuuid',
'transfer-encoding': 'chunked',
'set-cookie': 'JSESSIONID=abcdef;Path=/sopapi;Secure',
'expires': 'Thu, 01 Jan 1970 00:00:00 GMT',
'server': 'Jetty(8.1.3.v20120416)',
'x-sopapi-version': '1.0.0',
'date': 'Wed, 21 Jan 2015 04:49:51 GMT',
'content-type': 'application/json'},
'{"id":"fakeuuid","properties'
'":{"resource-name":"","resource-type":"share","creation-'
'timestamp":1421815791,"completion-status":"PROCESSING","'
'completion-details":"Saving changes","completion-substat'
'us":"RUNNING","resource-action":"ADD","percent-complete"'
':75,"resource-id":"fakeuuid"'
',"target-node-name":"Node005","target-node-id":"fakeuuid'
'","spawned-jobs":false,"spawned-jobs-list-uri":""}}')
for x in xrange(301)]
httpret.append(({'status': '200',
'content-location':
'https://1.2.3.4/sopapi/jobs/fakeuuid',
'transfer-encoding': 'chunked',
'set-cookie': 'JSESSIONID=abcdef;Path=/sopapi;Secure',
'expires': 'Thu, 01 Jan 1970 00:00:00 GMT',
'server': 'Jetty(8.1.3.v20120416)',
'x-sopapi-version': '1.0.0',
'date': 'Wed, 21 Jan 2015 04:49:51 GMT',
'content-type': 'application/json'},
'{"id":"fakeuuid","propert'
'ies":{"resource-name":"fakeuuid","resource-type":"sha'
're","creation-timestamp":1421815791,"completion-statu'
's":"COMPLETE","completion-details":"Adding share comp'
'leted","completion-substatus":"OK","resource-action"'
':"ADD","percent-complete": 100,"resource-id":"fakeuui'
'd","target-node-name":"Node005","target-node-id":"fa'
'keuuid","spawned-jobs":false,"spawned-jobs-list-uri"'
':""}}'))
self.mock_object(httpclient, 'request', mock.Mock(side_effect=httpret))
self.mock_object(time, 'sleep', mock.Mock())
self.assertRaises(exception.SopAPIError,
self._driver._wait_for_job_completion,
httpclient, 'fakeuri')
httpcalls = [mock.call('fakeuri',
'GET',
body='',
headers=fake_authorization)
for x in xrange(301)]
self.assertEqual(httpcalls, httpclient.request.call_args_list)
timecalls = [mock.call(1) for x in xrange(301)]
self.assertEqual(timecalls, time.sleep.call_args_list)
def test_add_share_sopapi(self):
httpclient = httplib2.Http(disable_ssl_certificate_validation=True,
timeout=None)
httpret = ({'status': '202',
'content-length': '0',
'x-sopapi-version': '1.0.0',
'set-cookie': 'JSESSIONID=abcdef;Path=/sopapi;Secure',
'expires': 'Thu, 01 Jan 1970 00:00:00 GMT',
'server': 'Jetty(8.1.3.v20120416)',
'location': 'https://1.2.3.4/sopapi/jobs/fakeuuid',
'date': 'Wed, 21 Jan 2015 05:29:35 GMT'}, '')
self.mock_object(httpclient, 'request',
mock.Mock(return_value=httpret))
waitforret = json.loads('{"id":"fakeuuid'
'","properties":{"resource-name":"fakeuuid'
'","resource-type":"share",'
'"creation-timestamp":1421815791,"c'
'ompletion-status":"COMPLETE","completion-de'
'tails":"Adding share completed","completion'
'-substatus":"OK","resource-action":"ADD","p'
'ercent-complete":100,"resource-id":"fakeuui'
'd","target-node-name":"Node005","target-nod'
'e-id":"fakeuuid1","spawned-jobs":false,"spaw'
'ned-jobs-list-uri":""}}')
self.mock_object(self._driver, '_wait_for_job_completion',
mock.Mock(return_value=waitforret))
fakepayload = {
'description': '',
'type': 'NFS',
'enabled': True,
'tags': '',
'name': 'fakeuuid',
'file-system-id': 'fakeuuid',
}
fsadd = self._driver._add_share_sopapi(httpclient, fakepayload)
self.assertEqual('fakeuuid', fsadd)
httpcalls = [mock.call('https://' +
self.server['backend_details']['ip'] +
'/sopapi/shares/',
'POST',
body=json.dumps(fakepayload),
headers=fake_authorization)]
self.assertEqual(httpcalls, httpclient.request.call_args_list)
self._driver._wait_for_job_completion.assert_called_once_with(
httpclient, 'https://' +
self.server['backend_details']['ip'] +
'/sopapi/jobs/fakeuuid')
def test_create_share_success(self):
self.mock_object(self._driver, '_add_file_system_sopapi', mock.Mock())
self.mock_object(self._driver, '_get_file_system_id_by_name',
mock.Mock(return_value='fakeuuid'))
self.mock_object(self._driver, '_add_share_sopapi',
mock.Mock(return_value='fakeuuid'))
result = self._driver.create_share(
self._context, self.share, share_server=self.server)
self.assertEqual('https://1.2.3.4:/fakeuuid', result)
fakepayload = {
'quota': 1073741824,
'enabled': True,
'description': '',
'record-access-time': True,
'tags': '',
'space-hwm': 90,
'space-lwm': 70,
'name': 'fakeid',
}
fakepayload1 = {
'description': '',
'type': 'NFS',
'enabled': True,
'tags': '',
'name': 'fakeid',
'file-system-id': 'fakeuuid',
}
self._driver._add_file_system_sopapi.assert_called_once_with(
mock.ANY, fakepayload)
self._driver._get_file_system_id_by_name.assert_called_once_with(
mock.ANY, 'fakeid')
self._driver._add_share_sopapi.assert_called_once_with(
mock.ANY, fakepayload1)
def test_get_share_stats_refresh_false(self):
self._driver._stats = {'fake_key': 'fake_value'}
result = self._driver.get_share_stats(False)
self.assertEqual(result, self._driver._stats)
def test_get_share_stats_refresh_true(self):
test_data = {
'driver_handles_share_servers': False,
'share_backend_name': 'HDS_SOP',
'vendor_name': 'Hitach Data Systems',
'driver_version': '1.0',
'storage_protocol': 'NFS',
'reserved_percentage': 0,
'QoS_support': False,
'total_capacity_gb': 1234,
'free_capacity_gb': 2345,
}
self.mock_object(self._driver, '_get_sop_filesystem_stats',
mock.Mock(return_value=(1234, 2345)))
self._driver._update_share_stats()
self.assertEqual(test_data, self._driver._stats)
self._driver._get_sop_filesystem_stats.assert_called_once_with()
def test_allow_access_rw(self):
payload = {
'action': 'add-access-rule',
'all-squash': True,
'anongid': 65534,
'anonuid': 65534,
'host-specification': '1.2.3.4',
'description': '',
'read-write': True,
'root-squash': False,
'tags': 'nfs',
'name': 'fakeid-1.2.3.4'
}
self.mock_object(self._driver, '_get_share_id_by_name',
mock.Mock(return_value='fakeuuid'))
self.mock_object(self._driver, '_wait_for_job_completion', mock.Mock())
self.mock_object(httplib2.Http, 'request', mock.Mock(
return_value=({'status': '202',
'content-length': '0',
'x-sopapi-version': '1.0.0',
'set-cookie': 'JSESSIONID=abcdef;Path=/sopapi;S'
'ecure',
'expires': 'Thu, 01 Jan 1970 00:00:00 GMT',
'server': 'Jetty(8.1.3.v20120416)',
'location': 'https://1.2.3.4/sopapi/jobs/fakeuu'
'id',
'date': 'Wed, 21 Jan 2015 05:29:35 GMT'}, '')))
access = {
'access_type': 'ip',
'access_to': '1.2.3.4',
'access_level': 'rw',
}
self._driver.allow_access(
self._context, self.share, access, share_server=self.server)
headers = dict(Authorization=self._driver.get_sop_auth_header())
httplib2.Http.request.assert_called_once_with(
'https://1.2.3.4/sopapi/shares/fakeuuid', 'POST',
body=json.dumps(payload),
headers=headers)
self._driver._get_share_id_by_name.assert_called_once_with(
mock.ANY, 'fakeid')
self._driver._wait_for_job_completion.assert_called_once_with(
mock.ANY, 'https://' +
self.server['backend_details']['ip'] +
'/sopapi/jobs/fakeuuid')
def test_allow_access_ro(self):
payload = {
'action': 'add-access-rule',
'all-squash': True,
'anongid': 65534,
'anonuid': 65534,
'host-specification': '1.2.3.4',
'description': '',
'read-write': False,
'root-squash': False,
'tags': 'nfs',
'name': 'fakeid-1.2.3.4'
}
self.mock_object(self._driver, '_get_share_id_by_name',
mock.Mock(return_value='fakeuuid'))
self.mock_object(self._driver, '_wait_for_job_completion', mock.Mock())
self.mock_object(httplib2.Http, 'request', mock.Mock(
return_value=({'status': '202',
'content-length': '0',
'x-sopapi-version': '1.0.0',
'set-cookie': 'JSESSIONID=abcdef;Path=/sopapi;S'
'ecure',
'expires': 'Thu, 01 Jan 1970 00:00:00 GMT',
'server': 'Jetty(8.1.3.v20120416)',
'location': 'https://1.2.3.4/sopapi/jobs/fakeuu'
'id',
'date': 'Wed, 21 Jan 2015 05:29:35 GMT'}, '')))
access = {
'access_type': 'ip',
'access_to': '1.2.3.4',
'access_level': 'ro',
}
self._driver.allow_access(
self._context, self.share, access, share_server=self.server)
headers = dict(Authorization=self._driver.get_sop_auth_header())
httplib2.Http.request.assert_called_once_with(
'https://1.2.3.4/sopapi/shares/fakeuuid', 'POST',
body=json.dumps(payload),
headers=headers)
self._driver._get_share_id_by_name.assert_called_once_with(
mock.ANY, 'fakeid')
self._driver._wait_for_job_completion.assert_called_once_with(
mock.ANY, 'https://' +
self.server['backend_details']['ip'] +
'/sopapi/jobs/fakeuuid')
def test_deny_access(self):
payload = {
'action': 'delete-access-rule',
'name': 'fakeid-1.2.3.4',
}
self.mock_object(self._driver, '_get_share_id_by_name',
mock.Mock(return_value='fakeuuid'))
self.mock_object(self._driver, '_wait_for_job_completion', mock.Mock())
self.mock_object(httplib2.Http, 'request', mock.Mock(
return_value=({'status': '202', 'content-length': '0',
'x-sopapi-version': '1.0.0',
'set-cookie': 'JSESSIONID=abcdef;Path=/sopapi;S'
'ecure',
'expires': 'Thu, 01 Jan 1970 00:00:00 GMT',
'server': 'Jetty(8.1.3.v20120416)',
'location': 'https://1.2.3.4/sopapi/jobs/fakeuuid',
'date': 'Wed, 21 Jan 2015 05:29:35 GMT'}, '')))
access = {
'access_type': 'ip',
'access_to': '1.2.3.4',
'access_level': 'rw',
}
self._driver.deny_access(
self._context, self.share, access, share_server=self.server)
headers = dict(Authorization=self._driver.get_sop_auth_header())
httplib2.Http.request.assert_called_once_with(
'https://1.2.3.4/sopapi/shares/fakeuuid', 'POST',
body=json.dumps(payload),
headers=headers)
self._driver._get_share_id_by_name.assert_called_once_with(
mock.ANY, 'fakeid')
self._driver._wait_for_job_completion.assert_called_once_with(
mock.ANY, 'https://' +
self.server['backend_details']['ip'] +
'/sopapi/jobs/fakeuuid')

View File

@ -9,6 +9,7 @@ alembic>=0.7.2
Babel>=1.3
eventlet>=0.16.1
greenlet>=0.3.2
httplib2>=0.7.5
iso8601>=0.1.9
lxml>=2.3
oslo.config>=1.6.0 # Apache-2.0