Merge "EMC VNX Manila Driver Refactoring"

This commit is contained in:
Jenkins 2015-11-13 21:18:10 +00:00 committed by Gerrit Code Review
commit 7566440a65
14 changed files with 8808 additions and 5003 deletions

View File

@ -595,6 +595,10 @@ class EMCVnxLockRequiredException(ManilaException):
message = _("Unable to acquire lock(s).")
class EMCVnxInvalidMoverID(ManilaException):
message = _("Invalid mover or vdm %(id)s.")
class HP3ParInvalidClient(Invalid):
message = _("%(err)s")

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,168 @@
# Copyright (c) 2015 EMC Corporation.
# 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 pipes
from oslo_concurrency import processutils
from oslo_log import log
from oslo_utils import excutils
import six
from six.moves import http_cookiejar
from six.moves.urllib import error as url_error # pylint: disable=E0611
from six.moves.urllib import request as url_request # pylint: disable=E0611
from manila import exception
from manila.i18n import _
from manila.i18n import _LE
from manila.share.drivers.emc.plugins.vnx import constants
from manila import utils
LOG = log.getLogger(__name__)
class XMLAPIConnector(object):
def __init__(self, configuration, debug=True):
super(XMLAPIConnector, self).__init__()
self.storage_ip = configuration.emc_nas_server
self.username = configuration.emc_nas_login
self.password = configuration.emc_nas_password
self.debug = debug
self.auth_url = 'https://' + self.storage_ip + '/Login'
self._url = ('https://' + self.storage_ip
+ '/servlets/CelerraManagementServices')
https_handler = url_request.HTTPSHandler()
cookie_handler = url_request.HTTPCookieProcessor(
http_cookiejar.CookieJar())
self.url_opener = url_request.build_opener(https_handler,
cookie_handler)
self._do_setup()
def _do_setup(self):
credential = ('user=' + self.username
+ '&password=' + self.password
+ '&Login=Login')
req = url_request.Request(self.auth_url, credential,
constants.CONTENT_TYPE_URLENCODE)
resp = self.url_opener.open(req)
resp_body = resp.read()
self._http_log_resp(resp, resp_body)
def _http_log_req(self, req):
if not self.debug:
return
string_parts = ['curl -i']
string_parts.append(' -X %s' % req.get_method())
for k in req.headers:
header = ' -H "%s: %s"' % (k, req.headers[k])
string_parts.append(header)
if req.data:
string_parts.append(" -d '%s'" % req.data)
string_parts.append(' ' + req.get_full_url())
LOG.debug("\nREQ: %s.\n", "".join(string_parts))
def _http_log_resp(self, resp, body):
if not self.debug:
return
headers = six.text_type(resp.headers).replace('\n', '\\n')
LOG.debug(
'RESP: [%(code)s] %(resp_hdrs)s\n'
'RESP BODY: %(resp_b)s.\n',
{
'code': resp.getcode(),
'resp_hdrs': headers,
'resp_b': body,
}
)
def _request(self, req_body=None, method=None,
header=constants.CONTENT_TYPE_URLENCODE):
req = url_request.Request(self._url, req_body, header)
if method not in (None, 'GET', 'POST'):
req.get_method = lambda: method
self._http_log_req(req)
try:
resp = self.url_opener.open(req)
resp_body = resp.read()
self._http_log_resp(resp, resp_body)
except url_error.HTTPError as http_err:
err = {'errorCode': -1,
'httpStatusCode': http_err.code,
'messages': six.text_type(http_err),
'request': req_body}
msg = (_("The request is invalid. Reason: %(reason)s") %
{'reason': err})
if '403' == six.text_type(http_err.code):
raise exception.NotAuthorized()
else:
raise exception.ManilaException(message=msg)
return resp_body
def request(self, req_body=None, method=None,
header=constants.CONTENT_TYPE_URLENCODE):
try:
resp_body = self._request(req_body, method, header)
except exception.NotAuthorized:
LOG.debug("Login again because client certification "
"may be expired.")
self._do_setup()
resp_body = self._request(req_body, method, header)
return resp_body
class SSHConnector(object):
def __init__(self, configuration, debug=True):
super(SSHConnector, self).__init__()
self.storage_ip = configuration.emc_nas_server
self.username = configuration.emc_nas_login
self.password = configuration.emc_nas_password
self.debug = debug
self.sshpool = utils.SSHPool(ip=self.storage_ip,
port=22,
conn_timeout=None,
login=self.username,
password=self.password)
def run_ssh(self, cmd_list, check_exit_code=False):
command = ' '.join(pipes.quote(cmd_arg) for cmd_arg in cmd_list)
with self.sshpool.item() as ssh:
try:
out, err = processutils.ssh_execute(
ssh, command, check_exit_code=check_exit_code)
self.log_request(command, out, err)
return out, err
except processutils.ProcessExecutionError as e:
with excutils.save_and_reraise_exception():
msg = (_LE('Error running SSH command: %(cmd)s. '
'Error: %(excmsg)s.'),
{'cmd': command, 'excmsg': six.text_type(e)})
LOG.error(msg)
def log_request(self, cmd, out, err):
if not self.debug:
return
LOG.debug("\nSSH command: %s.\n", cmd)
LOG.debug("SSH command output: out=%(out)s, err=%(err)s.\n",
{'out': out, 'err': err})

View File

@ -12,6 +12,7 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
STATUS_OK = 'ok'
STATUS_INFO = 'info'
STATUS_DEBUG = 'debug'
@ -19,10 +20,23 @@ STATUS_WARNING = 'warning'
STATUS_ERROR = 'error'
STATUS_NOT_FOUND = 'not_found'
MSG_GENERAL_ERROR = "13690601492"
MSG_INVALID_VDM_ID = "14227341325"
MSG_GENERAL_ERROR = '13690601492'
MSG_INVALID_VDM_ID = '14227341325'
MSG_INVALID_MOVER_ID = '14227341323'
MSG_FILESYSTEM_NOT_FOUND = "18522112101"
MSG_JOIN_DOMAIN_FAILED = '17986748527'
MSG_FILESYSTEM_EXIST = '13691191325'
MSG_VDM_EXIST = '13421840550'
MSG_SNAP_EXIST = '13690535947'
MSG_INTERFACE_NAME_EXIST = '13421840550'
MSG_INTERFACE_EXIST = '13691781136'
MSG_INTERFACE_NON_EXISTENT = '13691781134'
MSG_JOIN_DOMAIN = '13157007726'
MSG_UNJOIN_DOMAIN = '13157007723'
IP_ALLOCATIONS = 2

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -12,6 +12,7 @@
# 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 types
from oslo_config import cfg
@ -41,7 +42,7 @@ def log_enter_exit(func):
return func
def inner(self, *args, **kwargs):
LOG.debug("Entering %(cls)s.%(method)s",
LOG.debug("Entering %(cls)s.%(method)s.",
{'cls': self.__class__.__name__,
'method': func.__name__})
start = timeutils.utcnow()
@ -49,7 +50,7 @@ def log_enter_exit(func):
end = timeutils.utcnow()
LOG.debug("Exiting %(cls)s.%(method)s. "
"Spent %(duration)s sec. "
"Return %(return)s",
"Return %(return)s.",
{'cls': self.__class__.__name__,
'duration': timeutils.delta_seconds(start, end),
'method': func.__name__,

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,224 @@
# Copyright (c) 2015 EMC Corporation.
# 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.
from eventlet import greenthread
import mock
from oslo_concurrency import processutils
from six.moves.urllib import error as url_error # pylint: disable=E0611
from six.moves.urllib import request as url_request # pylint: disable=E0611
from manila import exception
from manila.share import configuration as conf
from manila.share.drivers.emc.plugins.vnx import connector
from manila import test
from manila.tests.share.drivers.emc.plugins.vnx import fakes
from manila.tests.share.drivers.emc.plugins.vnx import utils as emc_utils
from manila import utils
class XMLAPIConnectorTestData(object):
FAKE_BODY = '<fakebody></fakebody>'
FAKE_RESP = '<Response></Response>'
FAKE_METHOD = 'fake_method'
FAKE_KEY = 'key'
FAKE_VALUE = 'value'
@staticmethod
def req_auth_url():
return 'https://' + fakes.FakeData.emc_nas_server + '/Login'
@staticmethod
def req_credential():
return (
'user=' + fakes.FakeData.emc_nas_login
+ '&password=' + fakes.FakeData.emc_nas_password
+ '&Login=Login'
)
@staticmethod
def req_url_encode():
return {'Content-Type': 'application/x-www-form-urlencoded'}
@staticmethod
def req_url():
return (
'https://'
+ fakes.FakeData.emc_nas_server
+ '/servlets/CelerraManagementServices'
)
XML_CONN_TD = XMLAPIConnectorTestData
class XMLAPIConnectorTest(test.TestCase):
@mock.patch.object(url_request, 'Request', mock.Mock())
def setUp(self):
super(XMLAPIConnectorTest, self).setUp()
emc_share_driver = fakes.FakeEMCShareDriver()
self.configuration = emc_share_driver.configuration
xml_socket = mock.Mock()
xml_socket.read = mock.Mock(return_value=XML_CONN_TD.FAKE_RESP)
opener = mock.Mock()
opener.open = mock.Mock(return_value=xml_socket)
with mock.patch.object(url_request, 'build_opener',
mock.Mock(return_value=opener)):
self.XmlConnector = connector.XMLAPIConnector(
configuration=self.configuration, debug=False)
expected_calls = [
mock.call(XML_CONN_TD.req_auth_url(),
XML_CONN_TD.req_credential(),
XML_CONN_TD.req_url_encode()),
]
url_request.Request.assert_has_calls(expected_calls)
def test_request_with_debug(self):
self.XmlConnector.debug = True
request = mock.Mock()
request.headers = {XML_CONN_TD.FAKE_KEY: XML_CONN_TD.FAKE_VALUE}
request.get_full_url = mock.Mock(
return_value=XML_CONN_TD.FAKE_VALUE)
with mock.patch.object(url_request, 'Request',
mock.Mock(return_value=request)):
rsp = self.XmlConnector.request(XML_CONN_TD.FAKE_BODY,
XML_CONN_TD.FAKE_METHOD)
self.assertEqual(XML_CONN_TD.FAKE_RESP, rsp)
def test_request_with_no_authorized_exception(self):
xml_socket = mock.Mock()
xml_socket.read = mock.Mock(return_value=XML_CONN_TD.FAKE_RESP)
hook = emc_utils.RequestSideEffect()
hook.append(ex=url_error.HTTPError(XML_CONN_TD.req_url(),
'403', 'fake_message', None, None))
hook.append(xml_socket)
hook.append(xml_socket)
self.XmlConnector.url_opener.open = mock.Mock(side_effect=hook)
self.XmlConnector.request(XML_CONN_TD.FAKE_BODY)
def test_request_with_general_exception(self):
hook = emc_utils.RequestSideEffect()
hook.append(ex=url_error.HTTPError(XML_CONN_TD.req_url(),
'error_code', 'fake_message',
None, None))
self.XmlConnector.url_opener.open = mock.Mock(side_effect=hook)
self.assertRaises(exception.ManilaException,
self.XmlConnector.request,
XML_CONN_TD.FAKE_BODY)
class MockSSH(object):
def __enter__(self):
return self
def __exit__(self, type, value, traceback):
pass
class MockSSHPool(object):
def __init__(self):
self.ssh = MockSSH()
def item(self):
try:
return self.ssh
finally:
pass
class CmdConnectorTest(test.TestCase):
def setUp(self):
super(CmdConnectorTest, self).setUp()
self.configuration = conf.Configuration(None)
self.configuration.append_config_values = mock.Mock(return_value=0)
self.configuration.emc_nas_login = fakes.FakeData.emc_nas_login
self.configuration.emc_nas_password = fakes.FakeData.emc_nas_password
self.configuration.emc_nas_server = fakes.FakeData.emc_nas_server
self.sshpool = MockSSHPool()
with mock.patch.object(utils, "SSHPool",
mock.Mock(return_value=self.sshpool)):
self.CmdHelper = connector.SSHConnector(
configuration=self.configuration, debug=False)
utils.SSHPool.assert_called_once_with(
ip=fakes.FakeData.emc_nas_server,
port=22,
conn_timeout=None,
login=fakes.FakeData.emc_nas_login,
password=fakes.FakeData.emc_nas_password)
def test_run_ssh(self):
with mock.patch.object(processutils, "ssh_execute",
mock.Mock(return_value=('fake_output', ''))):
cmd_list = ['fake', 'cmd']
self.CmdHelper.run_ssh(cmd_list)
processutils.ssh_execute.assert_called_once_with(
self.sshpool.item(), 'fake cmd', check_exit_code=False)
def test_run_ssh_with_debug(self):
self.CmdHelper.debug = True
with mock.patch.object(processutils, "ssh_execute",
mock.Mock(return_value=('fake_output', ''))):
cmd_list = ['fake', 'cmd']
self.CmdHelper.run_ssh(cmd_list)
processutils.ssh_execute.assert_called_once_with(
self.sshpool.item(), 'fake cmd', check_exit_code=False)
@mock.patch.object(
processutils, "ssh_execute",
mock.Mock(side_effect=processutils.ProcessExecutionError))
def test_run_ssh_exception(self):
cmd_list = ['fake', 'cmd']
self.mock_object(greenthread, 'sleep', mock.Mock())
sshpool = MockSSHPool()
with mock.patch.object(utils, "SSHPool",
mock.Mock(return_value=sshpool)):
self.CmdHelper = connector.SSHConnector(self.configuration)
self.assertRaises(processutils.ProcessExecutionError,
self.CmdHelper.run_ssh,
cmd_list,
True)
utils.SSHPool.assert_called_once_with(
ip=fakes.FakeData.emc_nas_server,
port=22,
conn_timeout=None,
login=fakes.FakeData.emc_nas_login,
password=fakes.FakeData.emc_nas_password)
processutils.ssh_execute.assert_called_once_with(
sshpool.item(), 'fake cmd', check_exit_code=True)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,155 @@
# Copyright (c) 2015 EMC Corporation.
# 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 doctest
from lxml import doctestcompare
import mock
from oslo_log import log
import six
LOG = log.getLogger(__name__)
CHECKER = doctestcompare.LXMLOutputChecker()
PARSE_XML = doctest.register_optionflag('PARSE_XML')
class RequestSideEffect(object):
def __init__(self):
self.actions = []
self.started = False
def append(self, resp=None, ex=None):
if not self.started:
self.actions.append((resp, ex))
def __call__(self, *args, **kwargs):
if not self.started:
self.started = True
self.actions.reverse()
item = self.actions.pop()
if item[1]:
raise item[1]
else:
return item[0]
class SSHSideEffect(object):
def __init__(self):
self.actions = []
self.started = False
def append(self, resp=None, err=None, ex=None):
if not self.started:
self.actions.append((resp, err, ex))
def __call__(self, rel_url, req_data=None, method=None,
return_rest_err=True, *args, **kwargs):
if not self.started:
self.started = True
self.actions.reverse()
item = self.actions.pop()
if item[2]:
raise item[2]
else:
if return_rest_err:
return item[0:2]
else:
return item[1]
class EMCMock(mock.Mock):
def _get_req_from_call(self, call):
if len(call) == 3:
return call[1][0]
elif len(call) == 2:
return call[0][0]
def assert_has_calls(self, calls):
if len(calls) != len(self.mock_calls):
raise AssertionError(
'Mismatch error.\nExpected: %r\n'
'Actual: %r' % (calls, self.mock_calls)
)
iter_expect = iter(calls)
iter_actual = iter(self.mock_calls)
while True:
try:
expect = self._get_req_from_call(next(iter_expect))
actual = self._get_req_from_call(next(iter_actual))
except StopIteration:
return True
if not isinstance(expect, six.binary_type):
expect = six.b(expect)
if not isinstance(actual, six.binary_type):
actual = six.b(actual)
if not CHECKER.check_output(expect, actual, PARSE_XML):
raise AssertionError(
'Mismatch error.\nExpected: %r\n'
'Actual: %r' % (calls, self.mock_calls)
)
class EMCNFSShareMock(mock.Mock):
def assert_has_calls(self, calls):
if len(calls) != len(self.mock_calls):
raise AssertionError(
'Mismatch error.\nExpected: %r\n'
'Actual: %r' % (calls, self.mock_calls)
)
iter_expect = iter(calls)
iter_actual = iter(self.mock_calls)
while True:
try:
expect = next(iter_expect)[1][0]
actual = next(iter_actual)[1][0]
except StopIteration:
return True
if not self._option_check(expect, actual):
raise AssertionError(
'Mismatch error.\nExpected: %r\n'
'Actual: %r' % (calls, self.mock_calls)
)
def _option_parser(self, option):
option_map = {}
for item in option.split(','):
key, value = item.split('=')
option_map[key] = value
return option_map
def _option_check(self, expect, actual):
if '-option' in actual and '-option' in expect:
exp_option = expect[expect.index('-option') + 1]
act_option = actual[actual.index('-option') + 1]
exp_opt_map = self._option_parser(exp_option)
act_opt_map = self._option_parser(act_option)
for key in exp_opt_map:
exp_set = set(exp_opt_map[key].split(':'))
act_set = set(act_opt_map[key].split(':'))
if exp_set != act_set:
return False
return True