From f7ebe429c614f9cdeb86258570c78952326a59e2 Mon Sep 17 00:00:00 2001 From: Alex Meade Date: Tue, 7 Jul 2015 15:04:01 -0400 Subject: [PATCH] NetApp E-Series: Add debug tracing Add method and api level debug tracing to the NetApp E-Series drivers using the utils.trace_method decorator. This is enabled in the driver via the 'trace_flags' configuration option. Change-Id: Ie23cbde27792001cb9424af96cba502d06555638 --- .../drivers/netapp/eseries/test_client.py | 102 +++++++++++++++++- .../volume/drivers/netapp/eseries/client.py | 65 +++++++---- .../drivers/netapp/eseries/host_mapper.py | 2 + .../volume/drivers/netapp/eseries/library.py | 1 + 4 files changed, 150 insertions(+), 20 deletions(-) diff --git a/cinder/tests/unit/volume/drivers/netapp/eseries/test_client.py b/cinder/tests/unit/volume/drivers/netapp/eseries/test_client.py index 537219b7b..bd3169cac 100644 --- a/cinder/tests/unit/volume/drivers/netapp/eseries/test_client.py +++ b/cinder/tests/unit/volume/drivers/netapp/eseries/test_client.py @@ -35,7 +35,9 @@ class NetAppEseriesClientDriverTestCase(test.TestCase): self.my_client = client.RestClient('http', 'host', '80', '/test', 'user', self.fake_password, system_id='fake_sys_id') - self.my_client.invoke_service = mock.Mock() + fake_response = mock.Mock() + fake_response.status_code = 200 + self.my_client.invoke_service = mock.Mock(return_value=fake_response) def test_register_storage_system_does_not_log_password(self): self.my_client.register_storage_system([], password=self.fake_password) @@ -188,3 +190,101 @@ class NetAppEseriesClientDriverTestCase(test.TestCase): 'hostGroupRef') self.assertEqual([volume_mapping_2], mappings) + + def test_to_pretty_dict_string(self): + dict = { + 'foo': 'bar', + 'fu': { + 'nested': 'boo' + } + } + expected_dict_string = ("""{ + "foo": "bar", + "fu": { + "nested": "boo" + } +}""") + + dict_string = self.my_client._to_pretty_dict_string(dict) + + self.assertEqual(expected_dict_string, dict_string) + + def test_log_http_request(self): + mock_log = self.mock_object(client, 'LOG') + verb = "POST" + url = "/v2/test/me" + headers = {"Content-Type": "application/json"} + headers_string = """{ + "Content-Type": "application/json" +}""" + body = {} + body_string = "{}" + + self.my_client._log_http_request(verb, url, headers, body) + + args = mock_log.debug.call_args + log_message, log_params = args[0] + final_msg = log_message % log_params + self.assertIn(verb, final_msg) + self.assertIn(url, final_msg) + self.assertIn(headers_string, final_msg) + self.assertIn(body_string, final_msg) + + def test_log_http_request_no_body(self): + mock_log = self.mock_object(client, 'LOG') + verb = "POST" + url = "/v2/test/me" + headers = {"Content-Type": "application/json"} + headers_string = """{ + "Content-Type": "application/json" +}""" + body = None + body_string = "" + + self.my_client._log_http_request(verb, url, headers, body) + + args = mock_log.debug.call_args + log_message, log_params = args[0] + final_msg = log_message % log_params + self.assertIn(verb, final_msg) + self.assertIn(url, final_msg) + self.assertIn(headers_string, final_msg) + self.assertIn(body_string, final_msg) + + def test_log_http_response(self): + mock_log = self.mock_object(client, 'LOG') + status = "200" + headers = {"Content-Type": "application/json"} + headers_string = """{ + "Content-Type": "application/json" +}""" + body = {} + body_string = "{}" + + self.my_client._log_http_response(status, headers, body) + + args = mock_log.debug.call_args + log_message, log_params = args[0] + final_msg = log_message % log_params + self.assertIn(status, final_msg) + self.assertIn(headers_string, final_msg) + self.assertIn(body_string, final_msg) + + def test_log_http_response_no_body(self): + mock_log = self.mock_object(client, 'LOG') + status = "200" + headers = {"Content-Type": "application/json"} + headers_string = """{ + "Content-Type": "application/json" +}""" + body = None + body_string = "" + + self.my_client._log_http_response(status, headers, body) + + args = mock_log.debug.call_args + log_message, log_params = args[0] + final_msg = log_message % log_params + self.assertIn(status, final_msg) + self.assertIn(headers_string, final_msg) + self.assertIn(body_string, final_msg) diff --git a/cinder/volume/drivers/netapp/eseries/client.py b/cinder/volume/drivers/netapp/eseries/client.py index 867183c1b..9a547ab15 100644 --- a/cinder/volume/drivers/netapp/eseries/client.py +++ b/cinder/volume/drivers/netapp/eseries/client.py @@ -31,6 +31,7 @@ from six.moves import urllib from cinder import exception from cinder.i18n import _, _LE +import cinder.utils as cinder_utils from cinder.volume.drivers.netapp.eseries import utils @@ -83,13 +84,8 @@ class WebserviceClient(object): " Error - %s."), e) raise exception.NetAppDriverException( _("Invoking web service failed.")) - self._eval_response(response) return response - def _eval_response(self, response): - """Evaluates response before passing result to invoker.""" - pass - class RestClient(WebserviceClient): """REST client specific to e-series storage service.""" @@ -125,34 +121,65 @@ class RestClient(WebserviceClient): def _invoke(self, method, path, data=None, use_system=True, timeout=None, verify=False, **kwargs): """Invokes end point for resource on path.""" - scrubbed_data = copy.deepcopy(data) - if scrubbed_data: - if 'password' in scrubbed_data: - scrubbed_data['password'] = "****" - if 'storedPassword' in scrubbed_data: - scrubbed_data['storedPassword'] = "****" - - LOG.debug("Invoking rest with method: %(m)s, path: %(p)s," - " data: %(d)s, use_system: %(sys)s, timeout: %(t)s," - " verify: %(v)s, kwargs: %(k)s.", - {'m': method, 'p': path, 'd': scrubbed_data, - 'sys': use_system, 't': timeout, 'v': verify, 'k': kwargs}) url = self._get_resource_url(path, use_system, **kwargs) if self._content_type == 'json': headers = {'Accept': 'application/json', 'Content-Type': 'application/json'} + if cinder_utils.TRACE_API: + self._log_http_request(method, url, headers, data) data = json.dumps(data) if data else None res = self.invoke_service(method, url, data=data, headers=headers, timeout=timeout, verify=verify) - return res.json() if res.text else None + res_dict = res.json() if res.text else None + + if cinder_utils.TRACE_API: + self._log_http_response(res.status_code, dict(res.headers), + res_dict) + + self._eval_response(res) + return res_dict else: raise exception.NetAppDriverException( _("Content type not supported.")) + def _to_pretty_dict_string(self, data): + """Convert specified dict to pretty printed string.""" + return json.dumps(data, sort_keys=True, + indent=2, separators=(',', ': ')) + + def _log_http_request(self, verb, url, headers, body): + scrubbed_body = copy.deepcopy(body) + if scrubbed_body: + if 'password' in scrubbed_body: + scrubbed_body['password'] = "****" + if 'storedPassword' in scrubbed_body: + scrubbed_body['storedPassword'] = "****" + + params = {'verb': verb, 'path': url, + 'body': self._to_pretty_dict_string(scrubbed_body) or "", + 'headers': self._to_pretty_dict_string(headers)} + LOG.debug("Invoking ESeries Rest API, Request:\n" + "HTTP Verb: %(verb)s\n" + "URL Path: %(path)s\n" + "HTTP Headers:\n" + "%(headers)s\n" + "Body:\n" + "%(body)s\n", (params)) + + def _log_http_response(self, status, headers, body): + params = {'status': status, + 'body': self._to_pretty_dict_string(body) or "", + 'headers': self._to_pretty_dict_string(headers)} + LOG.debug("ESeries Rest API, Response:\n" + "HTTP Status Code: %(status)s\n" + "HTTP Headers:\n" + "%(headers)s\n" + "Body:\n" + "%(body)s\n", (params)) + def _eval_response(self, response): """Evaluates response before passing result to invoker.""" - super(RestClient, self)._eval_response(response) status_code = int(response.status_code) # codes >= 300 are not ok and to be treated as errors if status_code >= 300: diff --git a/cinder/volume/drivers/netapp/eseries/host_mapper.py b/cinder/volume/drivers/netapp/eseries/host_mapper.py index 507531fa0..29c08df30 100644 --- a/cinder/volume/drivers/netapp/eseries/host_mapper.py +++ b/cinder/volume/drivers/netapp/eseries/host_mapper.py @@ -33,6 +33,7 @@ from cinder.volume.drivers.netapp.eseries import utils LOG = logging.getLogger(__name__) +@cinder_utils.trace_method @cinder_utils.synchronized('map_es_volume') def map_volume_to_single_host(client, volume, eseries_vol, host, vol_map, multiattach_enabled): @@ -83,6 +84,7 @@ def map_volume_to_single_host(client, volume, eseries_vol, host, raise exception.NetAppDriverException(msg % volume['id']) +@cinder_utils.trace_method @cinder_utils.synchronized('map_es_volume') def map_volume_to_multiple_hosts(client, volume, eseries_vol, target_host, mapping): diff --git a/cinder/volume/drivers/netapp/eseries/library.py b/cinder/volume/drivers/netapp/eseries/library.py index 84d8c85d7..2b68c1b70 100644 --- a/cinder/volume/drivers/netapp/eseries/library.py +++ b/cinder/volume/drivers/netapp/eseries/library.py @@ -52,6 +52,7 @@ CONF.register_opts(na_opts.netapp_transport_opts) CONF.register_opts(na_opts.netapp_san_opts) +@six.add_metaclass(cinder_utils.TraceWrapperMetaclass) class NetAppESeriesLibrary(object): """Executes commands relating to Volumes.""" -- 2.45.2