Merge "EMC VNX Manila Driver Refactoring"
This commit is contained in:
commit
7566440a65
@ -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
168
manila/share/drivers/emc/plugins/vnx/connector.py
Normal file
168
manila/share/drivers/emc/plugins/vnx/connector.py
Normal 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})
|
@ -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
1957
manila/share/drivers/emc/plugins/vnx/object_manager.py
Normal file
1957
manila/share/drivers/emc/plugins/vnx/object_manager.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -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
1504
manila/tests/share/drivers/emc/plugins/vnx/fakes.py
Normal file
1504
manila/tests/share/drivers/emc/plugins/vnx/fakes.py
Normal file
File diff suppressed because it is too large
Load Diff
1145
manila/tests/share/drivers/emc/plugins/vnx/test_connection.py
Normal file
1145
manila/tests/share/drivers/emc/plugins/vnx/test_connection.py
Normal file
File diff suppressed because it is too large
Load Diff
224
manila/tests/share/drivers/emc/plugins/vnx/test_connector.py
Normal file
224
manila/tests/share/drivers/emc/plugins/vnx/test_connector.py
Normal 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
3027
manila/tests/share/drivers/emc/plugins/vnx/test_object_manager.py
Normal file
3027
manila/tests/share/drivers/emc/plugins/vnx/test_object_manager.py
Normal file
File diff suppressed because it is too large
Load Diff
155
manila/tests/share/drivers/emc/plugins/vnx/utils.py
Normal file
155
manila/tests/share/drivers/emc/plugins/vnx/utils.py
Normal 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
|
Loading…
Reference in New Issue
Block a user